声明:
- 文中内容收集整理自《C++ Primer 中文版 (第5版)》,版权归原书所有。
- 学习一门程序设计语言最好的方法就是练习编程
现对于小型的软件系统,大规模编程对程序设计语言和程序员的要求更高,它们往往具有以下要求:
1:更严格的正常运转时间以及更强壮的错误检测和错误处理。
2:运用各种库进行开发。
3:能够处理更复杂的应用概念。
C++中所具有的异常处理、命名空间和多重继承可以很好的满足这些要求。
一、异常处理
1、异常处理(exception handing)机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并作出相应的处理。将问题的检测和问题的解决分离,可以使程序员更专心实现程序的主要逻辑功能。
2、异常是通过抛出(throw)异常对象而引发的。该对象的类型决定应该激活哪个处理代码。被选中的处理代码是调用链中,与该对象类型匹配且离抛出位置最近的那个。
3、不存在数组或函数类型的异常,都会转化成指针。所以所有的对象都可被抛出。
4、执行throw时,不会执行throw之后的语句,而是将控制从throw转移到匹配的catch。该catch可以是同一函数中局部的catch,也可以在直接或间接调用发生异常的另一个函数中。在处理异常时局部存储会被释放,因此被抛出的对象就不能在局部存储,而是用throw表达式初始化一个被抛出异常对象的副本。异常对象由编译器管理并且保证驻留在可能被激活的任意catch都可以访问的空间。在处理完异常后,异常对象将会被撤销。
5、throw语句类似于return,通常作为条件语句的最后一部分或者某个函数的最后(或唯一)一条语句。
6、栈展开:抛出异常时将暂停当前函数执行,开始查找匹配catch子句。首先检查throw是否在catch块内部,如果是,检查该try相关的catch子句看是否与其中一个catch对象相匹配。如果找到匹配的catch,就处理异常。如果没找到,就退出当前函数,并继续在调用函数中查找。这个过程被称为堆栈展开。栈展开期间,会释放局部对象所用的内存并运行类的析构函数。
7、如果没有找到匹配的catch子句,则程序退出,执行terminate,终止执行过程。
8、如果一个块从堆中分配资源,而在释放之前发生了异常,则从堆中分配的内存不会被释放。因此使用类管理从堆中分配的对象并在析构函数中释放是一个很好的处理方式。在发生异常后,类的析构函数肯定会被调用,用于释放资源。这就保证了资源的安全使用。
9、栈展开期间会经常执行析构函数。在执行析构函数时,已经引发了异常但还没有处理它,如果在这时候析构函数本身又抛出新的异常将会导致调用标准库的terminate函数。该函数会调用abort函数,强制整个程序非正常退出。在实际中,析构函数主要是为释放资源,所以它不太可能会抛出异常。
10、构造函数经常会抛出异常。如果在构造函数中发生了异常,则对象可能只是部分被构造,它的一些成员可能已经初始化,某些资源可能已经分配,因此要适当的撤销已构造的成员,释放已分配的资源。
11、异常与catch异常说明符的匹配规则,比匹配实参和形参类型规则更为严格。除了几种可能的转换之外,大多数转换都不允许:
1:允许从非const到const的转换。
2:允许从派生类型到基类类型的转换。(分割)
3:数组转换为指向数组类型的指针,函数转换为适当的指针。
除此之外,不允许其他转换,既不允许标准算术转换,也不允许为类类型定义的转换。(转换函数)
12、搜索catch语句时,最终找到的未必是最佳匹配,选出来的应该是第一个与异常匹配的catch语句,越是专门的catch越应该置于整个catch的最前端。
13、异常说明符的静态类型,决定catch子句可以执行的动作。如果抛出的异常对象(非引用)是派生类类型的,但由接受基类类型的catch处理,那么将发生分割,catch不能使用派生类特有的任何成员。
14、如果catch形参为引用类型,异常对象的的静态类型可以与catch对象所引用的动态类型不同。通过引用调用时才发生动态绑定,通过对象调用不发生动态绑定。
15、catch子句处理因继承而相关的类型的异常,它就应该将自己的形参定义为引用,基类的引用会处理所有派生类类型的异常。
catch子句按出现的次序匹配异常对象,因此必须对子句进行排序,以便派生类型的处理代码出现在其基类类型的catch之前。
catch可以通过重新抛出,将异常传递给函数调用链更上层的函数。重新抛出语句为:throw;
空语句代表重新抛出异常对象。它只能出现在catch或catch调用的函数中。如果在其他地方遇到空的throw语句,就调用terminate函数。
1 catch (my_error &eObj){ //引用类型 2 eObj.status = errCodes::severeErr; //修改了异常对象 3 throw; //异常对象的status成员是serverErr 4 }catch(other_error eObj){ //非引用类型 5 eObj.status = errCodes::badErr; //只修改了异常对象的局部副本 6 throw; //异常对象的status成员没有改变 7 }
16、一次性捕捉所有的异常,使用省略号作为异常声明(catch-all)。catch(...)
捕获所有异常的catch子句与任意类型的异常都匹配。
如果捕获所有异常与其他catch子句结合使用,它必须是最后一个,否则任何在它后面的catch子句都不能被匹配。
17、通过值(非指针)抛出异常,通过引用捕获异常是配合最好的组合。
18、noexpect说明指定某个函数不会抛出异常。编译器不会再编译时检查noexpect说明。
noexpect运算符是一个一元运算符,返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。noexpect不会求其运算对象的值。
函数指针及该指针所指的函数必须具有一致的异常说明。如果一个虚函数承诺了不会抛出异常,则后续派生出来的虚函数也必须做出同样的承诺。反之,如果基类虚函数允许抛出异常,则派生类对应函数既可以允许也可以不允许抛出异常。
19、异常类层次:
1:exception头文件定义了最常见的异常类,是所有异常类的基类。
2:stdexcept头文件定义了几种常见的异常类,它们为:
exception 最常见的问题。
run_time_error 运行时错误:仅在运行才能检测到问题。
range_error 运行时错误:越界。
overflow_error 运行时错误:上溢。
underflow_error 运行时错误:计算下溢。
logic_error 逻辑错误:可在运行前检测到的问题。
domain_error 逻辑错误:参数的结果值不存在。
invalid_argument 逻辑错误:不合适的参数。
length_error 逻辑错误:超出该类最大长度。
out_of_range 逻辑错误:使用一个超出有效范围的值。
3:new头文件定义了bad_alloc异常类型。提供因无法分配内存而由new抛出异常。
4:type_info头文件定义了bad_cast异常类型。转制异常。
5:what虚成员函数是在exception定义的唯一成员函数,该函数返回const*char对象,用于返回在抛出位置构造异常对象的信息。
20、异常说明指定函数是否抛出异常以及会抛出哪种异常。它跟在函数形参表之后,在关键字throw跟着一个由圆括号括住的异常类型列表:
Void func()throw (run_time_error);它指明如果该函数抛出异常,则该异常将是run_time_error对象或者其派生类对象。
21 、异常说明是函数接口的一部分,函数定义以及该函数的任意声明必须具有相同的异常说明。如果一个函数声明没有指定异常说明,则该函数可以抛出任意类型的异常。空列表指出函数不抛出任何异常。注意与throw;空语句相区别。(重新抛出异常)
二、命名空间
1、独立开发的库构成的复杂程序更有可能遇到名字冲突,因为库倾向于使用全局名字:模板名、类型名或函数名。命名冲突问题被称为:命名空间污染(namespace pollution)。
2、命名空间为防止名字冲突,提供了更加可控的机制。命名空间能够划分全局命名空间。一个命名空间是一个作用域,通过在命名空间内定义库中的名字,就可以避免全局名字固有的限制。
3、命名空间以namespace开始,后接命名空间的名字。后面是一系列由花括号括起来的声明和定义。包括类、变量、初始化操作、函数及其定义、模板和其他命名空间。
1 namespace cplusplus_primer 2 { 3 class Sales_data{*....*} 4 { 5 Sales_data operator+{const Sales_data&,const Sales_data&; 6 class Query{*....*} 7 class Query_base{*.....*} 8 }//命名空间结束后无须分号,与块相似 9
4、命名空间中每个名字都必须表示该空间内的唯一实体,不同命名空间的作用域不同所以不同命名空间内可以有相同的名字的成员。
5、命名空间可以是不连续的。可以将几个独立的接口和实现文件组成一个命名空间。可以用分离的接口文件和实现文件构成命名空间。
6、虽然可以在命名空间定义的外部定义命名空间,但只有包围成员声明的命名空间,可以包含成员的定义。
7、因为没有空间名,所以未命名空间的成员可以直接使用。如果头文件定义了未命名的名字空间,那么,在每个包含该头文件的文件中,该命名空间的名字将定义不同的局部实体。
在使用namespace定义命名空间时,也可以不指定名字。此时命名空间就是未命名的。未命名的空间局部于特定的文件,不跨越多个文本文件。同一个未命名空间的定义也可以是不连续的。
8、除了在函数或其他作用域内部,不应该在头文件中使用using声明或using指示。头文件应该只定义作为其接口的一部分的名字。
9、全局作用域中定义的名字(即在所有类、函数及命名空间之外定义的名字)也就是定义在全局命名空间中(global namespace)。
10、内层命名空间声明的名字将隐藏外层命名空间声明的同名成员。在嵌套的命名空间中定义的名字只在内层空间中有用,外层命名空间中的代码想要访问必须加上限定符。
11、内层命名空间声明的名字将隐藏外层命名空间声明的同名成员。在嵌套的命名空间中定义的名字只在内层命名空间中有效,外层命名空间中的代码想要访问它必须在名字前加限定符。
12、未命名的命名空间(unnamed namespace)指关键字namespace后紧跟花括号的一系列声明语句。未命名的命名空间定义的变量拥有静态生命周期,在第一次使用前创建,并且直到程序结束才销毁。
在给定文件内不连续,但不能跨越多个文件。
13、不能对未命名的命名空间的成员使用作用域运算符。
14、一个命名空间别名(namespace alias)可以有好几个同义词或别名。所有别名都等价。
15、一条using声明只能引入命名空间的一个成员。using指示可以出现在全局作用域、局部作用域和命名空间作用域中,但不能出现在类的作用域中。
16、using声明只是在当前作用域内创建一个别名,只在局部作用域有效。
而using指示是将命名空间成员提升到包含命名空间本身和using指示的最近作用域,令整个命名空间的所有内容有效。using指示出现在最近的外层作用域中。
1 #include<iostream> 2 #include<string.h> 3 4 namespace blip{ 5 int i = 16,j=15,k=23; 6 //其他声明 7 } 8 //int j = 1; 9 int main(){ 10 using namespace blip;//using指示,blip被添加到全局作用域 11 ++i; 12 //++j; 13 //++::j; 14 ++blip::j; 15 int k = 97; 16 ++k; 17 std::cout<<i<<std::endl; //17 18 std::cout<<j<<std::endl; //16 19 std::cout<<k<<std::endl; //98 20 std::cout<<blip::i<<std::endl; //17 21 22 return 0; 23 24 }
17、头文件如果在其底层作用域中含有using指示或using声明,则会将名字注入到所有包含了该头文件的文件中。通常,头文件应该只负责定义接口部分的名字,而不是实现部分的名字。
using指示容易引起二义性,只能在冲突名字被使用时才能被发现。在程序中对命名空间的每个成员分别使用using声明效果更好。using指示用于在命名空间本身的实现文件中就可以使用。
18、对命名空间内部名字查找遵循规则:由内向外依次查找每个外层作用域。对命名空间内部使用的名字而言,外围作用域可能是一个或多个嵌套的命名空间,最终以全包围的全局命名空间结束。只考虑已经在使用点之前声明的名字,而该使用仍在开放的块中:
1 namespace A 2 { 3 int i; 4 namespace B 5 { 6 int i; //B中隐藏了A::i 7 int j; 8 int f1() 9 { 10 int j;//j是f1的局部变量,隐藏了A::B::j 11 return i;//返回B::i 12 } 13 }//B 空间结束 14 15 int f2() 16 { 17 return j; //Error,没有定义 18 } 19 20 int j = i; //用A::i初始化 21 }
19、当类包在命名空间中的时候:首先在成员中找,然后在类(包括基类)中找,再在外围作用域中找,外围作用域中的一个或多个可以是命名空间:名字必须先声明后使用。
1 namespace A 2 { 3 int i; 4 int k; 5 class C1 6 { 7 public: 8 C1():i(0),j(0) {} //C1::i,C1::j 9 int f1() 10 { 11 return k; //A::k 12 } 13 int f2() 14 { 15 return h; //Error 16 } 17 int f3(); 18 19 private: 20 int i; 21 int j; 22 }; 23 24 int h = i; //A::i 25 } 26 27 int A::C1::f3() 28 { 29 return h; //A::h 30 }
20、当类声明一个友元时,该友元声明并没有使得友元本身可见。然而一个另外的未声明的类或函数如果第一次出现在友元声明中,则认为是最近的外层命名空间的成员。
如果类在命名空间内部定义,则没有另外声明的友元函数在同一命名空间中声明。
21、每个命名空间维持自己的作用域,因此,作为两个不同命名空间的成员的函数不能互相重载。但是,给定命名空间可以包含一组重载函数成员。
命名空间对函数匹配有两个影响。一个影响是明显的:using声明或using 指示可以将函数加到候选集合。另一个影响则微妙得多。
正如前节所见,有一个或多个类类型形参的函数的名字查找包括定义每个形参类型的命名空间。这个规则还影响怎样确定候选集合,为找候选函数而查找定义形参类(以及定义其基类)的每个命名空间,将那些命名空间中任意与被调用函数名字相同的函数加入候选集合。即使这些函数在调用点不可见,也将之加入候选集合:
1 namespace NS 2 { 3 class Item_base 4 { 5 //... 6 }; 7 8 void display(const Item_base &) 9 { 10 //... 11 } 12 } 13 14 class Bulk_item :public NS::Item_base 15 { 16 //... 17 }; 18 19 int main() 20 { 21 Bulk_item book; 22 display(book); //OK 23 }
22、如果命名空间内部的函数是重载的,那么,该函数名字的using声明声明了所有具有该名字的函数。
由using声明引入的函数,重载出现using声明的作用域中的任意其他同名函数的声明。
如果using声明在已经有同名且带相同形参表的函数的作用域中引入函数,则using声明出错,否则,using定义给定名字的另一重载实例,效果是增大候选函数集合。
23、using指示将命名空间成员提升到外围作用域。如果命名空间函数与命名空间所在的作用域中生命的函数同名,就将命名空间成员加载到重载集合中:
1 namespace libs_R_us 2 { 3 extern void print(int); 4 extern void print(double); 5 } 6 //普通声明 7 void print(const std::string &); 8 //这个using指示把名字添加到print调用的候选函数集 9 using namespace libs_R_us; 10 11 void fooBar(int val) 12 { 13 print("Value: ");//全局函数print(const string &) 14 print(val);//libs_R_us::print(int) 15 }
24、如果存在许多using指示,则来自每个命名空间的名字成为候选集合的组成部分:
1 namespace AW 2 { 3 int print(int); 4 } 5 namespace Primer 6 { 7 double print(double); 8 } 9 //using指示从不同命名空间中创建了一个重载函数集合 10 using namespace AW; 11 using namespace Primer; 12 13 long double print(long double); 14 15 int main() 16 { 17 print(1); //AW::print(int) 18 print(3.1); //Primer::print(double) 19 }
三、多重继承与虚继承
1、
2、
3、