本文发自 http://www.binss.me/blog/my-excerpt-of-effective-c++/,转载请注明出处。

最近把Effective C++过了一遍,受益匪浅,然而也有许多未理解之处,期望日后第二遍时有更深入的理解。

2015-04-15

第一遍阅读。写文记之。

2015-12-21

进行第二遍阅读,对以下摘录和体会进行了修改。

以下是读书过程中的摘录和体会:

导读

对于等号运算符,如果有新对象被定义(如A b = c;),则调用拷贝构造函数,而如果没有新对象被定义(如b = c;),则调用拷贝赋值构造函数。

1 视C++为一个语言联邦:

C++包含以下四个次语言:

  1. C
  2. Object-Oriented C++:面向对象(封装、继承、多态、虚特性)
  3. Template C++:泛型编程(模版),模版元编程(TMP)
  4. STL:标准模版库(容器,迭代器,算法,函数对象)

2 尽量以const,enum,inline替换 #defines

#define 可能在预处理阶段就已经处理完,出现编译错误时难以定位。

#define 不重视域,不仅不能用来定义class的专属变量,也不能提供任何封装性。

对于常量,用const对象或enums替换#defines

对于形似函数的宏,用inline替换#defines

3 尽可能使用const

通过编译器(抛出错误)来约束值不允许被改变。

对于指针,const出现在*左边,表示指针所指对象是常量,出现在*右边,表示指针自身是常量。

对于对象的传参,const约束了不可通过引用来修改原参数。

对于成员函数,const约束了函数不能改变对象内的任何非静态成员对象。

对于STL容器,const_iterator约束了迭代器所指的对象不允许改动。

使用const函数来实现非const的相同函数(通过static_cast转为const对象来调用,然后再const_cast转回非const)可以避免函数重复。但是反之不行:因为为了对调用非const的函数,要使用const_cast释放掉const属性,导致对象可能被改动,违反了const的承诺。

4 确定对象被使用前已先被初始化

确保构造函数对每一个成员变量都进行初始化。对于对象,应该使用初始化列表(只调用拷贝构造函数,相比=节省了调用默认构造函数的开销)。

对象初始化的顺序为:父类构造函数->成员变量(按声明顺序)->自身构造函数,与初始化列表的顺序无关。

对于no-local static对象(处于global或者namespace作用域内,或者在class或file作用域内被声明为static),为了避免某编译单元内的某个no-local static的初始化动作使用另一个编译单元内未初始化的no-local static对象(因为C++对其初始化次序没有明确定义),应该使用local static对象替换no-local static对象:定义同名函数,在函数中将对象声明为static(local static),然后返回它的引用。这样会在使用时调用同名函数,然后对象lazy地被初始化。

5 了解c++默默编写并调用哪些函数

只有当有需要(被调用)时,编译器才会创建默认构造函数、析构函数、拷贝构造函数、拷贝赋值操作符。但如果自动生成的代码不合法或无意义,则编译器会报错。如赋值操作符函数中对const成员进行赋值或修改引用。

6 若不想使用编译器自动生成的函数,就应该明确拒绝

如果不想编译器自动生成拷贝构造函数、拷贝赋值操作符函数,把他们声明为private且不去实现它,这样函数被调用时会在连接阶段抛出错误。

可以把错误提前至编译器:定义一个类,将构造和析构函数声明为protected,将拷贝构造函数、拷贝赋值操作符函数声明为private,然后作为私有继承之。

7 为多态基类声明virtual析构函数

基类指针指向的子类对象在析构时只会调用了基类析构函数,导致内存泄漏,应该将基类的析构函数声明为virtual。

因此不应该继承string或STL容器,它们没有实现virtual析构函数。

如果类不准备作为基类或不是用作多态,那么就不要声明析构函数为virtual,因为virtual会产生指向虚表的指针,增加类对象所占的空间。

8 别让异常逃离析构函数

若析构函数抛出异常,可能将导致程序过早结束或其他不明确行为。因此析构函数应该捕捉所有异常,然后吞下它们(记录后中止传播)或结束程序。

如果客户需要对异常作出反应,应该提供一个普通函数执行析构函数做的操作,避免客户因异常被吞产生怨言。

9 绝不在构造和析构过程中调用virtual函数

子类对象在基类构造(调用基类构造函数)和基类析构(调用子类析构函数)期间,属于基类类型对象,因此调用的也是基类版本的虚函数。

10 令operator=返回一个reference to *this

便于连锁赋值。同理,还有+=,-=,*=等。

11 在operator=中处理“自我赋值”

operator=时,我们往往释放原来自身的对象,然后复制传入的对象赋给自身。然而当传入对象就是自身时,该对象会被释放,导致复制了已被删除的对象。

可以采用比较来源对象和目标对象的地址(判断是否相等),调整语句顺序(先用临时指针记住原来自身的对象,然后复制传入对象赋给自身,最后删除临时指针所指对象,这样还避免了复制抛出错误时删除语句的执行),copy-and-swap(使用复制构造函数复制对象,然后交换)等方法解决。

12 复制对象时勿忘其每一个成分

在写子类的拷贝构造函数时,不要忘记使用初始化列表调用父类的拷贝构造函数(: parent(rhs))。而在写子类的拷贝赋值操作符时,应该显式调用父类的拷贝赋值操作符(parent::operator=(rhs);)。

不要在拷贝赋值操作符中调用拷贝构造函数,也不要在拷贝构造函数中调用拷贝赋值操作符。若两者具有相近的代码,应该把代码放到私有成员函数init中,然后调用之。

13 以对象管理资源

为了避免对象因为各种原因在使用完后没有释放,应该在获取资源后立刻将对象放进管理对象中(RAII,资源获得时就是初始化时),使得在离开区块时,管理对象调用析构函数来释放资源。常用的管理对象有std::auto_ptr(C++11被std::unique_ptr取代),std::tr1::shared_ptr。本质上为智能指针。

例:std::tr1::shared_ptr<classname> p(createObject());

14 在资源管理类中小心copying行为

复制RAII对象时必须一起复制他所管理的资源。

常见的复制行为有:禁止复制(private)、引用计数(shared_ptr计数+重载删除器)、复制底部资源(深拷贝)、转移底部资源所有权(auto_ptr)等。

15 在资源管理类中提供对原始资源的访问

RAII类并不是为了封装资源而存在,而是为了确保资源释放行为的发生而存在。

每一个RAII类都应该提供获取其管理的原始资源的方法,如显式转换(提供get函数,比较安全)或隐式转换(提供隐式转换函数,比较方便)

16 成对使用new和delete时要采取相同形式

数组所用的内存通常还包括数组长度(n),以供delete时获取需要释放的对象数。

如果在new的时候使用[ ],则在相应的delete的时候也要使用[ ]。加上[ ]后,delete会认为指针指向一个数组,然后读取数组长度并依次多次调用析构函数。

尽量避免对数组做typedef操作,避免new时没看到[ ]而导致在delete时没使用[ ]。

17 以独立语句将newed对象置入智能指针

如果不使用独立语句,由于编译器有重新排列语句内的执行顺序的自由,若执行过程中抛出异常,有可能没有将对象置入智能指针中管理,从而导致资源泄漏。

18 让接口容易被正确使用,不易被误用

通过建立新类型来束缚对象值(比如说创建Year, Month, Day类型并设立数值范围)

保持接口的一致性(比如都用size()取长度)且和内置类型的行为兼容。

通过tr1::shared_ptr可以消除客户的资源管理责任。

19 设计class犹如设计type

设计类时,要考虑是否真的需要,然后考虑创建和销毁、初始化和赋值、值传递、合法值、继承、转换、操作符和成员函数、公有、私有、保护成员,还有是否使用模版(一般化)。

20 宁以pass-by-reference-to-const替换pass-by-value

传常引用可以避免传非内置类型参数时调用复制构造函数和析构函数的开销。

可以避免子类对象传值到父类参数时的切割问题(只调用了父类的复制构造函数导致子类特化信息丢失)。

对于内置类型以及STL迭代器和函数对象,传值效率会高些。

21 必须返回对象时,别妄想返回其reference

要返回引用,则必须创建返回对象,而为了避免创建的对象被释放,必须放在堆空间,然而由于无法对引用的对象进行析构,将导致内存泄漏。指针同理。

返回对象所带来的构造和析构可以被编译器优化,如RVO(return value optimization)和NRVO(named return value optimization),即使用临时对象来构造引用对象 和 使用引用对象替换返回对象进行操作。

22 将成员变量声明为private

声明为private然后用函数进行访问,满足了高封装性(体现为变量改变时所破坏代码的数量),为类的作者保留了实现的弹性(便于日后变更)。

从封装性的定义看,protected不比public更具封装性。

23 宁以non-member、non-friend替换member函数

面向对象手中要求高封装性。

用non-member、non-friend替换member函数可以增加封装性(减少了类暴露出的接口),包裹弹性(可分割)和机能扩充性(用户可扩展)。

24 若所有参数皆需类型转换,请为此采用non-member函数

对于运算符重载,作为member函数,要求第一个操作数必须为类对象(构造函数不为explicit,第二个操作数会调用构造函数做隐式转换),否则报错。所以应该使用non-member函数(放在类外,由于要调用的是public的构造函数,所以无须为friend),把该类对象作为参数。

25 考虑写出一个不抛异常的swap函数

如果默认的swap(std::swap)的效率低下,应该自己实现它:在类内提供一个public的swap成员函数(要避免抛出异常),然后在类所在的命名空间内提供一个non-member的swap来调用类的swap成员函数(对于类而非模版来说,应该特化std::swap)。

调用swap时,应该使用using std::swap来保证专属版本不存在时能够调用std版本。

26 尽可能延后变量定义式的出现时间

应该尽可能延后变量的定义,直至非得使用该变量的前一刻为止,甚至延后到能够在定义时直接赋初值。这样可以避免没必要的构造和析构,提高效率。

27 尽量少做转型动作

const_cast:移除对象的常量特性(const -> non-const)

dynamic_cast:执行安全向下转型(base * -> derived ),亦可执行向上转型(derived * -> base

reinterpret_cast:执行低级转型(int pointer -> int,很少见)

static_cast:用来强迫隐式转换(non-const -> const,int -> double,void * -> int ,非安全的base * -> derived 等)

转型开销大,应该尽量避免。

如果必须使用转型,应该尽量使用易于辨识且分工明确的新式转型(虽然语法很恶心)。

调用父类的虚函数时,应该使用Base::function而不是对this进行转型后调用function,因为转型后function改动的并非this,而是this的副本。

28 避免返回handles指向对象的内部成分

尽量不要返回private,protected成员的引用、指针和迭代器(handles,用来获取某个对象的号码牌),否则这些成员能够在类外被修改,导致封装性降低。

就算指定handles为const,也可能导致handles虚吊(因为临时对象在语句执行完后被析构导致handles指向一个不存在的成员)。

29 为“异常安全”而努力是值得的

异常安全函数的保证有三个级别:

  1. 基本承诺:若有异常被抛出,保证任何事物(包括数据结构)仍然保持有效

  2. 强烈保证:如果函数失败(抛出异常),则回滚到调用函数之前的状态。可以通过copy-and-swap实现(难以实现)。

  3. 不抛掷(nothrow)保证:承诺绝不抛出异常

函数的异常安全保证取决于其调用函数的最弱者,若最弱者没有异常安全保证,则只能将它设为“无任何保证”(短板理论)。

int doSomething() throw();

成员函数声明后面跟上throw()承诺了函数不会抛出异常,在使用该方法的时候,不必把它至于 try/catch 异常处理块中。因此在声明一个不抛出异常的函数后,应该保证在函数的实现里面不会抛出异常。

30 透彻了解inlining的里里外外

模版和inline函数常常都被定义在头文件(编译器要做inline替换),函数模版不一定是inline。

对于inline函数的每一个调用,都用函数本体去替换之,避免了函数调用的开销。替换后,编译器可能可以对它执行语境相关最优化。

缺点:增加目标码大小,导致代码膨胀,从而出现额外的换页,降低了指令高速缓存的命中率。一旦对inline函数进行修改将导致大量的重编译(而不仅仅是重连接)。无法断点,不利于调试。应该谨慎使用。

inline只是对编译器的申请,编译器在以下情况下会忽略之:复杂函数(循环,递归),virtual函数,通过函数指针进行调用。

31 将文件间的编译依存关系降至最低

如果include的头文件及它们所依赖的其他头文件被改变,任何使用它的文件也必须重新编译。这种编译依赖关系使得有时小小修改后造成大量的重新编译和连接工作。

编译器必须在编译期间知道对象的大小,然后分配相应的空间。

为了降低编译依存关系,可以使用interface class和handle class两种手段来将声明和实现分离。

handle class:

handle class和implementation的成员函数完全相同,handle class使用shared_ptr指向handle class的对象,并通过该对象调用其对应的相同函数。

interface class:

interface class直接实现纯虚函数,然后拥有指向自身对象(调用子类的构造函数生成)的static shared_ptr。然后在子类中实现父类中的虚函数。

32 确定你的public继承塑模出is-a关系(make sure public inheritance models “is-a”)

public继承意味着是is-a关系。适用于基类的函数一定要能够适用于派生类(企鹅继承鸟就会出问题,因为鸟会飞而企鹅不会飞)。

如果不想继承父类的所有函数,那么就不要使用public继承,否则会违反is-a原则。

33 避免遮掩继承而来的名称

子类的名称会掩盖父类的名称,如果父类实现了多个同名函数(重载),而子类只实现了一个,那么其他同名的父类函数会被掩盖,违反了public继承的定义。

解决方法:

  1. 使用using:using Base::mf1;

  2. 使用转交函数:virtual void mf1(){ Base::mf1(); }

    通过private继承+转交函数,可以屏蔽不想继承的函数。不是public继承,也就不会违反其定义。

34 区分接口继承和实现继承

public继承下,派生类总是继承父类的接口:

声明纯虚函数的目的是让派生类只继承函数接口。

声明虚函数(非纯)的目的是让派生类继承该函数的接口和缺省实现。

声明非虚函数的目的是让派生类继承函数的接口和一份强制性实现(不变性凌驾于特异性,一般来说子类不应该重载)。

可以为纯虚函数提供定义,但只能通过Base::function()调用,适用于子类必需实现却又需要缺省实现的情况。

35 考虑virtual函数以外的其他选择

  1. 通过non-virtual interface实现template method模式

    通过公有的非虚成员函数(wrapper,包装器)间接调用私有(或保护)的虚函数

    包装器能确保在虚函数调用之前设定好场景,并在调用结束后清理场景。

  2. 通过function pointers实现strategy模式

    将函数实现成类外的普通函数,然后将函数指针当作参数传入,被成员函数调用,但该函数无法访问类的非公有成员。

  3. 通过tr1::function完成strategy模式

    使用tr1::function来保存函数提高了更大的弹性(参数和返回类型都可以被隐式转换,参数可以为:函数指针、函数对象、bind成员函数)

  4. 古典的strategy模式

    使用指针指向函数类的对象,函数类实现虚函数,然后通过类对象调用。

36 绝不重新定义继承而来的non-virtual函数

对于非虚成员函数,通过什么类的指针调用的就是什么类对应的函数(静态绑定),若子类重新定义了父类的non-virtual函数,则指向子类对象的基类指针会调用基类的函数。

不要重新定义继承而来的non-virtual函数,因为违反了public继承“不变形凌驾于特异性”原则。

37 绝不重新定义继承而来的缺省参数值

静态类型:声明的类型

动态类型:目前所指对象的类型

静态绑定:根据静态类型调用函数

动态绑定:根据动态类型调用函数

虚函数是动态绑定,而缺省参数值却是静态绑定,当使用指向子类对象的基类指针调用虚函数时,会调用子类的函数,然而使用的却是基类提供的缺省参数值。

即使子类函数与父类函数的缺省参数值一致,也会有代码重复和相依性问题。可以采用NVI,使用成员函数提供接口和缺省参数值,然后调用私有的虚函数。

38 通过复合塑模出has-a或“根据某物实现出”

复合发生在应用域的对象之间时,表现出has-a(拥有)关系;发生在实现域对象之间时,表现出is-implemented-in-terms-of(根据某物实现出)关系。

在继承无法解决时,考虑复合。比如自定义容器时包含STL容器。

39 明智而审慎地使用private继承

和public继承不同,编译器不会将private继承的子类对象转化为基类对象。

private继承意味着is-implemented-in-terms-of(根据某物实现出),只继承了实现而没有接口。因此纯粹是一种实现技术,而没有设计层面的意义。

private继承可以访问基类的protected成员、重新定义虚函数,然后阻止子类重新定义虚函数。

对于大小为0的独立(非附属)对象,C++默默安插一个char到空对象内(sizeof>0)。由于内存对齐,可能会导致又占用了额外的空间。

而EBO(空白基类最优化)可以通过继承来避免使用额外的空间,但只能用于单继承。因此如果对空间要求高,可以选择private继承而不是复合。

除此之外,应该尽可能使用复合。

40 明智而审慎地使用多重继承

为了消除多重继承中同名函数的歧义,应该使用域运算符指出。

对于钻石型多重继承,应该让子类使用虚继承基类。

虚继承会带来对象大小增加、访问速度降低、初始化(及赋值)负担增加等成本。若必需使用,尽量避免在基类中放置数据。

多重继承的一种合理用途:public继承接口类,private继承协助实现类。

41 了解隐式接口和编译器多态

classes:接口为显式,基于函数签名(声明)。多态通过虚函数实现,发生于运行期。

template参数:接口为隐式,基于有效表达式。多态通过template具现化和函数重载解析(对象在隐式转换后必须能够执行表达式的操作),发生于编译期。

加诸于class对象上的显式接口和加诸于template参数上的隐式接口都会在编译器完成检查。

42 了解typename的双重意义

声明template参数时,class和typename意义相同。typename更好,因为class通常泛指自定义类型,不包括基本类型。

嵌套从属名称:template内出现的名称依赖于某个template参数,且在class内呈嵌套状,如C::const_iterator。规定嵌套从属名称默认情况下不是类型。若要用作类型,需要在声明时加typename,即typename C::const_iterator iter

同理还有:typename std::iterator_trait<IterT>::value_type value_type; 意为IterT对象所指物的类型,因为依赖于IterT参数,所以需要加typename。

在继承父类列表和构造函数的成员初始列中,不允许使用typename。

43 学习处理模板化基类内的名称

对于继承模版化基类的子类,成员函数内不能直接调用父类的成员函数。因为编译器不知道它继承的类被什么具现化:若基类用全特化另外实现了某个类型,则该特化类的这个成员函数可能不存在。

为了能够调用父类的成员函数,有以下方法:

  1. 使用this->调用函数。如this->sendClear(info);

  2. 使用using声明式。如using MsgSender<Company>::sendClear;

  3. 使用域作用符。如MsgSender<Company>::sendClear(info);。但如果sendClear是虚函数,则会丢失虚特性。

44 将与参数无关的代码抽离

template只有在被使用到的情况下才会具现化,尽管如此,在类体系中使用模版常常容易造成代码膨胀。

因非类型模版而造成的代码膨胀,往往可以通过函数参数或类成员变量替换template参数来消除。

因类型参数(比如带基本类型的参数)而造成的代码膨胀,往往可以通过让带有完全相同二进制表述的具现类型共享实现码来降低。

45 运用成员函数模版接收所有兼容类型

同一个template的不同具现体之间并没有什么固有关系。为了具现间能够进行转换,引入成员函数模版。如:

template<typename T>
class SmartPtr{
    template<typename U>
    SmartPtr(const SmartPtr<U>& other);
    ......
};

对于任何类型U和T,可根据SmartPtr<U>生成SmartPtr<T>

除了构造函数,成员函数模版一般还应用于拷贝构造函数和拷贝赋值构造函数,用于支持具现间的赋值操作。

即使声明了泛化的模版拷贝构造函数和拷贝赋值函数,也需要声明普通的拷贝构造函数和拷贝赋值函数(不然编辑器会自动生成)。

46 需要类型转换时请为模板定义非成员函数

#24 的模板化版本。然而实现时编译出错,因为对于传入的第二个参数int,编译器无法推算出类型T(编译器不会利用隐式类型转换,如构造函数,进行template推导)

这时需要声明函数为friend并在类中实现(内部friend)。这样函数会随着第一个参数的声明而具现,成为一个函数而非函数模版,从而能够进行隐式转换。

当然,也可以采用在类中声明为friend,然后调用普通类外模版函数的方法。这样可以避免声明式inline的开销。

这样既实现了函数的自定具现化(能够具现模版),又实现了非成员函数的要求(能够类型转换)。

47 请使用traits classes表现类型信息

STL迭代器分类:

  1. input:只能向前移动,一次一步,只读,执行一次,如istream_iterator

  2. output:只能向前移动,一次一步,只写,执行一次,如ostream_iterator

  3. forward:只能向前移动,一次一步,可读和写,可执行多次,如hashed容器的迭代器

  4. bidirctional:可向前向后移动,一次一步,可读和写,可执行多次,如list、set、map迭代器

  5. random access:可向前向后移动,一次多步,可读和写,可执行多次,如string、vector、deque

如果我们用template实现迭代器移动函数,则需要根据其类型来进行相应的移动操作(一次一步 or 一次多步)。为了得知模版传入的迭代器类型,可以采用 traits,使类型相关信息在编译期可用 。通过在不同的迭代器内通过 typedef 定义相同变量为不同 tag,然后通过实现以不同tag为参数的函数重载即可。

总结,定义:

  1. 确定将来希望取得的类型相关信息,并为该信息选择一个名称(如iterator_category)

  2. 提供一个template和一组特化版本(如iterator_traits),内含希望支持的类型相关信息。

使用:

  1. 建立一组重载函数(劳工)或函数模板,差异只在于traits参数。令每个函数实现与其接收的traits相对应。

  2. 建立一个控制函数(工头)或函数模板,调用劳工函数并传递traits class所提供的信息。

48 认识template元编程

TMP(template metaprogramming)将工作由运行期提前至编译期,实现了早期错误侦测和更高的执行效率。如:

template<unsigned n>
struct Factorial{
     enum{ value = n * Factorial<n-1>::value };
};
// 特化
template<>
struct Factorial<0>{
     enum { value = 1 };
};
// 调用
Factorial<5>::value;

TMP的用途:

  1. 确保量度单位正确(编译期检查)

  2. 优化矩阵运算。(消除临时对象,合并循环)

  3. 可以生成客户定制设计模式的实现品

49 了解new-handler的行为

当new无法满足内存申请时,错误处理函数new-handler会不断被调用,直到获取到足够内存为止。

new-handler必须通过set_new_handler()指定,其接受一个函数指针,返回一个函数指针。

设计良好的new-handler必须做到:

  1. 让更多内存可被使用:程序一开始执行就分配一大块内存,当handler第一次被调用时将它们释放给程序使用。

  2. 安装另一个new-handler:替换为有能力解决问题的new-handler。

  3. 卸除原来的new-handler:set_new_handler(null),从而在内存分配不成功时抛出异常。

  4. 抛出bad_alloc异常:不会被new捕捉,会被传播到内存索求处

  5. 不返回:调用abort或exit

若想使不同类有不同的new-handler,应该定义类模版,然后有需要的类继承他的具现化:class Widget: public NewHandlerSupport<Widget>

这样做的目的是令类拥有不同的NewHandlerSupport(里面有static成员)

使用nothrow的new可以在分配内存失败时不抛出异常,而是返回null。然而却不能保证后面调用的构造函数不会抛出异常,所以没有使用的必要。

50 了解new和delete的合理替换时机

替换new和delete的理由:

  1. 为了检测运用上的错误(delete失败、多次delete、overrun,underrun)

  2. 为了收集动态分配内存的使用统计信息

  3. 为了增加分配和归还的速度(定制比泛用型快)

  4. 为了降低缺省内存管理器带来的空间额外开销(定制比泛用型省内存)

  5. 为了弥补缺省分配器中的非最佳齐位

  6. 为了将相关对象成簇集中(减少缺页错误)

  7. 为了获得非传统的行为(如共享内存)

51 编写new和delete时需固守常规

new里面应该包含一个无限循环,不断调用handler,只有两种终止方式:

  1. 分配成功

  2. 分配失败,找不到handler,抛出错误

还要能够处理0-byte的申请。

delete在收到null指针时应该不做任何事。对于类的专属版本,要考虑被继承的情况,处理“比正确大小更大的(错误)申请”,最佳做法是改为调用标准的new/delete。

52 写了placement new也要写placement delete

placement new是添加了自定义参数的new,当然,第一个参数依然需要是 size 。常见的应用有从内存池中分配内存:

void* operator new(size_t sz, Pool& p)
{
    return p.allocate(sz);
}
void operator delete(void *ptr, Pool& p)
{
    return p.deallocate(ptr);
}

如果placement new执行失败,会在运行期调用参数个数和类型都和其相同的placement delete。如果该placement delete不存在,则可能会导致内存泄露。

placement delete只会在placement new失败时调用。在使用delete手动析构时,调用的是delete。

为了避免placement new和placement delete遮盖正常(global)版本,应该建立一个包含正常版本new和delete的基类然后继承它,在要用的时候使用using声明。

53 不要轻易忽略编译器的警告

严肃对待编译器发出的警告信息,努力实现没有warning。

不要过度依赖编译器的警告,因为警告的标准和方式依赖于编译器。

54 让自己熟悉包括TR1在内的标准程序库

TR1代表technical report 1,是一份C++标准规范

TR1提供的新组件:

  1. 智能指针:tr1::shared_ptr和tr1::weak_ptr

  2. 可调用物tr1::function:只要签名符合(隐式转换亦可),就可以表示函数。 如:void registerCallback( std::tr1::function<std::string (int)> func )

    表示参数func为函数,该函数接受一个int(或可转换为int)参数并返回string(或可转换为string)。

  3. 绑定器:tr1::bind

  4. Hash tables:实现tr1::unordered_set,tr1::unordered_multiset,tr1::unordered_map,tr1::unordered_multimap。

  5. 正则表达式:包括查找和替换

  6. Tuples(变量组):类似于python中的元组

  7. Array:大小固定的stl化数组(固定长度的向量)

  8. mem_fn:成员函数指针

  9. reference_wrapper

  10. 随机数生成工具

  11. 数学特殊函数

  12. C99兼容扩充

  13. type traits:提供类型的编译期信息

  14. result_of:推导函数调用的返回类型

55 让自己熟悉boost

boost是一个C++程序库开发的社群,在C++标准化过程中扮演重要角色。它提供了很多TR1规范的实现品(库)。

boost虽好,几百M的大小和麻烦的安装过程令人望而却步。相比之下,python下利用pip按需安装库显得方便的多。