第07章_类
定义抽象数据类型
- 默认情况下, this 指针的类型是指向类类型非常量版本的常量指针(class_name *const)
构造函数
- 构造函数不能被声明成 const(当我们创建类的一个 const 对象时, 直到构造函数完成初始化过程, 对象才能真正取得其常量属性. 因此, 构造函数在 const 对象的构造过程中可以向其写值)
- 类没有显示地定义任何构造函数时, 那么编译器就会为我们隐式地定义一个默认构造函数, 其初始化类的数据成员流程如下:
- 如果存在类内的初始值, 用它来初始化成员
- 否则, 默认初始化该成员
- 如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数, 那么编译器将无法初始化该成员
- 构造函数不应该轻易覆盖掉类内的初始值, 除非新赋的值与原值不同
- 如果不能使用类内初始值(编译器不支持), 则所有构造函数都应该显式地初始化每个内置类型的成员
- 没有出现在构造函数初始值列表的成员将通过相应的类内初始值(如果存在的话)初始化, 或者执行默认初始化
拷贝、赋值和析构
- 不主动定义这些操作, 编译器会自动生成, 将对对象的每个成员执行拷贝, 赋值和销毁操作
访问控制与封装
- 定义在 public 说明符之后的成员在整个程序内可被访问
- 定义在 *private 说明符之后的成员可以被类的成员函数访问, 但是不能被使用该类的代码访问
- class 和 strut 唯一的区别是默认访问权限不一样. struct(public), class(private)
友元
- 类可以允许其他类或者函数访问它的非共有成员, 方法是令其他类或者函数成为它的友元
- 友元声明只能出现在类定义的内部, 但是在类内出现的具体位置不限. 友元不是类的成员, 不受它所在区域访问控制级别的约束(一般来说, 最好在类定义开始或结束前的位置集中声明友元)
- 友元的声明仅仅指定了访问的权限, 而非一个通常意义上的函数声明. 如果希望类的用户能够调用某个友元函数, 那么就必须在友元声明之外再专门对函数进行一次声明
类的其他特性
类成员再探
- 使用 =default 告诉编译器合成默认的构造函数
- 定义在类内部的成员函数是自动 inline 的
- 通过在成员变量的声明中加入 mutable 关键字可以让其能在 const 成员函数中修改
- 当提供一个类内初始值时, 必须以符号 = 或花括号表示
返回 *this 的成员函数
- 一个 const 成员函数如果以引用形式返回 *this, 那么它的返回类型将是常量引用
- 通过区分成员函数是否是 const 的, 可以对其进行重载. (即: this 隐式参数类型是 type* const 还是 const type* const)
类类型
- 即使两个类的成员列表完全一致, 它们也是不同的类型
- 就像可以把函数的声明和定义分离开来, 也能仅仅声明类而暂时不定义它
- class name; 这种声明有时被叫做前向声明, 它向程序中引入了名字并且指明是一种类类型, 在它声明之后和定义之前是一个不完全类型
- 不完全类型只能在非常有限的情景下使用: 可以定义指向这种类型的指针或引用, 也可以声明(但是不能定义)以不完全类型作为参数或返回类型的函数
- 对于一个类来说, 在创建它的对象之前该类必须被定义过, 而不能仅仅被声明
友元再探
- 类还可以把其他类定义成友元, 也可以把其他类的成员函数定义成友元
- 友元关系不存在传递性, 即每个类负责控制自己的友元类或友元函数
- 如果一个类想把一组重载函数声明成它的友元, 需要对这组函数中的每一个分别声明
- 友元声明的作用是影响访问权限, 它本身并非普通意义上的声明(就算在类的内部定义友元函数, 也必须在类的外部提供相应的声明从而使得函数可见)
类的作用域
- 当成员函数定义在类的外部时, 返回类型中使用的名字都位于类的作用域之外. 这时, 返回类型必须指明它是哪个类的成员
名字查找与类的作用域
- 编译器处理完类中的全部声明后才会处理成员函数而定义(所以成员函数能使用类中定义的任何名字)
构造函数再探
构造函数初始值列表
- 如果没有在构造函数的初始值列表中显式地初始化成员, 则该成员将在构造函数体之前执行默认初始化
构造函数的初始值有时必不可少
- 如果成员是 const 或者是引用的话, 必须将其初始化
- 当成员属于某种类类型且该类型没有定义默认构造函数时, 也必须将这个成员初始化
- 随着构造函数体一开始执行, 初始化就完成了. 因此 const, 引用或者属于某种为提供默认构造函数的类类型, 必须通过构造函数初始值列表为这些成员提供初值
成员初始化的顺序
- 成员初始化的顺序与它们在类定义中的出现顺序一致: 第一个成员先被初始化, 然后第二个
委托构造函数
- 一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程
- 当一个构造函数委托给另一个构造函数时, 受委托的构造函数的初始值列表和函数体被依次执行
默认构造函数的作用
隐式的类类型转换
- 如果构造函数只接受一个实参, 则它实际上定义了转换为此类类型的隐式转换机制, 有时我们把这个构造函数称作转换构造函数
- 编译器只会自动地执行一步类型转换
- 通过将构造函数说明为 explicit 阻止转换
- 关键字 explicit 只对一个实参的构造函数有效
- 只能在类内声明构造函数时使用 explicit 关键字, 在类外部定义时不应重复
- 尽管编译器不会将 explicit 的构造函数用于隐式类型转换, 但是还是可以使用这样的构造函数显式地强制进行转换
聚合类
字面值常量类
- 数据成员都必须是字面值类型
- 类必须至少含有一个 constexpr 构造函数
- 如果一个数据成员含有类内初始值, 则内置类型成员的初始值必须是一条常量表达式; 或者如果成员属于某种类类型, 则初始值必须使用成员自己的 constexpr 构造函数
- 类必须使用析构函数的默认定义, 该成员负责销毁类的对象
constexpr 构造函数
- 尽管构造函数不能是 const 的, 但是字面值常量类的构造函数可以是 constexpr 函数
类的静态成员
- static 成员
- 可以是 public 的或着是 private 的
- 当在类的外部定义静态成员时, 不能重复 static 关键字, 该关键字只出现在类内部的声明语句
- 因为静态数据成员不属于类的任何一个对象, 所以它们并不是在创建类的对象时被定义的
- 类似于全局变量, 静态数据成员定义在任何函数之外. 因此一旦被定义, 就将一直存在与程序的整个生命周期中
- 要想确保对象只定义一次, 最好的办法是把静态数据成员的定义与其他非内联函数的定义放在同一个文件中
- 通常情况下, 类的静态成员不应该在类的内部初始化. 然而, 可以为静态成员提供 const 类型的类内初始值, 不过要求静态成员必须是字面值常量类型的 constexpr
1
2
3
4
5
6class Test {
public:
static constexpr int i = 100;
static const int i = 100; // const 和 constexpr 在这里应该是一样的(不确定)
static constexpr double j = 3.14;
}; - 即使一个常量静态数据成员在类内部被初始化了, 通常情况下也应该在类外部定义一下该成员(内部提供了初始值, 外部定义不能再制定一个初始值了)
- 静态数据成员可以是不完全类型(静态数据成员的类型可以是它所属的类类型)
1
2
3
4
5
6class Test {
public:
static Test si; // yes, 静态成员可以是不完全类型
Test *pi; // yes, 指针成员可以是不完全类型
Test i; // error, 数据成员必须是完全类型
};