第七章 类
类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程(以及设计)技术。封装实现了类的接口和实现的分离。
类想要实现数据抽象和封装,需要首先定义一个抽象数据类型。
7.1 定义抽象数据类型
7.1.2 定义改进的Sales_data类
成员函数的声明必须在类的内部,它的定义则既可以在类的内部也可以在类的外部。
非成员函数的定义和声明都在类的外部。
定义在类内部的函数是隐式的inline函数。
引入this
成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象,当我们调用一个成员函数时,用请求该函数的对象地址初始化this,编译器负责把请求该函数的对象地址传递给隐式形参this。
任何自定义名为this的参数或变量的行为都是非法的。
this是一个常量指针。
引入const成员函数
this的类型是指向类类型非常量版本的常量指针,因此我们不能在一个常量对象上调用普通的成员函数。
C++语言的做法是允许把const关键字放在成员函数的参数列表之后,表示this是一个指向常量的指针,像这样使用const的成员函数被称作常量成员函数。
常量对象,以及常量对象的引用或指针都只能调用常量成员函数。
类作用域和成员函数
类本身就是一个作用域。
编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体。因此,成员函数体可以随意使用类中的其他成员而无需在意这些成员出现的次序。
在类的外部定义成员函数
如果成员被声明成常量成员的函数,那么它的定义也必须参数列表后明确指定const属性。
类外部定义的成员的名字必须包含它所属的类名。
定义一个返回this对象的函数
7.1.3 定义类相关的非成员函数
如果函数在概念上属于类但是不定义在类中,则它一般应与类声明(非定义)在同一个头文件中。在这种方式下,用户使用接口的任何部分都只需要引入一个文件。
7.1.4 构造函数
无论何时只要类的对象被创建,就会执行构造函数。
构造函数不能被声明为const。
合成的默认构造函数
默认构造函数无须任何实参。
如果我们的类没有显式地定义构造函数,那么编译器就会为我们隐式地定义一个默认构造函数,又被称为合成的默认构造函数。
某些类不能依赖于合成的默认构造函数
对于一个普通的类来说,必须定义它自己的默认构造函数:
1、一旦我们定义了一些其他的构造函数,那么除非我们再定义一个默认的构造函数,否则类将没有默认构造函数;
2、如果类中含有内置类型或复合类型成员,它们默认初始化的值将是未定义的;
3、有的时候编译器不能为某些类合成默认的构造函数,例如,如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数。
=default的含义
如果我们需要默认的行为,那么可以通过在参数列表后面写上=default来要求编译器生成构造函数。=default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。
构造函数初始值列表
构造函数初始值列表负责为新创建的对象的一个或几个数据成员赋初值。
当某个数据成员被构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐式初始化。
7.15 拷贝、赋值和析构
除了定义类的对象如何初始化之外,类还需要控制拷贝(初始化变量以及以值的方式传递或返回一个对象等)、赋值和销毁对象时发生的行为。
如果我们不主动定义这些操作,则编译器将替我们合成它们。一般来说,编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁操作。
同样,对于某些类来说合成的版本无法正常工作,特别是,当类需要分配类对象之外的资源时,合成的版本尝尝会失效。
7.2 访问控制与封装
在C++语言中,我们使用访问说明符加强类的封装性:
public成员定义类的接口;private部分封装了类的实现细节。
我们可以使用struct和class关键字中的任何一个定义类。唯一的一点区别是,struct和class的默认访问权限不太一样。
如果我们使用struct关键字,则定义在第一个访问说明符之前的成员是public的;相反,如果我们使用class关键字,则这些成员是private的。
7.2.1 友元
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可。
友元声明只能出现在类的内部,友元不是类的成员也不受它所在区域访问控制级别的约束。
友元的声明
友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们必须在友元声明之外再专门对函数进行一次声明。为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中。
7.3 类的其他特性
定义一个类型成员
类可以自定义某种类型在类中的别名,由类定义的类型名字和其他成员一样存在访问限制,可以是public或者private。
用来定义类型的成员必须先定义后使用。
令成员作为内联函数
可以在类的内部把inline作为声明的一部分显式地声明成员函数,同样的,也能在类的外部用inline关键字修饰函数的定义。无须在声明和定义的地方同时说明inline,但这么做其实是合法的。最好只在类外部定义的地方说明inline。
inline成员函数也应该与相应的类定义在同一个头文件中。
可变数据成员
如果我们希望在一个const成员函数内修改类的某个数据成员,可以通过在变量的声明中加入mutable关键字做到这一点。
一个可变数据成员永远不会是const,即使它是const对象的成员。
类数据成员的初始值
当我们提供一个类内初始值时,必须以符号=或者花括号表示。
7.3.2 返回*this的成员函数
返回引用的函数是左值的,意味着这些函数返回的是对象而非对象的副本。
一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。
通过区分成员函数是否是const的,我们可以对其进行重载(因为相当于底层const)。
7.3.3 类类型
每个类定义了唯一的类型,即使两个类的成员完全一样,它们也是两个不同的类型。
我们可以仅声明类而暂时不定义它,这种声明有时被称作前向声明,在它声明之后定义之前是一个不完全类型:可以定义指向这种类型的指针或引用(但不能访问成员);可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。
创建一个类的对象之前,必须完成定义而不是仅仅被声明。
因为只有当类全部完成后类才算被定义,所以一个类的成员类型不能是该类自己。
然而,一旦一个类的名字出现后,它就被认为是声明过了,因此类允许包含指向它自身类型的引用或指针。
7.3.4 友元再探
普通的非成员函数、其他的类和其他类(之前已定义过的)的成员函数都可以定义成友元。
友元函数能定义在类的内部,这样的函数是隐式内联的。
类之间的友元关系
如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
友元关系不存在传递性,每个类负责控制自己的友元类或友元函数。
令成员函数作为友元
当把一个成员函数声明成友元时,我们必须明确指出该成员函数属于哪个类。
友元声明和作用域
类和非成员函数的声明不是必须在它们的友元声明之前(成员函数的声明必须在友元声明之前)。
就算在类的内部定义该函数,也必须在类的外部提供相应的声明从而使得函数可见。
友元声明并非普通意义上的声明,只是影响访问权限。
7.4 类的作用域
每个类都会定义它自己的作用域。
在类的作用域之外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符,对于类类型成员则使用作用域运算符访问。
作用域和定义在类外部的成员
在类的外部定义成员函数时必须同时提供类名和函数名。一旦遇到了类名,定义的剩余部分就在类的作用域之内了(参数列表和函数体)。
当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。这时,返回类型必须指明它是哪个类的成员。
7.4.1 名字查找与类的作用域
编译器处理完类中的全部声明后才会处理成员函数的定义,因此成员函数能使用类中定义的任何名字。
用于类成员声明的名字查找
但声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。
1、首先在类中查找;
2、在定义该类的作用域中查找(类定义之前)
类型名要特殊处理
在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
成员定义中的普通块作用域的名字查找
成员函数中使用的名字按照如下方式解析:
1、在成员函数内查找;
2、在类内查找(所有成员);
3、在成员函数定义之前的作用域内继续查找(不仅仅是类定义之前)
如果类的成员被上述查找规则隐藏了,我们可以通过加上类的名字(类名::)或显式地使用this指针来强制访问成员。
如果我们需要的是外层作用域的名字,可以显式地通过作用域运算符(::)来进行请求。
7.5 构造函数再探
7.5.1 构造函数初始值列表
如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化。
构造函数的初始值有时必不可少
1、如果成员是const或者是引用的话,必须将其初始化
2、当成员属于某种未提供默认构造函数的类类型时
养成使用构造函数初始值的习惯:1、底层效率问题;2、一些数据成员必须被初始化
成员初始化的顺序
成员的初始化顺序与它们在类定义中的出现顺序一致,构造函数初始值列表中初始值的前后顺序关系不会影响实际的初始化顺序。(最好保持一致)
默认实参和构造函数
如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造参数。
7.5.2 委托构造函数
当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行,然后控制权才会交还给委托者的函数体。
7.5.3 默认构造函数的作用
当对象被默认初始化或值初始化时自动执行默认构造函数。
默认初始化
1、在块作用域内不使用任何初始值定义一个非静态变量或者数组
2、当一个类本身含有类类型且使用合成的默认构造函数时
3、当类类型的成员没有在构造函数列表中显式地初始化时
值初始化
1、在数组的初始化过程中如果我们提供的初始值数量少于数组的大小时
2、不使用初始值定义一个局部静态变量时
3、书写形如T()的表达式显式地请求值初始化时,其中T是类型名。
显式使用T()或T{},T为类型,括号里为空,称为值初始化。
使用默认构造函数
如果想定义一个使用默认构造函数进行初始化的对象,正确的方法是:
Sales_data obj;
7.5.4 隐式的类类型转换
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换规则,有时我们把这种构造函数称作转换构造函数。
隐式转换生成一个临时量,只能传入常量引用。
编译器只会自动地执行一步类型转换。
在要求隐式转换的程序上下文中,我们可以通过将构造函数声明为explicit来抑制构造函数定义的隐式转换。
关键字explicit只对一个实参的构造函数有效。需要多个实参的构造函数不能用于执行隐式转换,所以无须将这些构造函数指定为explicit。
只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复。
发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(使用=)。当我们用explicit关键字声明构造函数时,它将只能以直接初始化的形式使用。而且,编译器不会在自动转换过程中使用该构造函数。
可以使用explicit的构造函数显式地强制进行转换:
item.combine(static_cast<Sales_data>(cin));
标准库中含有显式构造函数的类:
1、接受一个单参数的const char*的string构造函数,不是explicit的
2、接受一个容量参数的vector构造函数,是explicit的。
7.5.5 聚合类
聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法,满足如下条件:
1、所有成员都是public的;
2、没有定义任何构造函数;
3、没有类内初始值;
4、没有基类,也没有virtual函数
我们可以提供一个花括号括起来的成员初始值列表,初始值的顺序必须与声明的顺序一致。
与初始化数组元素的规则一样,如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化,初始值列表的元素个数绝对不能超过类的成员数量。
7.5.6 字面值常量类
某些类也可以是字面值类型。
数字成员都是字面值类型的聚合类是字面值常量类。如果一个类不是聚合类,但它符合下属要求,则它也是一个字面值常量类:
1、数据成员都是字面值类型
2、类必须至少含有一个constexpr构造函数
3、如果一个数据成员还有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
4、类必须使用析构函数的默认定义
constexpr构造函数
字面值常量类的构造函数可以是constexpr函数,通过前置关键字constexpr声明。constexpr构造函数可以声明成=default的形式或者是删除函数的形式。
constexpr构造函数体一般来说应该是空的。constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数或者是一条常量表达式。
constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型。
7.6 类的静态成员
声明静态成员
有的时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联。
我们通过在成员的声明之前加上关键字static使得其与类关联在一起,可以是private或public的。
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。类似的,静态成员函数也不与任何对象绑定在一起,它们不包含this指针。
使用类的静态成员
1、我们使用作用域运算符直接访问静态成员;
2、可以使用类的对象、引用或者指针来访问静态成员;
3、成员函数不用通过作用域运算符就能直接使用静态成员
定义静态成员
静态成员函数可以在类的内部定义,也可以在类的外部定义。
当在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句。
必须在类的外部定义和初始化每个静态数据成员,一旦被定义,就将一直存在于程序的整个声明周期中。
要想确保对象只定义一次,最好的办法是把静态数据成员的定义与其他非内联函数的定义放在同一个文件中。
静态成员的类内初始化
我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr,初始值必须是常量表达式。
即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员,但定义时不能再指定一个初始值了。
静态成员能用于某些场景,而普通成员不能
1、静态数据成员可以是不完全类型。特别的,静态数据成员的类型可以就是它所属的类类型。
2、可以使用静态成员作为默认实参
术语表
类有两项基本能力:一是数据抽象,即定义数据成员和函数成员的能力;二是封装,即保护类的成员不被随意访问的能力。
转换构造函数:可以用一个实参调用的非显式构造函数,这样的函数隐式地将参数类型转换成类类型。
默认构造函数:当没有提供任何实参时使用的构造函数
显式构造函数:可以用一个单独的实参调用但是不能用于隐式转换的构造函数
不完全类型:已经声明但是尚未定义的类型。不完全类型不能用于定义变量或者类的成员,但是用不完全类型定义指针或者引用是合法的。
实现:类的成员(通常是私有的),定义了不希望为使用类类型的代码所用的数据及任何操作。
接口:类型提供的(公有)操作。通常情况下,接口不包括数据成员。
合成的默认构造函数:检查类的数据成员,如果提供了类内初始值,就用它执行初始化操作;否则就对数据成员执行默认初始化。
=default:一种语法形式,位于类内部默认构造函数声明语句的参数列表之后,要求编译器生成构造函数,而不管类是否已经有了其他构造函数。