express做静态网站,自己做的旅游网站简介,两学一做11月答题网站,网站建设专题页面C初学者指南-3.自定义类型(第一部分)-基本自定义类型/类 文章目录 C初学者指南-3.自定义类型(第一部分)-基本自定义类型/类1.类型种类#xff08;简化#xff09;2.为什么选择自定义类型#xff1f;单向计数器提升序列 3.限制成员访问成员函数公共(public) vs. 私有(private…C初学者指南-3.自定义类型(第一部分)-基本自定义类型/类 文章目录 C初学者指南-3.自定义类型(第一部分)-基本自定义类型/类1.类型种类简化2.为什么选择自定义类型单向计数器提升序列 3.限制成员访问成员函数公共(public) vs. 私有(private)的可见性const成员函数成员声明 vs. 定义操作符成员函数 3.初始化成员初始化构造函数默认构造函数与自定义构造函数显式构造函数 ↔ 隐式转换构造函数委托最令人困扰的解析 4.设计约定和风格接口中的数据类型成员和非成员避免使用Setter/Getter对命名使用专用类型 5.示例实现示例1单向的计数器示例2递增序列 1.类型种类简化
基本类型void, bool, char, int, double, …简单聚合主要目的数据分组聚合可能包含一个或多个基本类型或其他兼容的聚合类型无法控制组成类型的相互作用。简单如果只有编译器生成默认构造函数/析构函数/拷贝构造函数/赋值函数标准的内存布局所有成员按声明顺序连续排列如果所有成员都具有相同的访问控制例如全部是公共的更复杂自定义类型主要目的确保正确性和安全性保证。自定义不变量和对成员相互作用的控制。限制成员访问。成员函数用户定义的构造函数/成员初始化。用户自定义析构函数/拷贝构造函数/赋值函数。可以是多态的包含虚成员函数。
2.为什么选择自定义类型
正确性保证
不变量 从不改变的行为和/或数据属性通过控制/限制对数据成员的访问来避免数据损坏使用专用类型来限制函数的输入/输出值
可复用的抽象
隐藏低级实现细节的易于使用的接口不受内部实现变化影响的稳定接口可重用的常用功能的抽象(例如动态数组)
资源管理 也被称为 RAII资源获取即初始化
在构造对象时获取一些资源内存、文件句柄、连接等当对象被销毁时释放/清理资源释放内存、关闭连接等
单向计数器
存储整数初始化为0不变性计数只能增加不能减少或重置
monotonous_counter c;
cout c.reading(); // prints 0
c.increment();
cout c.reading(); // prints 1
c.increment();
c.increment();
cout c.reading(); // prints 3简单的聚合类型不能保证
struct frail_counter {int count;
};
frail_counter c;
cout c.count; // any value
c.count;
c.count 11;整数成员未自动初始化为 0可以自由修改聚合类型的任何整数成员⇒仅使用整数没有任何优势
提升序列
应该存储整数不变性元素数量只能增加即只能插入新元素而不能移除它们不变性元素必须始终按升序排序 简单的聚合不能保证
struct chaotic_sequence {std::vectorint nums;
};
chaotic_sequence s;
s.nums.push_back(8); 8
s.nums.push_back(1); 8 1
s.nums.push_back(4); 8 1 4
s.nums.pop_back(4); 8 1 可能违反要求
数字不一定按升序排序我们可以随意操作数字比如删除数字等与使用普通std::vector相比没有什么优势
3.限制成员访问
成员函数
class monotonous_counter {int count_; // ← 数据成员
…void increment () { // ← 成员函数count_; }
};
class ascending_sequence {std::vectorint seq_; // ← 数据成员
…void insert (int x) { // ← 成员变量// 在正确的位置将 x 插入到 nums 中}
};成员函数可用于
操作或查询数据成员控制/限制对数据成员的访问隐藏低级实现细节确保正确性保持/保证不变量确保清晰度为不同类型的用户设计良好结构的接口确保稳定性内部数据表示大部分独立于接口避免重复/模板避免重复/样板:对于潜在的复杂操作只需调用一次即可
公共(public) vs. 私有(private)的可见性
私有成员只能通过成员函数访问
class ascending_sequence {
private:std::vectorint seq_;// … more private members
public:void insert (int x) { … }auto size () const { return seq_.size(); }// … more public members
};
int main () {ascending_sequence s;s.insert(8); // insert 是公共的auto n s.size(); // size 是公共的auto i s.seq_[0]; // 编译错误: seq_ 是私有的auto m s.seq_.size(); // 编译错误s.seq_.push_back(1); // 编译错误
}struct 与 class – 主要区别在于默认可见性
关键字通常用于struct公共数据的简单聚合class私有数据、成员函数、不变量……
const成员函数
只有带 const 修饰的成员函数才能被 const 对象调用
class ascending_sequence {std::vectorint seq_;
public: …void insert { … }auto size () const { return seq_.size(); }
};
int main () {ascending_sequence s;s.insert(88); // s不是const的auto const cs s; cs.insert(5); // 编译错误: insert 不是const的
}接受常量引用参数的函数不仅承诺不修改它这个承诺还将被编译器检查并强制执行。
void foo (ascending_sequence const s) {// s is const reference ^^^^^auto n s.size(); // size 是 consts.insert(5); // 编译错误: insert 不是 const
}const成员函数内部的成员是const
class monotonous_counter {int count_;
public: …int reading () const { // 编译错误: count_ 是 const:count_ 2;return count_;}
};class ascending_sequence {std::vectorint seq_;
public: …auto size () const { // seq_ 是 const// 编译错误: 调用非const的push_backseq_.push_back(0); // vector的成员 size() 是const的return seq_.size(); }
};成员函数可以通过const进行重载 如果一个成员函数是const-限定的另一个不是它们可以有相同的名称和参数列表。这样可以清楚地区分只读访问和读写操作。
class interpolation { …int t_;…
public:…// 读/写函数对:void threshold (int t) { if (t 0) t_ t; }int threshold () const { return t_; }// 可写访问一个nodenode at (int x) { … }// 只读访问一个nodenode const at (int x) const { … }
};成员声明 vs. 定义
class MyType {int n_;// 更多的成员 …
public:// 声明 内联定义int count () const { return n_; } // 只声明double foo (int, int);
};
// 独立定义
double MyType::foo (int x, int y) {// lots of stuff …
}通常复杂的成员函数的定义会放在类外面放到单独的源文件中。然而像接口适配器函数、获取器如 count这样的小成员函数应该嵌入实现即直接在类体中以达到最佳性能。暂时我们会将所有成员函数保持内联直到我们了解有关分离编译的知识。
操作符成员函数
特殊成员函数
class X { …Y operator [] (int i) { … }
};使用下标运算符。
X x;
Y y x[0];3.初始化
成员初始化
1.成员初始化器 C11
class counter {// counter 应该从0开始int count_ 0;
public:…
};
class Foo {int i_ 10;double x_ 3.14;
public:…
};2.构造函数初始化列表 构造函数ctor 创建对象时执行的特殊成员函数
class counter {int count_;
public:counter(): count_{0} { }…
};
class Foo {int i_; // 1stdouble x_; // 2nd
public: Foo(): i_{10}, x_{3.14} { }// same order: i_ , x_ …
};提示确保初始化列表中的成员顺序始终是与成员声明顺序相同
构造函数
构造函数ctor 创建对象时执行的特殊成员函数
构造函数的 函数名称 类名称没有返回类型可以通过初始化列表初始化数据成员可以在第一次使用对象之前执行代码可用于建立不变量默认构造函数 不带参数的构造函数 构造函数的独立定义 与其他成员函数的方式相同
class MyType { …
public:MyType (); // 声明…
};
// 独立定义
MyType::MyType (): … { … }注意确保初始化列表中的成员顺序始终是 与成员声明顺序相同
初始化列表中的不同顺序可能会导致未定义的行为例如访问未初始化的内存。这里在默认构造函数中我们需要确保只有在min_和max_被初始化之后才能访问v_{min_,max_}。有些编译器会对此发出警告例如 g/clang 使用 -Wall 或 -Wreorder 选项这就是为什么要始终启用并且决不忽略编译器警告的另一个原因
默认构造函数与自定义构造函数
没有用户定义的构造函数⇒编译器生成一个
class BoringType { public: int i 0; };
BoringType obj1; // 正确
BoringType obj2 {}; // 正确至少有一个特殊构造函数 ⇒ 编译器不生成默认构造函数
class SomeType { …
public:// special constructor:explicit SomeType (int x) … { … }
};
SomeType s1 {1}; // 特殊 (int) 构造函数
SomeType s2; // 编译错误: 没有默认构造函数!
SomeType s3 {}; // 编译错误: 没有默认构造函数!TypeName() default; ⇒ 编译器生成默认构造函数的实现编译器实现没有参数的构造函数就是默认构造函数
显式构造函数 ↔ 隐式转换
// 函数有一个 Counter 参数
void foo (Counter c) { … }
void bar (Counter const c) { … }隐式转换不好的方式
class Counter {int count_ 0;
public:Counter (int initial):count_{initial} {}…
};
// 从‘2‘创建了Counter对象
foo(2); // 正确
bar(2); // 正确
foo(Counter{2}); // 正确
bar(Counter{2}); // 正确显式构造函数推荐的方式
class Counter {int count_ 0;
public:explicitCounter (int initial):count_{initial} {}…
};
// 没有隐式转换:
foo(2); // 编译错误
bar(2); // 编译错误
foo(Counter{2}); // 正确
bar(Counter{2}); // 正确注意默认情况下让用户定义的构造函数显式
隐式转换是难以发现的错误的主要来源只有在绝对必要且含义明确时才使用非显式构造函数如果需要直接从参数类型进行转换。一些较老的教材和使用 C98 的人可能会告诉你只需要关心单参数构造函数的隐式转换。然而自C11以来情况已经改变因为现在你也可以从花括号括起的值列表中隐式地构造对象。
构造函数委托 调用初始化列表中的其他构造函数
class Range {int a_;int b_;
public:// 1) 特殊构造函数explicit Range (int a, int b): a_{a}, b_{b} {if (b_ a_) std::swap(a_,b_);}// 2) 特殊[a,a]构造 - 委托给[a,b]构造函数explicit Range (int a): Range{a,a} {}// 3) default constructor - delegates to [a,a] ctorRange (): Range{0} {}…
};
Range r1; // 3) ⇒ r1.a_: 0 r1.b_: 0
Range r2 {3}; // 2) ⇒ r2.a_: 3 r2.b_: 3
Range r3 {4,9}; // 1) ⇒ r3.a_: 4 r3.b_: 9
Range r4 {8,2}; // 1) ⇒ r4.a_: 2 r4.b_: 8最令人困扰的解析
由于C语法中的歧义无法使用空括号进行对象构造
class A { … };
A a (); // 声明了没有参数和返回值的函数a
A a; // 构造一个A类型对象
A a {}; // 构造一个A类型对象4.设计约定和风格
每种类型都应该有一个目的
因为这样可以减少将来对它的修改可能性。降低出现新错误的风险根据您的类型保持代码更加稳定
保持数据成员私有并使用成员函数访问/修改数据
这样用户只能通过稳定的接口与您的类型进行交互。避免数据损坏 / 允许不变量保证。如果你改变了类型的内部实现类型的用户不需要改变他们的代码。
const - 限定所有非修改成员函数
为了清楚地表明对象的内部状态如何以及何时发生改变。使您更难错误地使用您的类型。启用编译器可变性检查。更好地推理正确性特别是在涉及同时访问对象的情况下例如来自多个线程。
接口应该易于正确使用并且难以错误使用。— Scott Meyers函数或类型的用户不应该对其目的、参数的意义、先决条件/后置条件和副作用感到困惑。
接口中的数据类型
#include cstdint
#include numeric_limits
class monotonous_counter {
public:// 公共类型别名using value_type std::uint64_t;
private:value_type count_ 0;
public:value_type reading () const { return count_; }…
};
const auto max std::numeric_limitsmonotonous_counter::value_type::max();不要泄露实现细节
只有当别名类型在您的类的公共接口中使用时即作为公共成员函数的返回类型或参数时才将类型别名公开。如果别名类型只在私有成员函数中使用或用于私有数据成员请不要将类型别名公开。
成员和非成员
如何实现一个特性/添加新功能
只需要访问公共数据例如通过成员函数访问⇒ 实现为独立函数需要访问私有数据⇒作为成员函数实现
示例间隔类型 gap类 如何实现一个函数使新的间隔对象的两个边界都移动相同的量
class gap {int a_; int b_;
public:explicit gap (int a, int b): a_{a}, b_{b} {}int a () const { return a_; }int b () const { return b_; }
};推荐的独立式函数实现
gap shifted (gap const g, int x) {return gap{g.a()x, g.b()x};
}实现仅依赖于gap的公共接口我们没有更改类型 gap 本身 ⇒ 依赖它的其他代码不需要重新编译
不推荐的成员函数实现
class gap { …gap shifted (int x) const {return gap{a_x, b_x};}
};gap的其他用户可能想要一个具有不同语义的移位函数但他们现在只能使用我们的函数了。所有其他代码取决于 gap都需要重新编译。
避免使用Setter/Getter对
使用动作/动词函数而不是仅仅使用设置器Setter。通常可以更好地对问题进行建模。更精细的控制。更好的代码可读性/意图表达。
推荐的描述性操作
class Account { …void deposit (Money const);Money try_withdraw (Money const);Money const balance () const;
};不推荐的Setter/Getter对
class Account { …void set_balance (Money const);Money const balance () const;
};命名
名称应反映类型/函数的用途 推荐的可理解的
class IPv6_Address {…};
class ThreadPool {…};
class cuboid {…};
double volume (cuboid const) {…}不推荐的太笼统了
class Manager {…};
class Starter {…};
class Pool {…};
int get_number (Pool const) {…}不要在类型、变量、函数、私有数据成员等名称中使用前导下划线或双下划线
以下划线开头和/或包含双下划线的名称是保留给标准库和/或编译器生成的实体的。使用具有前置下划线或双下划线的名称可能会引发未定义行为一个常见且没有问题的约定是在私有数据成员后面加下划线。
使用专用类型
限制输入参数值确保中间结果的有效性保证返回值有效性
⇒编译器作为正确性检查器如果它能编译通过它应该是正确的
// 明确的接口
double volume (Cuboid const);
// 输入保证角度以弧度为单位
Square make_rotated (Square const, Radians angle);
// 只接受有效数量例如 0
Gadget duplicate (Gadget const original, Quantity times);
// 结果保证向量已被规范化。
UnitVector3d dominant_direction (WindField const);
//避免混淆使用一个好的单位库。
si::kg mass (EllipsoidShell const, si::g_cm3 density);
bool has_cycles (DirectedGraph const);
// 易于理解的控制流程和逻辑
Taxon species1 classify(image1);
Taxon species2 classify(image2);
Taxon lca taxonomy.lowest_common_ancestor(species1, species2);5.示例实现
示例1单向的计数器
新计数器从 0 开始只能往上数不能往下数。对当前计数值的只读访问
#include iostream // std::cout
#include cstdint // std::uint64_t
class monotonous_counter {
public:using value_type std::uint64_t;
private:value_type count_ 0; // initial
public:monotonous_counter () default;explicit monotonous_counter (value_type init) noexcept: count_{init} {}void increment () noexcept { count_; }[[nodiscard]] value_type reading () const noexcept { return count_; }
};
int main () {monotonous_counter c;c.increment();std::cout c.reading(); // prints 1c.increment();c.increment();std::cout c.reading(); // prints 3
}运行示例
示例2递增序列
存储整数通过索引对存储元素进行只读访问只能插入新元素但不能删除它们元素始终按升序排序只能通过公共接口修改内容
‘insert’ 操作的实现以及 ‘begin’ 和 ‘end’ 成员函数的作用在我们学习了迭代器和标准库中的算法后会变得更加清晰。
#include iostream // std::cout
#include vector // std::vector
#include algorithm // std::lower_bound
class ascending_sequence {
public:using value_type int;
private:using storage_t std::vectorvalue_type;storage_t seq_;
public:using size_type storage_t::size_type;void insert (value_type x) {// use binary search to find insert positionseq_.insert(std::lower_bound(seq_.begin(), seq_.end(), x), x);}[[nodiscard]] value_type operator [] (size_type idx) const noexcept { return seq_[idx]; }[[nodiscard]] size_type size () const noexcept { return seq_.size(); }// enable range based iteration[[nodiscard]] auto begin () const noexcept { return seq_.begin(); }[[nodiscard]] auto end () const noexcept { return seq_.end(); }
};
int main () {ascending_sequence s; // s.seq_: s.insert(7); // s.seq_: 7s.insert(2); // s.seq_: 27s.insert(4); // s.seq_: 247s.insert(9); // s.seq_: 2479s.insert(5); // s.seq_: 24579std::cout s[3]; // prints 7for (auto x : s) {std::cout x ; // 2 4 5 7 9}// use type aliasesascending_sequence::value_type x 1;ascending_sequence::size_type n 2;
}运行示例
附上原文地址 如果文章对您有用请随手点个赞谢谢^_^