常量对象,常量成员函数
常量对象的引用和指针不能调用类的普通的成员函数。只能调用常量成员函数。
常量成员函数:把const放在类成员函数参数列表后。(表示隐含的this是一个指向常量的指针)
当创建一const对象时,直到构造函数完成初始化过程,对象才取得其常量属性。
友元
类可以允许其他类或函数访问它的非公有成员,方法是令其他类或函数成为它的友元。
如果类想把一个函数作为它的友元,在类内为对应函数(成员函数或非成员函数)或类加上一个 friend 关键字开头的函数声明语句。
友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。
友元不是类的成员也不受它所在区域访问控制级别的约束。
友元的声明
友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。
如果希望类的用户能够调用某个友元函数,那么就必须在友元声明之外再专门对函数进行一次声明。
为了使友元对类的用户可见,通常把友元的声明与类本身放置在同一个头文件中(类的外部)。因此,我们的 Sales_data 头文件应该为 read、print 和 add 提供独立的声明(除了类内部的友元声明之外)。
内联函数
类内部定义函数的默认为内联的。
类外部定义函数时,加上inline修饰使其内联。此定义放在头文件。
内联是否有效取决于编译器判断。
可变数据成员
mutable 类型 变量名;
然后可以在const成员函数里访问和修改此数据成员。
类内初始值
// 有些编译器不支持类内初始值
// 类内初始值两种赋值形式。=,{}
class x
{
private:
int a = 0;
vector<int> b{0};
};
类声明
类在声明后定义前这段区间属于不完全类型。
此时只能,定义指向此类型的指针或引用。声明以其为形参或返回类型的函数。
类内部不能有类自己的成员声明。指向类自己的指针或引用可以。
构造函数
每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。
构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
构造函数的名字和类名相同。
不同于其他成员函数,构造函数不能被声明成 const 的。
大部分创建类的一个 const 对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。
因此,构造函数在 const 对象的构造过程中可以向其写值。
类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。默认构造函数无须任何实参。
编译器创建的构造函数又被称为合成的默认构造函数。
类成员初始化顺序和他们在类定义中出现顺序一致。(构造函数初始值列表不限定顺序)。
对象被默认初始化或值初始化时,自动执行默认构造函数。
默认初始化:
块作用域内定义不含初始值非静态变量或数组;
类类型成员没在构造函数初始值列表中初始化。
值初始化:
数组初始化时,初始值数量少于数组大小;
定义局部静态变量无初始值;
显示请求值初始化T()。
对基本数值类型,默认初始化后,值大小为未知。值初始化后,为0。
对类类型,默认初始化,值初始化均指向默认构造函数。
如果构造函数只接受一个实参,则它实际上定义了通过实参类型自动转化为此类类型的隐式转换机制。
在构造函数声明前加explicit可以阻止此隐式转换发生。(定义处不用加explicit)。
聚合类
聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。
当一个类满足如下条件,说它是聚合类:
- 所有成员都是public的。
- 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类,也没有virtual函数。
例,一个聚合类:
struct Data {
int ival;
string s;
}
可以提供一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员:
// val1.ival = 0; val1.s = string("Anna")
Data val1 = {0, "Anna"};
初始值的顺序必须与声明的顺序一致,也就是说,第一个成员的初始值要放在第一个,然后是第二个,以此类推。
显式地初始化类的对象的成员存在三个明显的缺点:
- 要求类的所有成员都是 public 的。
- 将正确初始化每个对象的每个成员的重任交给了类的用户(而非类的作者)。
- 添加或删除一个成员之后,所有的初始化语句都需要更新。
类的静态成员
声明静态成员
通过在成员的声明之前加上关键字 static 使得其余类关联在一起。
和其他成员一样,静态成员可以是 public 的或 private 的。
静态数据成员类型可以是常量、引用、指针、类类型等。
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。
因此,每个 Account 对象将包含两个数据成员:owner 和 amount 。只存在一个 interestRate 对象而且它被所有 Account 对象共享。
类的静态成员函数也不与任何堆栈绑定在一起,它们不包含this指针。
使用类的静态成员
使用作用域运算符直接访问静态成员:
double r;
r = Account::rate(); // 使用作用域运算符访问静态成员
可通过类对象,引用或指针来访问静态成员:
Account ac1;
Account *ac2 = &ac1;
// 调用静态成员函数 rate 的等价形式
r = ac1.rate(); //通过 Account 的对象或引用
r = ac2->rate(); // 通过指向 Account 对象的指针
成员函数不用通过作用域运算符就能直接使用静态成员。
定义静态成员
在类外部定义静态成员时,不能重复static。
静态数据成员,不是在构造函数中初始化的。必须在类外部定义和初始化每个静态成员。
定义静态数据成员,需要指定对象的类型名,然后是类名、作用域运算符以及成员自己的名字:
// 定义并初始化一个静态成员
double Account::interestRate = initRate();
这条语句定义了名为 interestRate 的对象,给对象是类 Account 的静态成员,其类型是 double 。
静态成员的类内初始化
通常情况下,类的静态成员不应该在类的内部初始化。
可以为静态成员提供 const 整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的 constexpr 。
初始值必须是常量表达式,因为这些成员本身就是常量表达式,所有它们能用在所有适合于常量表达式的地方。
如果在类的内部提供了一个初始值,则成员的定义不能再指定一个初始值了:
//一个不带初始值的静态成员的定义
constexpr int Account::period; // 初始值在类的定义内提供
不同于普通成员处:
-
静态数据成员可以是不完全类型;
静态数据成员的类型可以就是它所属的类类型。而非静态数据成员则受到限制,只能声明成它所属类的指针或引用:class Bar { public: // ... private: static Bar mem1; //正确:静态成员可以是不完全类型 Bar *mem2; //正确:指针成员可以是不完全类型 Bar mem3; //错误:数据成员必须是完全类型 };
-
可以用静态成员做默认实参。
class Screen { public: // bkground 表示一个在类中稍后定义的静态成员 Screen& clear(char = bkground); private: static const char bkground; };
非静态数据成员不能作为默认实参,因为它的值本身属于兑现的一部分,这么做的结果是无法真正提供一个对象以便从中获取成员的值,最终将引发错误。
学习参考资料:
C++ 中文版 Primer (第5版)