类的基本思想是数据抽象和封装:
- 数据抽象:依赖于接口和实现分离的编程技术,类的接口包括用户所能执行的操作,类的实现包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数;
- 封装:实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节,类的用户只能使用接口而无法访问实现部分;
目录
7.1 定义抽象数据类型
抽象数据类型通过接口使用一个类对象,但不能访问对象的数据成员。
7.1.1 设计Sales_data类
7.1.2 定义改进的Sales_data类
- 成员函数的声明必须在类的内部,定义可以在类的内部或者外部;
- 接口组成部分的非成员函数,定义和声明都在类的外部;
所有成员都必须在类的内部声明,但是成员函数的定义可以在类外
- isbn函数定义在类内;
- combine和avg_price定义在类外;
isbn函数:
参数列表为空,返回一个string对象,调用时使用点运算符(例如total是一个Sales_data对象,total.isbn()返回bookNo,isbn返回bookNo时也就是返回total.bookNo)
成员函数通过一个名为this的隐式参数访问调用它的那个对象:
- 调用一个成员函数时,用请求该函数的对象地址初始化this(this被初始化为total的地址);
- 成员函数内部可以直接使用调用该函数的对象的成员(total的成员,比如bookNo);
- 返回bookNo其实是返回this->bookNo;
- this是一个常量指针,不允许改变this中保存的地址;
const关键字修改隐式this指针的类型,像这样使用const的成员函数被称为常量成员函数
avg_price函数:
avg_price声明在类内,定义在类外。Sales_data::意思是该函数剩余的代码位于类的作用域内,因此avg_price使用revenue和units_sold时其实使用了Sals_data的成员。
combine函数:
调用时: 更新变量total当前的值
this被绑定到total的地址上,trans被被绑定到rhs上,最后返回total的引用(左值,因此combine函数返回的是引用类型)
7.1.3 定义类相关的非成员函数
辅助函数:add、read、print。
- 这些函数的操作属于类的接口的组成部分,但是不属于类本身。
- 这种“在概念上属于类但不定义在类中”的函数,它一般应该与类声明在同一个头文件内,这样用户使用接口的任何部分都只需要引入一个文件。
read函数:从给定流中将数据读到给定的对象里,第二个参数是一个Sales_data对象的引用
print函数: 将给定对象的内容打印到给定的流中,第二个参数是一个Sales_data对象的引用
- io类不能被拷贝,read和print分别接受一个各自io类型的引用作为参数,通过引用传递;
- 读取和写入操作会改变流的内容,所以两个函数接受的都是普通引用;
- read(cin, total);print(cout, total);
7.1.4 构造函数
构造函数:控制类的对象初始化过程的特殊成员函数
- 用来初始化类对象的数据成员,无论何时类的对象被构建就会执行构造函数;
- 没有返回类型,有一个(可能为空的)参数列表和一个(可能为空的)函数体;
- 类可以包含多个构造函数,不同的构造函数之间必须在参数数量或参数类型上有所区别;
- 构造函数不能被声明为const的;
- 构造函数在const对象的构造过程中可以向其写值(当创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正获得“常量”属性);
默认构造函数:
Sales_data() = default;
不接受任何实参,这个函数的作用完全等同于之前使用的合成默认构造函数;
构造函数初始值列表:
Sales_data(const std::string &s): bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) { }
- 花括号定义了(空的)函数体,构造函数唯一目的就是为数据成员赋初值因此没有函数体;
- 加粗部分的代码为构造函数初始值列表,负责为新创建的对象的数据成员赋初值;
- 构造函数初始值是成员名字组成的列表,每个名字后面紧跟括号括起来的成员初始值;
在类的外部定义构造函数:
以istream为参数的构造函数需要读取输入为数据成员赋初值
对于所有成员函数,在类的外部进行定义时必须指明该函数是哪个类的成员:
Sales_data::Sales_data
Sales_data::指明我们定义Sales_data类的成员,该成员为Sales_data,由于该成员的名字和类名相同因此它是一个构造函数。虽然构造函数的初始值列表是空的,但是执行了构造函数体,所以对象的成员仍然能被初始化。
7.1.5 拷贝、赋值和析构
编译器能够自动合成拷贝、赋值和析构(销毁)的操作,但对于某些类来说无法正常工作。对于需要动态内存的类应使用vector对象或者string对象管理必要的存储空间,进一步讲,包含vector数据成员或者string数据成员的类,编译器自动合成的拷贝、赋值和析构能够正常工作。
7.2 访问控制与封装
使用访问说明符加强类的封装性:
- 定义在public说明符之后的成员:可以在整个程序内被访问,public成员定义类的接口;
- 定义在private说明符之后的成员:只可以被类的成员访问,private部分封装了类的实现细节;
- public:构造函数和isbn、combine函数
- private:数据成员和作为实现部分的函数
使用class关键字定义仅仅是形式上有所不同,另外默认访问权限有所不同:
- struct关键字:定义在第一个访问说明符之前的成员是public的;
- class关键字:定义在第一个访问说明符之前的成员是private的;
由于Sales_data的数据成员是private的,read、print和add函数无法正常编译(尽管它们是类的接口的一部分但并不是类的成员),但是可以另其它类或者函数成为它的友元允许其访问它的非公有成员,增加一条以friend关键字开始的函数声明语句即可:
友元声明出现在类定义的内部 ,为了类能够调用某个友元函数,需要在友元声明(类内)之外再对函数进行一次声明。友元的声明于类本身放置在同一个头文件中(类外),非成员组成部分提供独立的声明(类外)。
7.3 类的其他特性
7.3.1 类成员再探
首先定义一对相互关联的类Screen(表示显示器中的一个窗口)和Window_mgr:
Screen:
Window_mgr:窗口管理类,表示显示器上的一组Screen。这个类包含一个Screen类型的vector,每个元素表示一个特定的Screen。
7.3.2 返回*this的成员函数
接下来向Screen类继续添加一些函数,负责设置光标所在位置的字符,或者其它位置的字符:
返回*this的成员函数返回的是调用该函数的对象的引用(左值),返回的是对象本身而非对象的副本
返回Screen&的目的是返回对象本身(返回一个左值):
继续添加一个display操作打印Screen的内容,由于显示一个Screen不需要改变它的内容,因此令display为一个const成员,this将是一个指向const的指针,*this将是一个const对象。为了令display能够与之前定义的set和move出现在同一序列中,定义一个do_display私有成员负责打印Screen的实际工作,display操作调用这个函数然后返回执行操作的对象:
- 使用display的常量版本调用do_display函数时,this指针隐式地传递给do_display,最后返回一个常量引用;
- 使用display的非常量版本do_display函数时,this指针隐式地转换成指向常量的指针传递给do_display,最后返回一个普通的引用;
- 定义两个版本的display可以根据对象是否为const选用合适的display;
7.3.3 类类型
- 每个类定义了唯一的类型,对于两个类来说即使成员完全一样,也是两个不同的类型。
- 可以将类的声明和定义分开,但是在创建类的对象之前必须先定义类。
- 直到类被定义之后数据成员才能被声明成这种类类型,所以一个类的成员类型不能是该类自己,但是可以是指向它自身类型的引用或指针。
7.3.4 友元再探
类不仅可以把普通的非成员函数定义成友元,还可以把其它的类、其它类的成员函数定义成友元。友元函数也可以定义在类的内部,这样的函数是隐式内联的。为Window_mgr添加一个clear成员负责把指定的Screen内容清空,这需要把Window_mgr设为Screen的友元。
一个类制定了友元类,则友元类的成员函数可以访问该类所有成员。
如果clear不是Screen的友元,将无法访问height、width和contents成员,无法通过编译。友元关系不具有传递性, Window_mgr的友元不是Screen的友元。
另外一种方法是只令clear称为Screen的友元:
需要按照如下顺序设计程序:
- 定义Window_mgr类,声明clear函数但是不定义它;
- 定义Screen,包括对clear的友元声明;
- 定义clear,此时clear才能使用Screen的成员;
7.4 类的作用域
一个类就是一个作用域。在类的作用域之外,需要对象、引用或者指针使用成员访问运算符(.)来访问数据成员和函数成员。假设向Window_mgr添加一个addScreen函数,负责向显示器添加一个新的屏幕,返回类型为ScreenIndex,用户可以通过它定位到指定Screen:
由于返回类型出现在类名之前,因此返回类型ScreenIndex的定义出现在类的作用域之外,必须知名哪个类定义了它。
名字查找:寻找与所用名字最匹配的声明的过程
7.5 构造函数再探
7.5.1 构造函数初始值列表
如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化:
与之前7.1.4的构造函数对比效果相同:
区别在于原始的版本是初始化了它的数据成员,这个版本对数据成员执行了赋值操作。
7.5.2 委托构造函数
委托构造函数使用它所属类的其它构造函数执行初始化过程,或者说它把自己的职责委托给了其它构造函数:
7.5.3 默认构造函数的作用
当对象被默认初始化或值初始化时自动执行默认构造函数
默认初始化在以下情况下发生(定义变量时没有指定初值,没有括号):
- 当在块作用域内不使用任何初始值定义一个非静态变量或者数组时;
- 当一个类本身含有类类型成员且使用合成的默认构造函数时;
- 当类类型的成员没有在构造函数初始值列表中显式地初始化时;
值初始化在以下情况发生(用数值初始化变量,如果没有给定初始值会根据类型提供一个初始值,有括号):
- 当数组初始化的过程中提供的初始值数量少于数组的大小时;
- 当不使用初始值定义一个局部静态变量时;
- 当通过形如T( )的表达式显式地请求值初始化时(T是类型名,vector的构造函数只接收一个实参用于说明vector大小,使用一个这种形式的实参进行值初始化);
7.5.4 隐式的类类型转换
转换构造函数:如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,但是只允许一步类类型转换。
在Sales_data类中接受string的构造函数(第二行)和接受istream的构造函数(第四行)分别定义了从这两种类型向Sales_data类隐式转换的规则,也就是说在需要使用Sales_data的地方可以使用string或者istream作为替代:
相当于将string或者istream类型隐式转换为类类型。
可以更改为:
7.5.5 聚合类
使得用户可以直接访问其成员,当一个类满足如下条件时称为聚合类:
- 所有成员都是public的;
- 没有定义任何构造函数;
- 没有类内初始值;
- 没有基类,也没有virtual函数;
初始化聚合类的数据成员需要与声明的顺序一致。
7.5.6 字面值常量类
构造函数不能是const的,但是字面值常量类的构造函数可以是constexpr,一个字面值常量类必须至少提供一个constexpr构造函数。constexpr构造函数用于生成constexpr对象以及constexpr函数的参数返回类型。
7.6 类的静态成员
当成员与类本身直接相关,而不是与类的各个对象保持关联的时候可以声明为静态成员。
声明静态成员:
static double interestRate;
- 类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据;
- 静态成员函数不予任何对象绑定在一起,不包含this指针;
- 静态成员函数不能声明成const的,也不能再static函数体内使用this指针;
使用静态成员:
可以使用类的对象、引用或者指针访问静态成员;
成员函数可以不通过作用域运算符使用静态成员
定义静态成员:
既可以在类的内部也可以在类的外部定义静态成员函数,在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句。由于静态数据成员不属于类的任何一个对象,因此必须在类的外部定义和初始化每个静态成员。
静态成员的类内初始化:
类的静态成员不应该在类的内部初始化,但是可以为静态成员提供const整数类型的类内初始值,要求静态成员必须是字面值常量类型的constexpr
静态成员能用于某些场景,而普通成员不能:
1. 静态数据成员可以是不完全类型,静态数据成员的类型可以是它所属的类类型,非静态数据成员只能声明成它所属类的指针或者引用:
2. 可以使用静态成员作为默认实参: