1. 异常处理
对在运行时出现的问题进行通信并做相应的处理。异常使得问题的检测和解决过程分离开。
1.1 抛出异常
跟在throw后的语句不会执行,这点和return有点像。
如果异常发生在构造函数内,可能当前对象只构造了部分,即使这种情况,也要保证已构造的元素能正确的销毁。
栈展开过程本身就是处理异常的过程,如果又抛出了无法在本对象的析构函数内部处理完毕的异常,在本析构函数执行完之后本对象将会释放,此时就有两个异常待处理。程序会终止。
throw后跟的是一个称为异常对象的特殊对象,该对象是用throw的异常抛出表达式来初始化的,且该异常对象用于初始化后面catch的异常声明。如果该表达式是:
类类型:需要该类有可访问的析构函数和可访问的移动/拷贝构造函数。且初始化后对象的类型为表达式的静态类型。
数组/函数类型:表达式会被转化为指针。注意,抛出指向局部对象的指针是错误的形为,因为在栈展开过程中会释放掉调用链对应的块。
1.2 捕获异常
和传统函数一样:
当catch参数类型为基类,传入一个派生类对象,会切掉派生类的非基类部分。
如果catch参数类型是基类引用,传入一个派生类对象,会以常规方式将catch参数绑定至该派生类对象。
在栈展开过程中,找到的第一个catch不是最佳的,而是第一个能匹配的。所以要将更专门的处理代码放前面。即:当有多个存在继承关系的异常对象时,对派生类的处理catch要在基类前。
catch的参数匹配很严格,除以下情况,都要求精确匹配:
用空throw语句重新抛出异常,将异常对象向上传递。空throw语句只能出现在catch语句或者catch语句直接或间接调用的函数里。如果在这些地方以外遇到了空的throw语句,编译器将会terminate
。
如果不知道异常对象的类型,可用...
捕获所有类型的异常。
1.3 函数try语句块与构造函数
构造函数初始值列表抛出的异常无法被函数体内的try语句块捕捉。
try语句块既能处理构造/析构函数体,也能处理构造函数的初始化过程或析构函数的析构构成。
如下,try出现在构造列表的冒号前,在构造函数的参数列表之后。
传参过程发生的错误属于调用表达式的一部分,需要在调用者的上下文处理。初始化和函数体的错误可以使用函数try语句块。
1.4 noexcept异常说明
在函数参数列表后,const以及引用限定符后,final,override或虚函数的=0之前接noexcept
关键字指出该函数不会抛出异常。这种操作称为不抛出声明。
编译器不在编译时检查noexcept是否正确,在运行时才会检查程序是否违反声明,如果违反,会terminate。
异常说明可以跟常量表达式实参,实参必须是bool类型或者能转为bool类型。如果实参为true,则可能抛出异常,否则不会抛出异常。
1.4.1 noexcept运算符
noexcept运算符用于判断表达式是否允许抛出异常。和sizeof类似,noexcept不会执行表达式。
还可以这样,使得两个函数的异常抛出与否一致:
具体来说,当提供的表达式不含throw并且只调用了做出不抛出说明的函数时,返回true
;否则返回false;
1.4.2 异常说明与指针、虚函数、拷贝控制
函数指针和指针所指的对象。如果指针不抛,指针所指向对象不抛。如果指针允许抛,指针所指向的对象可抛可不抛。
基类的虚函数和派生类的虚函数。如果基类虚函数不抛,派生类虚函数不抛。否则派生类虚函数可抛可不抛。
编译器合成的拷贝控制成员。编译器合成拷贝控制成员同时会合成异常说明。所有成员和基类的所有操作都承诺不抛,合成的拷贝控制成员不抛。否则合成拷贝控制成员允许抛。
如果定义了析构函数但未提供异常说明,则会合成异常说明,且合成的异常说明和合成的析构函数附带的异常说明一致。
1.5 异常类层次
exception:仅定义了拷贝构造函数,拷贝赋值运算符,虚析构函数和名为what的虚成员。what返回一个以null结尾的const char*,并保证不会发生异常。
bad_cast
和bad_alloc
有默认构造函数,runtime_error
和logic_error
没有,只有接受char*和string
的构造函数。
2. 命名空间
2.1 命名空间的定义
命名空间的花括号后无需分号。花括号类可定义类,变量,函数,模板甚至其他的命名空间。命名空间可以定义在全局域,或是其他命名空间,但是不能定义在函数或者类的内部。.
命名空间内的名字可以被同一命名空间或者它内部嵌套的命名空间的其他成员直接访问。除此以外,访问该名字需要指明命名空间。
使用namespace可以是开辟新的命名空间,也可以是在原命名空间基础上新增内容。所以可将声明和定义分开放置在同一命名空间的不同部分,存放到不同的源文件,确保所需的函数和其他名字只定义一次。
模板特例化必须在模板的同一命名空间内,当模板特例化完成后,对新生成的实例可以在类外部定义。如下:
定义在所有类、函数和命名空间之外的名字即是定义在全局命名空间内。欲使用全局命名空间内的名字,无需指定命名空间的名称,使用::member
的形式。
内联命名空间是一种特殊的嵌套命名空间。内联命名空间的名字可被外层命名空间直接使用。在namespace关键字前加inline
将命名空间声明为内联命名空间,只需在第一次定义命名空间时加上inline,之后使用可以加inline,也可以不加。
如果namespace后紧跟的就是花括号,以这种方式定义的命名空间称为未命名的命名空间。未命名的命名空间里的变量都具有静态生命周期,可以用它取代static声明静态成员。
定义在未命名的命名空间内的名字可以直接使用。即未命名的命名空间内的名字的作用域和该命名空间所在的作用域相同。
命名空间污染:在程序中经常需要引用一些库,如C++编译系统提供的标准库、由第三方软件开发商提供的开发库或者用户自己开发的库等。如果在这些库中含有与程序中定义的全局实体同名的实体,或者不同的库之间有同名的实体,则在编译时都会出现名字冲突,这就称为全局命名空间污染
2.2 使用命名空间成员
2.2.1 为命名空间取别名
2.2.1.1 直接取别名
直接用namespace a=b,用a来取代b所对应的命名空间。如下:
2.2.1.2 using
using对命名空间分为两种使用方式,分别是using声明和using指示。
using声明引入命名空间的某个成员。在using声明的作用范围内,可以直接使用该成员而无需指明命名空间。形如:using xxx::memeber;
,在引入函数时,无需带上形参列表,using声明所声明的只是一个名字。
using指示引入整个命名空间,事实上并不是真的将所指示的命名空间的内容添加到using所在的命名空间,只是指明了一个额外的查找空间,这样就可以解释using指示即使包含同名名字也不会立刻报错(原文:这种冲突是允许存在的),只会在使用到该名字时产生二义性。
using指示可以出现在全局、局部、命名空间作用域内,但是不能在类内使用。形如:using namespace xxx;
使用using要注意引入的名字/命名空间不能包含和全局命名空间同名的名字,否则这两个相同的名字都可以直接访问,将引入二义性错误。
2.3 类,命名空间和作用域
对类内的名字,先在使用名字的成员内查找,然后在类和它的基类中查找,接着去外层作用域查找。
2.3.1 实参的查找和类类型的形参
当给函数传递类对象/类对象指针/类对象引用时,除了在常规作用域查找以外还会查找实参类所属的命名空间。即使实参所属的命名空间在调用函数处不可见,也会找。
如上,使用输出运算符>>
调用cin的位于std命名空间的operator>>
成员函数,发现并不需要std
和using
声明就能访问到该函数了。
事实上,查找过程是这样的:
使用>>
运算符,调用operator>>
函数
=>在当前语句的作用域内找,没有
=>去外层作用域找,没有
=>此函数形参是istream
类对象和string
类对象,所以去这两个类所属的名字空间(也就是std)找找
=>在std内找到。
move和forward都接受右值引用作为参数。右值引用可接收任何类型的参数,所以只要用户定义了名为move的函数,无论参数是什么类型,此move就必定与标准库的move冲突,如果直接用move调用标准库的版本或是用户版本,会产生二义性错误。所以在使用中,一般要指明命名空间,如std::move。
在类内声明友元,这个声明并不能使得该友元对类可见,需要在类外额外声明。如果没有额外的声明,则默认在最靠近类的外层命名空间隐士声明这些友元。
f2,f都没有显示声明,所以会在A中隐士声明这两个函数,在使用是,因为类C当成参数传给了f,由传类类型形参的查找规则,会在参数C的命名空间A内查找名字,而f在A内有隐士声明,所以可以找到。
友元和普通名字不一样,不会自动查找外层作用域。
2.4 重载与命名空间
using声明引入的函数会重载该声明语句所在命名空间的同名函数。和用普通方式重载一样:
=>using声明引入的名字会隐藏外层的相关声明。
=>如果using所在的命名空间已有相同名字,相同参数的函数,则报错。
=>using引入的函数会扩充候选函数集的规模。
using指示引入同名同参函数不会报错,运行时才会产生二义性错误,要避免错误,只需指明版本即可。
3. 多重继承和虚继承
多重继承是指从多个直接基类产生派生类的能力。
派生类的每个对象都包含了每个基类(不要求直接基类)的子对象。如:
Panda对象包含Bear子对象、ZooAnimal子对象和Panda独有的成员。
派生类的构造函数会初始化所有直接基类的子对象。如果在派生类的初始化列表未显式调用基类构造函数,则会调用基类的默认构造函数。注意,直接基类的构造函数调用顺序取决于派生列表中出现的顺序,而和初始化列表的调用顺序无关。
同样的,多重继承的派生类调用析构函数的顺序和调用构造函数的顺序也是相反的。
C++11允许派生类使用using
继承基类的构造函数。如果继承了多个形参列表相同构造函数,则报错。如下:
如果派生类定义了自己的拷贝构造函数和拷贝赋值运算符,则必须在完整的派生类对象上都完成构造、赋值和销毁操作,不能只对派生类独有的部分定义操作。
如果使用的是合成版本的拷贝构造函数和拷贝赋值运算符,则基类部分会自动完成构造、赋值和销毁操作。
3.1 类型转换与多个基类
可以将任意类型的基类指针/引用绑定到派生类对象上。
如果在函数重载时,以不同的基类指针/引用作为形参,传入派生类指针作为实参,则会产生二义性错误,因为编译器认为从派生类到所有直接基类的转换都是一样好的。
静态类型决定了我们能访问哪些成员。使用基类指针/引用访问派生类对象只能访问自己的部分,对其他基类的部分和派生类独有的部分无法访问。
3.2 多重继承下的类作用域
只有一个直接基类时,派生类的作用域嵌套在直接基类和间接基类的作用域内,查找时沿着继承体系网上找,派生类的名字隐藏基类的同名成员。
有多个直接基类时,查找方式和单个基类一样,不过多个查找过程会同时在所有直接基类中进行,如果名字在多个基类中被找到,则返回二义性。
所以如果派生类从多个基类继承了相同的名字,访问时需加上域作用符指定名字版本。
3.3 虚继承
派生类可能多次继承同一个类。如:
C通过A和B两次间接继承了基类BASE。
或是这样:
C直接和间接两次继承基类Base。
这种情况会导致C中出现两个Base类的子对象,这样是不行的。
虚继承的目的是让某个派生类做出声明,承诺愿意共享它的基类,在派生列表中使用了virtual的基类称为虚基类。虚基类无论被继承多少次多少代,其派生类都只会含有一个共享的虚基类子对象。
注意:
在派生列表中加上关键字virtual实现虚继承,virtual和public等访问控制符放一起,顺序随意。
虚基类不会影响基类的指针/引用指向派生类对象。
虚基类并不能解决二义性问题,如果两个派生类有相同名字,无论是从虚基类继承的还是自己定义的,对多重继承了这两个派生类的派生类而言,访问改名字会产生二义性。
3.3.1 虚继承的构造函数
虚继承的构造函数有别于普通构造函数,比如对如下图的情况:
仍然使用传统的派生类负责初始化直接基类的方法,Base类的子对象会分别被C和A初始化两次。
所以,在虚继承中,虚基类是由最底层的派生类负责初始化的,还是上面那个例子,在初始化C时,会初始化A和Base,A就不会初始化Base了。中间基类如果试图初始化虚基类,这些初始化会被忽略。
上述过程事实上也是含有虚基类的派生类的初始化过程,先是虚基类子对象初始化,然后按照普通继承的方式按照派生列表顺序初始化。
那要是有多个虚基类怎么办?
首先,虚基类还是要先与非虚基类构造,虚基类之间,则按照在派生列表的出现的顺序初始化,间接(越高层)虚基类早于直接虚基类初始化。
和往常一样,销毁顺序相反。