用于大型程序的工具
- 与仅需几个程序员就能开发完成的系统相比,大规模编程对程序设计语言的要求更高,大规模应用程序的特殊要求包括:
- [x] 在独立开发的子系统之间协同处理错误的能力
- [x] 使用各种库(可能包含独立开发的库)进行协同开发的能力
- [x] 对比复杂的应用概念建模的能力
异常处理
- 异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并作出相应的处理,异常使得我们能够将问题的检测与解决过程分离开来,程序的一部分检测问题的出现,然后使得我们能够将问题的检测与解决过程分离开来,程序的一部分检测问题的出现,然后解决该问题的任务传递给程序的另一部分,检测环节无需知道问题处理模块的所有细节,反之亦然
抛出异常
- 在C++语言中,我们通过抛出
throwing
一条表达式来引发raised
一个异常,被抛出的表达式的类型以及当前的调用链共同决定了哪段处理代码
将被用来处理该异常
- 当执行一个throw时,跟在throw后面的语句将不再被执行,相反,程序的控制权从throw转移到与之匹配的catch模块,该catch可能是同一个函数中的局部catch,也可能位于直接或间接调用了发生异常的函数的另一个函数中
- 控制权从一处转移到另一处,这有两个重要的含义:
- [x] 沿着调用链的函数可能会提早退出
- [x] 一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁
- 因为跟在throw后面的语句将不再被执行,所以throw语句的用法有些类似于return,它通常作为条件语句的而一部分或者作为某个函数的最后(或者唯一)一条语句
- 一个异常如果没有被捕获,则它将终止当前的程序
- 析构函数总是会被执行的,但是函数中负责释放资源的代码却是可能被跳过,这一特点对于我们如何阻止程序结构有重要影响,如果我们使用类来控制资源的分配,就能确保无论函数正常结束还是遭遇异常,资源都能被正确的释放
- 由于栈展开可能使用析构函数,析构函数不应该抛出不属于它自身处理的异常,即是,如果析构函数需要执行某个可能抛出异常的操作,则该操作应该被放置在一个try语句块当中,并且在析构函数内部得到处理
- 在实际的编成过程中,因为析构函数仅仅是释放资源,所以它不太可能抛出异常,所有标准库类型都能确保它们的析构函数不会引发异常
- 在栈展开的过程张,运行类类型的局部对象的析构函数。因为这些析构函数是自动执行的,所以它们不应该抛出异常,一旦在栈展开的过程中析构函数抛出了异常,并且析构函数自身没能捕获到该异常,则程序将被终止
捕获异常
-
catch子句
中的异常声明看起来像是只包含一个形参的函数形参列表,如果catch无需访问抛出的表达式的话,则我们可以忽略捕获形参的名字
- 通常情况下,如果catch接受的异常与某个继承体系有关,则最好将该catch的参数定义成引用类型
noexcept异常声明
- 在C++11的新标准中,我呢可以通过moexcept说明指定某个函数不会抛出异常,其形式是关键字noexcept紧跟在函数䣌形参列表后面,用以标识该函数不会抛出异常
void recoup(int) noexcept; //不会抛出异常
void alloc(int); //可能抛出异常
- 上述的recoup做了
不抛出说明
- 对于一个函数来说,noexcept说明要么出现在该函数的所有声明语句和定义语句中,要么一次也不出现
- except可以用在两个情况下
- [x] 确认我们的函数不会抛出异常
- [x] 我们根本不知道该如何处理异常
- 通常情况下,编译器不能也不必在编译时验证异常说明
- noexcept说明符接受一个可选的实参,该实参必须能转换为bool类型:如果实参是true,则函数不会抛出异常;如果实参是false,则函数可能抛出异常
- noexcept有两层含义:当跟在函数参数列表后面时它是异常说明符;而当作为noexcept异常说明的bool实参出现时,它是一个运算符
命名空间
- 当应用程序用到多个供应商提供的库时,不可避免的会发生某些名字相互冲突的情况,多个库名字放置在全局命名空间中将引发
命名空间污染
- 传统上,程序员通过将其定义的全局实体名字设置的很长4来避免命名空间污染问题,这样的名字中通常包含名字所属库的前缀部分
- 命名空间为防止名字冲突提供了更加可控的机制,命名空间分割了全局命名空间,其中每个命名空间是一个作用域,通过在某个命名空间中定义库的名字,库的作者(以及用户)可以避免全局名字固有的限制
命名空间的定义
- 一个命名空间的定义包含两部分:首先是关键字
namespace
,随后是命名空间的名字,在命名空间名字后面是一系列由花括号括起来的声明和定义
- 只要能出现在全局作用域中的声明就能置于命名空间内,主要包括:类、变量(及其初始化操作)、函数(及其定义)、模板和其他命名空间
namespace cplussplus_primer {
class Sales_data{/*...*/};
Sales_data operator+(const Sales_data&,
const Sales_data&);
class Query{/*...*/};
class Query_base{/*...*/};
}
- 命名空间结束后无须分号
- 命名空间的名字也必须在定义它的作用域内保持唯一,命名空间既可以定义在全局作用域内,也可以定义在其它命名空间中,但是不能定义在函数或类的内部
- 当出现一个命名空间的定义,如果之前没有该命名空间的定义,则会创建一个命名空间,否则,定义会打开已经存在的命名空间定义并为其添加一些新成员的声明
- 命名空间的定义可以不连续的特性使得我们可以将几个独立的接口和实现文件组成一个命名空间,此时,命名空间的组织方式类似于我们管理自定义类及函数的方式
- [x] 命名空间的一部分成员的作用是定义类,以及声明作为类接口的函数及对象,则这些成员应该置于头文件中,这些头文件将被包含在使用了这些成员的文件中
- [x] 命名空间成员的定义部分则置于另外的源文件中
- 在程序中某些实体只能定义一次:如非内联函数、静态数据成员、变量等,命名空间中定义的名字也需要满足这一要求,我们可以通过上面的方式组织命名空间并达到目的
- 这种接口和实现分离的机制确保我们所需的函数和其他名字只定义一次,而只要是用到这些实体的地方都能看到对于实体名字的声明
- 定义多个类型不相关的命名空间应该使用单独的文件分别表示每个类型(或关联类型构成的集合)
- 全局作用域中定义的名字(即所在类、函数及命名空间之外定义的名字)也就是定义在
全局命名空间
中。全局命名空间以隐式的方式声明,并且在所以程序中都存在,全局作用域中定义的名字被隐式地添加到全局命名空间中
未命名的命名空间
-
未命名的命名空间
是指关键字namespace后紧跟花括号括起来的一系列声明语句,未命名的命名空间中定义的变量拥有静态生命周期:它们在第一次使用前创建,并且知道程序结束才销毁
- 和其他命名空间不同,未命名的命名空间仅在特定的文件内部有效,其作用范围不会横跨多个不同的文件
多重继承与虚继承
-
多重继承
是指从多个直接基类中产生派生类的能力,多重继承的派生类继承了所有父类的属性
- 尽管概念上非常简单,但是多个基类相互交织产生的细节可能会带来错综复杂的设计问题与实现问题
多重继承
class Bear : public ZooAnimal{/*...*/};
class Panda : public Bear , public Endangered{/*...*/};
- 每个基类包含一个可选的访问说明符,如果访问说明符被忽略掉了,则关键字class对应的默认访问说明符是private,关键字struct对应的是public
- 和只有一个基类的继承一样,多重继承的派生列表也只能包含已经被定义过的类,而且这类不能是final的,对于派生类能够继承的基类个数,C++没有特殊规定;但是在某个给定的派生类列表中,同一个基类只能出现一次
- 多重继承的派生类从每个基类中继承状态
- 派生类构造函数初始化所有基类
- [x] 构造一个派生类的对象将同时构造并初始化它的所有基类子对象,与从一个基类进行的派生一样,多重继承的派生类的构造函数初始值也只能初始化它的直接基类
- [x] 派生类的构造函数初始值列表将实参分别传递给每个直接基类,其中基类的构造顺序与派生列表中基类的出现顺序保持一致,而与派生类构造函数初始值列表中基类的顺序无关
- [x] 在C++11新标准中,允许派生类从它的一个或几个基类中继承构造函数,但是`如果从多个基类中继承了相同的构造函数,则程序产生错误`
- [x] 派生类的析构函数只负责清除派生类本身分配的资源,派生类的成员以及基类都是自动销毁的
- [x] 与只有一个基类的继承一样,多重继承的派生类如果定义了自己的拷贝/赋值构造函数和赋值运算符,则必须在完整的对象上执行拷贝、移动或赋值操作,只有当派生类使用的是合成版本的拷贝、移动或赋值成员时,才会自动对基类部分执行这些操作
- [x] 在合成的拷贝控制成员中,每个基类分别使用自己的对应成员隐式的完成构造、赋值或销毁等工作
类型转换与多个基类
- 在只有一个基类的情况下,派生类的指针或引用能自动转换成一个可访问基类的指针或引用,多个基类的情况与之类似
- 基于指针类型或引用类型查找
— [x] 与只有一个基类的继承一样,对象、指针和引用的静态类型决定了我们能够使用哪些成员
多重继承下的类作用域
- 在只有一个基类的情况下,派生类的作用域嵌套在直接基类和间接基类的作用域中,查找过程沿着继承体系自底部向上进行,直到找到所需的名字,派生类的名字将隐藏基类的同名成员
- 在多重继承的情况下,相同的查找过程在所有直接基类中同时进行,如果名字在多个基类中都被找到,则对该名字的使用将具有二义性
- 当一个类拥有多个基类时,有可能出现派生类从两个或更多基类中继承了同名成员的情况,此时,不加前缀限定符直接使用该名字将引发二义性
虚继承
- 尽管在派生列表中同一个基类只能出现一次,但实际上派生类可以多次继承同一个类,派生类可以通过它的两个直接基类分别继承同一个间接基类,也可以直接继承某个基类,然后通过另一个基类再一次间接继承该类
- 在C++语言中存在虚继承的机制,虚继承的目的时令某个类做出证明,承诺愿意共享它的基类,其中,共享的基类子对象成为虚基类,在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象
- 虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身
//关键字public和virtual的顺序随意
class Raccoon : public virtual ZooAnimal{/*...*/};
class Bear : virtual public ZooAnimal{/*...*/};
- virtual说明符表明了一种愿望,即在后续的派生类当中共享虚基类的同一份实例,至于什么样的类能够作为虚基类并没有特殊规定
构造函数与虚继承
- 在虚派生中,虚基类时由最低层的派生类初始化的
- 虚继承的对象的构造方式
- [x] 含有虚基类的对象的构造顺序与一般的顺序稍有区别:首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生列表中出现的次序依次对其进行初始化
- [x] 虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关
- [x] 一个类可以有多个虚基类,这些虚基类的子对象按照它们在派生列表中出现的顺序从左向右依次构造