类的基本思想是数据抽象和封装。
数据抽象是一种依赖于接口和实现分离的编程(以及设计)技术。类的接口包括用户可以执行的操作,类的实现包括类的数据成员,负责接口实现的函数体以及定义类所需的各种私有函数。
封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节,类的用户只能使用接口而无法访问实现部分。
7.1 类的定义
定义在类内部的函数时隐式inline
的。
this
是一个常量指针。
7.1.1 const 成员函数
通过在紧随参数列表之后添加const
,可以定义const
成员函数。const
的作用是修改隐式this
指针的类型。
默认情况下,this
指针的类型是指向类类型非常量版本的常量指针(顶层常量)。这意味着我们不能把this
绑定到一个常量对象上。
常量对象,以及常量对象的引用或者指针都只能调用常量成员函数。
7.1.2 类作用域和成员函数
编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体(如果有的话)。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的顺序。
编译器处理完类中的全部声明后才会处理成员函数的定义。
这种两阶段的处理方式只适用于成员函数中使用的名字。声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找。
typedef double Money;
string bal;
class Account {
public:
Money balance() { return bal; }
private:
Money bal;
// ...
};
当编译器看到 balance 函数的声明语句时,它将在 Account 类的范围内寻找对 Money 的声明。编译器只考虑 Account 中在使用 Money 前出现的声明,因为没找到匹配的成员,所以编译器会接着到 Account 的外层作用域中查找。在上面的例子中,编译器会找到 Money 的typedef
语句,该类型被用作 balance 函数的返回类型及数据成员 bal 的类型。另一方面,balance 函数体在整个类可见后才被处理。因此,该函数的return
语句返回名为 bal 的成员,而非外层作用域的string
对象。
7.1.3 构造函数
构造函数不能被声明成const
的。
当我们创建类的一个const
对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const
对象的构造过程中可以向其写值。
如果我们的类没有显式定义构造函数,编译器会生成合成的默认构造函数。对于大多数类来说,合成的默认构造函数按照如下规则初始化类的数据成员:
- 如果存在类内的初始值,用它来初始化成员
- 否则,执行默认初始化
在 C++ 11中,如果我们需要默认的行为,那么可以通过在参数列表后面写上 = default 来要求编译器生成构造函数。如果 = default 出现在类的内部,则默认构造函数是内联的;如果它出现在外部,则该成员默认情况下不是内联的。
7.1.3.1 构造函数初始值列表
构造函数初始值列表:Sales_data(const std::string &s) : bookNo(s) { }
如果成员是const
或者引用的话,必须将其初始化。类似的,当成员属于某种类类型且该类类型没有定义默认构造函数时,也必须将这个成员初始化。
随着构造函数体一开始执行,初始化就完成了。我们初始化const
或者引用类型的唯一机会就是通过构造函数初始化。
构造函数初始值列表只说明用于初始化成员的值。成员的初始化顺序与它们在类定义中出现顺序一致。
7.1.3.2 委托构造函数
一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数。
class Sales_data {
public:
// 非委托构造函数
Sales_data(std::string s, unsigned cnt, double price) :
bookNo(s), units_sold(cnt), revenue(cnt*price) { }
// 其余构造函数都委托给另外一个构造函数
Sales_data() : Sales_data("", 0, 0) { }
Sales_data(std::string s) : Sales_data(s, 0, 0) { }
Sales_data(std::istream &is) : Sales_data() { read(is, *this); }
//其他成员
}
受委托的构造函数先执行。
7.2 访问控制与封装
定义在 public 说明符之后的成员在整个程序内可被访问,public 成员定义类的接口。
定义在 private 说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问。
class
和struct
定义类唯一的区别就是默认的访问权限。
友元
通过友元,类可以允许其他类或者函数访问它的非公有成员。
如果类想把一个函数作为它的友元,只需要增加一条以friend
关键字开始的函数声明即可。友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。友元不是类的成员也不受它所在区域访问控制级别的约束。
友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们必须在友元声明之外再专门对函数进行一次声明。
友元关系不存在传递性。
如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
7.3 类的其他特性
7.3.1 定义类型成员
类可以自定义某种类型在类中的别名。要注意,用来定义类型的成员必须先定义后使用。
7.3.2 可变数据成员
有时我们希望能够修改类的某个数据成员,即使是在一个const
成员函数内。通过关键字mutable
可以达到这种效果。一个可变数据成员永远不会是const
,即使它是const
对象的成员。
7.3.3 返回 *this 的成员函数
一个const
成员函数如果以引用的方式返回*this,那么它的返回类型将是常量引用。
基于const
的重载:
class Screen {
public:
// 根据对象是否是 const 重载 display 函数
Screen &display(std::ostream &os)
{ do_display(os); return *this; }
const Screen &display(std::ostream &os) const
{ do_display(os); return *this; }
private:
// 该函数负责显示 Screen 的内容
void do_display(std::ostream &os) const { os << contents; }
};
7.3.4 类类型
声明方法:Sales_data item1;
或class Sales_data item1;
后一种从 C 语言继承而来。
类的声明:前向声明,在类声明之后定义之前,类是一个不完全类型。
不完全类型只能在非常有限的情景下使用:定义指向这种类型的引用或指针,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。
直到类被定义之后数据成员才可以被声明成这种类类型。然而,当一个类的名字出现后,它被认为是声明过了(但尚未定义),因此允许包含指向它自身类型的引用或者指针(有没有想到链表结构)。
7.3.4.1 隐式的类类型转换
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称为转换构造函数。
只允许一步类类型转换。类类型转换不是总有效。
7.3.4.2 抑制构造函数定义的隐式转换
可以通过将构造函数声明为explicit
加以阻止。
关键字explicit
只对一个实参的构造函数有效。需要多个实参的构造函数不能用于执行隐式转换,所以无须将这些构造函数指定为explicit
。
只能在类内声明构造函数时使用explicit
关键字,在类外部定义时不应重复。
explicit
构造函数只能用于直接初始化。
尽管编译器不会将explicit
的构造函数用于隐式转换过程,但是我们可以使用这样的构造函数显式地强制类型转换:
item.combine(static_cast<Sales_data>(cin));
7.3.5 聚合类
当一个类满足如下条件时,我们说它是聚合的:
- 所有成员都是
public
的 - 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有虚函数
例子:
struct Data {
int ival;
string s;
}
可以使用初始化列表初始化,初始值的顺序必须与声明的顺序一致。如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化。
7.3.6 字面值常量类
字面值类型的类可能含有constexpr
函数成员,它们是隐式const
成员函数。
数据成员都是字面值类型的聚合类是字面值常量类。
如果一个类不是聚合类,符合以下要求也是一个字面值常量类:
- 数据成员都必须是字面值类型
- 类必须至少有一个
constexpr
构造函数 - 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;如果成员属于某种类类型,则初始值必须使用成员自己的
constexpr
构造函数 - 类必须使用析构函数的默认定义,该成员负责销毁类的定义
尽管构造函数不能是const
的,但是字面值常量类的构造函数可以使constexpr
函数。事实上,一个字面值常量类都必须至少提供一个constexpr
构造函数。通过前置关键字constexpr
就可以声明一个constexpr
构造函数。
constexpr
构造函数可以声明成=default
或者删除函数的形式,否则,constexpr
构造函数就必须既符合构造函数的要求(意味着不能包含返回语句),又符合constexpr
函数的要求(意味着它唯一可执行语句就是返回语句),综合这两点,constexpr
构造函数体一般来说应该是空的。
constexpr
构造函数必须初始化所有数据成员,初始值或者使用constexpr
构造函数,或者是一条常量表达式。
7.4 类的静态成员
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。类似的,静态成员函数也不与任何对象绑定在一起,它们不包含this
指针。作为结果,静态成员函数不能声明成const
的,而且我们也不能在静态函数体内使用this
指针。
成员函数不用通过作用域运算符就能直接使用静态成员。
当在类的外部定义静态成员时,不能重复static
关键字,该关键字只出现在类内部的声明语句。
必须在类的外部定义和初始化每个静态成员(一个例外,如果静态成员是字面值类型的constexpr
可以在类内给它提供一个整数类型的常量表达式初始值),一个静态成员只能被初始化一次。
类似于全局变量,静态数据成员定义在任何函数之外,一直存在于程序的整个生命周期中。
静态数据成员可以是不完全类型,非静态数据成员只能声明成它所属类的指针或引用。
静态数据成员可以作为默认实参,非静态数据成员不行,因为它的值本身属于对象的一部分,这么做的结果是无法真正提供一个对象以便从中获得成员的值(类似于循环定义),将引发错误。