xuexi|高质量程序设计指南--笔记

第1章 软件质量属性
功能性属性
1.正确性:软件按照需求正确执行任务的能力;(描述需求范围之内的行为)
2.健壮性:在异常情况下,软件能够正常运行的能力;(描述需求范围之外的行为)
容错能力:发生异常情况时系统不出错误的能力;
恢复能力:软件发生错误后(不论死活)重新运行时,能否恢复到没有发生错误前的状态的能力。(重新启动后)
容错比恢复健壮。
3.可靠性:在一定的环境下,在一定的时间段内,程序不出现故障的概率,因此是一个统计量,通常用平均无故障时间(MTTF)来衡量。
非功能性属性
1.性能:软件的“时间—空间”效率,而不仅是指软件的运行速度。
关键任务:找出限制性能的“瓶颈”,不要在无关痛痒的地方瞎忙。
2.易用性:用户使用软件的容易程度。(软件的易用性由用户评价)
3.清晰性:工作成果易读、易理解。(可理解的东西通常是简洁的)
4.安全性:信息安全,防止系统被非法入侵的能力,即属于技术问题又属于管理问题。
5.可扩展性:软件“适应”变化的能力。(变化:需求、设计的变化,算法的改进、程序的变化等)(系统设计阶段要重点考虑的质量属性)
6.兼容性:两个或两个以上的软件相互交换信息的能力。
兼容性的商业规则:弱者设法与强者兼容,否则无容身之地;强者应该避免被兼容,否则市场被瓜分。
7.可移植性:软件不经修改或稍加修改就可以运行于不同软硬件环境(CPU、OS和编译器)的能力,主要体现为代码的可移植性。(高级语言的可移植性更好)
第4章 C++/C程序设计入门
1.主函数:main(),程序入口应该,但可根据具体的语言实现环境决定是否用main()函数作为应该程序的启动函数。
2.类型转换:不会改变变量原来的类型和值,而是生成了新的临时变元,临时变元的类型为目标类型。
3.类型转换:一个低级数据类型对象总是优先转换为能够容纳得下它的最大值的、占用内存最少的高级数据类型。(如int,如果转换为long就能满足编译器的要求,则不会转换为double)
4.void类型指针:标准C语言允许任何非void类型指针与void类型指针之间进行直接的相互转换。但在C++中,可以把任何类型的指针直接指派给void类型指针,因为void类型指针是一种通用指针;但是不能反过来将void类型指针直接指派给任何非void类型指针,除非进行强制类型转换。(考虑内存扩张和截断【值截断和内存截断】)
5.强制类型转换:必须确保内存访问的安全性和转换结果的安全性。
6.标识符命名的原则:用最短的名字包含最多的信息量。(见名知义)
7.表达式:任何表达式都是有值的。
8.能够在编译时求值的程序元素是否需要分配运行时的存储空间呢?要看它是什么类型的程序。例如基本数据类型的字面常量、枚举常量、sizeof()、常量表达式等就不需要分配存储空间,因此也没有存储类型。
9.在if/else结构中,要尽量把为TRUE概率较高的条件判断置于前面,这样可以提高该段程序的性能。
10.布尔类型与零值比较:bool flag = false; if(flag) 或者if(!flag);
11.整型变量与零值比较:int value; if(0 == value) 或者 if(0 != value);
12.浮点数与零值比较:假设精度定义为:EPSILON = 1e-6;if(abs(x) <= EPSILON)【x = 0】 或者 if(abs(x) > EPSILON)【x != 0】
13.指针与零值比较:int *p; if(NULL == p) 或者 if(NULL != p);
14.switch语句存在的理由:多分支判断时,switch的效率比if/else结构高。
15.switch语句中即使不需要default,也请写上default,为了防止别人误以为你忘了default处理。
16.循环结构:如果循环体中出现continue语句,则要防止它跳过循环控制变量的修改语句。(尽量少用continue)
17.如果循环是确定的,最好使用for结构,否则使用while结构,do/while结构不常用。
确定循环还有一个不常见的用法:起到延迟作用。
18.for循环嵌套遍历:“先列后行”遍历发生的页面交换次数要比“先行后列”多,且cache命中率相对也低。这恰恰就是导致“先列后行“遍历效率降低的真正原因。
19.针对大型数组、大规模矩阵:当行数远大于列数时,”先行后列“遍历的效率高于”先列后行“遍历的效率。 优先使用”先行后列“遍历
20.如果循环体内存在逻辑判断,循环次数很大,宜将逻辑判断移到循环体的外面。
21.少用、慎用goto语句,但不是禁用。
第5章 C++/C常量
1.字面常量:也许是程序中使用得最多的常量了,例如直接出现的各种进制的数字、字符(’’括住的单个字符)或字符串(””括住的一系列字符)。实际上,只存在基本数据类型的字面常量。(只能引用,不能修改保存在符号表中,而非一般的数据区)(常量合并,如果编译器支持,请打开)
2.符号常量:用#define定义的宏常量(本质上是字面常量)和用const(标准C++:默认内连接,分配存储,可定义在头文件中)定义的常量。
3.契约性常量:void func(cons tint& num); func(n); n为契约性常量。
4.枚举常量:enum,标准C++/C规定枚举常量是可以扩展的,并不受限于一般整型数的范围。
5.应尽量使用含义直观的的符号常量来表示那些将在程序中多次出现的数字或字符串。
如:const PI = 3.14159;
6.可以把不同模块的常量集中存放在一个公用的头文件中。
7.const与#define的比较:
const常量:有数据类型,编译器会对其进行静态类型安全检查,有些IDE下可调试;
宏常量:没有数据类型,只进行字符替换,无类型安全检查,字符替换时可能会发生意料不到的错误(边际效应);不可调试。
C++程序中应尽量使用const来定义符号常量,包括字符串常量。
8.被const修饰(const常量、数据成员、成员函数、返回类型、参数)的东西都受到C++/C语言实现的静态类型安全检查机制的强制保护,可以预防意外修改。
9.不能在类声明中初始化非静态const数据成员。因为在类的对象被创建之前编译器无法知道其值。(const数据成员不是整个类中都恒定的常量,它属于每一个对象的成员,只在某个对象的生存期内是常量,对整个类来说它是可变的)
10.非静态const数据成员的初始化只能在类的构造函数的初始化列表中进行。
11.建立在整个类中都恒定的常量:用类中的枚举常量实现,或者使用static const。
枚举常量:不占用对象的存储空间,编译时被求值,定义的是一个匿名枚举类型,其缺点是不能表示浮点数和字符串。
12.什么常量最浪费空间?字符串常量,尤其是较长的字符串常量。
编译单元:当一个c或cpp文件在编译时,预处理器首先递归包含头文件,形成一个含有所有必要信息的单个源文件,这个源文件就是一个编译单元。(在标准C++中,每一个cpp文件就是一个编译单元)
如:const char* const ERR_DESP_NO_MEMERY = “ ”; 常量指针常量
1) 如果放在头文件中定义并初始化:包含了该头文件的每一个编译单元不仅会为每一个常量指针常量创建一个独立的拷贝项,而且也会为那个长长的字符串字面常量创建一个独立的拷贝项。
2)在源文件中定义并初始化:在头文件中声明所有常量指针常量,在源文件中定义并初始化它们,则每个包含该头文件的编译单元访问的不仅是常量指针常量的唯一实体,而且每个字符串字面常量也是唯一的实体。
常量合并的优化可交给编译器和连接器完成。
第6章 C++/C函数设计基础
1.需要使用某个函数时,查看现有的库中是否已有此函数,优先使用库中的函数。
2.函数调用中参数传递的本质:用实参来初始化形参而不是替换形参。
3.声明函数原型时应写出形参名称,可使函数具有“自说明”和“自编档”能力。
4.不要在函数体内定义与形参同名的局部变量,因为形参也被看作本地变量。
5.函数调用:必须通过堆栈完成。
6.函数堆栈:使用的是程序的堆栈段内存空间(静态数据区),但函数堆栈是在调用到它的时候才动态分配,即不能在编译时就为函数分配好一块静态空间作为堆栈;函数堆栈在预先分配好的内存空间上创建,比动态内存分配速度快且安全。
7.堆栈是自动管理的,即局部变量的创建和销毁、堆栈的释放都是函数自动完成,不需要程序员的干涉。
8.函数堆栈的三个用途:在进入函数前保存环境变量和返回地址、在进入函数时保存实参的拷贝、在函数体内保存局部变量。
9.函数调用规范:函数必须指定调用规范。
10.普通静态链接库(.Lib)、动态链接库(.Dll)
11.参数传递规则:值传递、址传递、引用传递(传递效果像指针传递,使用方式像值传递)。
12.如果函数没有参数,那么使用函数参数区使用void而不要空着。
13.参数命名要恰当,输入参数和输出参数的顺序要合理。(一般地,输出参数放在前面,输入参数放在后面,并且不要交叉出现输入输出参数)
14.如果是指针,且仅做输入用,则应在类型前加const,以防止该指针指向的内存单元在函数体内无意中被修改。
15.应避免函数有太多的参数,参数个数尽量控制在5个以内。如果参数太多,在使用时容易将参数类型和顺序搞错。此时,可以将这些参数封装为一个对象并采用地址传递或引用传递方式;
16.尽量不要使用类型和数目不确定的参数列表。这种风格的函数在编译时丧失了严格的静态类型安全检查。
17.从二进制角度讲,地址(指针)才是内存单元真正的引用。
18.函数返回值的两种途径:使用return语句(函数返回值可能多于一个)和使用输出参数。
19.不要省略函数返回值的类型,如果函数没有返回值,应声明为void类型。
20.函数名字与返回值类型在语义上不可冲突。
21.不要将正常值和错误标志混在一起返回。建议正常值用输出参数获得,而错误标志用return语句返回。
22.有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。
23.如果函数的返回值是一个对象,有些场合下可以用“返回引用”替换“返回对象值”,这样可以提高效率,而且还可以支持链式表达。而有些场合下只能用”返回对象值”而不能用“返回引用”,否则会出错。(注意局部变量指针对象会被销毁问题)
24.在函数体的“入口处”,对参数的有效性进行检查。
25.在函数体的“出口处”,对return语句的正确性和效率进行检查。
26.return语句不可返回指向“堆栈内存”的“指针”或者“引用”,因为该内存单元在函数体结束时被自动释放。
27.要搞清楚返回的究竟是“对象的值”、“对象的指针”,还是“对象的引用”。
28.如果函数返回值是一个对象,要考虑return语句的效率。
29.不要把return (x+y) 写成 int temp = x+y; return temp; (效率相对要低)
30.字符串常量的存放位置:程序的静态数据区。
31.函数的功能要单一,即一个函数只完成一件事情,不要设计多用途的函数。函数体的规模要小,尽量控制在50行代码之内。
32.不仅要检查输入参数的有效性,还要检查通过其他途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。
33.用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解错误情况。
34.尽量避免函数带有“记忆”功能,建议尽量少用static局部变量,除非必须。
35.任何可执行的程序最终都会变成一系列的机器指令和数据。
36.显式地声明为static的全局变量和全局函数具有static存储类型,只能被同一个编译单元内的函数调用。
37.全局常量的默认存储类型为static的,除非在定义了它的编译单元之外的其他编译单元中显式地用extern声明,否则不能被访问。
38.作用域:一个标识符能够起作用的程序范围。(程序范围:文件、函数、程序块、函数原型、类、名字空间【可跨文件】)
39.尽管语法允许,但是请不要在内层程序块中定义会遮蔽外层程序块中的同名标识符,否则会损害程序的可读性。
40.连接类型:外连接、内连接、无连接。表明了一个标识符的可见性。
外连接:一个标识符能够在其他编译单元中或者在定义它的编译单元中的其他范围内被调用。外连接的标识符需要分配运行时的存储空间
内连接:一个标识符能在定义它的编译单元中的其他范围内被调用,但是不能在其他编译单元中被调用。
无连接:仅能够在声明它的范围内被调用。
41.递归函数的实现首先必须检测基本条件:如果基本条件出现,则返回基本值;否则修改规模并调用自己进入下一轮递归。
42.
43.不要使用间接递归,即一个函数通过调用另一个函数来调用自己,因为它会损害程序的清晰性。
44.程序一般分为Debug版本和Release版本,Debug版本用于内部调试,Release版本用于发行给用户使用。
45.由于assert(expression)的宏体全部被条件编译伪指令#ifdef _DEBUG和#endif所包含,因此assert()只在Debug版本里有效。(assert是宏)
46.在函数的入口处,建议使用断言来检查参数的有效性(合法性)。
47.请给assert语句加注释,告诉人们assert语句究竟在干什么。
48.合法的程序并不等于正确的程序。断言的目的是捕捉运行时不应该发生的非法情况。
49.区分非法情况和错误情况(程序运行过程中自然存在的并且是一定要主动做出处理的)
50.
51.如果输入参数采用“指针传递”,那么加const修饰可以防止意外地改动该指针指向的内存单元,起到保护的作用。
52.ADT/UDT:抽象数据类型/用户自定义数据类型
第7章 C++/C指针、数组和字符串
1.可执行程序的组成:指令、数据、地址。
2.指针:实质是变量,指针的值就是内存单元的地址;变量是引用内存单元的值的别名指针的值就是变量的地址。
3.指针危险的原因:利用指针访问了非法的或无效的内存单元,就会导致运行时错误。
4.虽然类型名和““的组合是一种指针类型,但是编译器解释的时候,”“是和其后果的变量名结合的。
5.不管指针变量是全局的还是局部的、静态的还是非静态的,应当在声明它的同时初始化它,要么赋予它一个有效的地址,要么赋予它NULL;
6.不能对void类型指针使用”*“来取其所指向的变量。
7.指针传递:传递的是一个地址,而不是该指针指向的对象;
8.可以把一个对象的地址在整个程序中的各个函数之间传来传去,只要保证每次使用时它都指向合法的和有效的内存单元。
9.数组:同类型,在内存在连续字节存放;
10.当使用“[]”来引用数组元素的时候,编译器必须把它转换为同类型的指针表示形式,然后再进行编译;如a[3] = 100(保持了程序的清晰性); *(a + 3) = 100; || cout << a[3]; cout << (a + 3)。
11.标准C++/C不会对用户访问数组是否越界进行任何检查,无论是静态的(编译时)检查还是动态的(运行时)检查。
12.静态类型检查:在编译时实现的类型检查。
13.二维数组:以“行序优先”来存储元素。
14.可以省略第一维的长度而必须说明其他维的大小的原因:行优先,因此需要列数确定一行有多少个元素;计算元素地址时不需要第一维的长度,但需要其他维的长度;不能对数组进行越界访问检查;
15.数组传递:默认是地址传递。
16.(删除数组)delete[]的语义:告知编译器要释放的是一个数组,“请”编译器去取指针指向的数组的大小信息(数组的元素个数,已被编译器保存至某个位置),再按照这个大小来释放动态内存。
17.字符数组:元素为字符变量的数组;
18.字符串:以’\0’为结束字符的字符数组;字符数组并不一定是字符串;
19.如果用一个字符串字面常量来初始化一个字符数组,数组的长度至少要比字符串字面常量的长度大一,因为还要保存结束符’\0’。
20.当用字符指针来引用一个字符变量的时候,千万要小心,因为C++/C默认char
表示字符串;如char ch = ‘a’; char *pChar = &ch; cout<< *pChar << endl;
21.字符串的拷贝请使用库函数strcpy或strncpy,不要企图用”=“对字符串进行拷贝,因为那是字符指针的赋值。
22.函数地址:编译时常量;函数名就是函数的首地址;
23.函数连接:把函数地址绑定到函数的调用语句上;
24.静态连接:编译时完成连接;动态连接:运行时连接(绑定);
25.引用:创建时必须初始化;
不存在NULL引用,引用必须与合法的存储单元关联;
不要用字面常量来初始化引用;
初始化为指向一个对象后,不能再改变为对另一个对象的引用;
引用的创建和销毁不会调用类的构造函数和析构函数;
主要用途:修饰函数的形参和返回值。引用既具有指针的效率,又具有变量使用的方便性和直观性。
引用体现了最小特权原则:给予程序元素足以完成其功能的最小权限。
26.指针:创建时不必初始化;指针可以是NULL;可随时改变指向;
第8章 C++/C高级数据类型
1.在C++语言中,如果不特别指明,struct成员的默认访问限定符为public,而class成员的默认访问限定符为private。
2.为了不使程序产生混乱和妨碍理解,建议还使用struct定义简单的数据集合;而定义一些具有行为的ADT时最好采用class,如果采用struct似乎感觉不到面向对象的味道了。
3.一个对象不能自包含,无论是直接的还是间接的,因为编译器无法为它计算sizeof值,也就不知道该给这样的对象分配多个存储空间。
4.在设计位域的时候,最好不要让一个位域成员跨越一个不完整的字节来存放,因为这样会增加计算机运算的开销。
5.请使用sizeof()运算符来计算位域的大小而不要自己估算,因为你很容易估算错误。
6.使用位域节省存储空间会导致程序运行速度的下降,因为计算机无法直接寻址到单个字节中的某些位,必须通过额外的代码来实现。
7.自然对齐要求最严格;
8.按照从大到小的顺序从前到后依次声明每一个数据成员,并且尽量使用较小的成员对齐方式。
9.类的数据成员类型的选择、声明顺序即排列、采用的成员对齐方式都将影响对象的实际大小和访问效率。
10.联合:union:不同类型数据成员之共享存储空间,同时可实现不同类型数据成员之间的自动类型转换。联合对象同一时间只能存储一个成员的值(即只有一个数据是活跃的)
11.联合的内存大小取决于其中字节数最多的成员,而不是累加,联合也会进行字节对齐;不能包含虚拟成员函数和静态数据成员,不能作为其他类型的基类或者派生自其他类型。
12.枚举:定义特定用途的一组符号常量,它表明这种类型的变量可以取值的范围。如果不指定其中标识符的值,则第一个标识符的值将为0,后面依次加1;如果指定了某个标识符的值,则该标识符后面的标识符依次加1,除非同时指定了它们的值;
13.使用匿名枚举来定义程序中出现的相关常量集合是一个好主意,它可以取代宏常量const符号常量。
14.不存在一种固定的“文件记录”,任何有意义的文件格式都是由具体的应用领域决定的。(如ASCII字符和二进制字符)
15.在C++/C中,文件操作是通过和“流”(字节流)这种对象关联而进行的;操作系统维护一个保存当前系统中所有打开文件FCB(文件控制块)数组,并利用每一个FCB来管理对每一个文件的操作。
16.使用C++的I/O操作方式,因为它是类型安全的。
第9章 C++/C编译预处理
1.C++/C的编译预处理器对预编译伪指令进行处理后生成中间文件作为编译器的输入,因此所有的预编译指令都不会进入编译阶段。
2.文件包含:#include ””或#include <>:用于包含一个头文件,头文件中存放在一般是模块接口,编译预处理器在扫描到该伪指令后就用对应的文件内容替换它。
#include <>:开发环境提供的库头文件,指示预编译处理器在开发环境设定的搜索路径中查找所需的头文件;
#include ””:包含自己编写的头文件;指示预处理器首先在当前工作目录下搜索头文件,如果找不到,则再到开发环境设定的路径中去查找。
可加绝对路径或相对路径。
3.内部包含卫哨:防止同一个编译单元包含同一个头文件超过一次;(#inndef #define #endif)
4.外部包含卫哨:可显著提高编译速度,因为当一个头文件被源文件反复包含多次时(递归的#include指令展开后导致),可以避免多次查找和打开头文件的操作。
5.建议外部包含卫哨和内部包含卫哨使用同一个标志宏,可少定义一个宏;
6.可仅在头文件中包含其他文件时使用外部包含卫哨,源文件中可不使用,基本不会影响编译速度。
7.头文件包含顺序:
头文件中包含其他文件:包含当前工程中所需要的自定义头文件(顺序自定)包含第三方程序库的头文件包含标准头文件。
源文件中包含其他文件:包含该源文件对应的头文件(如果存在)包含当前工程中所需要的自定义头文件包含第三方库的头文件包含标准头文件。
8.宏定义:#define,在其定义位置后可使用该宏;
9.宏定义不是C++/C语句,因此不需要使用语句结束符“; ”,否则它也被看做的一部分;
10.任何宏的编译预处理阶段都只是进行简单的文本替换,不做类型检查和语法检查,这个工作留给编译器在编译器进行。参数替换发生在宏扩展之前。
11.宏定义可嵌套。
12.带参数的宏定义不是函数,因此没有函数调用开销,但是其每一次扩展都会生成重复的代码,结果使可执行代码的体积增大。
13.inline函数不可能完全取代宏,各有各的好处。
14.取消宏定义:#undef 宏名。
15.如果需要公布某个宏,那么该宏定义应当放置在头文件中,否则旋转在实现文件(.cpp)的顶部。
16.定义新类型名:使用typedef,而不要使用宏;
17.尽量使用const取代宏来定义符号常量。
18.较长的使用频率较高的重复代码段,建议使用函数或模板;
较短的重复代码段,可使用带参数的宏定义,这不仅是出于类型安全的考虑,也是优化和折衷的体现。
19.条件编译:可以控制预处理器选择不同的代码段作为编译器的输入,从而使源程序在不同的编译条件下产生不同的目标代码,类似于程序控制结构中的选择结构。(在编译预处理阶段【正式编译之前】起作用,不生成运行时代码,但后者在运行时起作用)
20.条件编译的作用:为程序的移植和调试带来了极大的方便,可以用它来暂时或永久地阻止一段代码的编译。
21.条件编译伪指令:#if、#ifdef、#ifndef、#elif、#else、#endif、defined;每一个条件编译块都必须以#if开始,以#endif结束,且要配对;defined必须结合#if或#elif使用,不能单独使用。
第10章 C++/C文件结构和程序版式
1.程序版式不会影响程序的性能,但会影响程序的清晰性。
2.程序开发目录结构:工程文件Include\、Source\、Shared\、Resource\、Debug\、Release\、Bin\、IDE生成的文件、Readme、其他临时文件、数据文件、配置文件、DLL;
1)Include\:存放应用程序的头文件(.h),还可以再细分子目录;
2)Source\:存放应用程序的源文件(.c或.cpp),还可以再细分子目录;
3)Shared\:存放一些共享的文件;
4)Resource\:存放应用程序所用的各种资源文件,如图片、音频、视频等,可再细分子目录;
5)Debug\:存放应用程序调试版本生成的中间文件;
6)Release\:存放应用程序发行版本生成的中间文件;
7)Bin\:存放程序员自己创建的lib文件和dll文件;
3.区分编译时相对路径(#include “…\include\abc.h”)和运行时相对路径(OpenFile(“…\abc.ini”))。
4.头文件:保存程序的声明,(.h);源文件(定义文件):保存程序的实现,(.c/.cpp/.cc/.cxx)。
5.当源文件或头文件通过#include指令包含另一个头文件时,编译预处理器用头文件的内容取代#include伪指令,即头文件的所有内容最终都会被合并到某一个或某几个源文件中,如此将每一个包含的头文件递归地展开后形成的源文件就叫编译单元。
6.头文件的作用:通过头文件来调用库功能(只提供访问接口、隐藏实现的库);头文件能加强类型安全检查;头文件可以提高程序的可读性(清晰性)。
7.内联函数:定义应放在头文件中;内联调用语句最终会被展开,不会采用真正的函数调用机制。
8.头文件的顺序结构可参考以下:
1)头文件注释(包括文件说明、功能描述、版权声明等)【必须有】;
2)内部包含卫哨(#ifndef XXX/ #define XXX)【必须有】;
3)#include其他头文件【如果需要】;
4)外部变量和全局函数声明【如果需要】;
5)常量和宏定义【如果需要】;
6)类型前置声明和定义【如果需要】;
7)全局函数原型和内联函数的定义【如果需要】;
8)内部包含卫哨:#endif //XXX【必须有】;
9)文件版本及修订说明;
9.源程序的结构:(主要保存函数的实现和类的实现)
1)源文件注释(包括文件说明、功能描述、版权声明等)【必须有】;
2)预处理指令【如果需要】;
3)常量和宏定义【如果需要】;
4)外部变量声明和全局变量定义及初始化【如果需要】;
5)成员函数和全局函数的定义【如果需要】;
6)文件修改记录。
10.程序中使用空行的地方:

12.关于使用空格的建议:
13.代码行最大长度宜控制在70至80个字符以内。
14.长表达式要在低优先级运算符处拆分为多行,运算符放在新行之首(以示突出)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。
15.对齐与缩进:程序的分界符“{”和“}”应独占一行并且位于同一列,同时与引用它们的语句左对齐。{}之内的代码块在”{”右边数格处左对齐,建议采用一个“\t”字符或4个空格。各层嵌套请使用统一的缩进。在另外一些情况下,为了不使语句块和{}看起来有被架空的感觉,可将{}和其内部的语句块左对齐,并且都向右缩进4格。
16.建议将修饰符 * 和 & 紧靠变量名。或者使用typedef做一个类型映射。
17.注释通常用于:版本、版权声明;函数接口说明;重要的代码行或段落提示。
18.注释是对代码的“提示”,而不是文档。程序中的注释不可喧宾夺主,注释太多会让人眼花缭乱。注释的花样要少,不要用注释拼图案。
19.如果代码本来就是清楚的,则不必加注释。否则多此一举,令人厌烦。
20.边写代码边写注释,修改代码的同时修改相应的注释,以保证注释与代码的一致性。
21.不再有用的注释要删除掉。注释应当准确、易懂,防止出现二义性。错误的注释不但无益而且有害。
22.注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。
23.比较复杂的函数的函数头注释示例:
24.ADT/UDT版式:
1)以数据为中心:将private限定的成员写在前面,而将public限定的成员写在后面,重点关注其内部结构;
2)以行为为中心:将public限定的成员写在前面,而将private限定的成员写在后面,重点关注其提供的接口(服务)。
建议采用”以行为为中心”的方式来编写类,即首先考虑类应该提供什么样的接口(即函数)。(用户最关心的是接口)
第11章 C++/C应用程序命名规则
1.标识符的命名应当直观且可以拼读,可望文生义,不必进行“解码”。
2.标识符的长度应当符合“min-length & max-information”【最少的长度,最多的信息】原则。
3.程序中不要出现仅靠大小写来区分的相似标识符,虽然C++/C是大小写相关的。
4.不要使程序中出现局部变量和全局变量同名的现象,尽管由于两者的作用域不同而不会发生语法错误,但会使人误解。
5.变量的名字应当使用“名词”或者“形容词+名词”的格式来命名。
6.全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身。
7.用正确的反义词组命名具有相反意义的变量或相反动作的函数等。
8.尽量避免名字中出现数字编号,如value1、value2等,除非逻辑上的确需要如此。
9.Windows应用程序命名:
1)类型名和函数均以大写字母开头的单词组合而成;
2)变量名和参数名采用第一个单词首字母小写而后面的单词首字母大写的单词组合;
3)符号常量和宏名用全大写的单词组合而成,并在单词之间用单下划线分隔,注意首尾最好不要使用下划线;
4)给静态变量加前缀s_(表示static);
5)如果不得已需要全局变量,这时全局变量加前缀g_(表示global);
6)类的数据成员加前缀m_(表示member),这样可以避免数据成员与成员函数的参数同名;
7)为了防止某一软件库中的一些标识符和其他软件库中的冲突,可以统一为各种标识符加上能反映软件性质的前缀。
第12章 C++面向对象程序设计方法概述
1.UML:OOAD(面向对象分析与设计)建模语言的国际标准。
2.面向对象思想:把整个世界看做是由具有行为的各种对象组成的,任何对象都具有某种特征和行为。
3.OOAD把一个对象的特征称为属性,而把行为称为服务或方法。
4.对象的对外表现:能够提供一定的服务,当我们向一个对象传递参数并调用对应的函数时,就是在请求其提供服务。对象间通过它们能够提供的服务来交流,进而合作完成特定的任务。
5.对象软件模块;整个软件就是由各种各样的运行时对象构成的一个系统。
6.信息隐藏C++类的封装:类仅仅公开必须让外界知道的内容,而隐藏其他一切内容。
7.如果要让类对象不能拷贝和赋值,将类的默认拷贝函数和赋值函数声明为私有。
8.对象:类的一个实例,一个类可能有多个实例。如果将对象比做一个个房子,那么类就是房子的设计图纸。
9.继承特性:可以提高程序的可复用性。
10.如果类A和类B毫不相关,不可以为了使B的功能更多而让B继承A的功能与属性。
11.若在逻辑上B是A的”一种“(is-a-kind-of),则允许B继承A的功能和属性。
12.继承的概念在程序世界和现实世界中并不是完全相同的。
13.严格的继承规则:若在逻辑上B是A的“一种“,并且A的所有功能和属性对B而言都有意义,则允许B继承A的功能和属性。
14.继承:表示类的“一般与特殊”关系;
15.类的组合:一种类的复用技术,用于表示类的“整体与部分”关系;
16.若在逻辑上A是B的“一部分”(is-a-part-of),则不允许B从A派生,而是要用A和其他部分组合出B。
17.类的组合特性的表现形式:聚合(has-a)和关联(holds-a)【类之间的引用】。
18.静态特性:编译时确定;动态特性:运行时确定【C++虚函数、抽象基类、动态绑定、多态】。
19.虚函数:建议在基类和派生类中都声明为虚函数。【virtual】
20.抽象类:不能实例化出对象的类;唯一目的:让其派生类继承并实现它的接口方法。【抽象基类】
21.具体类(实现类):能够被实例化为对象的类;
22.纯虚函数:函数在声明时被初始化为0;如virtual void Draw(void) = 0;
函数名=函数的地址,函数地址=0不要为该函数编址,从而阻止该类的实例化行为;C++中只有虚函数可被初始化为0。
23.抽象基类的用途:接口与实现分离,隐藏数据成员和实现,只留一些接口给外部调用。抽象基类把数据和实现都隐藏在实现类中,而在抽象基类中提供丰富的接口函数【public的纯虚函数】供调用。
24.抽象基类的入口函数:最好是静态成员函数。
25.动态绑定(运行时绑定、晚绑定):如果将基类Shape的函数Draw()妇女节肉virtual的,然后用指向派生类对象的基类指针或引用来调用Draw(),那么程序会在运行时选择该派生类的Draw()函数而不是Shape::Draw()。
26.动态绑定可以使独立软件供应商(ISV)在不透露技术秘密的情况下发行软件包,即只发行头文件和二进制文件,不必公开源代码(实现代码)。【用户利用继承机制使用发行的软件包】
27.多态类:拥有虚函数编译器为多态类至少创建一个虚函数表(vtable)【存放该类所有的虚函数的地址及该类的类型信息】。vptr指向类的vtable,类型信息保存在vtable中的固定位置。
28.虚函数的动态绑定:采用了运行时函数寻址技术。
29.虚函数的参数类型安全检查:不在运行时进行;解决方法:派生类定义中的名字(对象或函数名)将义无反顾地遮蔽(即隐藏)掉基类中任何同名的对象或函数。【override】
同函数名、同参数(个数、类型、顺序),返回值可不同。
30.运行时多态:多个派生类继承了一个基类,每个派生类的对象都可被当成基类的对象来使用,这些派生类对象能对同一函数调用做出不同的反应。
31.C++支持多态的方法汇总:
1)隐含的转型操作:令一个public多态基类或者引用指向它的一个派生类的对象;
2)通过指针或引用的调用基类的虚函数,包括通过指针的反引用调用虚函数;
3)使用dynamic_cast<>和typeid运算符;
32.通过基类指针删除一个由派生类对象组成的数组,结果未定义(C++标准)。
33.多态和指针算术运算不能混合运用,而数组操作几乎总是会涉及到指针运算,因此多态和数组不应该混合运用。
34.不要在数组中直接存放多态对象,而是换之以基类指针或者基类的智能指针。
35.如果确实需要使用多态数组,请使用STL容器配合普通指针或者智能指针。
36.对象的内存映象:成员变量用户内存区【每个对象拥有独立的一份】;静态成员变量、多态类的vtable程序静态数据区【类内所有对象共享,只有一份,使同类对象具有一致的行为】;成员函数、构造函数、析构函数代码段【类内所有对象共享,只有一份】;
37.构成对象本身的只有数据,任何成员函数都不隶属于任何一个对象,非静态成员函数与对象的关系就是绑定,绑定的中介就是this指针。
38.虚函数访问需要经过vptr的间接寻址,增加了一层间接性,因此带来了一些额外的运行时开销。
39.vtable:函数指针数组;管理虚函数的地址多态,指明实际应调用的函数;vtable中虚函数指针的排列顺序:
1)一个虚函数如果在当前class中是首次出现(未在基类出现过),则将其地址插入到该class的每一个vtable的尾部;
2)如果派生类改写了基类的虚函数,则这个函数的地址在派生类vtable中的位置与它在其基类vtable中的位置一致,而与它在派生类中声明的位置无关;
3)派生类未改写基类虚函数被继承下来并插入派生类vtable中,派生类与基类中其地址在vtable中的位置相同;
4)派生类的vtable布局应该兼容其基类的vtable;
40.每个C++类都有一个vtable,每个类对象都有一个vtable的vptr。【前提:类表现出多态特性】
41.C++的标准规格说明:编译器必须要保证虚函数表的指针存在于对象实例中最前面的位置【为了正确取得虚函数的偏移量】。
42.隐含成员:若干vptr、默认构造函数、默认拷贝构造函数、析构函数、默认拷贝赋值函数。
43.编译器认为,同一个函数只存在一个实现(函数代码),不管是全局函数还是成员函数。
44.Name-Manging技术:规范编译器和链接器之间用于通信的符号表表示方法的协议,其目的在于按照程序的语言规范,使符号表具备足够多的语义信息以保证链接过程准确无误的进行。
45.编译器会对编写的函数进行Name-Mangling处理。
46.类的静态成员不依赖于对象的存在而存在,类的静态成员属于类;静态成员无this指针。静态成员的访问规则:类名::静态成员;或者:对象.静态成员。
第13章 对象的初始化、拷贝和析构
1.一个类只有一个析构函数,但可以有多个构造函数。
2.任意类,如果未显示声明其构造函数和析构函数,则自动产生4个public inline的默认函数:默认构造函数、默认拷贝构造函数、析构函数、默认赋值函数。
3.构造函数:对象的初始化;析构函数:对象的清除。【构造/析构函数与类同名】
4.构造/析构函数:无返回值;不要在构造函数内做与初始化对象无关的工作,不要在析构函数内做与销毁一个对象无关的工作。
5.对象创建的标志:为一个对象分配好原始内存空间。创建一个变量或动态对象时一定不要忘记初始化。
6.初始化和赋值:
1)初始化:在对象创建的同时使用初值直接填充对象的内存单元,因此不会有数据类型转换等中间过程,也就不会产生临时对象;
2)赋值:在对象创建好后任何时候都可以调用的而且可以多次调用的函数,由于它调用的是“=”运算符,因此可能需要进行类型转换,即会产生临时对象。
7.构造函数:对象创建时自动调用,且仅调用一次;其作用是:当对象的内存分配好后把它从原始状态弯为良好的可用的状态。
8.最好为每个类显式地定义构造函数和析构函数,即使它们暂时空着,尤其是当类含有指针成员或引用成员的时候。
9.构造函数的成员初始化列表:实现真正的初始化;发生在函数体内的任何代码被执行之前。
10.如果类存在继承关系,派生类可以直接在其初始化列表里调用基类的特定构造函数以向它传递参数。
11.类的非静态const数据成员和引用成员只能在初始化列表里初始化,因为它们只存在初始化语义,而不存在赋值语义。
12.类的数据成员的初始化可以采用初始化列表或函数体内赋值两种方式,这两种方式的效率不完全相同。
13.成员初始化列表中数据成员的初始化顺序:按照它们在类中声明的次序来初始化,因此,最好是按照它们的声明顺序来书写成员初始化列表:
1)调用基类的构造函数,向它们传递参数;
2)初始化本类的数据成员(包括成员对象的初始化);
3)在函数体内完成其他的初始化工作;
上述初始化顺序需要注意成员初始化存在的依赖关系。
14.对象的构造次序:任何一个对象总是首先构造最根类的子对象,然后逐层向下扩展,直到把整个对象构造起来;
15.对象的析构次序:析构会严格按照与对象构造相反的次序执行,该次序是唯一的,否则编译器无法自动执行析构过程。
16.构造函数和析构函数的调用时机:
17.不能同时定义一个无参数的构造函数和一个参数全部有默认值的构造函数,否则会造成二义性。
18.拷贝构造函数:第一个参数为本类对象的引用、const引用、volatile引用或const volatile引用,并且没有其他参数,或者其他参数都有默认值。【其参数必须是同类对象的引用】
19.如果一个类中显式声明了带参数的构造函数,则该类中无默认构造函数。
20.重载的构造函数行为可能都差不多,对于可能存在的重复代码段,可提供一个非public的成员函数,以供在构造函数中适应的地方调用它。
21.如果不主动编写拷贝构造函数和拷贝赋值函数,编译器将以“按成员拷贝”的方式自动生成相应的默认函数。如果类中含有指针成员或引用成员,则这两个默认函数就可能隐含错误。
22.拷贝构造函数:在对象被创建并用另一个已经存在的对象来初始化它时调用。
23.赋值函数:只能把一个对象赋值给另一个已经存在的对象,使得那个已经存在的对象具有和源对象相同的状态。
24.函数strlen返回的是有效字符串长度,不把结束符“\0”计算在内。函数strcpy会连”\0”也一起拷贝。
25.将拷贝构造函数和拷贝赋值函数声明为private,并且不实现它们,但是不能像虚函数那样置为0(它只能用来初始化纯虚函数)可以避免其他人使用编译器自动生成的默认函数(阻止编译器自动生成相应的默认函数)
26.将类的构造函数和赋值函数声明为private,可以阻止该类被实例化。
27.派生类的构造函数应在其初始化列表里显式地调用基类的构造函数(除非基类的构造函数不可用)。
28.如果基类是多态类,那么必须把基类的析构函数定义为虚函数,这样就可以像其他虚函数一样实现动态绑定;否则可能造成内存泄漏。
29.在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值,这可通过调用基类的赋值函数来实现。
第14章 C++函数的高级特性
1.重载、内联:可用于全局函数和类的成员函数;
2.const、virtual:用于类的成员函数;
3.C++采用重载机制的理由:
1)便于记忆,提高函数的易用性;
2)类的构造函数需要重载机制,C++规定构造函数必须与类同名,因此只能有一名字;(类可以有多个同名的构造函数)
3)使各种运算符能支持对象语义;(让同一个运算符同时能支持对不同类型对象的操作)
4.最终的二进制可执行程序中不允许有同名函数出现,因此所有的函数最终都将转换成等效的全局函数,调用语句也会转换。
5.只能靠参数列表而不能仅靠返回值类型的不同来区分重载函数。编译器根据参数列表为每个重载函数产生不同的内部标识符。
6.Name-Mangling技术(名称修饰或者重命名机制):C++标准没有规定一种统一的重命名方案,因此不同的编译器对重载函数可能产生不同风格的内部标识符,这就是不同厂商的C++编译器和连接器不能兼容的一个主要原因。
7.C++中引用C文件:extern “C”处理。(连接规范)
8.并不是两个函数的名字相同就能构成重载。全局函数和类的成员函数同名不算重载,因为它们的作用域不同。
9.当心隐式类型转换导致重载函数产生二义性。
10.成员函数的重载:
1)具有相同的作用域(即同一个类定义中);
2)函数名字相同;
3)参数类型、顺序或数目不同(包括const参数和非const参数);
4)virtual关键字可有可无;
11.成员函数的覆盖(override,也叫改写):
1)不同的作用域(分别位于派生类和基类中);
2)函数名称相同;
3)参数列表完全相同;
4)基类函数必须是虚函数;
12.成员函数的覆盖:virtual关键字告诉编译器,派生类中相同的成员函数应该放到vtable中,并替换基类相应成员函数的槽位;
13.虚函数覆盖的两种方式:完全重写和扩展。扩展是指派生类虚函数首先调用基类的虚函数,然后再增加新的功能。
14.成员函数的隐藏:派生类的成员函数遮蔽了与其同名的基类成员函数;(跨越类边界的重载,覆盖是一种特殊的隐藏)
1)派生类的函数与基类的函数同名,但是参数列表有差异。无论有无virtual关键字,基类的函数在派生类中将被隐藏;
2)派生类的函数与基类同名,参数列表也相同,但是基类函数没有virtual关键字,此时,基类的函数在派生类中将被隐藏;(与覆盖区别)
15.如果确实想使用所谓的“跨越类边界的重载”,可以在派生类定义中的任何地方显式地使用using关键字。
16.参数的默认值:编译时,默认值由编译器自动插入。
17.参数默认值的使用规则:把参数默认值放在函数的声明中,而不要放在定义体中。
18.如果函数有多个参数,参数只能从后向前依次默认,否则将导致函数调用语句怪模怪样。
19.运算符重载:关键字operator加上运算符来表示函数;它是一种特殊形式的函数,运算符本身就是函数名。
20.运算符与普通函数在调用时的区别:对于普通函数,实参出现在圆括号内;而对于运算符,实参出现在其两侧(或一侧)。
21.如果运算符被重载为全局函数,那么只有一个参数的运算符叫做一元运算符,有两个参数的运算符叫做二元运算符。
22.如果运算符被重载为类的成员函数,那么一元运算符没有参数(++和—的后置版本除外),二元运算符只有一个右侧参数,因为对象自己成了左侧参数。
23.运算符重载规则:
24.如果重载为成员函数,则this对象发起对它的调用。
25.如果重载为全局函数,则第一个参数发起对它的调用。
26.禁止用户发明该语言运算符集合中不存在的运算符。
27.除了函数调用运算符“()”外,其他运算符重载函数不能有默认参数值。
28.不要试图改变重载运算符的语义,要与其内置语义保持一致。
29.某些运算符之间可推导,如逻辑运算符和关系运算符,从而可以只实现少数几个运算符,然后再用它们来实现其他运算符。
30.不能重载的运算符:.//:??:/sizeof()/typeid()/static_cast<>/dynamic_cast<>/const_cast<>/reinterpret_cast<>/#/##。
31.++/–应用于用户定义类型,尤其是大对象对,前置版本比后置版本的效率高很多。后置版本总是要创建一个临时对象,退出时要销毁。
32.函数内联:为了提高函数的执行效率(速度);用函数内联替代宏。
33.函数内联:调试(Debug)版本未真正内联,类似于普通函数;在发行(Release)版本里编译器才会实施真正的内联。
34.符号表:编译器用来收集和保存字面常量和某些符号常量的地方。
35.C++的函数内联机制既具备宏代码的效率,以增加了安全性,而且可以自由操作类的数据成员。
36.函数被内联后,编译器可以通过上下文相关的优化技术对结果代码执行更深入的优化;但普通函数体内无法单独进行,因为一旦进入函数体内它就脱离了调用环境的上下文。
37.关键字inline必须与函数定义体放在一起才能使函数真正内联,仅把inline放在函数声明的前面不起任何作用。
38.inline是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。
39.高质量C++/C程序设计风格的一个基本原则:声明与定义不可混为一谈。
40.定义在类声明之中的成员函数将自动地成为内联函数,但编译器是否将它真正内联则要看函数如何定义。
41.内联以代码膨胀(拷贝)为代码,仅仅省去了函数调用(参数压栈、跳转、退栈和返回等操作)的开销,从而提高程序的执行效率。
42.不宜使用内联函数的情况:
1)函数体内的代码过长;
2)函数体内出现循环或者其他复杂的控制结构。
43.编译器往往选择那些短小而简单的函数来内联(内联候选函数)。
44.类型转换函数:类型转换的本质是创建新的目标对象,并以源对象的值来初始化,所以源对象没有丝毫改变。不要把类型转换理解为“将源对象的类型转换为目标类型”。
45.explicit:要求用户必须显式地调用构造函数来初始化对象。
46.类型转换运算符定义以operator关键字开始,紧接着目标类型名和()。它没有参数,实际上this就是参数;也没有返回类型,实际上函数名就是返回类型。类型转换运算符只能定义为非静态成员函数。
47.static_cast(src_obj):相当于C风格的的强制转换,多重继承下,会正确地调整指针的值,但C风格的强制转换不会。在编译时进行的转换(静态)。
48. const_cast(src_obj):用于去除一个对象的const/volatile属性。
49.reinterpret_cast(src_obj):可以把一个整数转换成一个地址,或者在任何两种类型的指针之间转换。使用该运算符的结果很危险,请不要轻易使用。
50.dynamic_cast(src_obj):在运行时动态转换。
51.在C++程序中尽量不要再使用C风格的类型转换,除非源对象和目标类型都是基本类型的对象或指针,否则不安全。
52.const成员函数:任何不会修改数据成员的成员函数都应该声明为const类型。不能修改成员变量的值,不能调用非const成员函数。
53.const成员函数:const关键字只能放在函数声明的尾部。
54.不要混淆const成员函数和成员函数返回const类型。
55.static成员函数不能定义为const的。static函数只是全局函数的一个形式上的封装,而全局函数不存在const一说。(static成员不能访问类的非静态成员【没有this指针】)
56.const成员函数的访问规则:const对象可以访问const成员函数,但不能访问非const成员函数;非const对象可以访问const成员函数和非const成员函数。
第15章 C++异常处理和RTTI
1.健壮性:软件在异常环境下仍然能够正确运行的能力。
2.预防运行时错误:在错误即将发生前通过检测触发它的条件来阻止它。
3.C++保证:如果一个异常在抛出点没有得到处理,那么它将一直被抛向上层调用者,直至main函数,直到找到一个类型匹配的异常处理器,否则调用terminate结束程序。
4.异常处理机制实际上是一种运行时通知机制。
5.异常处理机制的本质:在真正导致错误的语句即将执行之前,并且异常发生的条件已经具备时,使用我们自定义的软件异常(异常对象)来代替它,从而阻止它。因此,当抛出异常时,真正的错误实际上并未发生。
6.底层错误传递至上层,使上层可人性化的反映出错误类型:抛出异常对象。
7.异常类型:任意类型。异常仅仅通过类型而不是通过值来匹配。
8.通常情况下,使用自定义一些异常类来具体描述我们需要的异常类型。
9.异常处理的语法结构:抛出异常(throw)、提炼异常(try{})、捕获异常(catch{})、异常对象本身。
1)throw:一条语句,一条throw语句只能抛出一个异常,throw语句在形式上像函数调用语句,在行为上更像一个goto语句;
2)try{}:引导一个程序块,包含可能会有异常抛出的代码段;
3)catch{}:引导一个程序块,包含用户定义的异常处理代码,即异常处理器(handler),一个catch子句只能捕获一种异常,catch子句相当于带有一个参数的函数;
10.异常抛出点常常和异常捕获点距离很远,异常抛出点可能深埋在底层软件模块内,而异常捕获点常常在高层组件中;异常捕获却必须和异常提炼(try块)综合使用,并且可以通过异常组合在一个地点捕获多种异常。
11.异常抛出点位于try块内的有两种情况:
1)throw语句位于当前try块内;
2)含有异常抛出点的函数被上层函数调用,而上层函数调用者位于try块内;
12.当可能抛出异常的代码没有位于try块内,或者即使位于try块内但是当前没有类型匹配catch块,则该异常将继续向上层调用者抛出,并且当前函数从异常抛出点退出,于是此后的语句都不会被执行。
13.在一个函数内尽量不要出现多个并列的try块,也不要使用嵌套的try块,否则不仅会导致程序结构复杂化,增加运行时的开销,而且容易出现逻辑错误。
14.每一个try块后面必须至少跟一个catch块。当抛出异常时,C++异常处理机制将从碰到的第一个catch块开始匹配,直到找到一个类型符合的catch块为止,紧接着执行该catch块内的代码。当异常处理完毕后,将跳过后面的一系列catch块,接着执行后面的正常代码。
15.对于每一个被抛出的异常,你总能找到一个对应的throw语句,只是有些是位于我们的程序之中,而另一些则是位于标准库中,这就是我们常常能catch到一个异常却看不到它在哪里被throw的原因。
16.由于异常处理机制采用类型匹配而不是值判断,因此catch块的参数可以没有参数名称,只需要参数类型,除非确实要使用那个异常对象。
17.虽然异常对象看上去像局部对象,但是它并非创建在函数堆栈上,而是创建在专用的异常堆栈上,因此它才可以跨接多个函数而传递至上层,否则在堆栈清退的过程中就会被销毁。
18.不要企图把局部对象的地址作为异常对象抛出,因为局部对象会在异常抛出后函数堆栈清退的过程中被销毁。
19.C++异常处理机制必须保存每一个throw语句抛出的异常对象的类型信息和每一个catch子句的参数类型信息,其目的就是在运行时执行异常对象与异常处理器的类型匹配。
20.C++规定:一个异常对象和catch子句的参数类型匹配成功能条件如下:
1)如果catch子句参数的类型就是异常对象的类型或其引用;
2)如果catch子句参数类型是异常对象所属类型的public基类或其引用;
3)如果catch子句参数类型为public基类指针,而异常对象为派生类指针;
4)catch子句参数类型为void
,而异常对象为任意类型指针;
5)catch子句为catch-all,即catch(…);
21.使用函数异常说明的基本原因:
1)告诉函数的调用者该函数可能抛出的异常类型,以便用户编写合适的异常处理器;
2)用户一般无法看到函数的实现,可便于用户浏览函数原型时知道函数可能抛出的异常类型。
22.异常说明的两种极端情况:不抛出任何异常(使用空列表的throw())和可以抛出任何异常(无任何说明)。
23.使用函数异常说明的好处:
1)可以约束函数的实现者,防止抛出异常说明列表中没有说明的异常。
2)可以指导函数的调用者编写正确的异常处理程序。
24.函数原型中的异常说明要与实现中的异常说明一致,否则容易引起异常冲突。
25.异常处理机制会检测到未说明的异常,并调用标准库函数unexpected(),unexpected()的默认行为就是调用terminate()结束程序。
26.尽量避免运行时的异常冲突:可使用set_unexpected()替换unexpected()。
27.当抛出异常时,异常处理机制保证:所有从try到throw语句之间构造起来的局部对象的析构函数将被自动调用(以与构造相反的顺序),然后清退堆栈(就像正常函数退出那样)。如果系统调用到了terminate(),则不能保证所有局部对象会被正确地销毁。
28.动态创建对象执行的操作:先在内存堆上分配一定数量的原始内存,如果成功则接着调用构造函数在这块内存上初始化一个对象。
29.在析构函数中抛出异常:首先检查当前是否有一个未捕获的异常正要被处理,如果没有,则是正常的销毁,则可在析构函数中抛出异常。最好的析构函数中处理掉所有异常而不要让异常传播出去。
30.全局对象在程序运行之前构造,因此如果它们的构造函数中有异常抛出的话,将永远不会被捕获;全局函数的析构函数在程序结束时才会被调用,这些异常只有操作系统可捕获,应用程序不能捕获。
31.一般情况下不要把异常处理机制当做正常的程序控制流程来使用,如果不使用异常处理机制就能够安全而高效地消除错误,那么就不要使用异常处理。异常处理机制可能用在某些安全领域,比如反跟踪。
32.catch块的传递应当使用引用传递而不是值传递。两个原因:效率和多态
33.在异常组合中,要合理安排异常处理的层次:一定要把派生类的异常捕获放在基类异常捕获的前面,否则派生类异常匹配永远也不会执行到。
34.在异常抛出后,当找到第一个类型匹配的catch子句时,编译器就认为该异常已经被处理(识别)了。至于catch块内的如何编写异常处理代码,由程序员决定。
35.如果实在无法判断到底会有什么异常抛出,那就使用“一网打尽“的策略好了:catch(void*)和catch(…)。但要记住:catch(void*)和catch(…)必须放在异常组合的最后面,并且catch(void*)放在catch(…)的前面。
36.编写异常说明时注意:派生类成员函数的异常说明和基类成员函数的异常说明一致,即派生类改写的虚函数的异常说明至少要和对应的基类虚函数的异常说明相同,甚至更加严格、更特殊。
37.异常重抛(rethrow):在catch块内抛出异常。
38.如果标准异常类型能够满足需要,就直接使用它们,不仅可减少工作量,还可能增强代码的可移植性。
39.RTTI(Run-time Type Identification):运行时类型信息,运行时类型识别。
40.RTTI:在运行时查询一个多态指针或引用指向的具体对象的类型。
41.typeid():以一个对象或者类型名作为参数,返回一个匹配的const type_info对象,表明该对象的确切类型。
42.dynamic_cast<>:执行运行时类型识别和转换;dynamic_cast(src);dest_type:转换的目标类型,src:被转换的对象。行为描述:如果运行时src和dest_type确实存在is-a关系,则转换可进行;否则转换失败。
1)dynamic_cast:可以用来转换指针和引用,但不能转换对象。当目标类型是某种类型的指针(包括void*)时,如果转换成功则返回目标类型的指针,否则返回NULL;当目标类型为某种类型的引用时,如果成功则返回目标类型的引用,否则抛出std::bad_cast异常,因为不存在NULL引用。
2)dynamic_case<>:只能用于多态类型对象(拥有虚函数或虚拟继承),否则将导致编译时错误。
3)dynamic_cast<>:实现两个方向的转换:upcast、downcast;
a.upcast:把派生类型的指针、引用转换成基类型的指针或引用,实际上可隐式转换;
b.downcast:把基类型的指针或引用转换成为数据派生类型的指针或引用。
43.使用RTTI时的注意事项:
1)打开编译器的RTTI支持;
2)对象所属类型必须是多态类;
3)如果要用dynamic_cast<>转换一个引用,要保证程序有一条catch()语句来处理std::bad_cast异常;
4)如果试图用typeid来检索NULL指针所指对象的类型信息,将抛出std::bad_typeid异常;
5)当用dynamic_cast<>转换一个指针的时候,要记住检查返回值是否为NULL。
44.RTTI:无论基本类型还是用户自定义类型,都需要额外的内存来存放type_info对象。在执行速度上和程序体积上都带来了额外开销。
45.多态类的type_info对象的存放:为每一个多态类增加一个指针成员、一个type_info对象,以及给虚函数表增加一项。
第16章 内存管理
1.内存分配方式:从静态存储区域分配【a】、在堆栈上分配【b】、从堆(heap)或自由存储空间上分配(动态内存分配)【c】。
1)a:内存在程序编译时就已经分配好(已经编址),这些内存在整个运行期间都存在。例如全局变量、static变量。
2)b:在函数执行期间,函数内局部变量(包括形参)的存储单元都创建在堆栈上,函数结束时这些存储单元自动释放(堆栈清退)。堆栈内存分配运算内置于处理器的指令集中,效率很高,并且一般不存在失败的危险,但是分配的内存容量有限,可能出现堆栈溢出。
3)c:程序在运行期间用malloc()或new申请任意数量的内存,程序员自己掌握内存的恰当时机(使用free()或delete)。动态内存的生存期由程序员决定,使用非常灵活,但也容易产生问题。
2.一般原则:如果使用堆栈存储和静态存储就能满足应用要求,那么就不要使用动态存储。(在堆上动态分配内存需要可观的额外开销)
3.动态内存分配可能出现的问题:
4.常见的内存错误及其对策:
1)内存分配未成功,却使用了它:在使用内存前检查指针是否是NULL。
2)内存分配虽然成功,但是尚未初始化就使用它:无论在何种方式创建数组,都不要忘记赋初值。
3)内存分配成功并且已经初始化,但操作越过了内存的边界:检查内存边界;
4)忘记释放内存或者只释放了部分内存,因此造成内存泄漏:动态内存的申请与释放必须配对使用;(malloc()和free()、new/delete)
5)释放了内存却还在继续使用它;
a.程序中的对象调用关系过于复杂:应该重新设计数据结构。
b.写错函数的return语句,注意不要返回指向“栈内存”的指针或者引用,因为该内存在函数结束时被自动释放。
c.使用free()或delete释放指针后,没有将指针设置为NULL,产生“野指针”;
d.多次释放同一块内存。
5.用malloc或new申请内存之后,应该立即检查指针值是否为NULL或者进行异常处理,以防止使用值为NULL的指针。
6.不要忘记初始化指针、数组、动态内存,防止将未初始化的内存作为右值使用。
7.避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
8.动态内存的申请与释放必须配对,防止内存泄漏。
9.用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
10.指针参数如何传递内存:编译器为函数的每个参数制作临时副本,可使用“指向指针的指针”或者“指针的引用”。
11.free()和delete:把指针所指的内存释放掉,并没有删除指针本身。不等于NULL的指针不一定就是有效的指针,所以一定不要忘记初始化指针变量为NULL或有效地址。
12.指针消亡,并不表示它所指向的内存会被自动释放;内存被释放,并不表示指针会消亡或者成了NULL。
13.野指针:指向“非法“内存的指针。if语句对其可能不起作用。
14.野指针的成因:
1)没有初始化指针变量。指针变量创建时是随机值,创建时要么设置指针为NULL,要么让它指向有效的内存。
2)指针被free()或者delete后,没有置为NULL,让人误以为它仍然是一个有效的指针。
3)指针操作超越了变量的作用范围。
15.malloc()和free():C++/C语言的标准库函数;new/delete:C++的运算符。都可用于申请和释放动态内存。
16.malloc()/free():不会调用构造函数和析构函数。
17.动态对象的内存管理应使用new/delete。
18.malloc的使用:int p = (int )malloc(sizeof(int) * length);
1)malloc()函数返回值的类型是void
,所有在调用malloc()时要显式地进行类型转换,将void
转换成所需要的指针类型。
2)malloc()函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。
19.free():void free(void *memblock);
20.new的三种形式:plain new、nothrow new、placement new;
1)plain new:普通的new,通常使用它;plain new失败后抛出标准异常std::bad_alloc而不是返回NULL;语法形式:char *p = new char[100];
2)nothrow new/delete:不抛出异常的运算符new的形式,nothrow new在失败时返回NULL。语法形式:char *p = new (nothrow)char[100];
3)placement new/delete:允许在一块已经分配成功的内存上重新构造对象或者对象数组,这不会分配内存,它所做的唯一一件事情就是调用对象的构造函数。语法形式:type-name *q = new§ type-name; p表示已经分配成功的内存区的首地址,它被转换为目标类型的q,因此p和q相等。
主要用途:反复使用一块较大的动态分配成功的内存来构造不同类型的对象或者它们的数组。
21.new/delete的各种用法:(new/delete的3种形式实际上是预先定义好的new/delete运算符重载)
22.new:内置了sizeof、类型转换和类型安全检查功能。
23.如果用new创建对象数组,那么只能使用对象的默认构造函数。
24.不论是何种类型,new/delete和new[]/delete[]总是应该正确配对使用。没有任何一种类型,对于其动态创建的数组来说delete p和delete []p是等价的。
25.多次delete一个不等于NULL的指针会导致运行时错误,但是多次delete一个NULL指针没有任何危险,因为delete运算符会首先检查这种情况,如果指针为NULL,则直接返回。
26.处理内存耗尽的方式:
1)判断指针是否为NULL,如果是则立刻用return语句终止本函数。
2)判断指针是否为NULL,如果是则立刻用exit(1)终止整个程序的运行。
3)为new和malloc预设异常处理函数。
4)捕获new抛出的异常,并尝试从中恢复。(C++中提倡使用)
27.对于32位以上的应用程序而言,一般情况下使用malloc()和new几乎不可能导致“内存耗尽。(32位操作系统支持“虚存”,即内存用完了,自动用硬盘空间顶替)
28.不加错误处理程序将导致程序的质量很差,千万不要因小失大。
29.接管:按bit拷贝语义:新对象的指针成员接管源指针指向的数据对象,即与源指针具有相同的值。
30.深拷贝:按成员拷贝:新对象的指针成员指向新创建的数据对象,并用源指针指向的对象来初始化该数据对象。
31.模板技术:即泛型技术,可以实现通用的目标。
32.泛型指针auto_ptr:当函数即将退出或有异常抛出时,不再需要显式地用delete来删除每一个动态创建的对象,C++保证会在堆栈清退的过程中自动调用每一个局部对象的析构函数,而析构函数会调用delete来完成这个任务。
33.带有引用计数的智能指针:兼有普通指针共享实值对象和auto_ptr自动释放实值对象的双重功能,并自动管理实值对象的生命周期和有效引用的计数,不会造成丢失引用、内存泄漏及多次释放等问题。
34.指针
35.指针:就是一个地址值,因此以指针为元素容器存放的就是一些内存地址,而不是真正数据。它只负责指针元素本身的内存动态分配和释放,而不会负责指针元素指向对象的内存管理任务,因为那是程序员的责任。
36.可以作为STL窗口元素的数据类型一般满足下列条件:
1)可默认构造的(Default Constructor),也即具有public的default constructor,不论是用户显式定义的还是编译器默认的。
2)可拷贝构造(Copy Constuctible)和拷贝赋值(Copy Assignable)的,即具有public的copy constructor和copy assignment operator,不论是编译器默认的还是用户显式定义的。
3)具有public的destructor,不论是编译器默认的还是用户显式定义的。
4)对于关联式窗口,要求其元素必须是可比较的(Comparable)。
37.auto_ptr的特点:接管和转移拥有权,而不是像原始指针那样可以共享实值对象,即auto_ptr在初始化时接管实值对象和拥有权,而在拷贝时(拷贝构造和拷贝赋值)会交出实值对象及其拥有权。
38.auto_ptr对象和它的拷贝不会共享实值对象,任何两个auto_ptr也不应该共享同一个实值对象。
39.STL容器管理元素的方法:动态拷贝元素,并负责管理这些动态分配的资源,即值的深拷贝语义(deep copy)。【A复制了B,修改A时B也发生变化,此为浅拷贝,即原始对象和新对象引用同一对象;修改A时B未变化,此为深拷贝,即原始对象和新对象为不同的对象】
40.智能指针可否作为容器的元素并非绝对的,不仅与STL的实现有关,而且与STL容器的需求和安全性及容器的语义有关。
41.auto_ptr在拷贝或赋值时会使原来的auto_ptr失效,那么只要防止其拷贝和赋值行为的发生就可以了,比如传递auto_ptr对象时使用const&或const*传递而不是值传递。
42.auto_ptr:对象化的智能指针,具有自动释放资源的能力,因此其真正有价值的用途是在发生异常时避免资源泄漏。
43.C++有一个保证:本地对象在函数退出时总是会被销毁,而不论函数以何种方式退出。
44.智能指针:方便管理堆内存;
第17章 使用和学习STL
1.STL(Standard Template Library):标准模板库,它是C++标准库的最主要和最重要的组成部分。
2.标准化的模板库的重要性体现在以下方面:
1)它可用来创建动态增长和减小的数据结构;
2)它是类型无关的,因此具有很高的可复用性;
3)它在编译时而不是运行时进行数据类型检查,保证了类型安全;
4)它是平台无关的,因此保证了最大的可移植性;
5)它可用于基本数据类型,包括指针和引用;
3.STL是一个标准规范,它只是为容器、迭代器和泛型算法等组件定义了一整套统一的上层访问接口及各组件之间搭配运用的一般规则,而没有定义组件底层的具体实现方法,因此不同的软件供应商都可以提供自己的STL实现版本。
4.STL:可看作是概念模型库或者一个组件架构。
5.STL主要包括的组件:I/O流、string类、容器类(Container)、迭代器(Iterator)、存储分配器(Allocator)、适配器(Adapter)、函数对象(Functor)、泛型算法(Algorithm)、数值运算、国际化和本地化支持,以及标准异常类等。
6.六大组件:容器、存储分配器、迭代器、泛型算法、函数对象、适配器。
7.容器类相当于数据结构,算法用于操作容器中的数据,而迭代器则是它们之间的桥梁。
8.STL各组件之间的关系:
9.C++标准规定,STL的头文件都不使用扩展名。
10.STL源代码头文件(实际上都是内联的)一般都存放在开发环境的include目录下。STL组件都被纳入了名字空间std::,所以在使用其中的组件之前需使用using声明或using指令,或者也可以在每一处都直接使用完全限定名std::。
11.STL容器的头文件:
12.泛型算法:只要是由一系列元素构成的结构原则上都可以应用泛型算法。
13.迭代器:用来遍历元素序列或元素集合的“通用指针”,但是每一种容器都定义了适合自己使用的迭代器。
14.容器:能够容纳其他对象作为其元素的对象。它本身是一个C++类的对象,其大小在运行时不可能改变。
15.不同容器的存储模型一般互不相同,但是容器对象和其元素对象之间的关系是类似的。
16.STL容器设计基本模型:
17.存储方式:连续存储(vector)和随机存储(list)(不连续存储)。不同的存储方式决定了元素的不同访问方式:(连续存储:随机访问;非连续存储:顺序访问)。
18.随机访问:可以直接通过开销恒定的算术运算来得到任一元素的内存地址的访问方法。
19.顺序访问:必须从第一个元素开始遍历,直到找到所需的元素对象为止,而无法直接得到任一中间元素对象的地址。
20.只要底层存储机制采取连续存储方式的容器,就可以随机访问其中任一元素对象,否则只能顺序访问;而任何容器都可以顺序访问,即遍历。
21.容器适配器:stack、queue及priority_queue在概念和接口上都不支持随机访问和遍历,这是由它们的语义决定的,而不是由底层存储方式决定的,因此没有迭代器。
22.顺序容器和关联式容器(联合容器):上层接口表现出现的访问形式。
23.顺序容器:采用向量和链表及其组合作为存储结构,如堆和各种队列。
24.关联式容器:采用平衡二叉搜索树作为底层存储结构。如集合。
25.各种容器的存储方式和访问方式:
26.由于关联式容器在概念上是无序的,所以它只能通过元素的值来定位其中的元素对象,这就是关联式容器具有find()函数的原因,也是调用这种容器的insert()函数和erase()函数时可以不指定插入位置和删除位置而仅指定元素值或者索引值的原因(关联式容器会自动地为新插入的元素安排一个合适的位置)。
27.顺序容器:通过元素对象在容器中的位置来标识一个元素的,而不是通过元素的值(因为它可以存储值相等的多个元素对象,而且它们的位置不一定相邻)。这也是调用顺序容器的insert()函数和erase()函数时必须指定插入位置和删除位置而不能仅指定元素值的原因。(无find())
28.顺序容器的实现并不像关联式容器那样在增加和删除元素时对其元素进行自动排序,它的概念决定了自动排序对查找(定位)其中任一元素的平均效率没有任何贡献(否则反而会影响插入和删除的效率),永远是O(N),因为你只能顺序访问它。
29.map具有operator[]运算符函数的原因:提供通过key值来定位value对象的接口。
30.关联式容器的find()和operator[]函数反映了它们在应用层面的“随机访问”方式。但从底层实现上看,它们采用的二叉树存储结构决定了访问方式仍然是顺序访问。
31.如果元素查找是经常做的操作,插入和删除反而返回是很少做的操作,那么就不适合使用顺序容器,而应该使用关联式容器;反之就是用顺序容器。
32.STL容器:“前闭后开”区间法,即[first,last):迭代器last不会指向最后一个有效元素。
33.存储空间重分配:起源于容器元素对象的动态创建和连续存储特性;因此只有连续存储的容器才可能需要运行时的存储空间重分配,典型的就是vector。
34.存储重分配或者元素移动均会带来效率问题。
35.如果在创建一个容器时就能够预先估计出它可能存放的最大元素数目,那么你就可以给它预先分配足够数量的存储空间,从而可以避免频繁的存储空间重分配操作。某些STL容器带有这种功能的构造函数及reserve()方法。
36.尽量在容器的尾部执行插入操作,因为这里的插入操作效率最高。对于顺序容器来说,它们本身并不要求排序,因此完全没有必要刻意地在开头或中间插入元素;对于关联式容器,它们在实现时就是有序的,即调用insert()的时候会自动进行重新排序(伴随着树的平衡、旋转等操作),因此也没有必要刻意在开头或中间进行插入操作。
37.使用动态内存的一般原则:谁申请的内存就应该由谁来释放。
38.STL容器采用拷贝方式来接收待插入的元素对象:在插入的时候容器自动新建等量的元素对象,并用待插入对象依次初始化它们(调用拷贝构造函数);在删除元素时,容器负责释放其内存资源(对采用随机存储策略的容器)或者仅仅调用元素的析构函数(对采用连续存储策略的容器)。
39.容器只负责其元素对象本身一级的存储分配和释放,而不负责元素对象包含的额外内存的管理问题,这需要用户自己负起这个责任,典型的就是用指针作为容器元素。
40.可作为STL容器的元素一般要符合以下要求:
1)可默认构造的。但不是在任何情况下都需要满足这一条。
2)可拷贝构造的。
3)可拷贝赋值的。但也不是在任何情况下都需要。
上述几条对基本数据类型及不含指针成员和引用成员的类型都适用。
4)具有public、采用拷贝的方式显式定义的拷贝构造函数、拷贝赋值函数和析构函数;适用于含有指针成员或引用成员的对象。
41.基本数据类型如int、long、char等都具有默认构造函数的拷贝构造函数。
42.引用不能作为STL容器的元素类型:
1)引用在创建时必须初始化为一个具体的对象,而STL容器不能满足这一要求;
2)引用没有构造函数和析构函数,更没有赋值语义。
43.STL容器只支持对象语义,而不支持引用语义。
44.关联式容器的元素类型的要求可能更苛刻一些。元素类型必须至少定义“<”运算符重载函数。
45.在STL中,容器的迭代器被作为容器元素对象或者I/O流的对象的位置指示器,因此可以把它理解为面向对象的指针——一种泛型指针或通用指针,它不依赖于元素的真实类型。
46.迭代器的“通用”是一种概念上的通用,所有的泛型容器和泛型算法都使用“迭代器”来指示元素对象,所有的迭代器都具有相同或相似的访问接口,但是每一种容器都有自己的迭代器类型,毕竟每一种容器的底层存储方式不尽相同,所以迭代器的实现方式就会不同。千万不要以为存在一种“通用的迭代器”——它可以应用于任何类型的容器。
47.迭代器是为了降低容器和泛型算法之间的耦合性而设计的,泛型算法的参数不是容器,而是迭代器。
48.指针代表真正的内存地址,即对象在内存中的存储位置;而迭代器则代表元素在容器中的相对位置(当遍历容器的时候,关联式容器的元素也就具有了“相对位置”)。
49.STL迭代器的类别:输入/输出迭代器 < 前进迭代器 < 双向迭代器 < 随机访问迭代器。
50.随机迭代器是一种双向迭代器;双向迭代器是一种前进迭代器;前进迭代器是一种输入迭代器。
51.vector的迭代器为随机访问迭代器,因为它就是原始指针;
list的迭代器是双向迭代器,因为list是双向链表;
slist的迭代器是前进迭代器;
deque的迭代器是随机访问迭代器;
set/map的迭代器是双向迭代器。 迭代器分类:性能考虑
52.连续存储的容器,其元素的位置指示器有两种:下标和迭代器。下标的类型为unsigned int(size_t),有效范围为0~size_t(-1),迭代器的有效范围则从begin()到end()。
53.尽量使用迭代器类型,而不是显式地使用指针。例如使用vector::iterator,而不是int *,虽然它们是等价的。
54.只使用迭代器提供的标准操作,不要使用任何非标准操作,以避免STL版本更新的时候出现不兼容问题。
55.当不会改动容器中元素值的时候,请使用const迭代器(const_iterator)。
56.使用无效的迭代器就像使用无效的指针(野指针)一样危险。
57.迭代器失效:当容器底层存储发生改变时,原来指向容器中某个或某些元素的迭代器由于元素的存储位置发生了改变而不再指向它们,从而成为无效的迭代器。
58.可能引起容器存储的变动的操作:reserve()/resize()/push_back()/pop_back()/insert()/erase()/clear();泛型算法:sort()/copy()/replace()/remove()/unique()等;以及集合操作(交、并、差)算法等。
59.解决迭代器失效的办法:
1)操作已分配的迭代器前重新获取迭代器;
2)在修改容器前为其预留足够的空闲空间避免存储空间重分配。
60.顺序容器vector和string都可用reserve()和resize()来预留空间或调整它们的大小;reserve()用来保留(扩充)容量,它并不改变容器的有效元素个数;resize()则调整容器大小(size,有效元素的个数),而且有时候也会增大容器的容量。当把这两个函数与assign()/insert()/push_back()/replace()及泛型算法搭配起来使用的时候,需要小心从事。
61.容量:为了减少那些使用连续空间(线性空间)存储元素的容器在增加元素时重新分配内存的次数的一种机制,即当增加元素且剩余空间不足时,按照一定比例(通常是原来比例的2或1.5倍)多分配出一些空闲空间以备将来再增加元素时使用,以提高插入操作的性能。容量包含有效元素空间在内。
62.多余出来的容量(空闲存储空间)是未经初始化的(注意:并不是调用元素类型的默认构造函数来初始化)。区分vector的size()和capacity(),通常后者大于前者。
63.一个容器可以没有任何有效元素,但是却有许多冗余的容量;或者一个容器可以没有任何冗余的容量,全是有效元素;再或者既有一些有效元素,又有一些冗余容量。
64.reserve(size_type n):n表示用户请求保留的总容量的大小(在不重新分配内存的情况下可容纳元素的个数)。如果n大于容器现有的容量(即capacity()),则需要重分配空间,然后将容器内所有有效元素从旧位置全部拷贝到新位置(调用拷贝构造函数),最后释放旧位置的所有存储空间并调整容器对象的元素位置指示器(就是让那三个指针指向新内存区的相应位置);否则,什么也不做。
65.resize():调整容器的大小(size),有时也扩大容器的容量。不管容器当前包含多少个有效元素,也不管容器的冗余容量有多少,它都将容器的有效元素个数调整为用户指定的个数。
66.resize()的实现策略:
67.resize()和赋值操作及insert()、push_back()等都可以合作。
68.使用reserve()和resize()都不能缩减容器的容量。
69.如何压缩容器的多余容量:使用容器的拷贝构造函数和swap()函数,因为拷贝构造函数可以根据已有容器的大小决定一次性分配多少元素空间,就不会产生冗余容量。
70.尽量不要在遍历容器的过程中对容器进行插入元素、删除元素等修改操作,这和不要在for循环中修改计数器是一个道理,特别是连续存储的容器中。(这些操作会使一些迭代器失效)
71.修改容器和修改容器中的元素对象的值是两码事儿。修改容器可能引起容器底层存储的变动,因此可能使迭代吕失效,而后者则不会。
72.顺序容器允许直接修改其中元素对象的值;而关联式容器则不允许修改其元素的key值。
73.STL容器元素的存储空间是动态分配和释放的,不同的硬件平台和操作系统对内存的管理方法和使用方式各不相同。
74.STL为容器类定义了一个专门负责存储管理的类——allocator,但它仅针对内存管理。
75.allocator:一个模板,作为容器模板的一个policy参数,它不仅与将要为之分配空间的数据对象的类型无头,并且为动态内存的分配和释放提供了面向对象的接口。
76.适配器(Adapter):利用一种已有的比较通用的数据结构(通过组合而非继承)来实现更加具体的、更加贴近实际应用的数据结构。
77.“窄化(narrow)”:常用在类型转换中,专指类型安全的向下转型。
78.泛型算法:STL定义了一套的泛型算法,可施行于容器或其他序列上,它们不依赖具体容器的类型和元素的数据类型。
79.算法从迭代器得到一个数据对象,而迭代器则指示数据对象在容器中的位置。迭代器对象用来查找容器中下一个元素对象的位置并把它告诉算法,算法就能逐个地访问容器中所有元素对象。
80.算法的工作:改变迭代器,并按照用户指定的方式(即函数对象或谓词,可以没有),逐个地对迭代器指向的对象进行定制的操作。
81.泛型算法一般接受下列参数类型的一种或几种:
1)迭代器:标示容器或区间的范围,以值传递;
2)谓词:返回bool值的函数对象,指定算法的操作方式,例如find_if()的第三个参数;
3)函数对象:用户指定要做的操作,例如for_each()的第三个参数;
4)容器元素:用户指定的基准对象,例如find()的第三个参数。
82.存在空容器,但不存在空数组。容器虽然不包含任何元素,但容器对象本身还是存在于内存中,只不过它的那些指针数据成员都为0而已。
83.从容器中删除元素前一定要检查元素是否属于该容器。
【xuexi|高质量程序设计指南--笔记】84.STL容器特征总结:

    推荐阅读