Effective C++读书笔记

0 导读

  • default构造函数:没有参数或者每个参数有缺省值。
  • 尽量用explicit修饰构造函数:阻止构造函数隐式转换。
  • copy构造函数与copy assignment操作符:有新对象一定调用一个构造函数,不可能调用赋值。没有新对象就没有构造函数调用。
    • 函数参数传递pass-by-value调用copy构造函数。
      Widget(const Widget& rhs); //copy构造函数
      Widget& operator=(const Widget& rhs); //copy assignment
      //使用
      w1(w2); //copy构造函数
      w1 = w2; //copy assignment
      Widget w1 = w2; //copy构造函数
      

1 让自己习惯C++

条款01:视C++为一个语言联邦
  • C++是多重范型编程语言,包含四种次语言。每次在一个次语言中遵守各自的守则;不同次语言的守则不同。
    1. C
    2. Object-Oriented C++:C with Classes
    3. Template C++:泛型编程。Template威力强大,带来了TMP(模板元编程)
    4. STL:是Template程序库
  • 当切换次语言时,高效编程当守则要求很可能改变。例如传参:内置类型即C-like下pass-by-value高效于pass-by-reference,Object-Oriented C++尤其Template C++下自定义类型由于构造析构函数pass-by-reference更高效,STL由于迭代器和函数对象构造在指针上所以前者更好。
条款02:尽量以const,enum,inline替换#define
  • define被预处理器处理因而编译器无法看到该变量,可能导致在调试过程中的迷惑,用const使变量名称进入符号表。另外对浮点常量const可能有更小的码。const还可以限制该常量使用的作用域。
  • 当编译器不允许类内static变量的初值设定,并且类内一个数组使用该值作为大小,可用定义一个类内匿名enum。enum和define相似,不能取变量地址,不会导致不必要的内存分配。
  • template inline能获得宏带来的效率和一般函数的所有可预料行为和类型安全性。
条款03:尽可能使用const
  • 只要值保持不变是事实就应该确实说出来。
    • 指针const:以*为界限区分指针还是指向是const
      • const指针能指一个const和非const变量。不保护所指向的内容。
      • 指向const的指针能指多个const变量,保护所指向的内容。
    • 令函数返回const可降低错误造成的意外。
  • const成员函数
    • 两个成员函数只是常量性不同就可以被重载。
      • bitwise constness(编译器强制):const成员函数不可以更改对象内任何non-static成员变量。
      • logical constness:可以修改某些bit。用mutable修饰要改的变量。
  • const和non-const成员函数中避免重复:使用两次const转换使non-const函数调用const函数。
条款04:确定对象被使用前已先被初始化
  • 通常如果使用C part of C++并且初始化导致运行期成本则不保证初始化:array不保证而vector保证。
  • 在构造函数使用初始化列表而不是赋值会更高效:前者相当于使用参数调用default构造函数后者相当于调用构造函数后调用赋值函数。然而对于内置类型初始化与赋值的成本相同。
    • 为内置类型对象进行手工初始化,C++不保证初始化。
    • 总是在初值列中列出所有的成员变量。只有在构造函数太多为了避免重复可以单独用一个函数初始化赋值和初始化表现一样好的。
    • class成员变量按照声明次序初始化,初始值列也最好按照这个顺序写。
  • 不同编译单元non-local static对象初始化次序无明确定义
    • 将其搬到专属的函数内成为函数内静态变量(Singleton模式常见手法):调用期间首次遇上对象定义式时初始化。但任何一种non-const static对象无论是否local在多线程下都有麻烦。

2 构造/析构/赋值运算

条款05:了解C++默默编写并调用哪些函数
  • 如果自己没声明,编译器会自动声明如下的函数。这些都是public并inline的。只有当这些函数被调用才被编译器创建(应该是定义的意思)
    1. copy构造函数:单纯地将来源对象non-static成员变量拷贝到目标对象
    2. copy assignment操作符:同1。只有当生成的代码合法并且有适当机会证明有意义时才会生成
      • C++不允许让reference改指向不同对象。如果在含reference成员的class内支持赋值操作必须自己定义copy assingment操作符。内含const成员也一样
      • base class的函数为private时编译器拒绝为子类生成一个函数
    3. 析构函数
    4. default构造析构函数(如果没有声明任何构造函数):调用base classes和non-static成员变量的构造析构函数(先父后子)
条款06:若不想使用编译器自动生成的函数,就应该明确拒绝
  • 禁止对象的复制和赋值拷贝:将copy构造函数和copy assignment操作符声明为private。
    • 进一步禁止member和friend函数调用:只声明不定义,会获得连接错误
    • 将错误从连接期移到编译期:专门设计两个函数为private的base class并继承,这样可能导致多重继承。多重继承有时会阻止empty base class optimization。另,其析构函数不一定得是virtual。可以使用Boost提供的版本。
条款07:为多态基类声明virtual析构函数
  • derived class对象经由带non-virtual析构函数的base class指针被删除,其结果未有定义
  • 不被当作base class的类就不应该声明virtual析构函数:1.会浪费虚表指针的空间;2.不再可能将其对象传递给如C的其他语言,除非对vptr做补偿
  • C++没有机制禁止派生:string、STL等不带virtual析构函数
  • (特殊情况下,如果抽象类找不到其他pure virtual)为抽象class声明一个pure virtual析构函数;然后仍然提供一个定义
条款08:别让异常逃离析构函数
  • 普通方法
    1. 如果抛出异常就结束程序(调用std::abort())
    2. 吞下异常:坏主意,有时也比1好
  • 较佳策略:提供让用户关闭的接口自行处理异常,并追踪状态在析构函数中又关闭
    • 不会给客户带来负担而是给他们一个处理错误的机会
条款09:绝不在构造和析构过程中调用virtual函数
  • 在base class构造期间,virtual函数不是virtual函数
    • 在构造时候,derived class的成员变量并没有初始化,所以C++不允许
    • 构造期间类型是base而不是derived。virtual函数、运行期类型信息如dynamic_cast和typeid也会把对象视为base class类型
  • 构造和析构函数调用的函数也不应该调用virtual函数
  • 要让不同派生类记录不同的信息,可以令derived class将必要的构造信息向上传递至base class构造函数
条款10:令operator=返回一个reference to *this
  • 为了实现连锁赋值,返回reference指向操作符左侧的实参。这个适用于所有赋值相关运算如+=
条款11:在operator=中处理“自我赋值”
  • 在最前做证同测试,判断是否是同一个对象:仍然不具备异常安全性,因为如果new操作可能失败
  • 精心安排语句顺序。让operator=具备异常安全性往往自动自我赋值安全性
    • 此时为了效率可以加上证同测试,但要考虑赋值的频率有多高
      //operator=函数体
      Bitmap* pOrig = pb; //pb是成员变量
      pb = new Bitmap(*rhs.pb); //rhs是参数
      delete pOrig
      return *this
      
  • copy and swap技术:常见而够好的撰写方法
    Widget temp(rhs);
    swap(temp);
    return *this;
    
条款12:复制对象时勿忘其每一个成分
  • 当编写一个copying函数:
    1. 复制所有local成员变量
    2. 调用所有base class内的适当的copying函数:base class中可能含有private成员变量因而只能通过copying函数复制
  • 令copy assignment操作符调用copy构造函数或者反之都是逻辑荒谬的,乃至根本没有相关语法

3 资源管理

条款13:以对象管理资源
  • auto_ptr:析构函数自动调用delete。通过copy构造函数或copy assignment操作符复制它们,它们会变成null;这为了保证不会多个auto_ptr指向同一个对象
  • shared_ptr:RCSP,引用计数型智慧指针
  • 注意
    • 它们内部调用delete,因而不能让它们指向动态分配的array
    • vector和string几乎总是可以取代动态分配而得的数组
条款14:在资源管理类中小心copying行为
  • RAII守则:资源在构造期间获得,在析构期间释放
  • 对RAII对象复制行为应对方法
    1. 禁止复制:copying操作声明为private
    2. 对底层资源祭出“引用计数法”
      • 通常只要内含tr1::shared_ptr:允许指定删除器
    3. 复制底部资源
    4. 转移底部资源拥有权
条款15:在资源管理类中提供对原始资源对访问
  • 可以像auto_ptr和shared_ptr那样提供get()函数返回原始指针的复件
  • 可以重载指针取值操作符允许隐式转换为底部原始指针
  • 可以提供隐式转换函数:用operator操作符定义一个名字为目标类型名的函数如operator TerminalTypeName()
  • 使用何种方法:取决于RAII class被设计执行的特定工作以及被使用情况。通常显式转换函数如get()是比较受欢迎的例子
条款16:成对使用new和delete时要采取相同形式
  • new和delete、new[]和delete[]配套使用:数组使用的内存通常还包括“数组大小”的记录。对内置类型如int也有害,虽然其没有析构函数
  • 最好尽量不要对数组形式做typedef动作。否则其对象也要用delete[]而这样很容易被用错
条款17:以独立语句将newed对象置入智能指针
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority);
  • 上边的调用会导致资源泄露,因为编译器可能将上述语句转化为下边三行:
    1. 执行new
    2. 调用priority()
    3. 调用tr1::shared_ptr构造函数
  • 一旦priority()调用导致异常则指针会丢失。应该写为:
    std::tr1::shared_ptr<Wiget> pw (new Widget);
    processWidget(pw, priority());
    
  • PS:processWidget(new Widget, priority())的调用是无法通过编译的,因为tr1:shared_ptr构造函数需要原始阵阵,但构造函数的explicit要求不能进行隐式转换

4 设计与声明

条款18:让接口容易被正确使用,不易被误用
  • “理想上,如果客户企图使用某个接口却没有获得他所预期的行为,这个代码不该通过编译;如果代码通过的编译,它的作为就该是客户所想要的”
  • 例子一:防止Date(int month, int day, int year)传入错误的数据或者传入顺序错误
    1. 类型系统(type system)限制输入类型:导入简单的外覆类型。创建三个struct Daty、Month、Year。创建class个更好
    2. 限制输入的值正确性:可以用enum但不具备类型安全性,因为enum可以被当作int用。安全揭发是预先定义所有的Month,即class Month中包含各个月份命名的static函数
  • “除非有好理由,否则应该尽量令你的 types行为与内置types一致”。为了提供行为一致的接口
  • shared_ptr
    • tr1::shared_ptr缺省的删除其是来自其诞生所在的那个DLL的delete,所以没有cros-DLL problem
    • boost实现版:动态分配内存记录用途和删除器专用数据,有多线程同步开销不过可以通过修改预处理符号关闭
条款19:设计class犹如设计type
  • 对每个自定义class需要提的十二个问题
  • PS:
    • copy构造函数用来定义一个type的pass-by-value该如何实现。
    • 如果洗碗个允许类型T1被隐式转换为T2,就必须在class T1中写转换函数operator T()或在T2中写一个non-explicit-one-argument的构造函数。如果必须写explicit则要专门写出负责转换的和函数
条款20:宁以pass-by-reference-to-const替换pass-by-value
  • 优点
    • 避免可能存在的大量的构造函数析构函数的调用
    • 避免对象切割问题:derived class传递给base class只有base class的构造函数被调用
  • references往往以为指针实现出来,因此对内置类型往往pass-by-value效率更高,此也适用于STL的迭代器和函数对象
  • 并不是所有小型types都适合pass-by-value:
    1. 对象小不一定copy构造函数不昂贵:许多对象如STL容器需要“复制那西额指针所指向的每一样东西”
    2. 编译器对内置类型和自定义类型的态度截然不同:有的对纯粹的double对象放进缓存器但对只有double的class却不那么做
    3. 自定义类型大小容易变化
条款21:必须返回对象时,别妄想返回其reference
  • 错误做法:为了防止构造函数调用而返回reference
    • 在stack中的reference:坠入无定义行为
    • 返回在heap中的指针:无法delete,如Rational w = x y z;
    • 让返回指针指向static内部变量:多线程安全;如a b == c d的判断问题
  • 干脆直接传value吧,允许编译器实现者施行最优化,改善产出代码的效率却不改变其可观察行为
条款22:将成员变量声明为private
  • 不用public成员变量原因:
    • 语法一致性:public接口都是函数,避免记住是否应该用小括号
    • 使用函数可以让对成员变量的处理有更精确的控制
    • 重要原因——封装:以后改以某个计算替换成员变量而客户不会知道
  • 封装保留了日后变更实现的权力
  • 成员变量的封装性与“成员变量的内容改变时所破坏的代码量”成反比
条款23:宁以non-member、non-friend替换member函数
  • 主要是从包裹弹性角度考虑。任何功能的函数都写在class内部是对面向对象的真实意义的误解,会导致较低的包裹台行
  • 封装的粗糙测量:愈多的函数可访问它,数据的封装性就越低。因而non-member-non-friend函数有更强的封装性
  • 做法
    • 在其他语言如Java这种习惯所有函数必须位于class中,可以让其成为static member函数
    • 在C++中,让non-member函数位于相应的命名空间中。
      • 涉及许多种功能时候最直接做法是把不同种类的功能函数声明到不同的头文件中。比如吧书签相关便利函数声明于一个头文件,将cookie相关便利函数声明于另一个头文件。这正是C++标准程序库的组织方式。
条款24:若所有参数皆需类型转换,请为此采用non-member函数
class Rational
{
public:
    Rational(int numerator = 0, int denominator = 1);
    const Rational operator* (const Rational& rhs) const;
}

result = oneHalf*2; 
//正确,仅在Rational的构造函数non-explicit下,2被通过构造函数隐式转换为类似于Rational temp(2)
result = 2 * oneHalf;
//错误,2中找不到相应的operator*(),查找non-member函数也找不到正确参数类型的函数
  • 上述错误应该写为下列代码:
    class Rational { ... }
    const Rational operator* (const Rational& lhs, const Rational& rhs) { ... }
    result = oneForth * 2;
    result = 2 * oneForth;
    //上述两行代码都正确
    
条款25:考虑写出一个不抛出异常对swap函数
  • 当T类是pimpl手法即含有指向一个对象的指针,其operator=()实现为复制指针指向的对象,那么就会多出没必要的复制。其实只需要指针复制即可:
    namespace std
    {
    template <typename T>
    vois swap(T& a, T& b)
    {
      T temp(a); a = b; b = temp(a);
    }
    }
    
  • 可以创建std::swap()的全特化版本
    • template<>表示它是std::swap的一个全特化版本。通常不允许改变std命名空间任何东西但是被允许为标准template制造特化版本
    • 下边代码中Widget中含有swap()专门处理pimpl的复制。a.swap()形式是为了能够访问b中的私有数据
    • 和STL容器有一致性
      namespace std
      {
      template<>
      void swap<Widget>(Widget& a, Wiget& b)
      {
      a.swap(b);
      }
      }
      
  • 当Widget是一个class template时候
    • template<typename T> void swap<Widget<T>>(Widget<T>& a, Widget<&> b)这种叫偏特化,在模板函数上是不允许的,只允许在模板类中
    • 可以用重载的方法,但是std命名空间不应该如此增加新的内容。可以在自己的命名空间写类和重载函数
  • 在调用swap()的模板函数中使用using std::swap:首先查找T中的,再查找std全特化版,最后使用std的swap()
  • 总结,怎么做:
    1. 提供public swap()成员函数,不该抛出异常
    2. 在class或template的命名空间提供non-member swap,并调用上述swap()
    3. 如果编写的是class而不是class template则特化std::swap()并调用swap()成员函数
    4. 调用swap()时候,确定包含using std::swap;曝光函数

5 实现

条款26:尽可能延后变量定义式的出现时间
  • 不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该延后到能够给它初值实参
    • 可能在使用前被抛出的异常中断,这就真的没被用浪费构造析构开销
    • 有初值时候可以用初值作为参数进行构造避免另外的赋值开销
  • 在循环内还是外定义变量取决于赋值和构造析构的成本比较,还有作用域的名称影响也是考虑因素
条款27:尽量少做转型动作
  • 旧式转型
    //C风格转型
    (T) expression;
    //函数风格转型
    T( expression );
    
  • 新式转型
    1. const_cast<T>( expression ):唯一有能力的将对象常量性转除
    2. dynamic_cast<T>( expression ):“安全向下转型”决定某对象是否归属继承体系中的某个类型,可能耗费重大运行成本;许多实现版本执行速度相当慢
    3. reinterpret_cast<T>( expression ):意图执行低级转型实际动作可能取决于编译器,不可移植
    4. static_cast<T>( expression ):强迫隐式转换,如将non-const转为const
  • 唯一使用旧转型的时机是调用一个explicit构造函数将一个对象传递给一个函数时:
    class Widget
    {
      explicit Widget(int size);
    };
    DoSomeWork(Widget(15)); //没错,这是转型而不是创建新对象
    DoSomeWork(static_cast<Widget>(15)); //转型
    
  • 转型往往真的令编译器编译出运行期间执行的代码:单一对象如derived对象可能拥有一个以上的地址;int和double有不同的底层描述;对象布局方式和地址计算方式随编译器不同而不同
  • static_cast<BaseClass>(*this).VirtualFunc();:调用的是父类的函数,但是其操作的是转型建立的*this的base成分暂时副本
  • 优良的C++代码很少使用转型,应该即可能隔离转型动作通常隐藏在某个函数中。要避免使用效率低下的dymanic_cast包括一连串的dynamic_cast
条款28:避免返回handles指向对象内部成分
  • 成员变量的封装性最多只等于“返回其reference”的函数的访问级别
  • 也不要返不被公开使用的函数的handles
条款29:为“异常安全”而努力是值得的
  • 异常安全函数:1.不泄露任何资源;2。不允许数据败坏。(我们应让代码)提供以下三个保证之一:
    • 基本承诺:异常抛出任何事物仍然保持在内部前后一致状态;如恢复原状态或到某个缺省状态
    • 强烈保证:恢复到原状态
    • 不抛掷保证:内置类型上所有操作都提供nothrow保证
      int doSomething() throw();
      //带着“空白的异常明细”函数不一定是nothrow保证函数。而是如果抛出异常是严重错误会有某个意想不到函数调用,用set_unexpected()设置
      
  • 不要为了表示某件事情发生而改变对象状态,除非那件事情真的发生了:shared_ptr.reset(new)只有在new发生后才会改变值
  • 一般化设计策略典型地导致强烈保证——copy and swap:为原件做副本在副本上做修改
    • 如果只操作局部性状态,便容易提供强烈保证
    • 效率可能有问题,此时只提供基本保证也是可以的
条款30:透彻了解inlining的里里外外
  • 即使拥有虚拟内存,inline造成的代码膨胀亦会导致额外的换页行为,降低指令高速缓存装置的击中率
  • inline只是对编译器的一个申请,可以隐喻或明确提出。隐喻:定义于class定义式的函数(成员函数、friend函数)
    • 大部分编译器拒绝将太过复杂(如有循环和递归)的函数内联,对所有virtual函数的调用也是。通常不对通过函数指针进行的调用实施内联
    • 大多编译器提供了一个诊断级别:如果不能将要求的函数内联,会给警告信息
  • inline函数通常一定被置于头文件中。少量环境如.NET-CLI竟可在运行期间完成inlining
  • 有个异常在对象构造期间抛出,已构造好的部分会被自动销毁
  • inline函数无法随着程序库的升级而升级
  • 一开始除非必要先不要讲任何函数声明为inline:8-2法则
条款31:将文件间的编译依存关系降至最低
  • 问题:头文件include其他头文件时,被include的修改了,include路径上的都得修改导致整个程序被重新编译
  • 以“声明的依存性”替换“定义的依存性”:让头文件尽可能自我满足,不行则让它与其他文件内的声明式(而不是定义式)相依。策略:
    • 如果使用objects ref或者ptr能完成就不要使用objects
    • 尽量以class声明式替换class定义式
    • 为声明式和定义式提供不同头文件:c++提供export允许将template声明和定义分割在不同文件,不过支持的编译器少
  • Handle classes的另一个制作办法:抽象基类

6 继承与面向对象设计

条款32:确定你的public继承塑模出is-a关系
  • 只对public继承成立
  • 例子:企鹅和鸟,飞;正方形长方形
条款33:避免遮掩继承而来的名称
  • 派生类会掩盖基类中所有同名函数(基类因重载导致多个重名函数),无论是否基类中函数是否是virtual或者有不同形参列表(在public继承下这项规则违反了is-a关系)
  • 解决方案:用using声明让基类内函数在派生类作用域可见
    class Derived: public Base
    {
    public: //基类中public在public派生类也应该是public
      using Base::mf1; //从此父类中mf1()都可见,无论是否重载等
      virtual void mf1(); //继承机制仍然有用
    }
    
  • 在private继承下,只想继承部分函数(public继承的is-a关系告诉我们不应该这么做):编写简单的转交函数
    • inline转交函数的另一个用途是为那些不支持using声明(这不是正确行为)的老旧编译器提供解决方案
      class Derived: private Base
      {
      public:
      virtual void mf1() //暗自成为inline
      {
        Base::mf1();
      }
      }
      
条款34:区分接口继承和实现继承
  • 是可以为pure virtual函数提供实现代码的,但是调用的唯一途径是指出其class名称
  • “提供缺省实现给derived calsses,但除非他们明白要求否则免谈”即避免派生类忘记重新定义基类virtaul函数,使用默认的实现而导致错误,的方法:
    class Airplane
    {
    public:
      virtual void fly() = 0;
    protected:
      void defaultFly();
    }
    class ModelA : public Airplane
    {
    public:
      virtual void fly() //inline调用,有关系吗?
      { defaultFly(); }
    }
    
  • 上述方案可能导致class命名空间污染问题,但是接口和缺省实现也应该分开:可以用为pure virtual提供缺省实现解决(但这样就不可以设置两个函数为不同访问权)
  • 绝不应该在派生类中定义non-virtual函数
条款35:考虑virtual函数以外的其他选择
藉由non-vritual Interface实现Template Methond模式
  • 一个流派:virtual函数应该几乎总是private
    class GameCharacter
    {
    public:
      int healthValue();
      //virtual函数的wrapper/外覆器:做事前事后工作并调用doHealthValue()
    private:
      virtual int doHealthValue();
    }
    
藉由Function Pointers实现Strategy模式
  • 每个人物可以有不同的计算函数,运行期也可以改变
  • 但是不能访问non-public成员来计算结果,非得这样只能弱化封装:设non-member为friend或提供专门访问函数

    int defaultHealthCalc();
    class GameCharacter
    {
    public:
      typedef int (*HealthCalcFunc) ();
    
      GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) :
          healthFunc(hcf) { }
    private:
      HealthCalcFunc healthFunc;
    }
    
藉由tr1::function完成Strategy模式
//将以上代码中typedef改为下边即可
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
  • 有兼容性:可调用物的参数可被隐式转换为const GameCharacter&,返回类型可被隐式转换为int(意思是定义的函数格式可以不完全和typedef的一致,还可以接收函数对象、某个其他类成员函数)
    //函数对象
    struct HealthCalculator
    {
      int operator() (const GameCharacter&) const
      { }
    };
    GameCharacter gc( HealthCalculator() ); //使用方法
    //使用成员函数时候注意成员函数默认第一个参数为this。可以创建一个对象用bind()绑定来抵消第一个参数
    GameLevel currentLevel; //health()式GameLevel中成员函数
    GameCharacter gc2(
      std::tr1::bind(&GameLevel::health, currentLevel, _1)
      );
    
古典Strategy模式
  • 将HealthCalFunc变成一个集成体系的基类,GameCharacter类中放入其指针
条款36:绝不重新定义继承而来的non-virtual函数
  • non-virtual函数是静态绑定/前期绑定;virtual函数是动态绑定/后期绑定
条款37:绝不重新定义继承而来的缺省参数值
  • virtual函数是动态绑定,而缺省参数值是静态绑定
  • 静态类型:对象在程序中被声明的类型;动态类型:所指向的对象的真正类型
  • 不仅指针,对于引用调用也有影响
  • 动态指定默认参数需要运行期决定,影响效率;而当前是在编译期决定
  • 为了使得基类和派生类有相同的行为,应该设置相同的默认参数,但会导致代码重复以及基类默认参数改变导致大量派生类代码需同时改变,可用public函数调用private virtual函数即NonVirtualInterface
  • 题外话:默认参数可以写在声明或者定义中。声明定义都写在.h头文件的声明中,则调用者可以不写参数。如果定义在.cpp且默认参数写在定义则必须要写参数。结论是调用所在的.cpp中能否看到这个默认参数,看得到就可以不用写
条款38:通过复合塑模出has-a或“根据某物实现出”
  • 当复合发生在应用域之间的对象之间,表现出has-a关系;当发生在实现域是表现is-implemented-in-terms-of(根据某物实现出)关系:
    • 应用域:人汽车视频等;实现域:缓冲区互斥器等
    • has-a意思是人有个名称有个地址
    • is-implemented-in-terms-of理解:比如自行实现一个set,里边含有一个std::list用来存指针
条款39:明智而审慎地使用private继承
  • private继承下编译器不会自动将derived对象转换为base对象
  • private继承implemented-in-terms-of/根据某物实现出,意味只有实现部分被继承。在软件“设计”层面上没意义,只在软件实现层面有意义
  • 尽可能使用复合,必要时才使用private继承:涉及protected和virtual函数、空间方面十分必要时可用
    //需要重新定义virtual使用private继承情况:
    //Timer类自动会定时调用public virtual onTick(),因而可以private继承并重写这个函数达到自定义定时功能。如:
    class Widget : private Timer //private继承使客户无法调用Timer中的onTick() 
    {
    private: //private是必要的,使客户不误以为这是可调用接口
      void void onTick() const; //一些需要定时执行的功能
    }
    //上述是好设计,但是private继承不是必要的,可用复合替换:
    class Widget
    {
    private:
      class WidgetTimer: public Timer
      {
      public:
          virtual void onTick() const;
      }
      WidgetTimer timer;
    }
    
  • 训练自己思考多种做法是值得的
  • 阻止derived class 重新定义virtual函数:
    • 上述继承的方法无法阻止:条款35说了派生类可重新定义virtual函数,即使不可以调用它
    • 复合方法是C++模仿Java等禁止重定义virtual的一种模拟方法
  • 空间原因使用private继承:只适用于所处理的class不带任何数据。不含:non-static成员变量、virtual函数(vptr)、virtual base class
    • C++裁定凡是独立(非附属)对象都必须有非零大小。官方勒令默默安插一个char到空对象内。而由于字节对其原因甚至可能有一个int大小。所以一个类内的空对象成员至少有一个字节大小
    • 但是上述不适用于derived class中base成分(非独立)。此即EBO空白基类最优化(一般只在单一继承可行)
条款40:明智而审慎地使用多重继承
  • C++解析重载函数规则:先确实函数是最佳匹配,在检验可取用性
  • 会有菱形继承/钻石型多重继承问题
    • 默认是复制出两个顶层基类部分到对象中
    • 只产生一个用virtual base class:令所有直接继承的采用virtual继承
      • 不能广泛采用虚继承:虚继承的对象更大,访问virtual base classes成员变量更慢
      • 虚继承初始化由最底层class负责,需要认知其virtual base class
  • virtual base class忠告:
    • 非必要不使用
    • 如必须尽量避免放置数据
  • 书中多重继承的合理用途的例子(P198):public继承interface、private继承使用virtual函数

7 模板与泛型编程

  • C++ template最初发展动机:建立“类型安全” 的容器如vector、list和map等。但泛型编程(generic programming)更好:让写出的代码和其所处理的对象类型彼此独立,比如STL算法for_each、find和merge等。最终发现C++ template机制本身是一部完整的图灵机,于是导出了模板元编程(template metaprogramming)
条款41:了解隐式接口和编译器多态
  • C++普通类和继承关系中含有了显示接口(explicit interface)和运行期多态(runtime polymorphism)。而模板中的是隐式接口(implicit interface)和编译期多态(compile-time polymorphism)
  • 编译期多态:以不同的template参数具现化function template会导致调用不同的函数
  • 显式接口基于函数签名式,比如class的public声明区域的函数声明。隐式接口是由有效表达式(valid expression)组成
      template<typename T>
      void doProcwssing()
      {
          if (w.size() > 0 && w != soneNastWidget) { }
      }
      //if的表达式中隐含着T类型要支持size()、operator>等函数接口
      //但是由于操作符重载带来的可能性,T类型不一定得支持(比如可以继承自T的基类),而且由于隐式转换等原因使得模板提供了许多的可能
    
条款42:了解typename的双重意义
  • template内出现的名称如果相依于某个template参数,称之为从属名称(dependent names)。如果从属名称在class内呈嵌套状,称之为嵌套从属名称(nested dependent name):比如T::const_iterator是嵌套从属名称,其中T为template的一个模板参数
  • C++的一个编译规则:如果解析器在template中遭遇一个嵌套从属名称,缺省状态下便假设这个名称不是个类型
      template<typename C>
      void print2nd()
      {
          C::const_iterator * x; 
          //由于编译器不知道C的内容,从语法上甚至可以理解为C中有个静态变量const_iterator,则该语句理解为其和x的乘积,而不是定义了一个变量x
          //C++规则默认嵌套从属名称不是类型,因而其认为const_iterator是一变量
          typename C::const_iterator iter(); //正确:用typename明确说明其是类型
      }
    
    • 一般性规则:任何时候想在template中指涉一个嵌套从属类型名称,就必须在紧邻前用关键字typename修饰。注:typename只被用来验证嵌套从属类型名称,其他名称前不允许使用
    • 例外:typename不可以出现在base classes list内的嵌套从属类型名称前,也不可在member initialization list中作为base class修饰符
        template<typename T>
        class Derived : public Base<T>::Nested //不允许在base class list中
        {
            explicit Derived(int x) :
            Base<T>::Nested(x) //不允许在mem init list中
            {
                typename Base<T>::Nested tmp; //必须要typename
            }
        };
      
  • typedef和typename在一起还是和谐的
      //IterT为一个typename IterT模板参数
      typedef typename
      std::iterator_traits<IterT>::value_type value_type;
      value_type tmp;
      //这样减少了重复编码量
      //另,traits class的一种运用:如果IterT为vector<int>::iterator则tmp为int型
    
条款43:学习处理模板化基类内的名称
  • 模板全特化:特化是全面性的,一旦类型参数被定义为某个类(此为CompanyZ)再没有其他template参数可供变化
      template<typename Company>
      class MsgSender
      {
          void sendclear();
          {
              Company c;
              c.sendCleartext();
          }
          //do something
      };
      class CompanyZ;
      //针对CompanyZ全特化
      template<>
      class MsgSender<CompanyZ>
      {
         //do something else completely 
      };
    
  • 问题:以下代码无法通过编译
      template<typename Company>
      class LoggingMsgSender: public MsgSender<Company>
      {
          void sendClearMsg()
          {
              //发送前写日志
              sendClear(); //调用基类的功能
              //发送后写日志
          }
      }
    
    • 原因:编译器会抱怨sendClear不存在。编译器遭遇class template LoggingMsgSender定义式时并不确切知道它将继承自什么样的class。Company是模板参数,不到LoggingMsgSender被具现化无法确切知道是什么,也就无法知道class MsgSender是什么(无法知道是否有个sendClear函数)
    • 具体来说:上述例子中base class template有可能被特化,而特化版本可能不提供一般性的接口如sendClear,因而编译器拒绝在templatized base classes内寻找继承而来的名称(sendClear)
  • 三个解决办法解决C++不进入templatized base classes观察的问题
    1. 在base class函数调用动作前加上this->。如this->sendClear();
    2. 使用using声明式(条款33)将被掩盖的base class名称带入derived class作用域内。如using MsgSender::sendClear;告诉编译器假设sendClear位于base class内
    3. 明白指出被调用函数位于base class内。如MsgSender::sendClear();往往是最不满意的方法:如果被调用的是virtual函数则会关闭virtual绑定行为
    • 这些解法都相当于对编译器承诺base class template的任何特化版本都将支持其一般(泛化)版本所提供的接口
条款44:将与参数无关的代码抽离template
  • class template的成员函数只有在被使用时才暗中具现化
  • 问题:使用template可能导致代码膨胀(code bloat)
      //代码膨胀经典例子:非类型参数导致
      template<typename T, std::size_t n> //n为非类型参数
      class SquareMartix { ... };
      //将产生两副重复的代码
      SquareMartix<double, 5> sm1;
      SquareMartix<double, 10> sm2;
    
    • 解决方法:往往可以消除,做法是以函数参数或class成员变量替换template参数来产生一个共享版本
    • 效果分析:
      • 绑定尺寸的版本可能比共享版本产生更佳的代码,比如其尺寸是常量,因此可以通过常量的广传达到最优化。共享版本可减少可执行文件大小也就降低了程序的working set(虚存中process所使用的一组内存页),可能使程序更快速
        对象大小:绑定尺寸版本对象更大。当然也可通过其他方法消除但是会破坏封装性,有时候一点点代码重复反倒更好
  • 问题:type parameter(类型参数)也会导致膨胀:许多平台上int和long有相同二进制表述所以vector和vector成员函数可能完全相同,而一些连接器会合并相同的函数实现码有些不会。类似地,在大多数平台上所有指针类型有相同的二进制表述,因而list和list*>往往应该对每一个成员函数使用唯一一份底层实现
    • 解决方法:令操作强型指针(即T)的成员函数调用另一个操作无类型指针(即void)的函数(完成实际工作)
    *>
条款45:运用成员函数模板接受所有兼容类型
  • STL容器的迭代器几乎总是智能指针,因而可以用“++”移到另一个节点
  • 问题:真实指针做的很好的是支持隐式转换,而智能指针不行
Templates和泛型编程(Generic Programming)
  • 解决方法:为智能指针写一个构造模板,即member function template,作用为替class生成函数
      template<typename T>
      class SmartPtr
      {
      public:
          template<typename U> //刻意不声明为explicit:原始指针转换是隐式的
          SmartPtr(const SmartPtr<U>& other) : //member template:为了生成copy构造函数
              heldPtr(other.get()) //只有当存在某个隐式转换将U*转为T*时才能通过编译
              { }
          T* get() const { return heldPtr; } //返回原始指针
      private:
          T* heldPtr;
      };
    
    • 根据对象u生成对象v而u和v是同一template的不同具现体,有时称之为泛化(generalized)copy构造函数
  • member function template不限于构造函数,常用另一方法是支持赋值操作,比如TR1中shared_ptrs、auto_ptrs等之间赋值
      template<class T>
      class shared_ptr
      {
      public:
          template<class Y>
              explicit shared_ptr(Y* p); //构造来自任何兼容的内置指针
          template<class Y>
              explict shared_ptr(weak_ptr<Y> const& r); //explicit:weak_ptr到其shared_ptr的隐式转换不被认可
          template<class Y>
              shared_ptr& operator+(shared_ptr<Y> const& r);
      };
    
  • 在class内声明泛化copy构造函数并不会阻止编译器生成自己的copy构造函数(一个non-template),这也适用于赋值操作
条款46:需要类型转换时请为模板定义非成员函数
  • template实参推导过程中从不将隐式类型转换函数纳入考虑
      //根据条款24的代码改动的代码
      template<typename T>
      class Rational
      {
      public:
          Rational(const T& numerator = 0, const T& denominator = 1);
          //...
      };
      template<typename T>
      const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs) { } //do something
      Rational<int> oneHalf(1, 2);
      Rational<int> result = oneHalf * 2; //无法通过编译
    
    • 编译器找不到可调用函数继而考虑可被template具现出的operator*函数,于是首先得推导出T。oneHalf可以推导出T为int而由于template不考虑隐式类型转换故无法从常数2推导出类型
  • 解决办法:将operator*在Rational中声明为friend
      friend //在template Rational定义式中
      const Rational operator*(const Rational& lhs, const Rational& rhs);
      //在class template中template名称可被用来作为template和其参数的简略表达。故上述等价于
      friend
      const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);
    
    • 原理:此时可以通过编译了,因为当对象oneHalf被声明为Rational时class Rational被具现带着friend operator被声明出来。此时编译器调用operator时可用隐式转换。但此时无法通过链接因为只是声明了并未定义。
  • 最终解决:还需要紧接着函数在类中的friend声明式重新实现定义(暗自成为inline了,可通过其调用外部辅助函数减小inline冲击)
    1. non-member函数意义:让类型转换可能发生于所有实参身上,如2oneHalf和oneHalf2同样能通过编译
    2. friend作用:让函数自动被具现化
我的一些理解
  • 为什么声明为friend后还需要再定义一次?不可以直接使用全局的Rational operator*代码?
    • 在编译的模板处理阶段,friend保证类Rational类的代码产出时产出了关于Ratiion operator的声明,然后发现可以通过隐式转换(常数2转换为Rational)调用到该函数;由于只有声明故通过编译,留下链接的接口等待链接器。与上述同时进行的是,该过程中发现没有能实例化并调用全局template operator的地方,故其实现代码根本未产出。最终在链接阶段找不到函数定义
  • 为什么不直接在Rational中写member Function版operator*?
    • member function版不可能支持2oneHalf这种操作及其他类似操作。另外,实际上当前解决方案中,在2oneHalf的运算中,仍然是通过class中friend版本定义来进行的,全局Rational operator*定义代码从来未被使用过,甚至其可以只要声明不写定义
  • 为什么不用函数特化模板?
    • 用特化的需要分别写出针对int、float等的函数。当前解决方案支持许多类型
  • 关于inline
    • 逻辑应该是先找到了friend这个的定义,通过了编译,然后又找到了定义,通过了链接,最后发现可以inline就成为了inline函数。但是在这种特殊情况下,又无法把friend的定义放在类外(被全局的定义占了),故放在类内,就又是inline。似是矛盾两方面
条款47:请使用traits classes表现类型信息
  • STL的5种迭代器分类
    1. Input迭代器:只向前移动,一次一步,只可读取所指东西且只读一次,如istream_iterator。只适合一次性操作算法(one-pass algorithm)
    2. Output迭代器:只向前移动,一次一步,只可写所指东西且只写一次,如ostream_iterator。只适合一次性操作算法
    3. forward迭代器:包含前两类所有功能并可读或写一次以上。一些库(不包含STL)会提供单向linked list及TR1 hased容器中的迭代器可能也是(hashed可能为单向或双向,取决于实现)。的单向可用在多次性算法(multi-pass algorithm)
    4. Bidirectional迭代器:可向前向后移动。比如STL list、set、multiset、map、multimap的迭代器
    5. Random access迭代器:可执行迭代器算数即在常量时间向前向后跳跃任意距离。如vector、deque、string。内置指针可被当做random迭代器使用
  • C++标准库分别提供专属卷标结构(tag struct)加以确认(库中定义了该四个标志struct,用于库自带的traits确认类型身份)
      struct input_iterator_tag {};
      struct output_iterator_tag {};
      struct forward_iterator_tag : public input_iterator_tag{};
      struct bidirectional_iterator_tag : public forward_iterator_tag {};
      struct random_iterator_tag : public bidirectional_iterator_tag {};
    
  • 问题&需求:编写一advance()函数将迭代器iter移动d的距离,如
      template<typename IterT, typename DistT>
      void advance(IterT iter, DistT d)
      {
          if (iter is a random access iterator)
          {
              iter += d; //针对random access迭代器算数运算
          }
          else
          {
              //用while 循环++iter或者--iter
          }
      }
    
  • Traits:在编译期间取得某些类型信息。其并不是C++关键字或预先定义好的构件,而是一种技术,一个C++程序员共同遵守的协议
  • 下文基本上描述的是C++库中traits的实现原理和用法。实际上我们可以在自己实现class和相应的iterator等方面运用该技术
  1. 首先,不同STL容器类里定义的iterator类中(或我们自定义的class)用typedef将tag struct(代表该iterator所属的分类)赋别名为同一个名字:iterator_category
     template< ... > //略而未写参数
     class deque
     {
     public:
         class iterator
         {
         public:
             typedef random_access_iterator_tag iterator_category;
             //...
         };
         //...
     };
     //在list中相应语句是:typedef bidirectional_itreator_tag iterator_category;
    
  2. 其次,在C++库的traits模板类(或我们自定义的traits)中用typedef将模板参数类中iterator_category别名为iterator_category
     template<typename IterT>
     struct iterator_traits
     {
         typedef typename IterT::iterator_category iterator_category;
         //...
     };
     //针对指针类型提供一个偏特化版本(parital template specialization)
     template<typename IterT>
     struct iterator_traits<IterT*>
     {
         typedef random_access_iterator_tag iterator_category; //指针的行为和random access类似
         //...
     };
    
  3. 最后按照如下用法即可在编译期便得到IterT所属分类:不过该方法把编译期可以做的事情延到运行期,因为需要在运行时进行比较,typeid()需要在运行时动态得到iterator_category类型(即该tag struct的类型id,每个类都有一个类型id)。而且这有编译问题(见条款48)
     template<typename IterT, typename DistT>
     void advance(IterT& iter, DistT d)
     {
         if(typeid(typename std::iterator_traits<IterT>::iterator_category)
             == typeid(std::random_access_iterator_tag))
         //...
     }
    
  4. 最后优化:利用函数重载时,编译器会根据参数类型最优匹配调用不同的函数
     template<typename IterT, typename DistT>
     void advance(IterT& iter, DistT d)
     {
         doAdvance(
             iter, d,
             typename std::iterator_traits<IterT>::iterator_category);
     }
     //重载函数1:针对random access。注意重载函数的第三个形参类型
     template<typename IterT, typename DistT>
     void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag)
     {
         iter += d;
     }
     //重载函数2:针对input iterator。由于forward_iterator_tag是public继承自input_iterator_tag,故该函数也可以处理forward iterator
         template<typename IterT, typename DistT>
     void doAdvance(IterT& iter, DistT d, std::input_iterator_tag)
     {
         if (d < 0)
             throw std::out_of_range("Negative distance");
         while(d--)
             ++iter;
     }
    
  • Traits广泛用于标准程序库,除了iterator_category还提供另4份迭代器相关信息(包括value_type)。char_traits保存字符类型相关信息,numeric_limits保存数值类型相关信息。TR1包括is_fundamental判断是否内置类型,is_array判断是否数组类型,is_base_of判断T1T2相同或T1是T2的base class等总计50个以上traits class,>
  • 为什么需要traits,而不是直接利用iterator::iterator_catagory?
    • “traits必须能够释行于内置类型”而我们无法将信息嵌套于原始指针内。实际上我们强行在使用该技术时直接使用原始类中(而不是traits类)的catagory也是可以的
条款48:认识template元编程
  • Template metaprogramming(TMP模板元编程)是编写template-based C++程序并执行于编译期的过程。执行于编译器内的程序
  • TMP两个伟大效力
    1. 让某些事情更容易
    2. 将工作从运行期移到编译期:1.某些错误通常在运行期而现在可在编译期找出来;2.使用TMP的程序可能在每方面高效,比如较小可执行文件、较短运行期、较少内存需求。缺点:编译时间变长
  • 例子:条款47中advance函数可以通过typeid(运行期)实现也可通过traits(编译期)实现。而其提到的typeid方式可能导致编译期问题
    • typeid方式效率会比traits低:1.类型测试发生在运行期而不是编译期;2.运行期类型测试代码会出现在可执行文件中
  • TMP已被证明是图灵完全机器,即其威力大到足以计算任何事物。TMP主要是个“函数式语言”(functional language),其没有真正的循环构件,而是藉由递归完成
  • TMP起手程序是在编译期计算阶乘:厉害了!
      template<unsigned n>
      strcut Factorial
      {
          enum { value = n * Factorial<n-1>::value};
      };
      template<>
      struct Factorial<0> //特殊情况:终止条件
      {
          enum {value = 1};
      };
    
  • TMP值得学习,一些例子列出其能达成的目标
    • 确保度量单位正确。使用TMP可以确保(在编译期)程序中所有度量单位的组合都正确
    • 优化矩阵运算。
        //例如条款44中
        typedef SquareMatrix<double, 10000> BigMatrix;
        BigMatrix m1, m2, m3, m4, m5;
        //...
        BigMatrix result = m1 * m2 * m3 * m4 * m5;
        //“正常的”函数调用会产生4个临时对象及各自独立的矩阵运算。
        //而使用高级、与TMP相关的template技术即expression tmplates,就可能消除临时对象、合并循环,无需改变客户端用法,使用较少内存又有速度提升
      
    • 生成客户定制设计模式(custom design pattern)实现品。strategy、observer、visitor等可以多种方式实现。policy-based design之TMP-based技术,已经被用于编译期生成不同的智能指针类型,广义地成为generative programming(殖生式编程)的一个基础

8 定制new和delete

  • operator new和operator delete只适合用来分配单一对象;数组版是operator new[]和opeartor delete[]
  • STL容器使用的heap内存由容器拥有的分配器对象管理,不是被new和delete直接管理
条款49:了解new-handler的行为
  • operator new无法分配内存而抛出异常之前,会先调用客户指定错误处理函数new-handler。用set_new_hander()指定
    //声明于<new>中的标准程序库函数
    namespace std
    {
      typedef void (*new_handler) ();
      new_hander set_new_handler(new_handler p) throw(); //表示不抛出异常
    }
    //使用方法
    void outOfMem() { }
    std::set_new_handler(outOfMem);
    
  • operator new无法满足内存申请时会不断调用new-handler函数直到找到足够内存
  • 设计良好的new-handler函数需要做如下事情(不是很理解):
    1. 让更多内存可用:让operator new下一次分配动作可能成功。一个做法:程序开始分配大内存,new-handler释还给程序使用
    2. 安装另一个new-handler:安装另一个可分配内存的new-handler供下次operator new调用
    3. 卸载new-handler:将null指针传递给set_new_handler
    4. 不返回:通常调用abort或exit(后文中说不行了抛出bad_alloc异常或承认失败直接return,因为operator new要不停调用new-handler)
  • C++不支持class专属new-handler,替代方法:
    • class提供自己的set_new_handler:允许保存new-handler函数指针
    • class内opeartor new():调用set_new_handler()自定义函数,调用系统new operator,最后恢复默认new-handler
      //operator new(不管全局还是类内自定义)的标准形式
      void* operator new(std::size_t size);
      //来自More Effective C++:
      //new operator为系统new关键字不可改变,其中调用opeartor new()进行分配(其返回裸内存,之后调用构造函数进行初始化)。
      //用户可以重载opeartor new()自定义分配行为。
      
  • 书中两种实现专属new-handler例子(第二种真是精妙):
    1. 类中实现上述两者函数,用static成员变量装自定义函数指针
    2. 写base实现两个函数和static变量。为保证每个子类拥有各自的static成员(static是全类唯一),用奇异模板模式(CRTP)使对每个子类都复制基类代码使得static对每个子类是不一样的(这是模板参数T的唯一功能):class Widget : public NewHandlerSupport<Widget> { }
  • 新一代operator new无法分配足够内存时应该抛出bad_alloc异常(旧返回null)
  • nothrow形式的operator new:为了兼容传统的返回null
    //定义于<new>
    Widget* p = new (std::nothrow) Widget;
    //这行仍然可能异常:new不抛出异常但是Widget构造函数可能调用其他版new仍然抛出异常
    
条款50:了解new和delete的合理替换时机
  • 替换opeartor new或 operator delete三个最常见理由
    1. 检测运用错误:自定义operator new多分配内存尾部方特定byte patterns,自定义operator delete检查尾部签名看使用是否争取
    2. 强化效能:编译器所带函数考虑各种情况和碎片,无法满足大区块内存要求
    3. 收集使用上的统计数据
  • C++要求所有operator news返回的指针都有适当的对齐(取决于数据类型,即不同数据类型要求的对齐字节位不同,不遵循可能导致效率慢如x86 double 8B对齐块,甚至崩溃等硬件异常)(malloc()满足)
  • 写一个总能运行的内存管理器不难,难得是优良地运作,还是考虑:
    • 一些编译器内存管理函数调试状态和日志状态可能满足需求了。或商业版
    • 开源领域:Boost的Pool分配大量小型对象。TR1支持各类特定的对齐条件,很值得注意
  • 替换的其他原因:分配/归还速度、降低缺省管理器额外空间开销、弥补缺省非最佳对齐、相关对象集中、获得非传统行为
条款51:编写new和delete时需固守常规
  • opeartor new实际上不止一次尝试分配内存。C++规定即使客户要求0B也得返回一个合法指针。
  • Base class中的static operator new函数会被派生类继承和调用,因而在函数中需要判断大小是否等于基类大小调用不同operator new。但是在写operator new[]时只需要分配一块未加工raw内存,因为无法知道对象数量和大小,因为Base的array new可能被继承调用
  • 写opeartor delete情况更简单:保证删除null指针永远安全
  • 如果将被删除的对象派生自某个base class,而基类欠缺virtual析构函数,则C++传递给operator delete的size_t数值可能不正确
    //non-member
    void operator delete(void* rawMemory) throw();
    //member in class
    void operator delete(void* rawMemory, std::size_t size) throw();
    
条款52:写了placement new也要写placement delete
  • 用了正常形式(正常形参列表)的operator new分配成功而在对象构造函数失败时会自动调用两种(class内和全局)正常形式的operator delete进行释放
  • placement new:接收的参数除了一定会有的size_t还有其他。运行期系统在operator new在对象构造函数失败时会寻找并调用参数个数和类型都和operator new相同的某个operator delete;如果没找到就什么也不做
    //C++标准程序库中一个placement new,声明于<new>
    //众多placement new中特别有用的一个,是最早的版本
    //接受一个指针指向对象该被构造之处;用途之一:在vector未使用空间上创建对象
    void* operator new(std::size_t, void* pMemory) throw();
    //placement new的使用
    Widget* pw = new (pMemory) Widget;
    
  • 防止placement new内存泄漏,必须提供正常的operator delete和一个placement delete。只有当placement new在构造函数失败时会调用placement delete;而构造期间无异常,删除时使用正常delete,不会导致placement delete
  • 成员函数名称会掩盖外围作用域中的相同名称(无关参数列表)。派生类也会掩盖基类
    • 可以定义一个基类内含所有正常形式的new和delete:在其中直接调用::operator new(size);
    • 在派生类中using Base::operator new;引入然后自定义placement new
      //C++在global作用域提供的operator new
      void* operator new(std::size_t) throw(std::bad_alloc);
      void* operator new(std::size_t, void*) throw();
      void* operator new(std::size_t, const std::nothrow&) throw();
      

9 杂项讨论

条款53:不要轻忽编译器的警告
  • 不管怎样说,在打发某个警告之前,请确认了解它意图说出的精确意义。这很重要
条款54:让自己熟悉包括TR1在内的标准程序库
  • Technical Report 1
  • 不熟悉TR1机能而奢望成为一位高效C++程序员是不可能的
  • 命名空间是std::tr1::
  • tr1::shared_ptr无法应对环形情况。tr1::weak_ptr不参与引用计数计算;当最后一个指向某对象的shared_ptr被销毁就会被删除,此时weak_ptr会被自动标示无效
  • tr1::array本质是STL化数组即支持成员函数如begin和end的数组,不适用动态内存
  • 太多功能,请参考书以及其他技术文档
条款55:让自己熟悉Boost
  • 作者认为最重要的一个C++泛用型网站
  • 语言间支持:包括允许C++和Python之间的无缝互操作性
  • 太多功能,请参考书以及其他技术文档
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值