不做大项目的话估计下面的都暂时用不到,包括下一章
大规模应用程序要求:能使用各种库进行协调开发(多人多文件编程);能在独立开发的子系统之间协同处理错误(说人话就是我用了你写的库结果报错了,我们得协调处理好出错的地方);能对比较复杂的应用概念进行建模(定义合理的类,函数以及模板).
18.1 异常处理
异常处理机制允许程序中独立开发的部分能在运行时对于出现的问题进行通信并做出相应的处理.
18.1.1抛出异常
我们可以手动抛出(throw)一条表达式来引发异常.
执行throw时,throw后面的语句将不再被执行,程序的控制器从throw转移到与之匹配的catch模块.
抛出异常后,程序将会暂停并寻找与之匹配的catch子句.
当throw出现在try语句块(大多数情况都是这样),检查与之相匹配的catch则执行对应的代码块,找不到则向外层的try中寻找(若try没有被try嵌套,则持续在外层函数寻找).若一直找不到则退出主函数然后查找终止.
即一个异常若是没有被捕获,就会终止当前程序(所以有时候我们的代码即使报错也不会终止程序就是因为有异常捕获的异常处理机制).
类对象分配的资源将由类的析构函数负责释放,因此如果我们使用类来控制资源的分配,就能确保无论函数正常结束还是遭遇异常,资源都能被正确地释放.(这段话我不是很理解,所以把书里的原话放上来了.如果感兴趣的同学可以去翻阅原书,能够理解的话可以在评论区教我).总之就是析构函数不会因为异常而不执行.所以我们必须确保在析构函数里不能抛出捕获不了的异常,如果可能会抛出异常,而需要做好对应的异常处理机制.
异常对象是一种特殊的对象,编译器通过异常抛出表达式来对异常对象进行拷贝初始化,因此抛出(throw)的表达式必须是完全类型(声明并定义),如果该表达式是一个类类型,则需要有可访问的析构函数和可访问的拷贝构造函数或是移动构造函数.抛出一个指向局部对象的指针几乎是错误的行为.
18.1.2 捕获异常
捕获异常的基本格式如下,可以有多个catch语句,并且try中也可以嵌套try和catch.(但是建议不要写那么复杂)
try{
//可能抛出异常的代码块.
}catch( 异常对象1 ){
//若是try中抛出了异常对象1则执行这里的代码.
//也可以在这里接着将异常抛出.
throw ...
}catch( 异常对象2 ){
}catch( ... ){
//若是在catch后面的括号上写着 ... 则表示捕获所有异常,并且必须在所有catch的最后
}
catch后面的括号为异常声明,形如函数形参列表.通常情况下,如果catch接收的异常与某个继承体系有关,则最好将catch接收的异常定义为引用类型.需要将越专门越精细的catch放在最后(有可能发生类型转换),将捕获所有异常的catch放在最后.
异常与catch异常声明的匹配规则将会较为严格,绝大多数的类型转换不被允许,但允许
1,从非常量向常量转换
2,派生类向基类转换(子类变父类)
3,数组被转换成指向数组的指针
18.1.3 函数try语句块与构造函数
若要处理构造函数初始值抛出的异常,需要将构造函数写成函数try语句块(函数测试块)的形式,例如:
MyClass(int age) try:{
}catch( ... ){
}
18.1.4 noexcept异常说明
C++11新标准中,可以使用关键字noexcept说明指定某个函数不会抛出异常.noexcept紧跟在参数列表后,例如:
//函数声明
void fun(int a) noexcept;
但声明归声明,大部分编译器不会检查noexcept,即使你在使用了noexcept的函数中仍然抛出了异常编译器也不会警告.
所以noexcept只在以下两种情况下使用:1,我们确定函数不会抛出异常.2,我们不知道如果处理异常.
noexcept接收一个bool的参数,若为true,则表示确定不会抛出异常,为false则表示可能还是会抛出异常.以此可以使用某个变量值来判断是否要用noexcept标记某函数.
18.1.5 异常类层次
下图为标准 exception 类层次
exception定义了拷贝构造函数,拷贝赋值运算符,虚析构函数和一个名为that的虚成员(下面会介绍虚的概念).what函数返回一个const char* ,该指针指向一个以null结尾的字符数组,以此确保不会抛出任何异常.
exception,bad_cast,bad_alloc定义了默认构造函数.而runtime_error和logic_error没有默认构造函数,但是有一个接收C风格字符串或者标准库string类型实参的构造函数,实参负责提供关于错误的更多信息.
我们可以通过继承上列异常的方式来自定义异常.例如:
class my_error: public std::runtime_error{ //继承自runtime_error
public:
explicit my_error(const std::string &s):std::runtime_error(s) { }
};
18.2 命名空间
大型程序会有多个独立开发的库,不同库中若有相同的全局名字则会发生命名空间污染,在以前,程序员会把名字起的很长一次来避免和别人重复,但这样会很麻烦(俺觉得有时候起名字比写代码还难).
可以使用命名空间来防止名字冲突,每个命名空间是一个作用域,可以避免全局名字固有的限制.
18.2.1 命名空间定义
使用关键字namespace可以定义命名空间,例如:
namespace my_namespace{
//将要放在命名空间里的声明和定义放在块内
//类,变量(包括初始化操作),函数,模板,其他命名空间(!!!)
} //这里不用分号
命名空间内可以写其他命名空间,但是命名空间不能定义在函数或类的内部.
定义在某个命名空间中的名字可以被本命名空间的其他成员直接访问(不用加作用域),其他地方使用该命名空间的名字则需要加上作用域,例如:
my_namespace:: my_fun();
std::cout<<std::endl;
命名空间可以是不连续的,上上段代码可以是定义一个新的命名空间,也可以是为已有的命名空间添加新内容.因此可以将命名空间的声明和定义分文件编写.
定义多个类型不相关的命名空间应该使用单独的文件分别表示每个类型.
C++11新标准引入了一种新的嵌套命名空间,称为内联命名空间(inline namespace),内联命名空间可以直接被外层命名空间使用,无需在内联命名空间的名字前添加该命名空间的前缀就可以直接访问.
可以通过不写命名空间名字的方式来定义未命名的命名空间,未命名的命名空间可以不连续但是不能跨文件.
俺没有很理解上面内联命名空间和未命名的命名空间两个玩意存在的必要
18.2.2 使用命名空间成员
每次使用命名空间成员的时候写上前缀会很麻烦,我们可以使用using(之前有介绍过)和起命名空间的别名的方式来简化我们的操作.
下面的例子说明我们可以自己给命名空间起别名并且可以照常使用,而且同一个命名空间可以有多个别名,而且都可以使用.
namespace s = std;
namespace t = std;
namespace d = std;
s::cout<<"hello world"<<s:endl;
t::cout<<"hello world"<<t:endl;
d::cout<<"hello world"<<d:endl;
使用using声明可以引入命名空间的一个成员,有效范围从using声明的地方开始直到using所在的所用于结束,同时外层作用域的同名实体将会被隐藏.
using std::cout;
using std::endl;
using指示可以使得指出的命名空间的成员可以直接使用.(区分一下using声明和using指示)
using namespace std;
using指示的作用域比using声明的作用域要复杂得多,using指示可以将命名空间成员提升到包含命名空间本身和using指示的最近作用域.using指示使得整个命名空间的内容变得有效.通常情况下命名空间会含有一些不能出现在局部作用域的定义,因此using指示一般被看作是出现在最近的外层作用域中.
头文件如果在顶层作用域中含有using指示或using声明,则会将名字注入到所有包含此头文件的文件中.所以应当避免using指示,而使用using声明.(在命名空间本身的实现文件中可以使用using指示)
如果我们提供了一个对std等命名空间的using指示而未做出特殊控制的话,将重新引入由于使用了多个库而造成的名字冲突问题.
18.2.3 类,命名空间与作用域
除了类内部出现的成员函数定义之外,一般都是向上查找作用域.可以从函数的限定名推断出查找名字时检查作用域的次序,限定名以相反次序指出被查找的作用域.
标准库中的move和forward很容易被名字冲突,因此建议写全称(std::move,std::forward)
18.2.4 重载与命名空间
using声明的是一个名字而不是一个特定的函数,因此using声明的函数名字的形参若是与同名函数不冲突,则会被列入重载函数中
18.3 多重继承与虚继承
多重继承是指从多个直接基类中产生派生类的能力,多重继承的派生类继承所有父类的属性,因此多个基类相互交织产生的细节可能会带来各种问题.
18.3.1 多重继承
派生类的派生列表可以包含多个基类,例如:
class Son: public: Father , public Mother{ };
在多重继承关系中派生类的对象包含有每个基类的子对象.
多重继承的派生类的构造函数初始值只能初始化它的直接基类,派生类的构造函数初始值列表将实参分别传递给每个直接基类,构造顺序与派生类列表中基类的出现顺序保持一致(以上面的为例则是先构造Father类再构造Mother类).而析构函数的调用顺序则与构造函数相反.
继承构造函数的方法如下:
class Son: public Father, public Mother{
using Father::Father; //继承Father的构造函数
using Mother::Mother; //继承Mother的构造函数
Son(int a):Father(a),Mother(a){ } //调用继承来的构造函数
}
C++11新标准中允许派生类从它的一个或多个基类中继承构造函数,但是如果从多个基类中继承了相同的构造函数则会产生错误,此时就必须为该构造函数定义自己的版本.
18.3.2 类型转换与多个基类
在只有一个基类的情况下,派生类的指针或引用能自动转换成一个可访问基类的指针或引用(多态).多个基类则可以将父类的指针指向一个派生类对象.
18.3.3 多重继承下的类作用域
多重继承的情况下,可能会查找到多个基类都有的名字,则会产生二义性(例如Father的一个成员为age,而Mother也有一个成员叫age,则Son使用age时就会产生错误).不过使用时加上作用域则在一般情况下不会有什么太大的问题(例如Father::age,Mother::age),要彻底避免这种二义性最好那就是在派生类中定义一个该名字的新版本.
18.3.4 虚继承
派生类可以多次继承到同一个类,例如下面这种情况
如果同上图这样继承的话,潜水艇会继承到两份皮划艇的成员,而我们可以通过虚继承的方式将皮划艇变成虚基类,来解决问题.
在派生列表中添加关键字virtual就可以指定虚基类了,例如:
class Son:virtual public Father{ }//此处Son类虚继承了Father类
同上例,在Son后续的派生类中,共享虚基类的同一份实例.
每个共享的虚基类只有一个唯一的共享的子对象,所以该基类的成员可以直接访问而不会产生二义性,但如果虚基类的成员被一条派生路径覆盖,则我们仍然可以直接访问被覆盖的成员,但最好为被覆盖的成员自定义一个新版本.
18.3.5 构造函数与虚继承
在虚派生中,虚基类由最底层的派生类初始化,接下来再按照派生列表中的顺序依次初始化.因此虚基类总是先于非虚基类构造.析构顺序和构造顺序相反.