Effective C++ 总结


一、让自己习惯C++

条款01:视C++为一个语言联邦

条款02:尽量以const,enum,inline替换#define

  • #define或许不被视为语言的一部分,所以#define定义的宏不一定被看见,推荐用一个常量替换宏。且使用常量预处理器所需的代码量可能比#define更小。
    #define VAR 1.656  //  不推荐
    const double Var = 1.656;  //推荐
  • 若要定义常量字符串,使用char必须const两次,而string只需要一次,所以定义成string会更好一些。
    const char* const p1 = "test~";
    const std::string p2{"test~"}; 
  • #define无法创建一个class专属常量,因为#define不提供任何封装性,没有所谓的private,若用static const去封装,因为static成员只有整数类型(int,char,bool)可以声明时获得值,万一编译器不允许static const整数类型声明时候获取初始值,且class的static成员在class创建对象时就必须用到,就会使程序出错或者退出,这时候可以使用enum定义变量代替static const声明整数类型。并且获取enum地址是不合法的,可以有效避免别人修改数值。
    private:
        //static const int Num = 5;        //  假设编译器不允许这样做
        enum {Num = 5, Test};  //  枚举类型默认为static const int类型
        int scores[Num];        //  该类创建时要求知道Num值
  •  把看起来“像函数的宏”用inline函数替换,因为该宏使用容易出错,这里使用inline template
    #define CALL_WITH_MAX(a,b) f((a)>(b))?(a):(b))
    
    //  替换为
    template
    inline void callWithMax(const T& a, const T& b)
    {
        f(a > b ? a : b);
    }

 条款03:尽可能使用const

  • 令函数返回一个常量值,往往可以降低错误而造成的意外
  • 函数返回值若为右值,无法对其赋予变量等操作
    class gameplayer
    {
    public:
      
        char test1(unsigned position)  //  返回值为右值
        {return text[position];}
    
        char& test2(unsigned position)  //  返回值为左值
        {return text[position];}
    
    private:
        std::string text;
    };
    
    int main()
    {
        gameplayer p{"abcdefg"};
        p.test1(3) = 'w';    //  报错
        p.test2(3) = 'w';
        return 0;
    }

  • mutable可使non-static成员变量在const成员函数内可更改
  • 若const函数和non-const函数代码数量庞大且高度一致(两函数可以重载也可以不重载),为了避免代码重复,可以使用转型的方法
    class gameplayer
    {
    public:
        gameplayer(const std::string& str) :text(str) {}
        
        void const print() const
        {cout << this->text << endl;}
    
        const char& test1(unsigned position) const
        {
            /*******重复性代码*******/
            return text[position];
        }
    
        char& test1(unsigned position)
        {
            /*******重复性代码*******/
            return const_cast<char&>(
                static_cast<const gameplayer&>(*this).test1(position));
        }
    private:
        std::string text;
    };
    
    int main()
    {
        const gameplayer p1{"abcdefg"};
        //p1.test1(0) = 'x';    //  报错,表达式必须使可修改的左值
        p1.print();
    
        gameplayer p2{ "abcdefg" };
        p2.test1(0) = 'x';
        p2.print();
    
        return 0;
    }

     输出: 

     abcdefg

     xbcdefg

二、构造/析构/赋值运算

条款04:确定对象被使用前已先被初始化

  •  构造函数最后使用成员初始化列表,而不要在构造函数体内使用赋值操作,因为该操作会调用赋值函数。初始化列表列出的成员变量最好与声明顺序相同。若有多个构造函数和多个成员变量,可以将某些“赋值表现和初始化一样好”的成员变量放入某个函数中,然后在构造函数中调用。
  • 不同编译单元内的non-local static对象初始化次序使不确定的,为避免“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象(以下代码来自书)
    //////////你的程序库//////////
    class FileSystem{
    public:
        ...
        std::size_t numDisks() const;    //  成员函数之一
        ...
    };
    extern FileSystem tfs;    //  预备给客户使用的对象
    //////////你的程序库//////////
    
    
    //////////客户的程序库//////////
    class Directory{
    public:
        Directory(params)
        {
             ...
             std::size_t disks = tfs.numDisks();    //  使用tfs对象
             ...    
        }
    };
    //////////客户的程序库//////////
    
    //////当调用Directory构造函数时:
    Directory tempDir(params);    //  无法确定tfs是否在temDir之前初始化
    ////////改变方法////////////
    class FileSystem{...};
    FileSystem& tfs()    //  该函数替换tfs对象
    {
        static FileSystem fs;
        return fs;  
    }
    
    class Directory{...};
    Directory::Directory(params)
    {
        ...
        std::size_t disks = tfs().numDisks();    //  原本tfs改成tfs()
        ...
    }
    Directory& tempDir()    //  该函数替换tempDir对象
    {
        static Directory td;
        return td;  
    }

条款05:了解C++默默编写并调用哪些函数

  • 一个empty class,编译器会为它声明默认构造函数,拷贝构造函数,析构函数,赋值函数。
  • 若class内有引用或const成员变量,那么调用默认赋值函数是不合法的。第一,const成员变量值无法更改;第二,看代码
    class A
    {
        A(string& str):nameValue(str){}
    private:
        std::string& nameValue;    //  引用成员变量
    };
    
    int main()
    {
        std::string str1{"str1} ;
        std::string str2{"str2} ;
    
        A a1{str1};
        A a2{str2};
        a1 = a2;    //  报错,调用默认拷贝赋值函数,将a1.nameValue = a2.nameValue
        return 0;
    }
    
    //  C++不允许修改引用的对象(地址)
    //  编译器也不知道拷贝赋值函数里面应该做些什么
    //  是修改引用的地址还是值?
    //  所以编译器会直接删除默认拷贝赋值函数

条款06:若不想使用编译器自动生成的函数,就该明确拒绝

  • 若不想使用就直接“ = delete”
    class A
    {
    public:
        A();
        A(const A&) = delete;    //  不使用就delete
        A& operator = (const A&) = delete;
    };

条款07:多态基类声明virtual析构函数

  • 带有多态性质的基类应该声明一个virtual析构函数。如果class带由任何的virtual函数,就应该拥有一个virtual析构函数。特别是父类指针指向子类对象,当父类析构函数不是virtual时,是静态绑定(编译器绑定),只会调用父类析构函数,当父类析构函数是virtual时,会发生动态绑定(运行期绑定),先调用所指向的子类对象析构函数,再调用自身析构函数。
    class father
    {
    private:
        int* p1;
    public:
        father(int* num) :p1(num) {}
        ~father()      //  未加virtual
        {
            delete p1;
            p1 = nullptr;
            cout << "father的构造函数" << endl;
        }
    };
    
    class son : public father
    {
    public:
        son(int* num1, int* num2) :
            father(num1),
            p2(num2){}
        ~son()
        {
            delete p2;    //  num1让父类自己释放
            p2 = nullptr;
            cout << "son的析构函数" << endl;
        }
    
    private:
        int* p2;
    };
    
    int main()
    {
        int* a1 = new int{ 1 };
        int* a2 = new int{ 2 };
        int* a3 = new int{ 3 };
        int* a4 = new int{ 4 };
        son* s1 = new son(a1, a2);
        father* f1 = new son(a3, a4);    //  父类指向子类对象
        cout << "s1调用构造函数" << endl;
        delete s1;
        cout << "\nf1调用构造函数" << endl;
        delete f1;
        return 0;
    }
    
    输出:
    s1调用构造函数
    son的析构函数
    father的构造函数
    
    f1调用构造函数
    father的构造函数

     加了virtual后,输出

         

  •  Classes的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。

条款08:别让异常逃离析构函数

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

条款09:绝不在构造和析构过程中调用virtual函数

  •  在构造和析构期间调用virtual函数,因为这类调用从不下降至派生类

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

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

  •  确保对象自我赋值时 operator = 有良好行为。包括比较“来源对象”和“目标对象”的地址、周到的语句顺序、以及copy-and-swap。(注意“ = ”号两边可能是同一个对象)
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为任然正确。

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

  • 拷贝构造函数和赋值函数应该确保赋值“对象内的所有成员变量”以及“所有基类成分。(延伸:Qt中的QObject类以及继承QObject的子类都不能使用拷贝构造和赋值函数,因为信号槽机制,要求把每一个QObject对象看成是唯一的,如果通过拷贝或者赋值操作出一个一模一样的复制体,那么新的复制体名字应该叫什么,应该和老对象一样吗?那么发送或者接收信号的时候,该由哪个对象操作呢?为了省事,Qt直接在拷贝构造函数和赋值函数末尾添加了“ = delete”)
  • 不要尝试以某个copy函数(拷贝构造和赋值函数)实现另一个copy函数。应该将共同技能放入第三个函数(私有的)中,并又两个拷贝函数共同调用。

三、资源管理

条款13:以RAII对象管理资源

  •  为防止资源泄漏,请使用RAII对象(Resource Acquisition Is Initialization),它充分利用了C++语言局部对象自动销毁的特性来控制资源的生命周期。
  • 可以通过std::shared_ptr和std::unique_ptr管理指针,它们都是RAII对象,std::auto_ptr已经弃用。但并不代表所有指针都推荐使用智能指针,只有ownership/resource acquisition这个前提才使用smart ptr,其他情况应使用reference/raw ptr。比如Qt的QObject类以及子类,若它贯穿整个程序生命周期,意味着资源回收时期在程序关闭时,那么用智能指针来管理是没有必要且繁琐的。
  • 扩展:std::lock_guard和std::unique_lock是锁的RAII对象。

条款14:在资源管理类中小心coping行为

  •  RAII对象被复制,一般选择以下行为作为可能:

    1)禁止复制

    2)对底层资源使用“引用计数法”(类似std::shared_ptr)

    3)复制底部资源

    4)转移底部资源的所有权

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

  • APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。
  • 对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,隐式转换对客户比较方便。

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

  • new表达式使用[],delete表达式也使用[]。

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

  • 以独立语句将newed对象置入智能指针。否则,一旦异常被抛出,有可能导致难以察觉的资源泄漏。
    processWidget(std::shared_ptr(new Widget), priority());
    //  书上说该代码不应该一行完成,理由是new Widget之后
    //  不知道编译器是先调用priority还是shared_ptr构造函数
    //  若先调用priority后发生异常,就会产生内存泄漏
    // 所以改成如下
    std::shared_ptr pw(new Widget) processWidget(pw, priority()); // 但是现在不推荐用上面的方式分配内存 // 推荐使用std::make_shared方式 // 这样就不会出现调用顺序问题,因为new Widget在 // td::make_shared内部完成 // 就不需要分开写了 processWidget(std::make_shared, priority());

四、设计与声明

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

  • 好的接口容易被正确使用,不容易被误用。
  • “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
  • “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
  • std::shared_ptr和std::unique_ptr支持定制删除器(custom delete)。这可以方法DLL问题,可被用来自动解除互斥锁等等。

条款19:设计class犹如设计type

  • 详情看书

 

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

  •  尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。
  • 切割问题:通过pass-by-value方式将子类传入接收父类的函数中,拷贝出来的临时对象是以父类的拷贝构造函数构建的,无法调用子类的对应函数。引用可以解决该问题。
  • 内置类型,以及STL的迭代器和函数对象,往往使用pass-by-value比较适当

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

  • 函数返回对象时,绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。

条款22:将成员变量声明为private

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

  • 这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。以下为namespace例子:
    #pragma once
    #include
    using namespace std;
    
    namespace eat_fruits
    {
        class Apple
        {
        public:
            void first() { cout << "拿起苹果" << endl; }
            void second() { cout << "给苹果削皮" << endl; }
            void third() { cout << "吃苹果" << endl; }
        };
    
        void eatApple(Apple& a)
        {
            a.first();
            a.second();
            a.third();
        }
    }
    apple.h
    #pragma once
    #include
    using namespace std;
    
    namespace eat_fruits
    {
        class Orange
        {
        public:
            void first() { cout << "拿起橘子" << endl; }
            void second() { cout << "拨开橘子皮" << endl; }
            void third() { cout << "吃橘子" << endl; }
        };
    
        void eatOrange(Orange& o)
        {
            o.first();
            o.second();
            o.third();
        }
    }
    orange.h
    #include"orange.h"
    #include"apple.h"
    
    int main()
    {
        eat_fruits::Orange orange;
        eat_fruits::eatOrange(orange);
        cout << "/************/" << endl;
        eat_fruits::Apple apple;
        eat_fruits::eatApple(apple);
        return 0;
    }
    main.cpp

条款24:若所有参数皆需要类型转换,请为此采用non-menber函数

  • 如果构造函数是explicit,那么就不支持隐式类型转换(implicit type conversion)。

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

  •  当std::swap(内部是拷贝构造和赋值函数)对你的类型效率不高时,提供一个该类型swap成员函数,并确定这个函数不抛出异常。
  • 提供一个public swap成员函数,并在你的class或template所在的命名空间内提供一个non-menber swap,并令它调用上述swpa成员函数,对于classes(而非templates),也请特化std::swap
  • 调用swap时应针对std::swap使用using声明式,然后调用swap并不带任何“命名和空间资格修饰”,让编译器自己查找适当的swap并调用。
  • 在std里头添加新东西,程序可以编译和运行,但是,如果希望你的软件有可预期行为,请不要这样做。

五、实现

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

  • 这样做可以增加程序的清晰度并改善程序效率。
  • 清晰度:过早定义,可能忘记使用变量或多申请变量。
  • 改善效率:过早定义,至少会多调用一次默认构造函数,若该变量最终没有使用,还多调用一次析构函数。

条款27:尽量少做转型动作

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
  • 如果转型是必要的,试着将它隐藏于某个函数背后。
  • 宁可使用C++风格转型,不要使用旧式转型。前者不仅容易辨识,也有各自的职能。

条款28:避免返回handles指向对象内部成分

  • 避免返回handles(包括reference、指针、迭代器)指向对象内部,遵守这个条款可增加封装性。
  • 使const成员函数的行为像个const(函数返回值添加const修饰符)
  • 将产生悬空指针(dangling handles)的可能性降至最低。

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

  •  异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能保证:基本型、强烈型、不抛异常型。

条款30:透彻了解inlining的里里外外

  • 将大多数inline限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
  • 不要只因为function templates 出现在头文件,就将它们声明为inline。
  • 扩展:其实现代编译器非常聪明,它会自己分析是否需要将函数变为内联的,就算你在函数前面加了inline,也不会成为它判断是否内联的依据。结论,inline已经没用了,如果你还继续使用,说明你学的是上个世纪的教材。。。。

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

  • 一种方法是在头文件中前置声明该类,并只在指针、引用或者函数返回值中使用。然后在cpp文件中用#include添加
    //  h.文件
    class
    Test; // 前置声明 class Use { private: test t1; // 报错,没有导入Test头文件,编译器不知道其大小 test* t2; // 正确,因为指针大小是固定的,32bit4字节,64bit8字节 // 就算修改Test文件,编译器也认为test* t2没有变化 // 所以对于编译器来说不需要重新编译 test& t3也一样 test& t3; public: Test useTest(); // 没问题,这里只是声明返回值是Test,并没有调用 void showTest(Test t); // 没问题,理由同上 // 上面返回值使用引用,以及形参使用指针或引用都是可以的 };
  • 另一种方法是使用interface class,即类中都是纯虚函数,这样的类使用的时候只能以指针方式出现,同样可以减少编译依赖。
  • 扩展,继承类被前置声明无效。
    class Father;  //  前置声明
    class Son:public Father{};  //  报错

 

六、继承与面向对象设计

条款32:确定你的public继承塑模出is-a关系

  • “public 继承” 意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上(反过来不一定成立),因为每一个derived class 对象也都是一个base class对象。
  • 扩展,子类调用父类虚函数方法:son->father::func();

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

  •  derived classes 内的名称会遮掩base classes 内的名称。为了让子类调用父类同名函数,可以可以使用。

条款34:区分接口继承和实现继承

  • pure virtual 函数只具体指定接口继承(但是书上例子可以指定默认实现继承→_→)。
    class base
    {
       virtual void show() = 0 
       {
           cout<<"this is base."<<endl; 
       } 
    };    
    
    class derived
    {
       virtual void show()
       {
           base::show();    //  父类默认实现
       } 
    };
  • impure virtual 函数具体指定接口继承及默认实现继承。
  • non-virtual 函数具体指定接口继承以及强制性实现继承(non-virtual 代表子类不该 overwrite 这个函数)。

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

 本条款地根本忠告是:当你为解决问题而寻找某个设计方案时,不妨考虑 virtual 函数的替代方案。

  • 使用 non-virtual interface(NVI)手法,那是 Template Method 设计模式(与c++templates并无关联)的一种特殊形式。它以 public non-virtual 成员函数包裹较低访问性(private 或 protect)的virtual。
     1 class gamecharacter
     2 {
     3 public:
     4     gamecharacter(int physique = 10, int power = 10, int agility = 10) :
     5         m_physique(physique),
     6         m_power(power),
     7         m_agility(agility)
     8     {}
     9     int healthvalue() const { return dohealvalue(); }        //  生命值
    10     int getphysique() const { return m_physique; }
    11     int getpower() const { return m_power; }
    12     int getagility() const { return m_agility; }
    13 private:
    14     virtual int dohealvalue() const { return m_physique * 10; }        //  生命值计算方法
    15 private:
    16     int m_physique;    //  体格
    17     int m_power;    //  力量
    18     int m_agility;    //  敏捷
    19 };
    20 
    21 class Soldier : public gamecharacter
    22 {
    23 public:
    24     Soldier() :gamecharacter() {}
    25 private:
    26     virtual int dohealvalue() const { return (getphysique() * 10 + getpower() * 20); }
    27 };
    28 
    29 int main()
    30 {
    31     Soldier soldier{};
    32     cout << soldier.healthvalue() << endl;    //  输出150
    33     return 0;
    34 }
  • 将 virtual 函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。
     1 class GameCharacter;
     2 int defaultHealthCalc(const GameCharacter& gc);
     3 class GameCharacter
     4 {
     5     public:
     6     //  声明一个HealthCalcFunc指针类型,
     7     //  用于接受生命值计算方法函数指针
     8     typedef int  (*HealthCalcFunc)(const GameCharacter&);        
     9     explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc,
    10         int physique = 10, int power = 10, int agility = 10) :
    11         healthFunc(hcf),
    12         m_physique(physique),
    13         m_power(power),
    14         m_agility(agility){}
    15 
    16     int healthValue() const { return healthFunc(*this); }  //  生命值
    17     int getPhysique() const { return m_physique; }
    18     int getPower() const { return m_power; }
    19     int getAgility() const { return m_agility; }
    20 private:
    21     int m_physique;    //  体格
    22     int m_power;    //  力量
    23     int m_agility;    //  敏捷
    24     HealthCalcFunc healthFunc;  //  生命值计算方法函数指针
    25 };
    26 
    27 int defaultHealthCalc(const GameCharacter& gc) 
    28     { return (gc.getPhysique() * 10); }  //  默认生命计算方法
    29 
    30 //  坦克生命计算方法
    31 int tankHealthCalc(const GameCharacter& gc) 
    32     { return (gc.getPhysique()*10 + gc.getPower()*20); };    
    33 
    34 //  刺客生命计算方法    
    35 int assassinHealthCalc(const GameCharacter& gc) 
    36     { return (gc.getPhysique() * 10 + gc.getAgility() * 5); };  
    37 
    38 class Soldier : public GameCharacter
    39 {
    40 public:
    41     Soldier(HealthCalcFunc hcf = defaultHealthCalc) :GameCharacter(hcf) {}
    42 };
    43 
    44 int main()
    45 {
    46     Soldier soldier1(tankHealthCalc);
    47     cout<//  输出300
    48 
    49     Soldier soldier2(assassinHealthCalc);
    50     cout<//  输出150
    51     return 0;
    52 }
  • 以 std::function 成员变量替换 virtual 函数,因而允许使用任何可调用物(callable entity)搭配一个兼容于需求的签名式。这也是 Strategy 设计模式的某种形式。
     1 class GameCharacter;
     2 int defaultHealthCalc(const GameCharacter& gc);
     3 class GameCharacter
     4 {
     5 public:
     6     //  使用函数封装器std::function,用于存储生命值计算方法函数
     7     typedef std::function    <int(const GameCharacter&)> HealthCalcFunc;    
     8     explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc,
     9         int physique = 10, int power = 10, int agility = 10) :
    10         healthFunc(hcf),
    11         m_physique(physique),
    12         m_power(power),
    13         m_agility(agility) {}
    14 
    15      
    16     int healthValue() const { return healthFunc(*this); }   //  生命值
    17     int getPhysique() const { return m_physique; }
    18     int getPower() const { return m_power; }
    19     int getAgility() const { return m_agility; }
    20 private:
    21     int m_physique;    //  体格
    22     int m_power;    //  力量
    23     int m_agility;    //  敏捷
    24     HealthCalcFunc healthFunc;    //  生命值计算方法函数指针
    25 };
    26 
    27 int defaultHealthCalc(const GameCharacter& gc) 
    28     { return (gc.getPhysique() * 10); }  //  默认生命计算方法
    29 
    30 int testHealthCalc(const GameCharacter& gc) 
    31     { return gc.getPhysique(); }  //  测试生命计算方法
    32 
    33 //  老弱血量结构体
    34 struct OldHealth
    35 {
    36     int operator() (const GameCharacter& gc) const 
    37         { return (gc.getPhysique()*5); }
    38 };
    39 
    40 //  精英血量类
    41 class EliteHealth
    42 {
    43 public:
    44     int eliteHealthCalc(const GameCharacter& gc) const 
    45         { return (gc.getPhysique() * 10 + 
    46             gc.getPower() * 20 + 
    47             gc.getAgility() * 5); }  //  精英生命值计算方法
    48 };
    49 
    50 class Soldier : public GameCharacter
    51 {
    52 public:
    53     Soldier(HealthCalcFunc hcf = defaultHealthCalc) :GameCharacter(hcf) {}
    54 };
    55 
    56 int main()
    57 {
    58     Soldier soldier0{testHealthCalc};  //  传函数
    59     cout<endl;
    60 
    61     OldHealth oh;
    62     //  传结构体成员函数
    63     Soldier soldier1{ std::bind(&OldHealth::operator(), 
    64                                             oh, 
    65                                             std::placeholders::_1) }; 
    66     cout<endl;
    67 
    68     EliteHealth eh;
    69     //  传类成员函数
    70     Soldier soldier2{ std::bind(&EliteHealth::eliteHealthCalc, 
    71                                             eh, 
    72                                             std::placeholders::_1) };  
    73     cout<endl;
    74     return 0;
    75 }
    76 
    77 //  输出:
    78 //  10
    79 //  50
    80 //  350       
  • 将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数。这是 Strategy 设计模式的传统实现手法(代码略)。

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

条款37:绝不重新定义继承而来的默认参数值(默认形参)

  • 因为默认参数值都是静态绑定,而 virtual 函数--才是你唯一应该重写的东西,因为它是动态绑定。
    class base
    {
       virtual void show(int i = 10){ cout<<"base = "<< i << endl;} 
    };
    
    class derived: public base
    {
       virtual void show(int i = 20) override { cout<<"derived = "<< i << endl; } 
    };
    
    int main()
    {
      //  base* b 是静态绑定,其默认形参是静态绑定
      //  new derived 是动态绑定, 其默认形参是静态绑定,虚函数是动态绑定
      base* b = new derived;
      b->show();    //  输出"derived = 10"
      return 0;  
    }
  • 但如果你试着遵守这条规则,并且同时提供默认参数值给 base 和 derived ,将会导致代码重复和相依性(with dependencies),如果 base 虚函数默认参数值变了,那么 derived 默认参数值也必须要改变,这样及其不方便。解决办法之一是NVI(non-virtual interface)手法,参考条款35。
  • 扩展:关键字 override 很重要,一定要在 derived 虚函数后面添加。作用两个,第一,表示该虚函数是从 base 继承,非 derived 自己定义;第二,强制编译器检查虚函数是否 override ,例如形参改变,返回值改变,const是否添加等等,这些会导致 override 行为都会报错。
  • overload(重载):重载的概念最好理解,在同一个类声明范围中,定义了多个名称完全相同、参数(类型或者个数)不相同的函数,就称之为overload(重载)。重载的特征如下:
    1)相同的范围(在同一个类中);
    (2)函数名字相同;
    (3)参数不同;
    (4overload 并不是关键字。它只是C++编译器的一种行为方式。
  • override(重写):覆盖的概念其实是用来实现C++多态性的,即子类重新改写父类声明为virtual的函数。Override(覆盖)的特征如下(PS:我一直以为IDE里面翻译说的“重写”是overwrite,其实是不对的):
    1)不同的范围(分别位于派生类与基类);
    (2)函数名字相同;
    (3)参数列表完全相同;
    (4)基类函数必须有 virride 关键字。
  • overwrite:重写是指派生类的函数屏蔽(或者称之为“隐藏”)了与其同名的基类函数。正是这个C++的隐藏规则使得问题的复杂性陡然增加,这里面分为两种情况讨论:
    1)如果派生类的函数与基类的函数同名,但是参数不同。那么此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
    (2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。那么此时,基类的函数被隐藏(注意别与覆盖混淆)。
    (3)overwrite 并不是关键字。它只是C++编译器的一种行为方式。

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

  •  根据迪米特法则,一个对象应该对其他对象保持最少的了解。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大,也就越容易出错,所以尽量使用组合、聚合来代替继承。

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

  •  private 继承意味着is-implemented-in-terms of(根据某物实现出)。它通常比复合的级别低。但是当 derived class 需要访问 protect base class 的成员,或者需要重新定义继承而来的 virtual 函数时,这么设计是合理的。
  • 和复合不同,private 继承可以造成 empty base 最优化。这对致力于 “对象尺寸最小化” 的程序库开发者而言,可能很重要。

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

  •  多重继承比单一继承复杂,它可能导致新的歧义性。
  • virtual 继承会增加大小、速度,初始化(及赋值)复杂度等等成本。如果 virtual base classes 不带任何数据,将是最具有实用价值的情况。
  • 多重继承的确有正当用途。其中一个情节涉及“public 继承某个 Interface class” 和 “ private 继承某个协助实现的class ” 的两相组合。

七、模板与泛型编程

条款41:了解隐式接口和编译期多态

C++