第三部分
1、类
【inline】
在类内部定义的函数默认为
inline。在声明和定义处指定inline都是合法的。
【隐含的this指针】
成员函数具有一个附加的隐含形参,即
指向该类对象的一个指针,这个隐含形参即命名为this。
this的类型是一个指向类类型的const指针,可以改变this所指向的值,但不能改变this所保存的地址。在
const成员函数中,this的类型是一个指向const类类型对象的const指针,即既不能改变this所指向的对象,也不能改变this所保存的地址。
【可变数据成员mutable】
const成员函数可以改变mutable成员。要将数据成员声明为可变的,必须将关键字mutable放在成员声明之前。
【构造函数】
没有默认构造函数的类类型的成员,以及
const或引用类型的成员,不管是哪种类型,都必须
在构造函数初始化列表中进行初始化。
可以初始化const对象或引用类型的对象,但不能对它们赋值,所以
初始化const或引用类型数据成员的唯一机会是在构造函数初始化列表中。
构造函数初始化列表仅指定用于初始化成员的值,并不指定这些初始化执行的次序,
成员初始化的次序就是定义成员的次序。
内置和复合类型的成员(如指针和数组),
只对定义在全局作用域中的对象才初始化,当对象定义在局部作用域中时,内置或复合类型的成员不进行初始化。
即如果类包含内置和复合类型的成员,则该类不应该依赖于合成的默认构造函数。它应该定义自己的构造函数来初始化这些成员。
【explicit】
可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。而通过将构造函数声明为
explicit来防止在需要隐式转换的上下文中使用构造函数。通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为explicit。将构造函数设置为explicit可以避免错误,并且当转换有用时,用户可以显式地构造对象。
【static】
每个static数据成员是
与类关联
的对象,并不与该类的对象相关联。所以
static成员函数
没有this形参,它可以直接访问所属类的static成员,但不能直接使用非static成员。并且由于static成员函数不是任何对象的组成部分,所以
不能被声明为const和虚函数。
static数据成员可以声明为任意类型,但必须在
类定义体外部定义(正好一次),即不是通过类构造函数进行初始化,而是应该在定义时进行初始化。
const static数据成员在类的定义体中初始化时,该数据成员
仍必须在类的定义体之外进行定义。
static数据成员的类型可以时该成员
所属类类型,而
非static成员被限定声明为其
自身类对象的指针和引用,而且非static数据成员不能用作
默认实参,因为它的值不能独立于所属的对象而使用。
【public、protected、private】
public:都可访问
protected:类本身、友元或其派生类可访问
private:类本身及其友元可访问
2、复制控制
复制构造函数、赋值操作符和析构函数总称为复制控制。编译器自动实现这些操作,但类也可以定义自己的版本。
【复制构造函数】分配新元素并从被复制对象处复制值
一种特殊构造函数,具有单个形参,该形参(常用const修饰)是对该类型的引用。
当用于
类类型对象时,初始化的复制形式和直接形式有所不同:
直接初始化直接调用与实参匹配的构造函数,
复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。直接初始化和复制初始化仅在
低级别优化上存在差异。然而,
对于不支持复制的类型(IO类型),或者使用非explicit构造函数的时候(非explicit可隐式转换),它们有本质区别。比如由于
不能复制IO类型的对象,所以不能对那些类型的对象使用复制初始化。
合成复制构造函数:如果未定义复制构造函数,编译器会合成一个,与合成的默认构造函数不同,即使定义了其他构造函数,也会合成复制构造函数。
合成复制构造函数的行为是逐个成员初始化,将新对象初始化为原对象的副本。
必须定义复制构造函数的情况:【1】类中
有一个数据成员是指针,或者有成员表示在构造函数中分配的其他资源。【2】类在创建新对象时必须做一些特定工作
为了防止复制
,类必须显示声明其复制构造函数为private。通过声明但不定义private复制构造函数,可以禁止任何复制类类型对象的尝试。
【赋值操作符】撤销所保存的原对象并从右操作数向左操作数复制值
一般而言,如果类需要复制构造函数,它也会需要赋值操作符。
重载操作符
合成赋值操作符与合成复制构造函数的操作类似。它会执行逐个成员赋值:右操作数对象的每个成员赋值给左操作数对象的对应成员。
【析构函数】撤销对象
动态分配的对象只有在指向该对象的指针被删除时才撤销,如果没有删除指向动态对象的指针,则不会运行该对象的析构函数,对象就一直存在,从而导致内存泄漏,而且,对象内部使用的任何资源也不会释放。
三法则:如果类需要析构函数,则它也需要赋值操作符和复制构造函数。
析构函数与复制构造函数或赋值操作符之间的一个重要区别是,即使编写了自己的析构函数,合成析构函数仍然运行。
撤销内置或复合类型的成员无须做显式工作,特别地,析构函数的自动工作不会删除指针成员所指向的对象。【需要
防止内存泄漏
【管理指针成员】
包含指针的类需要特别注意复制控制,原因是复制指针时只复制指针中的地址,而不会复制指针指向的对象(导致该对象为多个指针共享,一旦删除某个指针,则其他指针就将称为悬垂指针)。大多数C++类采用以下三种方法之一管理指针成员:
1、指针成员采取常规指针行为。这样的类具有指针的所有缺陷但无需特殊的复制控制。
2、类可以实现所谓的“智能指针”行为。指针所指向的对象是共享的,但类能够防止悬垂指针。
3、类采取值型行为。指针所指向的对象是唯一的,由每个类对象独立管理
。
具有指针成员且使用默认合成复制构造函数的类具有普通指针的所有缺陷,尤其是类本身无法避免悬垂指针。
定义智能指针的通用技术是采用一个使用计数。
智能指针类
将一个计数器与类指向的对象相关联。使用计数跟踪该类有多少个对象共享同一指针。使用计数为0时,删除对象。使用计数有时也称为
引用计数
。实现使用计数有两种经典策略,其一是
定义一个单独的具体类用以封装使用计数和相关指针
。【计数类、智能指针类】
3、重载操作符与转换
重载操作符必须具有一个类类型或枚举类型的操作数。这条规则强制重载符不能重新定义用于内置类型对象的操作符定义。
作为类成员的重载函数,其形参看起来比操作数目少1。
作为成员函数的操作符有一个隐含的
this形参
,限定为第一个操作数。
一般将算术和关系操作符定义为非成员函数,而将赋值操作符定义为成员。操作符定义为非成员函数时,通常必须将它们设置为所操作类的友元。
不要重载具有内置含义的操作符。
赋值操作符、取地址操作符和逗号操作符对类类型操作数有默认含义。如果没有特定重载版本,编译器就自己定义以下操作:
1
、合成赋值操作符进行逐个成员赋值:使用成员自己的赋值操作符依次对每个成员进行赋值。
2、默认情况下,
取地址操作符和逗号操作符在类类型对象上的执行,与在内置类型对象上的执行一样。取地址操作符返回对象的内存地址,逗号操作符从左至右计算每个表达式的值,并返回最右边操作数的值。
3、
内置逻辑与和逻辑或操作符使用短路操作。如果重新定义该操作符,将失去操作符的
短路求值(先计算左操作数,然后再计算其右操作数,只有在仅靠左操作数的值无法确定该逻辑表达式的结果时,才会求其右操作数)特征。
如果定义了自己的版本就不能再使用这些内置含义。
操作符设置为类成员还是普通非成员函数原则:
1、
赋值【=】、下标【[ ]】、调用【()】和成员访问箭头【->】等操作符
必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
2、
复合赋值操作符通常应定义为类的成员。
3、改变对象状态或与给定类型紧密联系的其他一些操作符,如
自增、自减和解引用,通常应定义为类成员。
4、对称的操作符,如
算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
【输入输出操作符】
为了与IO标准库一致,
输出操作符应接受
ostream&作为第一个形参,对
类类型const对象的引用作为第二个形参,并返回对
ostream形参的引用。
输入操作符的第一个形参是一个
引用istream&,指向它要读的流,并且返回对
同一个流的引用。它的第二个形参是对要读入的对象的
非const引用,该形参必须为非const,因为输入操作符的目的是将数据读到这个对象中。
当定义符合标准库iostream规范的输入或输出操作符的时候,
必须使它成为非成员操作符,否则左操作数将只能是该类类型的对象。设计
输入操作符时,如果可能,要确定错误恢复措施。
【算术操作符和关系操作符】
为了与内置操作保持一致,加法
返回一个右值,而不是一个引用。
算术操作符通常产生一个新值,该值是两个操作数计算的结果,它不同于任一操作数且在一个局部变量中计算,返回对那个变量的引用是一个运行时错误。
根据
复合赋值操作符【如+=】来实现
算术操作符【如+】,比其他方式更简单有效。
关联容器以及某些算法,使用默认<操作符。
【赋值操作符】
类赋值操作符必须是类的成员,以便编译器可以知道是否需要合成一个。
赋值操作符可以重载,无论形参为何种类型,赋值操作符必须定义为成员函数。
【下标操作符】
类定义下标操作符时,一般需要定义两个版本:一个为
非const成员并返回引用,另一个为
const成员并返回引用。只要下标操作符返回引用,就可用作赋值的任意一方。
【成员访问操作符】
箭头操作符必须定义为类成员函数。
解引用操作符不要求定义为成员,但将它作为成员一般也是正确的。
重载箭头操作符必须返回指向
类类型的指针,或者返回定义了自己的箭头操作符的
类类型对象。如果
返回类型是指针,则内置箭头操作符可用于该指针,编译器对该指针解引用并从结果对象获取指定成员。如果
返回类型是类类型的其他对象(或是对这种对象的引用),则将
递归应用该操作符。编译器检查返回对象所属类型是否具有成员箭头,如果有就应用该操作符。这个过程继续下去,
直到返回一个指向带有指定成员的对象的指针。
【自增操作符和自减操作符】
C++语言不要求自增操作符或自减操作符一定作为类的成员,但是,因为这些
操作符改变操作对象的状态,所以更倾向于将它们作为成员。
后缀式操作符函数接受一个额外的(即无用的)
int型形参。使用后缀式操作符时,编译器提供0作为这个形参的实参。为了与内置操作符一致,后缀操作符
应返回旧值(即尚未自增或自减的值),并且作为值返回,而不是返回引用。
【调用操作符和函数对象】
可以为类类型的对象重载
函数调用操作符【
int operator
()
(参数表)
】,一般为表示操作的类重载调用操作符。定义了调用操作符的
类,其对象常称为
函数对象,即它们时行为类似函数的对象。
标准库定义了一组算术、关系与逻辑函数对象类,其中有两个一元函数对象类:
一元减【negate<Type>】和
逻辑非【logical_not<Type>】。其余的标准库函数对象都是表示
二元操作符的二元函数对象类。
标准库提供了一组函数适配器,用于特化和扩展一元和二元函数对象。函数适配器可分为如下两类:
【绑定器
】
一种函数适配器,通过将一个操作数绑定到给定值而
将二元函数对象转换为一元函数对象。
标准库定义了两个绑定适配器:
【bind1st】和【bind2nd】,每个绑定器接受一个函数对象和一个值。其中bind1st将给定值绑定到二元函数对象的第一个实参,其中bind2nd将给定值绑定到二元函数对象的第二个实参。
【求反器】
一种函数适配器,将
谓词函数对象的真值求反。
标准库定义了两个求反器:
【not1】和【not2】,not1将一元函数对象的真值求反,not2将二元函数对象的真值求反。
【转换与类类型】
转换操作符是一种特殊的类成员函数,它定义
将类类型值转变为其他类型值的转换。转换操作符在类定义体内声明,在保留字operator之后跟着转换的目标类型。【
operator type();】
转换函数必须是成员函数,不能指定返回类型,并且形参表必须为空。
类类型转换之后不能再跟另一个类类型转换。如果需要多个类类型转换,则代码将出错。
使用
构造函数执行隐式转换的时候,构造函数的形参类型不必与所提供的类型完全匹配。只要该类型能够转换为形参类型即可。
如果两个
转换操作符都可以用在一个调用中,而且在转换函数之后存在标准转换,则根据该标准转换的类别选择最佳匹配。当两个
构造函数定义的转换都可以使用时,如果存在构造函数实参所需的标准转换,就用该标准转换的类别选择最佳匹配。
避免二义性最好的方法时避免编写互相提供隐式转换的成对的类。
既为算术类型提供转换函数,又为同一类类型提供重载操作符,可能会导致重载操作符和内置操作符之间的二义性。【比如一个类类型与一个内置类型进行运算,又定义了类类型与内置类型之间的转换以及类类型之间的运算关系,那么在执行这个运算时,将导致是类类型之间的运算还是内置类型之间的运算的二义性。】