《编写高质量代码:改善C++程序的150个建议》读书笔记(1)

前一段时间看了这本《编写高质量代码——改善C++程序的150个建议》,感觉和Effective C++有点类似。看完还是有不少收获的,在此整理、记录一下。



第一部分 语法篇

建议1:区分0的4种面孔

  1. 整型0,32位(4个字节);
  2. 空指针NULL,指针与int类型所占空间是一样的,都是32位;
  3. 字符串结束标志’\0’,8位,一个字节,与’0’有区别;
  4. 逻辑FALSE/falseFALSE/TRUEint类型,而false/truebool类型。

建议5:不要忘记指针变量的初始化

  1. 可以将其初始化为空指针0(NULL);
  2. 对于全局变量来说,在声明的同时,编译器会悄悄完成对变量的初始化。

建议6:明晰逗号分隔表达式的奇怪之处

  1. 在使用逗号分隔表达式时,C++会确保每个表达式都被执行,而整个表达式的值则是最右边表达式的结果
  2. 在C++中,逗号分隔表达式既可以用作左值,也可以用作右值

建议9:防止重复包含头文件

注意在大型项目中的形式应类似下面:

1
2
3
4
#ifndef _PROJECT_PATH_FILE_H
#define _PROJECT_PATH_FILE_H
// ...
#endif


建议10:优化结构体中元素的布局

把结构体中的变量按照类型大小从小到大依次声明,尽量减少中间的填充字节


建议11:将强制转型减到最少

  1. const_cast<T*>(a):它用于从一个类中去除以下这些属性:constvolatile__unaligned
  2. dynamic_cast<T*>(a):它将a值转换成类型为T的对象指针,“安全的向下转型”,cost较大;
  3. reinterpret_cast<T*>(a):它能够用于诸如One_class*Unrelated_class*这样的不相关类型之间的转换,“强行转换”,因此它是不安全的;
  4. static_cast<T*>(a):它将a的值转换为模板中指定的类型T,但是,在运行时转换过程中,它不会进行类型检查,不能确保转换的安全性,最常用。

建议12:优先使用前缀操作符

对于整型和长整型的操作,前缀操作和后缀操作的性能区别通常是可以忽略的。
对于用户自定义类型,优先使用前缀操作符。因为与后缀操作符相比,前缀操作符因为无须构造临时对象而更具性能优势

1
2
3
class A; // 类名为A
A A::operator ++ (); // 前缀操作符
A A::operator ++ (A); // 后缀操作符,有函数参数


建议13:掌握变量定义的位置与时机

在定义变量时,要三思而后行,掌握变量定义的时机与位置,在合适的时机于合适的位置上定义变量。

尽可能推迟变量的定义,直到不得不需要该变量为止;同时,为了减少变量名污染,提高程序的可读性,尽量缩小变量的作用域。

“越local越好,尽量缩小scope。”

建议17:提防隐式转换带来的麻烦

提防隐式转换所带来的微妙问题,尽量控制隐式转换的发生;通常采用的方式包括:

  1. 使用非C/C++关键字的具名函数,用operator as_T()(具名函数,不会隐式调用)替换operator T()(T为C++数据类型)。
  2. 为单参数的构造函数加上explicit关键字。

建议18:正确区分void与void*

void是“无类型”,所以它不是一种数据类型;void*则为“无类型指针”,即它是指向无类型数据的指针,也就是说它可以指向任何类型的数据。

void发挥的真正作用是限制程序的参数与函数返回值:

  1. 如果函数没有返回值,那么应将其声明为void类型;
  2. 如果函数无参数,那么声明函数参数为void

对于void*

  1. 任何类型的指针都可以直接赋值给它,无须强制转型;
  2. 如果函数的参数可以是任意类型指针,那么应声明其参数为void*

√: 其他类型指针 —> void*指针

×: void*指针 —> 其他类型指针

建议19:明白在C++中如何使用C

若想在C++中使用大量现成的C程序库,就必须把它放到extern "C" {/* code */}中;
原因:C与C++编译、链接方式有区别,”函数签名”不同;C的函数签名不带函数类型信息,而C++中会附加函数类型信息;extern "C"的作用就是告诉C++链接器寻找调用函数的符号时,采用C的方式。

要实现在C++代码中调用C的代码,具体方式有以下几种:

  1. 修改C代码的头文件,当其中含有C++代码时,在声明中加入extern "C"
  2. 在C++代码中重新声明一下C函数,在重新声明时添加上extern "C"
  3. 在包含C头文件时,添上extern "C"

建议22:灵活地使用不同风格的注释

版权、版本声明:

1
/* ... */

内部注释:

1
//

宏定义尾端注释:

1
/* ... */


建议23:尽量使用C++标准的iostream

除了防止OJ中的TLE,一般推荐用C++的iostream

建议25:尽量用const、enum、inline替换#define

  • 对于简单的常量,应该尽量使用const对象或枚举类型数据,避免使用#define;
  • 对于形似函数的宏,尽量使用内联函数,避免使用#define。

总之一句话,尽量将工作交给编译器,而不是预处理器

建议26:用引用代替指针

与指针不同,引用与地址没有关联,甚至不占任何存储空间

建议27:区分内存分配的方式

C++中,内存被分成了5个区:

  1. 栈区:执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元将自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是所分配的内存容量有限
  2. 堆区:new / delete;如果程序员没有释放掉,那么在程序结束后,操作系统就会自动回收
  3. 自由存储区:malloc() / free()
  4. 全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有作此区分,它们共同占用同一块内存区;
  5. 常量存储区:这是一块比较特殊的存储区,里面存放的是常量,不允许修改。

stack v.s. heap

建议29:区分new的三种形态

  1. 如果是在堆上建立对象,那么应该使用new operator,它会为你提供最为周全的服务;
  2. 如果仅仅是分配内存,那么应该调用operator new,但初始化不在它的工作职责之内。如果你对默认的内存分配过程不满意,想单独定制,重载operator new是不二选择
  3. 如果想在一块已经获得的内存里建立一个对象,那就应该用placement new。但是通常情况下不建议使用,除非是在某些对时间要求非常高的应用中,因为相对于其他两个步骤,选择合适的构造函数完成对象初始化是一个时间相对较长的过程。

建议31:了解new_handler的所作所为

在使用operator new申请内存失败后,编译器并不是不做任何的努力直接抛出std::alloc异常,在这之前,它会调用一个错误处理函数(这个函数被称为new-handler),进行相应的处理。通常,一个好的new-handler函数的处理方式必须遵循以下策略之一:

  1. 使更大块内存有效;
  2. 装载另外的new-handler
  3. 卸载new-handler
  4. 抛出异常;
  5. 无返回。

建议32:借助工具检测内存泄露问题

内存泄露一般指的是堆内存的泄露。检测内存泄露的关键是能截获对分配内存和释放内存的函数的调用。通过截获的这两个函数,我们就能跟踪每一块内存的生命周期。每当成功分配一块内存时,就把它的指针加入一个全局的内存链中;每当释放一块内存时,再把它的指针从内存链中删除。这样当程序运行结束的时候,内存链中剩余的指针就会指向那些没有被释放的内存。这就是检测内存泄露的基本原理。

检测内存泄露的常用方法有如下几种:

  1. MS C-Runtime Library内建的检测功能,要在非MFC程序中打开内存泄露的检测功能非常容易,只须在程序的入口处添加以下代码:
    _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG)|_CRTDBG_LEAK_CHECK_DF);
  2. 外挂式的检测工具:MS下BoundsCheckerInsure++;Linux下RationalPurifyValgrind.

建议34:用智能指针管理通过new创建的对象

解决内存泄漏:

  1. 垃圾回收(Java);
  2. 2.智能指针(C++)

STL中的智能指针auto_ptr,要使用它,需要包含memory头文件:

  1. auto_ptr对象不可作为STL容器的元素;
  2. auto_ptr缺少对动态配置而来的数组的支持;
  3. auto_ptr在被复制的时候会发生所有权转移。
  4. 结论:不要使用auto_ptr!(C++11中已将其废弃)

Boost库中有更好用的智能指针

建议35:使用内存池技术提高内存申请效率与性能

经典的内存池技术,是一种用于分配大量大小相同的小对象的技术。通过该技术可以极大地加快内存分配/释放过程。内存池技术通过批量申请内存,降低了内存申请次数,从而节省了时间。

建议37:了解C++悄悄做的那些事

The “Big Three”:

  • 构造函数,one or more
  • 析构函数,one
  • 拷贝构造函数,one

建议38:首选初始化列表实现类成员的初始化

类成员的初始化可采用两种形式来完成:

  1. 在构造函数体重赋值完成;
  2. 用初始化类成员列表完成;

Others:

  1. const成员变量只能用成员初始化列表来完成初始化,而不能在构造函数内被赋值;
  2. 如果类B中含有A类型的成员变量,而类A中又禁止了赋值操作,此时要想顺利地完成B中成员变量的初始化,就必须采用初始化列表方式。即使没有禁用赋值操作,还是不推荐采用函数体内的赋值初始化方式。因为这种方式存在着两种问题:第一,比起初始化列表,此方式效率偏低;第二,留有错误隐患。

对于初始化列表,初始化的顺序与构造函数中的赋值方式不同,初始化列表中成员变量出现的顺序并不是真正初始化的顺序,初始化的顺序取决于成员变量在类中的声明顺序。只有保证成员变量声明的顺序与初始化列表顺序一致才能真正保证其效率

建议39:明智地拒绝对象的复制操作

在某些需要禁止对象复制操作的情形下,可以将这个类相应的拷贝构造函数、赋值操作符operator = 声明为private,并且不要给出实现。或者采用更简单的方法:使用boost::noncopyable作为基类。

建议40:小心,自定义拷贝函数

如果类内部出现了动态配置的资源,我们就不得不自定义实现其拷贝函数了。在自定义拷贝函数时,应该保证拷贝一个对象的All Parts:所有数据成员及所有的基类部分。

建议41:谨防因构造函数抛出异常而引发的问题

判断构造对象成功与否,解决办法:抛出一个异常。构造函数抛出异常会引起对象的部分构造,因为不能自动调用析构函数,在异常发生之前分配的资源将得不到及时的清理,进而造成内存泄露问题。所以,如果对象中涉及了资源分配,一定要对构造之中可能抛出的异常做谨慎而细致的处理。

建议42:多态基类的析构函数应该为virtual

虚函数的最大目的就是允许派生类定制实现。所以,用基类指针删除一个派生类对象时,C++会正确地调用整个析构链,执行正确的行为,以销毁整个对象。

在实际使用虚析构函数的过程中,一般要遵守以下规则:当类中包含至少一个虚函数时,才将该类的析构函数声明为虚。因为一个类要作为多态基类使用时,它一定会包含一个需要派生定制的虚函数。相反,如果一个类不包含虚函数,那就预示着这个类不能作为多态基类使用。同样,如果一个类的析构函数非虚,那你就要顶住诱惑,决不能继承它,即使它是“出身名门”。比如标准库中的stringcomplex、以及STL容器。

  1. 多态基类的析构函数应该是virtual的,也必须是virtual,因为只有这样,虚函数机制才会保证派生类对象的彻底释放;
  2. 如果一个类有一个虚函数,那么它就该有一个虚析构函数
  3. 如果一个类不被设计为基类,那么这个类的析构就应该拒绝为虚。

建议43:绝不让构造函数为虚

虚函数的工作机制:虚函数的多态机制是通过一张虚函数表来实现的。在构造函数调用返回之前,虚函数表尚未建立,不能支持虚函数机制,所以构造函数不允许设为虚。

建议44:避免在构造/析构函数中调用虚函数

成员函数、包括虚成员函数,都可以在构造、析构的过程中被调用。当一个虚函数被构造函数(包括成员变量的初始化函数)或者析构函数直接或间接地调用时,调用对象就是正在构造或者析构的那个对象。其调用的函数是定义于自身类或者其基类的函数,而不是其派生类或者最低派生类的其他基类的重写函数。

如果在构造函数或析构函数中调用了一个类的虚函数,那它们就变成普通函数了,失去了多态的能力

对象不能在生与死的过程中让自己表现出多态。

建议45:默认参数在构造函数中给你带来的喜与悲

合理地使用默认参数可以有效地减少构造函数中的代码冗余,让代码简洁而有力。但是如果不够小心和谨慎,它也会带来构造函数的歧义,增加你的调试时间。

建议46:区分Overloading、Overriding、Hiding之间的差异

  1. 重载(Overloading):是指同一作用域的不同函数使用相同的函数名,但是函数的参数个数或类型不同
  2. 重写(Overriding):是指在派生类中对基类中的虚函数重新实现,即函数名和参数都一样,只是函数的实现体不一样,派生类对基类中的操作进行个性化定制就是重写。重写需要注意的问题:
    1. 函数的重写与访问层级(publicprivateprotected)无关;
    2. const可能会使虚成员函数的重写失效;
    3. 重写函数必须和原函数具有相同的返回类型
  3. 隐藏(Hiding):是指派生类中的函数屏蔽基类中具有相同名字的非虚函数

建议47:重载operator=的标准三步走

  1. 不要让编译器帮你重载赋值运算符;
  2. 一定要检查自赋值;
  3. 赋值运算符重载需返回*this的引用,引用之于对象的优点在于效率,为了能够更加灵活地使用赋值运算符,选择返回引用绝对是明智之举;
  4. 赋值运算符重载函数不能被继承。如果需要给类的数据成员动态分配空间,则必须实现赋值运算符。

建议48:运算符重载,是成员函数还是友元函数

运算符重载的四项基本原则:

  1. 不可臆造运算符;
  2. 运算符原有操作数的个数、优先级和结合性不能改变;
  3. 操作数中至少一个是自定义类型;
  4. 保持重载运算符的自然含义。

运算符的重载可采用两种形式:成员函数形式和友元函数形式。

  1. 重载为成员函数时,已经隐含了一个参数,它就是this指针;对于双目运算符,参数仅有一个;
  2. 当重载友元函数时,将不存在隐含的参数this指针;如果运算符被重载为友元函数,那么它就获得一种特殊的属性,能够接受左参数和右参数的隐式转换,如果是成员函数版的重载则只允许右参数的隐式转换。
  3. 一般说来,建议遵守一个不成文的规定:对双目运算符,最好将其重载为友元函数,因为这样更方便些;而对于单目运算符,则最好重载为成员函数。

建议49:有些运算符应该成对实现

很多运算符重载时最好成对实现,比如==与!=、<与>、<=与>=、+与+=、-与-=、=、/与/=。

建议50:特殊的自增自减运算符重载

(1). 后缀形式的参数并没有被用到,它只是语法上的要求,为了区分而已;

(2). 后缀形式应该返回一个const对象;这样做是大有深意的。 我们知道操作符重载本质上是一个函数而已,它应该和操作符原来意义、使用习惯相似,而对于int内置类型来说的话,i++++是有语法错误的,故为了保持一致,重载之后的后缀形式也应该是这样的。假如返回的不是const类型,那么对于

1
2
MyInt t(1);
t++++; // t.operator++(0).operator++(0)

这样的形式将是正确的,这显然不是我们期望的。另外,只要看上面的后缀形式的定义即可知道,这样写t只是增加了1而不是我们期望的2,为什么呢?因为第二次的调用是用第一次返回的对象来进行的,而不是用t,它违反了程序员的直觉。因此,为了阻止这些行为,我们返回const对象。

(3). 我们应该尽可能调用前缀形式;为什么呢?看看后缀形式的定义就可以知道,我们定义了一个临时对象,临时对象的创建、析构还是很费时间的。而前缀形式则不一样,它的效率相对好一些。

(以上出自百度空间中的一篇文章)

建议51:不要重载operator&&、operator||以及operator,

&&”、“||”、“,”(逗号运算符)都具有较为特殊的行为特性,重载会改变运算符的这些特性,进而影响原有的习惯,所以不要去重载这三个可以重载的运算符。

建议52:合理地使用inline函数来提高效率

内联函数具有与宏定义相同的代码效率,但在其他方面却要优于宏定义。因为内联函数还遵循函数的类型和作用域规则。内联函数一般情况下都应该定义在头文件中

内联函数的定义分为两种方式:

  1. 显式方式:在函数定义之前添加inline关键字,内联函数只有和函数体声明放在一起时inline关键字才具有效力
  2. 隐式方式:将函数定义于类的内部。一个给定的函数是否得到内联,很大程度上取决于你正在使用的编译器。

使用内联函数应该注意:

  1. 内联函数的定义必须出现在内联函数第一次被调用之前。所以,它一般会置于头文件中;
  2. 在内联函数内不允许用循环语句(for, while)和开关语句(if-else, switch-case),函数不能过于复杂;
  3. 依据经验,内联函数只适合于只有1~5行的小函数;
  4. 对于内存空间有限的机器而言,慎用内联。过分地使用内联会造成函数代码的过度膨胀,会占用太多空间;
  5. 不要对构造/析构函数进行内联
  6. 大多开发环境不支持内联调试,所以为了调试方便,不要将内联优化放在调试阶段之前。

建议53:慎用私有继承

私有继承会使基类的所有东西(包括所有的成员变量与成员函数)在派生类中变成private的,也就是说基类的全部在派生类中都只能作为实现细节,而不能成为接口私有继承意味着“只有implementation 应该被继承,interface应该被忽略”,代表着是“is-implemented-in-terms-of”的内在关系。通常情况下,这种关系可以采用组合的方式来实现,并提倡优先使用组合的方案。但是如果存在虚函数和保护成员,就会使组合方案失效,那就应使用私有继承

建议54:抵制MI的糖衣炮弹

MI(多重继承)意味着设计的高复杂性、维护的高难度性,尽量少使用MI。

建议55:堤防对象切片

多态的实现必须依靠指向同一类族的指针或引用。否则,就可能出现著名的对象切片(Object Slicing)问题。所以,在既有继承又有虚函数的情况下,一定要提防对象切片问题

建议56:在正确的场合使用恰当的特性

  1. 虚函数:虚函数机制的实现是通过虚函数表指向虚函数表的指针来完成的。关键字virtual告诉编译器该函数应该实现晚绑定,编译器对每个包含虚函数的类创建虚函数表VTable,以放置类的虚函数地址。编译器密码放置了指向虚函数表的指针VPtr,当多态调用时,它会使用VPtr在VTable表中查找要执行的函数地址;
  2. 多重继承:对于多重继承来说,对象内部会有多个VPrt,所以这就使偏移量计算变得复杂了,而且会使对象占用的空间和运行时开销都变大;
  3. 虚基类:它与多重继承的情况类似,因为虚基类就是为了多重继承而产生的;
  4. 运行时类型检测(RTTI):是我们在程序运行时得到对象和类有关信息的保证。

建议57:将数据成员声明为private

将数据成员声明为private是具有相当充分的理由的:

  1. 实现数据成员的访问控制;
  2. 在将来时态下设计程序,为之后的各种实现提供弹性;
  3. 保持语法的一致性。

建议59:明了如何在主调函数启动前调用函数

如果想在主程序main启动之前调用某些函数,调用全局对象的构造函数绝对是一个很不错的方法。因为从概念上说,全局对象是在程序开始前已经完成了构造,而在程序执行之后才会实施析构

建议60:审慎地在动、静多态之间选择

虚函数机制配合继承机制,生效于运行期,属于晚绑定,是动多态

模板将不同的行为和单个泛化记号相关联发生在编译期,属于早绑定,被称为静多态

  1. 动多态:它的技术基础是继承机制和虚函数,它在继承体系之间通过虚函数表来表达共同的接口;
  2. 静多态:它的技术基础是模板。与动多态相比,静多态始终在和参数“较劲儿”,它适用于所有的类,与虚函数无关。

从应用形式上看,静多态是发散式的,让相同的实现代码应用于不同的场合;动多态是收敛式的,让不同的实现代码应用于相同的场合

从思维方式上看,前者是泛型式编程风格,它看重的是算法的普适性;后者是对象式编程风格,它看重的是接口与实现的分离度

两者区别:

  1. 动多态的函数需要通过指针或引用传参,而静多态则可以传值、传指针、传引用等,“适应性”更强;
  2. 在性能上,静多态优于动多态,因为静多态无间接访问的迂回代码,它是单刀直入的;
  3. 因为实现多态的先后顺序不同,所以如果出现错误,它们抛出错误的时刻也不一样,动多态会在运行时报错,而静多态则在编译时报错。

建议61:将模板的声明和定义放置在同一个头文件里

模板类型不是一种实类型,它必须等到类型绑定后才能确定最终类型,所以在实例化一个模板时,必须要能够让编译器“看到”在哪里使用了模板,而且必须要看到模板确切的定义,而不仅仅是它的声明,否则将不能正常而顺利地产生编译代码。

函数模板、类模板不同于一般的函数、类,它们不能像一般的方式那样进行声明与定义,标准要求模板的实例化与定义体必须放在同一翻译单元中。

实现这一目标有三种方法:

  1. 将模板的声明和定义都放置在同一个.h文件中;
  2. 按照旧有的习惯性做法来处理,声明是声明,实现是实现,二者相互分离,但是需要包含头文件的地方做一些改变,如,在使用模板时,必须用#include “Temp.cpp”替换掉#include “Temp.h”;
  3. 使用关键字export来定义具体的模板类对象和模板函数。
    但是2、3需要编译器支持,所以最优策略还是:将模板的声明和定义都放置在同一个.h文件中,虽然在某种程度上这破坏了代码的优雅性。

建议62:用模板替代参数化类型的宏函数

参数化的宏函数有着两个致命缺点:

  1. 缺乏类型检查;
  2. 有可能在不该进行宏替换的时候进行了替换,违背了作者的意图。

模板是实现代码复用的一种工具,它可以实现类型参数化,达到让代码真正复用的目的。

宏:

1
#define min(a, b) ( (a) < (b) ? (a) : (b) )

用模板函数替换上面的宏:

1
2
3
4
5
template <typename T>
const T min(const T &t1, const T &t2)
{
return t1 > t2 ? t2 : t1;
}


建议63:区分函数模板与模板函数、类模板与模板类

函数模板的重点在于“模板”两个字,前面的“函数”只是一个修饰词。其表示的是一个专门用来生产函数的模板。而模板函数重点在“函数”,表示的是用模板所生成的函数。

函数模板生成模板函数。

函数模板:

1
2
3
4
5
template <typename T>
void Func(const T &a)
{
// ......
}

使用其生成的模板函数:

1
2
3
Func<int>(a);
Func<float>(a);
// ......

类模板:

1
2
3
4
5
6
7
template <class T>
class List_item
{
public:
T m_val;
// ......
}

使用其生成的模板类:

1
2
3
List_item<int> list1;
List_item<float> list2;
// ......


建议64:区分继承与模板

模板的长处在于处理不同类型间“千篇一律”的操作。

建议66:传值throw异常,传引用catch异常

异常处理标准形式:throw byvalue, catch by reference

1
2
3
4
5
6
7
8
9
try
{
// ...
throw exception; // throw a value
}
catch(const exception &e) // by const reference
{
// ...
}


建议67:用”throw ;”来重新抛出异常

对于异常的重新抛出,需要注意:(1)重新抛出的异常对象只能出现在catch块或catch调用的函数中;(2)如果在处理代码不执行时碰到”throw ;”语句,将会调用terminate函数。

1
2
3
4
5
6
7
8
9
10
try
{
// ...
throw exception; // throw a value
}
catch(const exception &e) // by const reference
{
// ...
throw ; // 重新抛出异常
}


建议68:了解异常捕获与函数参数传递之间的差异

异常与函数参数的传递之间差异:(1)控制权;(2)对象拷贝的次数;(3)异常类型转换;(4)异常类型匹配。

建议69:熟悉异常处理的代价

异常处理在带来便利的同时,也会带来时间和空间上的开销,使程序效率降低,体积增大,同时会加大代码调试和管理的成本。

建议70:尽量保证异常安全

如果采用了异常机制,请尽量保证异常安全:努力实现强保证,至少实现基本保证。

Google不使用C++异常处理:(见Google C++ Style Guide上的说明)

We do not use C++ exceptions.


建议71:尽量熟悉C++标准库

C++标准库主要包含的组件:

  1. C标准函数库;
  2. 输入/输出(input/output);
  3. 字符串(string)
  4. 容器(containers)
  5. 算法(algorithms)
  6. 迭代器(iterators)
  7. 国际化(internationalization);
  8. 数值(numerics);
  9. 语言支持(languagesupport);
  10. 诊断(diagnostics);
  11. 通用工具(general utilities)。

字符串、容器、算法、迭代器四部分采用了模板技术,一般被统称为STL(Standard Template Library,即标准模板库)。

在C++标准中,STL被组织成了13个头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<algorithm>
<deque>
<functional>
<iterator>
<vector>
<list>
<map>
<memory>
<numeric>
<queue>
<set>
<stack>
<utility>


建议72:熟悉STL中的有关术语

  1. 容器:是一个对象,它将对象作为元素来存储;
  2. 泛型(Genericity):泛型就是通用,或者说是类型独立;
  3. 算法:就是对一个对象序列所采取的某些操作,例如std::sort()、std::copy()、std::remove();
  4. 适配器(Adaptor):是一个非常特殊的对象,它的作用就是使函数转化为函数对象,或者是将多参数的函数对象转化为少参数的函数对象
  5. O(h):它是一个表示算法性能的特殊符号,在STL规范中用于表示标准库算法和容器操作的最低性能极限;
  6. 迭代器:是一种可以当做通用指针来使用的对象,迭代器可以用于元素遍历、元素添加和元素删除。

建议73:删除指针的容器时避免资源泄露

STL容器虽然智能,但尚不能担当删除它们所包含指针的这一责任。

所以,在要删除指针的容器时须避免资源泄露:或者在容器销毁前手动删除容器中的每个指针,或者使用智能引用计数指针对象(比如Boost的shared_ptr)来代替普通指针。

建议74:选择合适的STL容器

容器分为:

  1. 标准STL序列容器:vectorstringdequelist
  2. 标准STL关联容器:setmultisetmapmultimap
  3. 非标准序列容器:slist(单向链表)和rope(重型字符串);
  4. 非标准关联容器:hash_sethash_multisethash_maphash_multimap
  5. 标准非STL容器:数组bitsetvalarraystackqueuepriority_queue

建议75:不要在STL容器中存储auto_ptr对象

auto_ptr是C++标准中提供的智能指针,它是一个RAII对象,它在初始化时获得资源,析构时自动释放资源。

C++标准中规定:STL容器元素必须能够进行拷贝构造和赋值操作。

禁止在STL容器中存储auto_ptr对象原因有两个:

  1. auto_ptr拷贝操作不安全,会使原指针对象变NULL
  2. 严重影响代码的可移植性。

建议76:熟悉删除STL容器中元素的惯用法

  1. 删除容器中具有特定值的元素:如果容器是vectorstringdeque,使用erase-remove的惯用法(remove只会将不应该删除的元素前移,然后返回一个迭代器,该迭代器指向的是那个应该删除的元素,所以如果要真正删除这一元素,在调用remove之后还必须调用erase);如果容器是list,使用list::remove;如果容器是标准关联容器,使用它的erase成员函数;
  2. 删除容器中满足某些条件的所有元素:如果容器是vectorstringdeque,使用erase-remove_if惯用法;如果容器是list,使用list::remove_if;如果容器是标准关联容器,使用remove_copy_if & swap组合算法,或者自己写一个遍历删除算法。

建议77:小心迭代器的失效

迭代器是一个对象,其内存大小为12(sizeof(vector<int>::iterator),vs2010,32bit)。引起迭代器失效的最主要操作就是插入、删除。对于序列容器(如vectordeque),插入和删除操作可能会使容器的部分或全部迭代器失效。因为vectordeque必须使用连续分配的内存来存储元素,向容器中添加一个元素可能会导致后面邻接的内存没有可用的空闲空间而引起存储空间的重新分配。一旦这种情况发生,容器中的所有的迭代器就会全部失效。

建议78:尽量使用vector和string代替动态分配数组

相较于内建数组,vectorstring具有几方面的优点:

  1. 它们能够自动管理内存;
  2. 它们提供了丰富的接口;
  3. 与C的内存模型兼容;
  4. 集众人智慧之大成。

建议79:掌握vector和string与C语言API的通信方式

使用vector::operator[]string::c_str是实现STL容器与C语言API通信的最佳方式。

1
2
3
4
vector<int> intContainer;
......
int *pData = NULL;
pData = &(intContainer[0]); // 因为vector的存储区连续


建议80:多用算法调用,少用手写循环

用算法调用代替手工编写的循环,具有几方面的优点:

  1. 效率更高;
  2. 不易出错;
  3. 可维护性更好。
1
2
3
4
5
6
7
// 使用循环
for (iter = Container.begin(); iter != Container.end(); )
{
iter->DoSomething();
}
// 使用算法
for_each(Container.begin(), Container.end(), mem_fun_ref(&ContainerElement::DoSomething));




未完,见:《编写高质量代码——改善C++程序的150个建议》读书笔记(2)