Effective C++

本文是对Effective C++一书的个人总结,其中错误和欠缺会陆续改正与补充.

文章目录

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

C++有4个次语言

  • C
    C是C++的基础,C++是C的较高级解法.C语言的局限:没有模板,没有异常,没有重载…
  • Object-Oriented C
    C with Classes:classes(构造,析构),封装,继承,多态,virtual函数…
  • Template C++
    带来template meta programming.
  • STL
    提供容器,迭代器,算法等.比如vector,map等.

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

尽量以编译器替换预处理器:

  • 对于常量,以const或enum替换#define,
  • 对于形似函数的宏,以inline替换#define.比如#define ASPECT_RATIO 1.653

# define ASPECT_RATIO 1.653, 编译器并没有把 ASPECT_RATIO放入记号表中.发生编译错误的时候,错误信息将提及1.653而非 ASPECT_RATIO.预处理器盲目替换宏,目标码中可能保存多份1.653,所以使用宏目标码体积可能较大.

class的专属常量值得注意

class GamePlayer {
  private: 
    static int const NumTurns = 5;//常量声明式
    int scores[NumTurns];
};
int const GamePlayer::NumTurns;//常量定义式

C++要求为所有东西提供定义式,但如果是类的static整数类型常量(包括int,char,bool)就不需要提供定义式,只需要给出声明即可.如果还想要取得这个static常量的地址,则需要提供定义式.

如果编译器不允许static整数型class常量完成类内初始值设定,可以改用enum hack方式.

class GamePlayer {
  private:
    enum { NumTurns = 5 };
    int scores[NumTurns];
};

尽量不要写宏函数,调用宏函数可能会遭遇麻烦.可以改用inline template函数.

条款3:尽可能使用const

  • const可施加于任何对象,函数参数,函数返回类型和成员函数.
  • 编译器强制实施bitwise constness,但是在编写程序时应该使用logical constness.
  • 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本避免代码重复.

令函数返回一个常量值,可以降低User造成意外错误,同时不会放弃安全性和高效性.

class Rational;
Rational const operator*(Rational const& lhs, Rational const& rhs);
Rational a, b, c;
(a* b) = c;//在a*b的结果上调用operator=

如果a和b都是内置类型,上述代码不合法.比如abc都是int类型,(a*b)=c显然不合法.

将const实施于成员函数的目的,是为了确认该成员函数可作用在const对象身上,理由有二.

  • 能直接看出哪些函数可以修改对象.
  • 使得操作const对象成为可能.为了减少复制,函数参数常以const引用传递,而const对象只能访问const成员函数.

注意:如果两个成员函数的常量性不同,可以被重载.常见的用法是数组索引访问操作arr[index].

class Text {
  private:
    string text;
 
  public:
    char const& operator[](int pos) const {
        return text[pos];
    }
 
    char& operator[](int pos) {
        return text[pos];
    }
};

bitwise constness和logical constness
bitwise const表明成员函数不能修改对象的任何bit.然而,许多成员函数不具备const性质却依然能够通过编译器的bitwise测试.比如返回对象某个成员的引用.

class Text {
  private:
    char* text;
 
  public:
    char& operator[](int pos) const {
        return text[pos];
    }
};

Text const text("Hello");//声明一个常量对象
char* pc=&text[0];//调用const operator[]取得一个指针
*pc='J';//现在有了“Jello”这样的内容

来看logical constness,它主张一个const成员函数可以修改它所处理的对象内的某些bits,但只有User察觉不到时可以使用.比如某个带有高速缓存的Text类.

class CTextBlock {
  public:
    size_t length() const {
        if (!lenghIsValid) {
            textLength = strlen(pText);//Error,不能在const成员函数内给这两个成员变量赋值
            lenghIsValid = true;
        }
        return textLength;
    }
 
  private:
    char* pText;
    size_t textLength;
    bool lenghIsValid;
};

length()函数不满足bitwise constness,其修改了成员变量的值,但是从逻辑而言,这样的修改可被接受.但编译器拒绝通过编译.解决方法是使用mutable释放掉non-const成员的bitwise constness约束.

class CTextBlock {
  public:
    size_t length() const {
        if (!lenghIsValid) {
            textLength = strlen(pText);//这些成员变量总是能够被修改,即使在const成员函数内
            lenghIsValid = true;
        }
        return textLength;
    }
 
  private:
    char* pText;
    size_t mutable textLength;
    bool mutable lenghIsValid;
};
 

成员函数的const版本和non-const版本常常有本质上的相似性.其内部实现常常具有大量相同代码,比如:

class CTextBlock {
  public:
    char const& operator[](int pos) const {
        // 边界检验代码
        // 日志数据访问
        // 检验数据完整性
        return text[pos];
    }
 
    char& operator[](int pos) {
        // 边界检验代码
        // 日志数据访问
        // 检验数据完整性
        //代码内容完全和const版本的operator[]一样
        return text[pos];
    }
 
  private:
    string text;
};

让non-const版本成员函数调用const版本成员函数减少代码冗余.令const版本调用non-const版本以避免重复不行.const成员函数承诺绝不改变其对象的逻辑状态,non-const成员函数没有这般承诺,如果在const函数内部调用non-const函数,就是冒了风险.non-const成员函数本来就可以对其对象做任何动作,所以在其中调用一个const成员函数并不会带来风险.

class CTextBlock {
  public:
    char const& operator[](int pos) const {
        // 边界检验代码
        // 日志数据访问
        // 检验数据完整性
        return text[pos];
    }
 
    char& operator[](int pos) {
        return const_cast<char&>(static_cast<CTextBlock const&>(*this)[pos]);
    }
 
  private:
    string text;
};

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

  • 手工初始化内置类型.
  • 构造函数用成员初值列而非在构造函数体内赋值.
  • 用local static对象替换non-local static对象;

在C++中,将对象初始化这件事,似乎反复无常.
int x;在某些语境下保证被初始化为0,在其他语境中,却不保证.

类的成员变量不保证正确的初始化.使用c part of cpp并且初始化导致运行期成本,则不会初始化,比如array.non-c part of cpp则保证,比如vector.规则如此混乱,最佳处理手段是:永远初始化.

不要混淆初始化和赋值.只有使用构造函数初值列才是赋值,构造函数体内的所有=都是赋值.因此对于一个class中的const对象,只能在初值列处初始化.

C++有着十分固定的成员初始化顺序.base class总是早于derived class,class成员变量总是以声明的顺序被初始化.

不同编译单元内定义的non-local static对象的初始化次序

static对象包括全局对象,namespace中的对象,class内static对象,函数内static对象,file作用域内static对象.函数内的static对象是local static对象.其余位置的static对象是non-static对象.

编译单元指的是产出单一目标文件的那些源码,基本上是单一源码文件加上其所含入的头文件.不同编译单元内定义的non-local static对象的初始化次序cpp没有明确定义.

class FileSystem {
  public:
    size_t numDisks() const;
};
 
extern FileSystem tfs;
 
class Directory {
  public:
    Directory() {
        size_t disks = tfs.numDisks();
    }
};
Directory tempDir;

上述代码中,除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs.

 
class FileSystem {
  public:
    size_t numDisks() const;
};
 
FileSystem& tfs() {
    static FileSystem fs;
    return fs;
}
 
class Directory {
  public:
    Directory() {
        size_t disks = tfs().numDisks();
    }
};
 
Directory& tempDir() {
    static Directory td;
    return td;
}

将每个non-local static对象搬到自己的专属函数中,然后这些函数返回一个reference指向它所含的对象.也就是用local static对象替换non-local static对象.这也是单例模式常见的实现手法.

C++保证函数内的local static对象会在该函数被调用期间,首次遇上该对象之定义式时被初始化.如果从未调用这个函数,就绝不会引发构造和析构的成本,non-local static做不到.此类函数很短,适合声明为inline.

注意,任何一种non-const static对象,不论它是local还是non-local,在多线程环境下都会有麻烦.

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

  • 如果没有声明构造函数,编译器会声明一个编译器版本的copy构造函数,copy assignment操作符,析构函数和一个默认构造函数.只有当这些函数被调用的时候,他们才会被编译器创建出来.
  • 注意,编译器产生的析构函数是non-virtual的,除非这个class 的base class自身声明有virtual析构函数.
class Empty {};
 //这个代码其实相当于下面的代码,编译器为我们做了一些工作.
class Empty {
  public:
    Empty() {
    }
 
    Empty(Empty const& rhs) {
    }
 
    ~Empty() {
    }
 
    Empty& operator=(Empty const& rhs) {
    }
};
 

copy构造函数和copy assignment操作符,编译器创建的版本只是单纯的将来源对象的每一个non-static成员变量拷贝到目标对象.NamedObject<int>的成员变量是string和int,编译器产生的copy构造函数会以string的构造函数复制string成员变量,并按照bit复制内置类型int.

template <class T>
class NamedObject {
  public:
    NamedObject(char const* name, T const& value);
    NamedObject(string consst& name, T const& value);
 
  private:
    string nameValue;
    T objectValue;
};

copy assignment操作符和copy构造函数如出一辙,但一般而言,只有当生成的代码合法并且有适当机会证明它有意义,编译器才会为class生成operator=,否则,拒绝为class生成operator=.

template <class T>
class NamedObject {
  public:
    NamedObject(string& name, T const& value);
 
  private:
    string& nameValue;
    T objectValue;
};

cpp不允许让reference改指向不同对象,因此对于上面的代码,cpp的响应是拒绝编译那一行赋值动作.另外,面对内含const成员的class,编译器的反应也一样,更改const成员是不合法的.此外,如果某个base class将copy assignment操作符声明为private,编译期拒绝为其derived class生成一个copy assignment.

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

  • 如果不想让某个类拥有拷贝构造和operator=,应该将这两个函数设置为private并且只声明不实现,这样类的定义外无法调用这两个函数.
  • 然而,这样的作法并非绝对安全,因为成员函数或friend函数还是可以调用private函数.一旦成员函数或friend函数调用他们,会获得一个链接错误.

如此方法将在链接期找到错误,将链接期错误转移到编译期是可能的,只要将拷贝构造函数和copy assignment设置为private就可以办到,但并不是在类本身上设置,而是专门定义一个禁止拷贝动作的base class,再用这个base class派生.

class Uncopyable {
  protected:
    Uncopyable() {
    }
 
    ~Uncopyable() {
    }
 
  private:
    Uncopyable(Uncopyable const&);
    Uncopyable& operator=(Uncopyable const&);
};
 
class SomeClass:private Uncopyable{
};

条款7:为多态类声明virtual虚构函数

  • 多态性质的base class应该声明一个virtual析构函数.
  • class的设计目的不是作为base class使用,或不是为了具备多态性,就不应该声明virtual析构函数.
class TimeKeeper {
  public:
    TimeKeeper();
    ~TimeKeeper();
};
 
class AtomicClock : public TimeKeeper {};
 
class WaterClock : public TimeKeeper {};
 
class WristClock : public TimeKeeper {};
//某个工厂函数,能够返回不同类型的Timekeeper
TimeKeeper* getTimekeeper() {
    return {};
}
 
int main() {
    TimeKeeper* ptk = getTimekeeper();
    delete ptk;
}

问题在于getTimeKeeper函数返回的是一个derived class对象,但这个对象却经由一个base class指针被删除,且这个base class有一个non-virtual析构函数.C++指出,derived class经由base class的指针被删除,同时base class带有一个non-virtual析构函数,结果未定义,实际执行时通常发生的时对象的derived成分(声明于派生类的成员变量)没被销毁,这种情况是导致资源泄露的绝佳途径.给base class一个virtual虚构函数就能消除这个问题.

给一个并不想当作base class的类一个virtual虚构函数是一个馊主意.

class Point {
  public:
    Point(int xCoord, int yCoord);
    ~Point();
 
  private:
    int x, y;
};

如果int占用32bit,那么Point对象可被塞入一个64bit的缓存器中.欲实现出virtual函数,对象必须携带某些信息,用来在运行期决定哪一个virtual函数该被调用,也就是vptr.每个带有virtual函数的class都有相应的vtbl.当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指向的那个vtbl,编译器在其中寻找适当的函数指针.virtual类额外带来了一个vptr指针,因此Point类如果带有virtual析构函数,将增加一个指针的大小.

class Base {
  public:
    virtual ~Base() = 0;
};
 
Base::~Base(){
 
}

必须为pure virtual析构函数提供一份定义.析构函数的运作方式是,most derived的那个类的析构函数最先调用,然后是其每一个base class的析构函数被调用.编译器会在Base的派生类的析构函数中创建一个对~Base()的调用动作,因此必须为这个函数提供一个定义,否则linker将报错.

并非所有的base class的设计目的都是为了多态用途.标准string和STL容器都不被设计作为base class使用,更别提多态了,有些class的设计目的是作为base class使用,但不是为了多态用途.

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

  • 析构函数绝对不要吐出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们(不传播)或结束程序.
  • 如果客户需要对某个操作函数运行时抛出的异常做出反应,class应该提供一个普通函数(而非析构函数)执行该操作.
class Widget {
  public:
    ~Widget() {
    }
};
 
void doSomething() {
    vector<Widget> v;
}

假设v中内含10个Widget,而在析构第一个元素期间,有个异常被抛出,其他9个Widget还是应该被销毁,因此v应该调用他们各个析构函数.但在调用期间,第二个Widget析构函数又抛出异常,目前有两个异常,这对C++而言太多了.两个异常同时存在的情况下,程序不是结束执行就是导致不明确行为.

// 使用一个class负责数据库连接
class DBConnection {
  public:
    static DBConnection create();
    void close();
};
//为了防止用户忘记调用close关闭数据库,使用DBConn来管理数据库的关闭.
class DBConn {
  public:
    ~DBConn() {
        db.close();
    }
 
  private:
    DBConnection db;
};

只要close成功,一切ok,但是如果close的调用导致了异常,DBConn的析构函数就会传播该异常,也就是允许异常离开这个析构函数,这会造成问题,抛出了难以驾驭的麻烦.

// 使用一个class负责数据库连接
class DBConnection {
  public:
    static DBConnection create();
    void close();
};
处理这个异常
class DBConn {
  public:
    ~DBConn() {
        try {
            db.close();
        } catch (...) {
            std::abort();//产生异常直接终止程序,或者在此处制作日志,记下调用失败的信息.
        }
    }
 
  private:
    DBConnection db;
};

一般而言,将异常吞掉是个坏主意,因为它压制了某些调用失败的重要信息.但吞下异常还是比一点异常处理都不做要好.上述两个异常处理的办法没什么好的,问题在于两者都无法对导致close抛出异常的情况做出反应.一个比较好的策略是重新设计DBConn接口,使得客户有机会对可能出现的问题做出反应.

class DBConn {
  public:
    // 提供给用户使用的新函数
    void close() {
        db.close();
        closed = true;
    }
 
    ~DBConn() {
        try {
            db.close();
        } catch (...) {
            std::abort();
        }
    }
 
  private:
    DBConnection db;
    bool closed;
};

把调用close的责任从DBConn析构函数手上转移到DBConn客户手上可能会给人“肆无忌惮转移负担”的印象,但是,如果某个操作可能在失败时抛出异常,而且又存在某种需要必须处理该异常,那么这个异常必须来自析构函数意外的某个函数.因为析构函数抛出异常就是危险,会带来过早结束程序或发生不明确行为的危险.由客户自己调用close并不会对他们造成负担,而是给他们一个处理错误的机会.

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

  • 在构造和析构期间不要调用virtual函数,因为这类调用从来不会下降至derived class那层.

假设有个class继承体系,用来表示交易的买进卖出的订单等,这样的交易需要经过审计,所以每当创建一个交易对象,审计日志中也需要创建一笔适当记录,下面是一个例子.

class Transaction {
  public:
    // 所有交易的base class
    Transaction();
    // 做出一份因类型不同而不同的日志记录
    virtual void logTransaction() const = 0;
};
 
Transaction::Transaction() {
    logTransaction();
}
 
class BuyTransaction : public Transaction {
  public:
    virtual void logTransaction() const;
};
 
class SellTransaction : public Transaction {
  public:
    virtual void logTransaction() const;
};

现在,执行BuyTransaction b;
现在会有一个BuyTransaction的构造函数被调用,但是base class的构造函数一定先被调用,base class的成分会在derived class的成分被构造之前先构造妥当.Transaction构造函数的最后一行调用的logTransaction虚函数是base class的版本,并非derived class的版本,即使创建的类型是derived class.或者用非正式的说法,在base class构造期间,virtual函数不是virtual 函数.相同的道理适用于析构函数,一旦derived class的析构函数开始执行,对象内的derived class成员变量呈现未定义值,C++将视他们仿佛不存在.进入base class析构函数后对象就成为一个base class对象,C++的任何部分包括virtual函数和dynamic_cast等也如此看待它.

有时候类拥有不止一个构造函数,会把每个构造函数中常用的初始化操作总结成一个init函数,在不同的构造函数中调用.然而,如果这个init函数中仍然不小心调用了一个virtual函数,危险同上述一样,并且,比直接在析构函数中调用虚函数更加恶劣的是,这样潜藏的调用编译器和连接器不会发出任何抱怨.很多时候base class的virtual函数还是pure virtual,调用pure virtual,大多数执行系统会终止程序.如果这个virtual函数存在一份定义,很容易留下一个百思不解的问题,为什么建立一个derived class对象时会调用错误版本的函数.唯一能够避免此类问题的做法就是:确保构造函数和析构函数中没有调用virtual函数.

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

有关赋值,可以把他们写成连锁形式:
int x,y,z;
x=y=z=15;
上述的连锁赋值被解析为:x=(y=(z=15));
为了实现连锁赋值,赋值操作符必须返回一个reference指向操作符左侧的实参.这是为class实现赋值操作时应该遵守的协议.

class Base {
  public:
    Base& operator=(Base const& b) {
        //...
        return *this;
    }
};

这个协议也适用于+=,-=,*=等等.这只是个协议,并无强制性,如果不遵守,代码同样可以通过编译,然而,cpp的内置类型,标准库提供的各种类型中都遵守这个协议,所以.没有一个很好的理由,应当遵守这个协议.

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

  • 确保对象自我赋值时拥有良好的行为,处理这个问题的技术有证同测试,精心周到的调整语句顺序以及copy and swap.
  • 确保任何函数如果操作一个以上的对象,其中多个对象是同一个对象时,其行为仍然正确.
  • 代码中会显式或隐式的使用自我赋值,w=w;需要良好的设计operator=.
class Bitmap {};
 
class Widget {
  private:
    Bitmap* pb;
    Widget& operator=(Widget const& rhs){
        delete pb;
        pb=new Bitmap(*rhs.pb);
        return *this;
    }
};

隐含着一些错误,如果operator=的参数就是*this本身,那么首先使用delete就已经释放了本身Bitmap成员的内存,最后pb持有了一个已经被删除了的对象.

改进:

class Bitmap {};
 
class Widget {
  private:
    Bitmap* pb;
 
    Widget& operator=(Widget const& rhs) {
        if (this == &rhs)
            return *this;
        delete pb;
        pb = new Bitmap(*rhs.pb);
        return *this;
    }
};

在operator=一开始做一个证同测试,这样就具备了自我赋值的安全性了.然而,一旦后面的new抛出异常(分配时内存不足或Bitmap的构造函数抛出异常),最终pb都将指向一个已经被删除的对象.

让operator=具备异常安全性往往自动获得自我赋值安全性的回报.因此对自我赋值的处理态度往往是不去管他,而是把焦点放在异常安全性.

class Widget {
  private:
    Bitmap* pb;
 
    Widget& operator=(Widget const& rhs) {
        Bitmap* old = pb;
        pb=new Bitmap(*rhs.pb);
        delete old;
        return *this;
    }
};

现在如果new动作发生异常,pb依然保持原状.即使没有证同测试,这段代码还是能够处理自我赋值.

一个兼具异常安全和自我赋值安全的替代方案是使用所谓的copy and swap技术.

class Widget {
  private:
    Bitmap* pb;
 
    void swap(Widget& rhs);//交换*this和rhs的数据,详见条款29
    Widget& operator=(Widget const& rhs) {
        Widget temp(rhs);
        swap(temp,*this);
        return *this;
    }
};

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

  • copying函数应该确保复制对象内的所有成员变量及所有base class成分.
  • 不要让某个copying函数实现另一个copying函数.

面向对象会将对象的内部封装起来,只留两个函数负责对象拷贝,copy构造函数和copy assignment.条款5指出编译器会在必要的时候为class创建copy构造函数,编译器版本的copy构造将被拷贝的所有成员变量都做一份拷贝.声明自己的copying函数,就是告诉编译器不喜欢缺省实现的版本,编译器仿佛遭到冒犯而回敬:当自己实现的版本几乎必然出错而不告诉你.

void logCall(string const& funcName); // 日志函数
 
class Customer {
  public:
    Customer(Customer const& rhs);
    Customer& operator=(Customer const& rhs);
 
  private:
    string name;
};
 
Customer::Customer(Customer const& rhs) {
    logCall("copy constructor");
}
 
Customer& Customer::operator=(Customer const& rhs) {
    logCall("copy assignment");
    name=rhs.name;
    return *this;
}

到目前为止,一切都好,现在做出一些改动,在成员变量中加入另一个类Date

class Customer {
  public:
    Customer(Customer const& rhs);
    Customer& operator=(Customer const& rhs);
 
  private:
    string name;
    Date lastTransaction;
};

这时候已有的copying函数执行的都是局部拷贝(partial copy).没有复制Date成员变量.因此,当为类添加一个成员变量,必须同时修改copying函数以及任何非标准形式的operator=.

一旦发生继承,更会暗藏玄机.

class PriorityCustomer : public Customer {
  public:
    PriorityCustomer(PriorityCustomer const& rhs);
    PriorityCustomer& operator=(PriorityCustomer const& rhs);
 
  private:
    int priority;
};
 
PriorityCustomer::PriorityCustomer(PriorityCustomer const& rhs) {
    logCall("prioritycustomer copy constructor");
}
 
PriorityCustomer& PriorityCustomer::operator=(PriorityCustomer const& rhs) {
    logCall("priority copy assignment");
    priority = rhs.priority;
    return *this;
}

看似复制了每个成员变量,然而PriorityCustomer还内含其继承的Customer成员变量,而那些成员变量却从未被复制.PriorityCustomer的copy构造函数没有指定实参传给base class构造函数(初值列中没有提及base class),因此base class会被默认构造函数初始化,将继承而来的成员name和lastTransaction执行缺省的初始化动作.任何时候只要为derived class编写copying函数,必须小心的复制其base class成分,那些成分往往是private,因此无法直接访问,应当让derived class的copying函数调用相应的base class函数.

PriorityCustomer::PriorityCustomer(PriorityCustomer const& rhs)
    : Customer(rhs), priority(rhs.priority) {//调用base class的copy构造函数
    logCall("prioritycustomer copy constructor");
}
 
PriorityCustomer& PriorityCustomer::operator=(PriorityCustomer const& rhs) {
    logCall("priority copy assignment");
    Customer::operator=(rhs);//对base class成分进行赋值
    priority = rhs.priority;
    return *this;
}

copy构造函数和copy assignment往往有着相似的实现本体,这可能会诱导你调用其一实现另外一个,这种精益求精的想法值得赞赏,然而却无法达到你想要的目标.令copy assignment调用copy构造函数不合理,这就像试图构造一个已经存在的对象,不该令copy assignment调用copy构造函数.反之,不该令copy构造函数调用copy assignment,构造函数用来初始化新对象,而assignment操作符只施行于已初始化的对象身上.如果copy构造函数的确于copy assignment有相近的代码,应当新建一个private的init函数来消除重复.

条款13:以对象管理资源

  • 复制RAII对象必须一并复制他所管理的资源,所以资源的copying行为决定RAII对象的copying行为.
  • 普遍常见的RAII class copying行为是,抑制copying,引用计数,其他行为也都可能被实现.
    为了防止资源泄露,应使用RAII对象.
class A;
A* createA();
 
void f() {
    A* temp = createA();//工厂函数
    //...
    delete temp;
}

若干情况下,f函数无法删除temp,…区域的代码可能写下过早返回语句,导致delete语句不会被触及…区域的代码还可能抛出异常,同样导致delete语句不执行.当然可以谨慎的编写程序来防止这类错误,但代码可能经由不同的人之手,随着时间渐渐被修改.为了确保createA函数返回的资源总是被释放,需要将资源放进对象内,当控制流离开f,该对象的析构函数会自动释放资源.把对象放进对象内,可以依赖C++的析构函数自动调用机制确保资源被释放.
许多资源被动态分配于heap而后被用于单一区块或函数内,应确保控制流离开区域块或函数后被释放.智能指针就是针对这种情况设计的特制产品.

获得资源后应立即放进管理对象中,这种用对象管理资源的观念被称为资源取得时机便是初始化时机(RAII),因为几乎总是获得资源后初始化某个管理对象.
管理对象运用析构函数确保资源被释放.利用析构函数离开作用域被自动调用的特性.

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

  • API往往要求访问原始资源,所以每一个RAII class应该提供一个返回其管理的原始资源的函数.
  • 对原始资源的访问可能经由显式转换或隐式转换,显式转换比较安全,隐式转换比较自然.

条款13引入了RAII的观念,并描述了智能指针如何在heap-based表现这个观念.可能某时也需要建立自己的资源管理类.
例如,假设我们使用C API函数Mutex互斥器对象,有lock和unlock两个函数可用.为了确保不会忘记将一个被锁住的Mutex解锁,可能希望建立一个class用来管理Mutex,这种class由RAII守则支配,在资源在构造期间获得,在析构期间释放.

class Mutex;
void lock(Mutex* m);
void unlock(Mutex* m);
 
class Lock {
  private:
    Mutex* mutexPtr;
 
    explicit Lock(Mutex* pm) : mutexPtr(pm) {
        lock(mutexPtr);
    }
 
    ~Lock() {
        unlock(mutexPtr);
    }
};
 
int main() {
    Mutex* m1=createMutex();//某种工厂方法
    Lock l(m1);
}

这很好,出了main的作用域,会自动释放资源,但是如果Lock对象被复制,会发生什么事情?大多数情况下会选择以下两种可能.
Mutex m;
Lock ml1(&m);
Lock ml2(ml1);

  • 禁止复制.许多时候允许RAII对象被复制并不合理,因此应该将copying操作声明为private.
class Lock:private Uncopyable{
    public:
         ...
};
  • 对底层资源祭出引用计数法.有时候我们希望保存资源,直到他的最后一个使用者被销毁.std::shared_ptr就是如此.
  • 复制底部资源,比如标准字符串由指向heap内存的指针构成,当这样的字符串被复制,不论指针或其所指内存都会被制作出一个副本,展现深拷贝行为.
  • 转移底部资源的拥有权.将资源的拥有权从被复制物转移到目标物.比如unique_ptr.
    copying函数可能被编译器自动创建出来,因此除非编译器版本做了你想要做的事,否则应该自己编写他们.

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

  • 资源管理类很棒,有效对抗资源泄露.然而许多API直接涉及资源本体,除非你发誓永不录用这样的API,否则,应该绕过资源管理对象直接访问原始资源.
class A;
A* createA();
void f(A const* a);
int main(){
    shared_ptr<A> pA(createA());
    //现在想这么调用f函数,然而发生了错误
    f(pA);
}

f函数需要的是一个A*,然而传给他的是一个shared_ptr<A> 对象.这时候需要一个函数来将RAII对象转换为其内含的原始资源.可以分别使用显式转换和隐式转换来实现.
显式转换的例子是,shared_ptr内部提供一个get函数,返回RAII对象管理的原始资源.有时候,接口的参数大量的使用原始资源而非RAII对象,这就需要频繁的调用RAII对象的get方法,如此这般的要求显示转换,足以让人倒尽胃口,不愿再使用这个RAII类,从而增加了泄露资源的可能.所以另一个办法就是隐士转换.

class Rsc;
 
class A {
  private:
    Rsc* r;
 
    operator Rsc*() const {
        return r;
    }
};

此时在函数参数为Rsc*的函数里填入A类对象作为参数将引发隐式转换.
提供一个显式转换还是隐式转换将RAII转换为其管理的底部资源,主要取决于RAII class被设计执行的特定工作以及他们被使用的情况.最佳设计是让接口容易被正确使用而不易被误用.通常显式转换如get函数是一个比较受欢迎的方法,因为他将非故意转换问题最小化,但是有时候隐式转换带来的“自然用法”又会引发天平倾斜.
RAII类内提供一个返回其管理的原始资源的函数,与封装发生了矛盾,但不为设计灾难.因为RAII类并非为了封装某物而存在,而是为了确保一个特殊行为——资源释放.

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

    string* stringArr=new string[100];
    delete stringArr;

看似井然有序,使用了new,也搭配了delete,但你的程序行为不明确,C++未有定义,最低限度,stringArr所含的100个string对象中的99个不太可能被适当删除,他们的析构函数未被调用.

通过new动态生成一个对象,发生两件事:第一,内存被分配出来,第二,针对此内存会有一个或更多构造函数被调用.通过delete删除一个对象,也有两件事发生:第一,针对此内存会有一个析构函数被调用,第二,内存被释放.delete的问题在于,被删除的内存究竟存有多少对象,这决定了有多少个析构函数必须被调用起来.这个问题可以更简单描述,被删除的那个指针,指向的是单一的对象还是对象数组.

单一对象和数组对象的内存布局不一样,数组内存布局通常还包括数组大小的记录.当对着一个指针使用delete,唯一能够让delete知道内存中是否存在一个数组大小记录的方法是:在使用delete时加上方括号,delete便认定指针指向一个数组,否则认定指针指向单一对象.

string* stringArr=new string[100];
delete[] stringArr;

如果对单一对象使用delete[]会发生什么事?结果未有定义,可能读取若干内存并解释其为数组大小记录,然后多次调用析构函数,浑然不知所处理的内存不但不是那个数组.
如果没有对数组对象使用delete[]会发生什么事?结果未有定义,可能猜想导致太少的析构函数被调用.
规则很简单,使用new[]就应该使用delete[],使用new就应该使用delete.当撰写一个class时,此class含有一个指针指向动态分配内存并提供多个构造函数时,此规则尤为重要,必须小心的在所有构造函数中使用相同形式的new将指针成员初始化.如果没有这样做,无法得知在析构函数中使用什么形式的delete.此规则对喜欢使用typedef的人也很重要,比如:

typedef string AddressLines[4];
 
    string* p = new AddressLines;
    delete p;//行为未有定义!
    delete[] p;//行为正确!

为了避免此类错误,最好不要对数组形式做typedef动作,cpp中的标准库可将对数组的需求降至几乎为0.

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

  • 以独立语句将newed对象存储于智能指针内,如果不这样,一旦有异常抛出,有可能导致资源泄露.
// 用来揭示处理程序的优先权
int priority();
class Widget;
// 用来在某些动态分配所得的Widget行进行某些带有优先权的处理
void processWidget(shared_ptr<Widget> pw, int priority);

考虑以下调用:

processWidget(new Widget, priority());

如下调用形式不能通过编译,调用processWidget函数前,编译器必须创建代码做三件事,调用priority,执行new Widget,调用shared_ptr构造函数.然而,只能确定new Widget在shared_ptr构造函数被调用前执行,但对priority的调用顺序无法确定.可能会产生以下调用顺序:
执行new Widget,调用priority函数,调用shared_ptr构造函数.如果在priority函数的调用过程中产生异常,new Widget返回的指针将会遗失,因为它尚未被置入shared_ptr中.避免此类问题的办法很简单,使用分离语句.分别写出创建widget,将它放入智能指针内,然后再传给processWidget函数的代码.

 
shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

编译器对于“跨越语句的各项操作”没有重新排列的自由,将代码位于不同的语句中,编译器不得在他们之间任意选择执行次序.

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

  • 好的接口容易被正确使用,不易被误用,应该在你的所有接口中努力达成这些性质.
  • 促进正确使用接口的方法包括接口的一致性,以及与内置类型的行为兼容.阻止误用的方法包括建立新类型,限制类型上操作,束缚对象值和消除客户的资源管理责任.
  • shared_ptr支持制定custom deleter,可以防止dll问题,可被用来自动解除互斥锁.
 
class Date {
  public:
    Date(int month, int day, int year) {
    }
};

乍见之下这个接口通情达理,然而易犯错误.第一,容易导致以错误的次序传递参数,比如Date d(30,3,1995);第二,可能传递一个无效的月份或天数,比如,Date d(2,3,1995).对于这样的问题,应该引入简单的wrapper types来区别天数,月份和年份,然后在Date的构造函数中使用这些类型.创建三个类,分别为Day,Month,Year,每个类仅包含一个int,作为int类型的wrapper types.这样在错误使用构造函数的时候,编译器会给出类型不正确的提示.对于月份的限制,可以使用local static的方法,比如:

class Month {
  public:
    Month(int _month) : m(_month) {
    }
    static Month Jan() {
        return Month(1);
    }
    //...
    static Month Dec() {
        return Month(12);
    }
 
  private:
    int m;
};

对于一些返回raw pointer的工厂函数,应该尽量让接口返回智能指针,消除客户管理内存的责任.另外,使用智能指针能有效消除潜在的cross-DLL problem.通过自定义智能指针的deleter来实现一些特定的需求.

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

  • class的设计就是type的设计,定义一个新type之前一定要仔细考虑本条款.

当定义一个新class,也就定义了一个新的type.这意味着你不仅仅是class设计者,还是type设计者.重载函数和操作符,控制内存的分配归还,定义对象的初始化和终结全部掌握在你手上.设计优秀的class是艰巨的工作,要有自然的语法,直观的语义以及一个或多个高效的实现.如何设计高效的class,必须面对一下几个问题.

  • 新type的对象应该如何被创建和销毁.这影响着构造函数和析构函数的设计以及内存分配函数和释放函数的设计.
  • 对象的初始化和对象的赋值该有什么样的区别.这决定构造函数和赋值操作符的行为,不要混淆了初始化和赋值.
  • 新type对象如果被passed by value,意味着什么.
  • 什么是新type的合法值.这决定了class必须维护的约束条件,决定了成员函数必须进行错误检查,函数异常明细列.
  • 新type需要配合某个继承图系吗.继承某系既有的class,就会受到那些class设计的束缚.
  • 什么样的操作符和函数对此新type而言是合理的.这决定你将为class声明哪些函数.
  • 什么样的标准函数应该驳回.这些应该驳回的函数应该被声明为private.
  • 谁该取用新type的成员.这帮助你决定哪个成员为public,哪个为protected,哪个为private.哪个class或function应该为friend.
  • 什么是新type的未声明接口.它对效率,异常安全性和资源运用提供什么保证.
  • 新type有多么一般化.如果其实并非定义一个新type而是定义一个type家族,应该定义一个class template.
  • 真的需要一个新type吗.如果定义新的derived class来为既有的class添加机能,说不定单纯定义一个non-member函数或template更加能够达到目标.

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

  • 以pass-by-reference-to-const替换pass-by-value.前者通常比较高效,且能避免对象切割问题.
  • 本条款不适用于内置类型,STL迭代器和函数对象,对他们而言,pass-by-value更加合适.

缺省情况下,C++以by value方式传递对象至函数.除非另外指定,否则函数参数都是实际实参的副本,调用端返回的也是函数返回值的一个副本.用传递const引用的方式效率高的多,没有任何构造函数或析构函数被调用,因为没有任何对象被创建.将引用声明为const是必要的,因为按值传递一定不会对原始对象做出任何改变.另外,按引用传递还能避免对象切割问题,因为引用的底层实现是指针.

对于内置类型而言,当有机会选择by value或by reference传递时,应该选择by value传递.这条规则同样适用于STL的迭代器和函数对象,习惯上他们都被设计为passed by value.内置类型都相当小,因此有人认为,小的对象都应passed by value,这是不靠谱的理论.对象小不意味着其copy构造函数不昂贵,比如STL容器,其内含的东西只比指针多一些,但复制这些对象却需要复制那些指针所指的每样东西.一般而言,可以合理假设pass by value并不昂贵的唯一对象时内置类型,STL迭代器和函数对象.

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

  • 不要返回指针或引用指向一个local stack对象.
  • 不要返回reference指向一个heap-allocated对象.
  • 不要返回一个指针或引用指向一个local static对象.

一旦程序员领悟了pass by value的效率,往往一心一意的想要根除pass by value带来的种种邪恶,在追求pass by reference的纯度中,一定会犯下一个致命错误:传递一些reference指向并不存在的对象.
考虑有一个有理数类,内含一个计算两个有理数乘积的函数.

 
class Rational {
  public:
    Rational(int numerator = 0, int denominator = 1);
 
  private:
    int n, d;
    friend const Rational operator*(Rational const& r1, Rational const& r2);
};

这个版本的operator*以by value方式返回结果,没有人想要为返回对象付出太大的构造和析构代价,考虑用别的方式返回这个返回值.

Rational const& operator*(Rational const& r1, Rational const& r2) {
    Rational result(r1.n * r2.n, r1.d * r2.d);
    return result;
}

这个版本返回了result的引用,然而,一旦result出了其作用域,由于其是一个stack对象,所以会被析构,这个返回的引用将指向一个已经成空的残骸.

Rational const& operator*(Rational const& r1, Rational const& r2) {
    Rational* result = new Rational(r1.n * r2.n, r1.d * r2.d);
    return *result;
}

将result放在堆里呢?这可以延长对象的声明周期,返回的引用将不会指向一个已经成空的残骸,然而,这是一个更加糟糕的写法.谁给对被你new出来的对象实施delete呢?即使调用者谨慎小心的delete,仍然容易写出下面的代码.

Rational w,x,y,z;
w=x*y*z;

调用了两次operator*,这就需要delete两次,然而没有合理的办法让调用者获取到operator*返回的reference背后的指针,这必然导致内存泄露.

还可以用static延长声明周期,尝试下面的写法.

Rational const operator*(Rational const& r1, Rational const& r2) {
    Rational static result;
    result = Rational(r1.n * r2.n, r1.d * r2.d);
    return result;
}

首先的问题是,使用static对象设计会造成对多线程安全性的疑虑.另外的问题考虑以下代码:

bool operator==(Rational const& r1, Rational const& r2);
Rational a, b, c, d;
if ((a * b) == (c * d)) {
 
} else {
 
}

问题是,无论a,b,c,d为何值,if内的条件总为true,因为operator*返回一个static对象,这个对象永远等于其本身!

如果一个函数必须返回一个新对象,那就让它返回一个新对象吧,对于Rational的operator*来讲,应该写成:

inline Rational const operator*(Rational const& r1, Rational const& r2) {
    return Rational(r1.n * r2.n, r1.d * r2.d);
}

你当然需要承受构造和析构带来的成本,然而从长远来看,这只是为了获得正确行为而付出的小小代价.编译器可以对代码实行优化,用以改善产出代码的效率,某些情况下能够将operator*的返回值构造析构成本安全的消除.当你必须在返回一个reference和返回一个对象之间抉择时,你的选择应该是行为正确的那个,剩下的应该交给编译器厂商.

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

  • 切记将成员变量声明为private赋予客户访问数据的一致性,精细的权限控制.
  • 另外记住,protect并不比public更具备封装性.

从语法一致性的角度,如果成员变量不是public,客户唯一能访问对象的方式就是通过成员函数,如果public接口内的每一样东西都是成员函数,客户就不需要再访问class的成员的时候记住是否该使用小括号.使用函数可以让你对成员变量的处理有着更加精确的控制,如果令成员变量public,每个人都可以读写它,但如果以函数取得或修改其值,则可以实现不准访问,只读访问和读写访问,甚至可以实现只写访问.成员变量因该被隐藏起来.隐藏成员变量有助于封装.

封装的重要性比最初见到它的时候还要重要.假设本有一个public成员变量,而最终取消了它,会有大量的代码需要被修改删除.protected同public一样,虽然这个结论有点违反直觉.一旦本有一个protected成员变量,而最终取消了它,所有使用了它的derived classes都会被破坏,这也是不可知的大量破坏.不论是public还是protected成员变量,一旦被删除,会有太多代码需要重写,测试,编写文档,重新编译.从封装的角度来看,其实只有两种访问权限,private(提供封装)和其他(不提供封装).

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

  • 宁可拿non-member,non-friend函数替换member函数,以增加封装性,包裹弹性和技能扩充性.

想象有一个class表示网页浏览器,这个class又清除缓存,清除历史,清除Cookies几个函数.

class WebBrowser {
  public:
    void clearCache();
    void clearHistory();
    void removeCookies();
};

许多用户可能会想一整个执行所有这些动作,因此WebBrowser也提供这样一个函数:

class WebBrowser {
  public:
    void clearEveryThing();
};

当然,这一机能也可用一个non-member函数调用适当的member函数提供出来.

void clearBrowser(WebBrowser& wb) {
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}

面向对象的守则要求,数据及操作的那些函数应该被捆绑在一块,这意味着member函数是一个很好的选择,然而这个建议并不正确.这是基于面向对象真实意义的一个误解.面向对象守则要求数据尽可能的被封装,然而与直觉相反,member函数clearEverything比non-member函数clearBrowser的封装性更低.non-memer函数允许WebBrowser相关机能有较大的包裹弹性,因此具有较低的编译相依度,增加WebBrowser的可延展性.在许多方面non-member比member更好.越少的代码看到class中的数据,意味着封装性越好.可以粗糙的用访问数据的函数数量估计封装性.越多函数可访问某数据,数据的封装性就越低.只因在意封装性让函数成为class的non-member并不意味着它不可以是另一个class的member.比如可以如java一样,将函数放到一个工具类中作为一个static member函数.

在C++中,比较自然的做法是将clearBrowser作为一个non-member函数并位于WebBrowser所在的同一个namespace中.这不仅仅是看起来自然,namespace和class不同,namespace可以跨越多个源码文件而class不能.

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

  • 如果你需要为某个函数的所有参数(包括this指针所指向的那个隐含参数)进行类型转换,那么这个函数必须是non-member.
    假设有个有理数类,想要和内置类型混合运算
class Rational {
  public:
    Rational(int numberator = 0, int denominator = 1);
    int numerator() const;
    int denominator() const;
    Rational const operator*(Rational const& rhs) const;
};

两个Rational相乘固然没什么问题,然而当你尝试混合运算的时候,却发现只行得通一半.

  Rational oneHalf(1, 2);
    Rational result;
    result = oneHalf * 2;//fine
    result = 2 * oneHalf;//Error

fine的一行,由于oneHalf有operator*这个函数,并且能够自动的将2隐式转换为Rational(Rational的构造函数non-explicit),所以调用成功.而Error的一样相当于2.operator(oneHalf),而2是没有这个成员函数的.

结论是,只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参与者.地位相当于this对象的那个隐喻参数,绝不是隐式转换的合格参与者. 让operator成为一个non-member函数,便允许编译器在每一个实参上执行隐式转换.
Rational const operator
(Rational const& lhs, Rational const& rhs) {
}
现在考虑,需不需要将这个原本想设计为member函数的operator声明为friend函数呢?答案是否定的.operator想要完成的功能完全可以借由Rational的public函数实现.无论何时,如果你可以避免friend,就该避免friend.

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

swap两对象值,就是将两对象的值彼此赋予对方.缺省情况下,swap动作可由标准库提供的swap算法完成.

template<class T>
void swap(T& a, T& b) {
    T temp(a);
    a = b;
    b = temp;
}

只要类型T支持copying函数(拷贝构造和copy assignment),缺省的swap就能为你完成swap动作而不需要再做额外的工作.这种缺省的实现朴实无华,但是对于某些类型而言,这些复制动作没有必要.其中最主要的就是“以指针指向一个对象,内含真正数据”的那种类型.这种设计类型的常见表现形式是所谓“pimpl”手法(pointer to implementation),例如下面的Widget class:

class WidgetImpl {
  public:
      //...
  private:
    int a, b, c;
    std::vector<double> v;//内含很多数据,意味着复制需要的时间很长
};
 
class Widget {
  private:
    WidgetImpl* pImpl;
  public:
    Widget(Widget& const rhs);
 
    Widget& operator=(Widget const& rhs) {
        //...
        *pImpl = *(rhs.pImpl);
        //...
    }
};

一旦要置换两个Widget class对象的值,唯一需要做的是置换其pImpl指针,但缺省的swap算法不知道这一点.它不止复制三个Widget class,还复制内含的WidgetImpl,非常缺乏效率.我们希望告诉std::swap当Widget被置换的时候真正要做的是交换内含的pImpl指针,确切实现这个的思路是将std::swap针对Widget特化,下面是基本构想,但目前这个形式无法通过编译:

namespace std {
template <>
void swap<Widget>(Widget& a, Widget& b) {
    swap(a.pImpl, b.pImpl);
}
}

template<>表示他是std::swap的全特化版本,之后的表示这一特化版本针对的T是Widget.换句话说,当一般的swap template施行与Widget class身上便会启用这个版本.通常我们不能改变std名称空间的任何东西,但可以为标准template制造特化版本,使它专属于我们自己的类.这个版本无法编译通过,因为它企图访问private成员.我们可以将这个特换版本声明为friend,但是按照STL的写法,我们令Widget声明一个名为swap的public成员函数做真正的置换工作,然后将std::swap特化,令他调用该成员函数:

class WidgetImpl {
  public:
    //...
    WidgetImpl();
  private:
    int a, b, c;
    std::vector<double> v; // 内含很多数据,意味着复制需要的时间很长
};
class Widget {
  private:
    WidgetImpl* pImpl;
 
  public:
    Widget(Widget& const rhs);
 
    Widget& operator=(Widget const& rhs) {
        //...
        *pImpl = *(rhs.pImpl);
        //...
    }
 
    void swap(Widget& other) {
        using std::swap;
        swap(this->pImpl, other.pImpl);
    }
};
 
namespace std {
template <>
void swap<Widget>(Widget& a, Widget& b) {
    a.swap(b);
}
} // namespace std

注: 这块有点问题,后面的偏特化没太看懂,先放在这,等看完C++template再来回顾此处.

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

  • 尽可能的延后变量定义式的出现,这样可以增加程序的清晰度并改善程序的效率.

只要定义了一个变量而且类型带有一个构造函数或析构函数,当程序控制流达到这个变量的定义式时,你就需要承受构造成本,当离开这个变量的作用域时,你还得承担析构成本,即使这个变量没被使用,仍任消耗这些成本,应该尽可能的避免这种情形.或许你会认为你不可能定义一个变量但不使用.考虑夏敏这个函数,它计算通行密码的加密版本后返回,前提是密码够长,如果密码短,函数抛出异常,类型为logic_error.

string encryptPassword(string const& password) {
    string encrypted;
    if (password.size() < MinimumPasswordLength) {
        throw logic_error("password is to short");
    }
    //...
    return encrypted;
}

对象enctypted在此函数中并非完全未被使用,但如果有个异常被抛出,他就真的没有被使用,也就是说,如果函数丢出异常,你得承受这个string对象的构造和析构成本.应该尽量延后这样的对象的定义式,直到确实需要它.

string encryptPassword(string const& password) {
    if (password.size() < MinimumPasswordLength) {
        throw logic_error("password is to short");
    }
    //...
    string encrypted;
    return encrypted;
}

你可能会对循环产生疑惑,如果变量只在循环中使用,那么把它定义在循环外并在每次循环迭代时给他赋值比较好还是将它定义在循环内比较好呢?

class Widget;
// 方法A
Widget w;
for (int i = 0; i < n; ++i) {
    w = 取决于i的某个值
}
//方法B
for (int i = 0; i < n; ++i) {
    Widget w = 取决于i的某个值
}

两种写法的成本如下:
在方法A中,1个构造函数,1个析构函数,n个赋值操作
在方法B中,n个构造函数,n个析构函数
如果class的一个赋值成本低于一组构造+析构成本,做法A大体而言比较高效 ,尤其当n很大的时候.否则做法B或许比较好.做法A造成名称w作用域比做法B更大,这有时对程序的可理解性和易维护性造成冲击.因此,除非你明确直到赋值比构造+析构成本低,否则你应该使用做法B.

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

  • 如果可以,应避免转型动作,特别是避免dynamic_cast.如果转型是必要的,将它隐藏于某个函数背后.宁可使用C++新式转型也不使用旧式转型.

C++规则的设计之一是保证类型错误绝不可能发生.理论上如果可以很干净的通过编译,就表示不企图在任何对象身上执行任何不安全无意义的操作,这是一个极具价值的保证,不要草率的放弃他.然而,转型破坏了类型系统.首先回顾一下C风格的转型:
(T)expression和T(expression),第二个是函数风格的转型动作,这两种形式没有差别,纯粹只是小括号的位置不同而已.C++提供四种新式转型.
const_cast,移除对象的常量性,是唯一具有此能力的转型操作符.
dynamic_cast,执行安全向下转型,也就是用来决定某对象是否归属继承体系中的某个类型,是唯一无法用旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作.
reinterpret_cast,执行低级转型,实际动作取决于编译器,不可移植.例如将一个int*转型为int,这一类转换型在低级代码以外很少见.原书中只在讨论如何针对raw memory写出一个调试用的分配器时使用过一次(条款50).
static_cast,强迫隐式转换,例如将non-const对象转换为const对象,或将int转换为double等.也可以执行上述多种转换的反向转换,例如将void*转换为有具体类型的指针,将pointer-to-base转换到pointer-to-derived.但他无法将const转换到non-const,这只有const-cast能完成.
旧式转换仍然合法,但新式转换更受欢迎.其一,很容易在代码中被辨认出来,简化找出破坏类型系统破坏点的过程.其二,各个转型动作的目标越窄化,编译器越可能诊断出错误的运用.
我们和容易写出某些似是而非的代码:

class Window {
  public:
    virtual void onResize() {
    }
};
 
class SpecialWindow:public Window {
  public:
    virtual void onResize() {
        static_cast<Window>(*this).onResize();
        //...特殊的onResize部分
    }
};

static_cast<Window>(*this).onResize()会首先创建一个*this的base成分的副本,然后再副本身上调用onResize,最后再处理自身特殊的onResize部分.解决方法是使用Window::onResize().

之所以使用dynamic_cast,通常是因为你想在一个你认定为derived class对象身上执行derived class操作函数,但你的手上却只有一个指向base成分的指针或引用.有两个一般性的做法避免这个问题.第一,直接使用指向derived class对象的指针,而不是使用base class的指针.第二,让base class也定义一个derived class才具有的virtual函数,这样可以将处理方法转向继承体系.一定一定要避免的是连串的dynamic_cast,比如:

class Window {};
 
class SpecialWindow1 : public Window {};
 
class SpecialWindow2 : public Window {};
 
class SpecialWindow3 : public Window {};
 
void func(vector<shared_ptr<Window>>& v) {
    for (auto iter = v.begin(); iter != v.end(); ++iter) {
        if (SpecialWindow1* psw1 = dynamic_cast<SpecialWindow1>(iter->get())) {
            //...
        } else if (SpecialWindow1* psw2 =
                       dynamic_cast<SpecialWindow2>(iter->get())) {
            //...
        } else if (SpecialWindow1* psw3 =
                       dynamic_cast<SpecialWindow3>(iter->get())) {
            //...
        }
    }
}

这样产生出来的代码又大又慢且基础不问题,每次修改Window class的继承体系都需要修改一次,一旦加入新的derived就要加入新的if分支,这样的代码总应该由virtual函数调用取代.
优良的C++代码很少使用转型,但说完全摆脱他们太过不切实际.应该尽可能的隔离转型动作,将它隐藏在某个函数内,函数的接口会保护调用者不受函数内部丑陋的动作影响.

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

  • 避免返回handles(包括reference,指针,迭代器)指向对象内部.
  • 遵守这个条款能够增加封装性,帮助const成员函数的行为像个const,并将发生虚吊号码牌(dangling handles)的可能性降到最低.
struct RectData {
    Point upLeftHandCorner;//左上角
    Point lowerRightHandCorner;//右下角
};
 
class Rectangle {
  private:
    std::shared_ptr<RectData> pData;
 
  public:
    Point& upperLeft() const {
        return pData->upLeftHandCorner;
    }
 
    Point& lowerRight() const {
        return pData->lowerRightHandCorner;
    }
};

upperLeft和lowerRight函数返回内部成员的reference,因此调用者可以使用这两个函数来修改内部成员的值!然而函数本身却声明为const!因此,虽然upLeftHandCorner和lowerRightHandCorner被声明为private,他们却是public的.上述问题源自于成员函数返回内部成员的reference,或者说是内部成员的handles(指针,引用或迭代器).解决问题的方式很轻松,令函数返回const-reference.即使如此,此两个函数返回了代表对象内部的handles有可能在其他场合带来问题,比如导致dangling handles,这样的handle指向的东西不复存在.

Rectangle const boundingBox(GUIObject const& obj);
GUIObject* pgo;
//用户有可能这样调用
Point const* pUpperLeft = &(boundingBox(*pgo).upperLeft());

对boundingBox的调用会获得一个新的,暂时的匿名Rectangle对象.这个对象没有名称,暂时称它为temp.随后upperLeft作用在temp身上,返回一个Point对象.于是pUpperLeft指向这个对象.目前为止一切都好,但是当这个语句结束后,temp将被销毁,间接导致temp内的Point对象被析构,因此pUpperLeft指向一个不复存在的对象,成为dangling handle.这就是函数返回一个handle代表对象内部成分总是危险的原因.这并不意味着你绝不可以让成员函数返回handle.有时候你必须这么做,比如string和vector中的operator[],但是这样的函数毕竟是例外,不是常态.

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

// 一个用于多线程环境的表现夹带背景图案的GUI
class PrettyMenu {
  private:
    std::mutex m;
    Image* bgImage;   // 当前背景
    int imageChanges; // 背景图像修改次数
  public:
    void changeBackground(istream& imageSrc);
};
 
void PrettyMenu::changeBackground(istream& imageSrc) {
    m.lock();
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imageSrc);
    m.unlock();
}

从异常安全性的角度来看,这个函数很糟糕.异常安全有两个条件,这个函数未具有其中任何一个.

  • 不泄露任何资源.一旦new Image导致异常,对unlock的调用绝对不会执行,互斥器永远被把持住了.
  • 不允许数据破坏.new Image抛出异常,bgImage将指向一个已被删除的对象,imageChanges被累加然而并未将新图像安装起来.

解决资源泄露比较容易,使用一个资源管理对象来管理担心泄露的资源即可,比如lock_guard.不担心资源泄露,专注解决数据的破坏.
异常安全函数提供三个保证之一:

  • 基本承诺.如果抛出异常,程序内的任何事物仍然保持在有效状态下,没有任何对象或数据结构会因此破坏.比如PrettyMenu对象抛出异常,它可以继续拥有原来图像或是令他拥有某个缺省的图像,但客户无法预期具体是哪种.
  • 强烈保证.如果抛出异常,程序状态不改变.调用这样的函数需要有这样的认知:如果函数成功就是完全成功,否则程序会回到调用函数之前的状态.
  • 不抛掷(nothrow)保证.承诺绝不抛出异常,因为它们总能完成其承诺的功能.C++内置类型身上的所有操作都提供nothrow保证.
    使用智能指针并重新安排changeBackground内的语句次序:
// 一个用于多线程环境的表现夹带背景图案的GUI
class PrettyMenu {
  private:
    std::mutex m;
    shared_ptr<Image> bgImage;   // 当前背景
    int imageChanges; // 背景图像修改次数
  public:
    void changeBackground(istream& imageSrc);
};
 
void PrettyMenu::changeBackground(istream& imageSrc) {
    lock_guard<mutex> lg(m);
    bgImage.reset(new Image(imageSrc));//以new Image执行结果设定内部指针
    //如果new Image执行中抛出异常,原始bgImage不会被delete也不会被修改.
    ++imageChanges;
}

另一种改进方式是把imageChanges和bgImage指针封装成一个整体struct/class,PrettyMenu包含这个struct,然后使用copy-and-swap策略.这会在副本上先做全部的修改,一旦修改抛出异常,原始数据仍然保持,如果未发生任何异常,最终使用一个nothrow的swap置换数据.

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

  • 将inline限制在小型,被频繁调用的函数上.不要只因为function template只出现在头文件中,就将它声明为inline.

inline函数看起来像函数,动作像函数,比宏好得多,调用他们又不承受函数调用的额外开销!但天下没有免费的午餐,每个inline调用都以函数本体替换之,这可能增加目标码的大小.过度热衷于inline会造成程序体积太大,inline造成的代码膨胀会导致额外的换页行为,降低cache命中率,因此产生效率损失.

inline只是对编译器的一个申请,不是强制命令,这个申请可以隐喻提出,可以明确提出.隐喻方式是将函数定义于class定义式内,这样的函数通常是成员函数,friend也可以定义于class内成为隐喻inline.

inline通常一定被放置于头文件内,因为大多数建置环境在编译过程中进行inlining,为了将一个函数调用替换成函数本体,编译器必须知道这个函数长什么样子.inlining在大多数cpp程序中是编译器行为.template通常也被放置于头文件中,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子.

大部分编译器拒绝将复杂(带有循环或递归)的函数inlining,对所有的virtual函数调用也会使inlining落空,因为virtual意味着等待,直到运行期才确定调用哪个函数,而inline意味着执行前先将调用动作替换为被调用函数本体.
有时候编译器愿意inlining某个函数,但还是可能为该函数生成一个函数本体.比如某个程序要取inline函数的地址,编译器必须为此函数生成一个outline函数本体,毕竟编译器没有能力提出一个指针指向一个不存在的函数.
inline void f(){//…} //假设编译器有意愿inline函数f
void (*pf)() =f;
f();//这个调用被inline
pf();//这个调用或许不被inline,因为它通过函数指针达成.

构造函数和析构函数往往是inline的糟糕候选人.比如将Derived的空白构造函数声明为inline,但其Base部分以及cpp为了完成构造和析构的代码很复杂.

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

  • 支持”编译依存性最小化“的一般构想是:相依声明式,不要相依于定义式.基于此的两个手段是handle class和interface class.程序库头文件应该以完全且仅有声明式的形式存在,不论是否涉及template都使用.
  • 假设你对C++某个class的实现文件做个轻微修改,注意修改的不是class的接口,而是实现,而且只修改private成分,然后重新构建整个程序,并预计只需要几秒就好,然而你大吃一惊,整个世界都被重新编译和连接了,这样的事情令人气恼.

问题出在C++并没有把“将接口从实现中分离”这件事做的很好.class的定义式不只是详细叙述了class接口,还包括十足的实现细节.

 
class Person {
  public:
    Person(string const& name, Date const& birthday, Address const& addr);
    string  name() const;
    string  birthDate() const;
    string address() const;
 
  private:
    string theName;
    Date theBirtyday;
    Address theAddress;
};

如果编译器没有取得其实现代码所用到的class string,Date和Address的定义式,这个Person class无法通过编译.这样的定义式通常由#include指示符提供,所以Person class上方很可能存在以下内容:
#include
#include"date.h"
#include"address.h"
不幸的是,这样便是在Person定义文件和其包含文件之间形成了一种编译依存关系.如果这些头文件中的任何一个被改变,或者这些头文件所依赖的其他头文件有任何改变,那么每一个含入Person class的文件就得重新编译,任何使用Person class的文件也必须重新编译,这样的连串编译依存关系会对许多项目造成难以形容的灾难.
为什么C++将class的实现细目置于class定义式中?为什么不如下定义Person class.

namespace std {
class string;//前置声明,不正确
}
class Date;//前置声明
class Address;
 
class Person {
  public:
    Person(string const& name, Date const& birthday, Adderss const& addr);
    string  name() const;
    string  birthDate() const;
    string address() const;
 
  private:
    string theName;
    Date theBirtyday;
    Address theAddress;
};

如果可以这么做,Person的客户就只需要在Person接口被修改过时才重新编译.这个想法存在问题.第一,string不是一个class,他是个typedef,实际为basic_string,因此上述前置声明不正确,正确的前置声明比较复杂,因为涉及到额外的template,但是不要紧,本来就不应该尝手工声明标准库的部分,应当仅仅使用#include完成目的,标准头文件不太可能成为编译瓶颈.第二,关于前置声明的每一个东西,编译器必须在编译期知道对象的大小.比如:

int main() {
    int x;
    Person P;
}

编译器必须在编译期间知道对象的大小.当编译器看到x的定义式,他知道必须分配多少内存于stack中才够持有一个int,这没问题,每个编译器都知道int有多大.当编译器看到p的定义式,他必须知道分配足够空间以放置一个Person,但他如何知道一个Person有多大呢?编译器唯一知道这个信息的方法就是查看Person class的定义式.如果class的定义式能够合法的不列出实现细目,编译器如何知道该分配多少空间?这样的问题在Java等语言上不存在,因为当我们以那些语言定义对象时,编译器只分配一个足够空间给一个指针使用,就好像这样:

int main() {
    int x;
    Person* P;
}

这当然也是合法的cpp代码,所以你也可以将对象实现细节隐藏于一个指针背后.针对Person,将其分割为两个class,一个只提供接口,另一个负责实现该接口.

#include <memory> //含入shared_ptr
#include <string>
class PersonImpl;
class Date;
class Address;
 
class Person {
  public:
    Person(string const& name, Date const& birthday, Address const& addr);
    string name() const;
    string birthDate() const;
    string address() const;
 
  private:
    shared_ptr<PersonImpl> pImpl;
};

这样的设计下,Person的客户就完全于Date,Address和Person的实现细目分离了.那些class的任何实现修改都不需要Person客户端重新编译.分离的关键在于以“声明的依存性”替换“定义的依存性“,这正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,做不到时再让他与其他文件内的声明式(而非定义式)相依.

  • 如果使用object reference或object pointer可以完成任务,就不要使用object.可以只用声明就定义出指向该类型的reference或pointer,然而却需要完整定义才能定义某类型的object.
  • 如果能够,尽量以class声明式替换class定义式.当你声明一个函数而它用到某个class的时候,你并不需要这个class的定义,纵使函数以by value方式传递该类型的参数亦然:
class Date;     // Date声明
Date today();   // 没问题,这里不需要Date的定义
void f(Date d); // 没问题,这里不需要Date的定义

声明函数而无需class定义,这样的能力或许令你惊讶,但他并非真的如此神奇.一旦任何人调用这些函数,调用之前Date定义式一定得先曝光才行.
• 为声明式和定义式提供不同的头文件.这种方式在cpp标准库中很常见,比如<iosfwd>,内含iostream各组件的声明式,其对应的定义则分布在若干不同的头文件内,包括sstream,streambuf,fstream,iostream.<iosfwd>另一个深具启发意义的原因式,它分外彰显本条款适用于template也适用于non-template,虽然条款30说过在许多构建环境中,template定义式通常被置于头文件中,但也有某些构建环境允许template放在非头文件内.这样就可以将只含声明式的头文件提供给template,iosfwd就是如此.cpp提供了export关键字,允许将template声明式和定义式分割于不同的文件内.

使用PImpl idiom的class往往被称为handle class.这样的class如何真正做点事情?办法之一是将他们所有函数转交给响应的实现类,并由实现类完成其实际工作.

// Person class的实现,Person.cpp
#include "person.h"
#include "personImpl.h"
Person::Person(string const& name, Date const& birthday, Address const& addr)
    : pImpl(new PersonImpl(name, birthday, addr)) {
}
 
string Person::name() const {
    return pImpl->name();
}
 

另外对于Person的实现,还可以使用一个pure base class作为interface class.

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

  • public继承意味着is-a,适用于base class身上的每件事情一定适用于derived class身上.
class Person{};
 
class Student : public Person {};

如果函数期望获得一个类型为Person(pointer或reference)的实参,也都愿意接收一个Student对象.这个论点只对public形式的继承成立.private继承的意义与此完全不同,详见条款39.至于protect继承,是一个令大神Scott Meyers都困惑的东西.
在public继承的精彩世界中,在其他领域学到的直觉恐怕无法如预期般的帮助你.比如让正方形public继承矩形会产生一些问题.public继承主张能够施行于base class身上的每件事情也能够施行于derived class身上.
is-a并非是唯一存在于class之间的关系.另外常见的关系有has-a和is-implemented-in-terms-of(根据某物是实现出).这些关系将在条款38,39中讨论.

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

  • derived class内的名称会遮掩base class内的名称,在public继承下从来没有人希望如此.为了让被遮掩的名称重见天日,使用using或转交函数.

当一个位于derived class成员函数内涉指任何base class内的某物时,编译器可以找到我们涉指的东西,因为derived class继承了声明于base class中的所有东西.实际运作方式是,derived class作用域被嵌套在base class作用域内.

class Base {
    class Derived {
 
    };
};

在Derived中,对于某个名字,首先查找local作用域,然后查找外围作用域,也就是class Derived覆盖的作用域,然后再查找外围作用域,Base class.如果还是没有找到,就像内含Base class的那个namespace作用域查,最后像global作用域查找.

class Base {
  private:
    int x;
 
  public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2();
    void mf3();
    void mf3(double);
};
 
class Derived : public Base {
  public:
    virtual void mf1();
    void mf3();
    void mf4();
};
 
int main() {
    Derived d;
    int x;
    d.mf1();//Derived::mf1
    d.mf1(x);//Error,被遮掩
    d.mf2();//Base::mf2
    d.mf3();//Derived::mf3
    d.mf3(x);//Error,被遮掩
}

避免被遮掩的手段是使用using让derived class看到Base class中关于某个名字的所有东西.

class Base {
  private:
    int x;
 
  public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2();
    void mf3();
    void mf3(double);
};
 
class Derived : public Base {
  public:
    using Base::mf1;
    using Base::mf3;
    virtual void mf1();
    void mf3();
    void mf4();
};
 
int main() {
    Derived d;
    int x;
    d.mf1();  // Derived::mf1
    d.mf1(x); // now ok,Base::mf1
    d.mf2();  // Base::mf2
    d.mf3();  // Derived::mf3
    d.mf3(x); // now ok,Base::mf3
}
 

使用using时,会让所有有关base class的某个名字的东西都被derive class看到,如果只想让derived看到一部分,应该使用转交函数,只看到一部分显然违反了public继承,但这样的方法却可以用到private继承中.另外,转交函数的方法也可以为不支持using的老旧编译器提供一条新路.

class Base {
  public:
    virtual void mf1() = 0;
    virtual void mf1(int);
};
 
class Derived : public Base {
  public:
    virtual void mf1() {
        Base::mf1();//转交函数,暗自成为inline
    }
};
 
int main() {
    Derived d;
    int x;
    d.mf1();  // nice,调用了Derived::mf1
    d.mf1(x); // Error,被遮掩
}

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

  • 接口继承和实现继承不同,public继承之下,总是继承base class的接口.
  • pure virtual函数只具体指定接口继承.
  • impure virtual函数具体指定接口继承及缺省实现继承.
  • non-virtual函数具体指定接口继承以及强制实现继承.
class Shape {
  public:
    virtual void draw() const = 0;
};
 
void Shape::draw() const {
    //...
}

声明一个pure virtual函数的目的是为了让derived class只继承接口.然而却可以给他提供一份定义,用处不大,调用的方式是提供其class名称.

Shape* ps=new Rectangle;
ps->Shape::draw();//调用有定义的纯虚函数

声明一个impure virtual的目的是让derived class继承该函数的接口和缺省实现.如果基类提供了impure virtual函数,derived class没有实现自己版本的该函数,将缺省使用基类的版本,这可能导致非预期的行为,比如我们不定义自己版本的该函数的目的是不想要这个缺省行为.这时应该这样做:

class Base {
  public:
    virtual void f() = 0;
};
 
void Base::f() {
    // 缺省行为
}
 
class DerivedA : public Base {
  public:
    virtual void f() {
        //显示使用base的缺省行为
        Base::f();
    }
};
 
class DerivedB : public Base {
  public:
    virtual void f() {
        //没有使用base的缺省行为
    }
};

声明non-virtual函数的目的是令derived class继承函数的接口及一份强制性实现.

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

  • 使用template method将virtual设置为private并藉由一个pulic函数调用.
  • 使用策略模式.

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

class B {
  public:
    void mf();
};
 
class D : public B {
  public:
    void mf();
};
 
int main() {
    D x;
    B* pB = &x;
    pB->mf();
    D* pD = &x;//调用B::mf
    pD->mf();//调用D::mf
}

D的非virtual函数mf遮掩了B的非virtual函数.non-virtual函数都是静态绑定的函数,哪个被调用取决于其被声明为什么类型.绝不要重新定义继承而来的non-virtual函数,否则,对象的成员函数调用很有可能出现精神分裂的不一致行为.

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

  • 绝不要重新定义一个继承而来的缺省参数值,缺省参数值是静态绑定,而virtual函数是动态绑定.

只能继承两种函数,virtual和non-virtual函数.但是在条款36中已经明确,继承non-virtual函数是不正确的.因此本条款的讨论仅限于继承一个带有缺省参数的virtual函数.记住,virtual函数是动态绑定的,而缺省参数值却是静态绑定.所谓的静态类型是它在程序中被声明时采用的类型.所谓动态类型是指目前所指对象的类型.

//静态类型都是Shape
Shape* ps;//无动态类型
Shape* pc = new Shape Circle;//动态类型circle
Shape* pr = new Shape Rectangle;//动态类型rectangle

virtual函数是动态绑定而来,调用一个virtual函数,取决于其动态类型.虽然virtual函数是动态绑定,但是其缺省参数值却是静态绑定.当调用一个derived class内的virtual函数时,却使用base class为它指定的缺省参数值,
pr->draw();//调用Rectangle::draw(Shape::Red);
由于pr的静态类型是Shape,所以draw的缺省参数是Shape的Red.为什么C++坚持用这种奇怪方式运作?答案在于运行期效率.如果缺省参数值也动态绑定,编译器必须在运行期以某种方式决定适当的参数缺省值,这比编译器决定缺省值慢很多,也更复杂.

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

  • 复合的意义和public继承完全不同,在应用域,复合意味着has-a,在实现域,则意味着“根据某物实现出”.

  • 复合是类型之间的一种关系,某种类型的对象内含其他类型的对象,便是这种关系.有时候public并不是非常好的选择,应该考虑使用复合.

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

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

C++以public继承表现is-a的关系,这意味着编译器将在必要时刻将某个派生类暗自转换为基类.现在重复条款32的例子,并以private继承替换public继承.

class Person {};
 
class Student : private Person {};
 
void eat(Person const& p);
void study(Student const& s);
 
int main() {
    Person p;
    Student s;
    eat(p);
    eat(s);//错误,难道学生不是人?
}

显然,private继承不意味着is-a的关系.private的规则如此:如果class之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象.另外,由private继承而来的所有成员,在derived class中聚会变成private属性,纵使他们原本是public或private.

private继承意味着“根据某物实现出”.如果让class D以private形式继承class B,用意是为了采用class B内已经准备妥当的某些特性,不是因为B对象和D对象存在任何观念上的关系.private纯粹是一种实现技术,因此private意味着只有实现部分被继承,接口部分被略去.以Dprivate继承B,意味着D对象是根据B对象实现而得,再没有其他含义了,private继承在软件设计层面上没有意义,仅在软件实现层面具有意义.然而,条款38指出,复合的意义也是“根据某物实现出”,如何在两者之间取舍呢?简单回答,尽可能使用复合,必要时才使用private继承.何时必要?当protected成员或virtual函数牵扯进来的时候.

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

  • 多重继承比单继承复杂,可能产生歧义性,必要的时候应该使用virtual继承,但virtual继承会增加大小,速度,初始化复杂度等成本,virtual base class不带任何数据是最具有实用价值的情况.
  • 多重继承的正当用途是涉及public继承某个Interface class和private继承某个协助实现的class的两相组合.
class File {};
 
class InputFile : public File {};
 
class OutputFile : public File {};
 
class IOFile : public InputFile,public OutputFile {};
这是致命的钻石型多重继承,IOFile将内含两个File的成分.需要使用virtual 继承来保留一个File成分.
 
class File {};
 
class InputFile :virtual public File {};
 
class OutputFile :virtual public File {};
 
class IOFile : public InputFile,public OutputFile {};
 
 

以下是多继承的重要用途,类似于其他编程语言中的Interface

// 这个class指出需要实现的接口
class IPerson {
  public:
    virtual ~IPerson();
    virtual string name() const = 0;
    virtual string birthday() const = 0;
};
 
// 用来表示数据库相关的信息,作为工厂函数的上下文
class DatabaseID {};
 
// 某个现有的库,能被用来实现Iperson
class PersonInfo {
  public:
    explicit PersonInfo(DatabaseID pid);
    virtual ~PersonInfo();
    virtual const char* theName() const;
    virtual const char* theBirthday() const;
    virtual const char* valueDelimOpen() const;
    virtual const char* valueDelimOpen() const;
};
 
// CPerson与PersonInfo的关系是,PersonInfo刚好有若干函数可帮助实现CPerson
// 也就是说“根据某物实现”,is-implemented-in-terms-of
class CPerson : public IPerson, private PersonInfo {
  public:
    explicit CPerson(DatabaseID pid) : PersonInfo(pid) {
    }
 
    virtual string name() const {
        return PersonInfo::theName();
    }
 
    virtual string birthday() const {
        return PersonInfo::theBirthday();
    }
 
  private:
    const char* valueDelimOpen() const {
        return "";
    }
 
    const char* valueDelimOpen() const {
        return "";
    }
};

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

  • class和template都支持接口和多态,class是显示接口和运行期多态,template是隐式接口和编译器多态.

面向对象的世界总是以显示接口和运行期多态解决问题.

class Widget {
  public:
    Widget();
    virtual size_t size() const;
    virtual void normalize();
    void swap(Widget& other);
};
 
template<typename T>
void doProgress(T& w) {
    if (w.size() > 10 && w != someNasttyWidget) {
        //...
    }
}

T类型的隐式接口看起来具有这些约束,必须提供一个size成员函数,且返回类型有>operator.必须支持一个!=operator且能与someNastyWidget比较.

条款42:了解typename的双重含义

声明template参数时,class和typename完全一样.使用typename标识嵌套从属关键字,但不得在base class list和member class list中以typename作为修饰符.

template <class T>
class Widget;
 
template <typename T>
class Widget;

这两种写法没有任何区别,当声明template参数时,class和typename的意义完全相同.

template <class C>
void print(C const& container) {
    if (container.size() >= 2) {
        C::const_iterator iter(container.begin());
        ++iter;
        int value = *iter;
        cout<<value;
    }
}

上面的代码看着没什么问题,然而实际上有很大问题. C::const_iterator是一个嵌套从属名称,C++会首先认为这不是一个类型,而是某个对象,解决的方法是使用typename强调这个嵌套从属名称是一个类型.

template <class C>
void print(C const& container) {
    if (container.size() >= 2) {
        typename C::const_iterator iter(container.begin());
        ++iter;
        int value = *iter;
        cout<<value;
    }
}

然而并非所有的嵌套从属类型都要加上typename来声明一下.base class list和member iniitial list中不可以出现typename,这种不一致让人苦恼,然而只能勉勉强强的接受.
对于比较长的嵌套从属名称,可以使用typedef,比如:

typedef typename std::iterator_traits<IterT>::value_type value_type;

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

  • 可在derived class template内通过this->指明base class template内的成员名称或藉由一个using写明白base class资格修饰符完成.

每个公司有一个类用来发送明文密文.现需要实现一个根据公司类别发送明文或密文的同时写log.

class CompanyA {
  public:
    // 发送明文
    void sendClearText(string const& msg);
    // 发送密文
    void sendEncrypted(string const& msg);
};
 
class CompanyB {
  public:
    void sendClearText(string const& msg);
    void sendEncrypted(string const& msg);
};
 
class MsgInfo {}; // 产生信息
 
template <class Company>
class MsgSender {
  public:
    void sendClear(MsgInfo const& info) {
        string msg;
        //...此处向msg中添加数据
        Company c;
        c.sendCleartext(msg);
    }
 
    void sendSecret(MsgInfo const& info) {
        //类似sendClear的代码结构
    }
};

使用class template完成上述功能:

template<class Company>
class LoggingMsgSender : public MsgSender<Company> {
  public:
    void sendClearMsg(MsgInfo const& info) {
        //写传送前log文件
        sendClear(info);
        //写传送后log文件
    }
};

当编译器遭遇class template LoggingMsgSender定义时,并不知道它继承什么样的class.它继承的显然是MsgSender,但其中的Company是个template参数,不知道具现化的时候会是什么,因而无法确定MsgSender里面到底是何定义.为了让C++不进入template base class中观察其定义的行为失效,有三个办法.

方法一是使用this:

template<class Company>
class LoggingMsgSender : public MsgSender<Company> {
  public:
    void sendClearMsg(MsgInfo const& info) {
        //写传送前log文件
        this->sendClear(info);
        //写传送后log文件
    }
};

方法二使用using声明式:

template<class Company>
class LoggingMsgSender : public MsgSender<Company> {
  public:
    using MsgSender<Company>::sendClear;
    void sendClearMsg(MsgInfo const& info) {
        //写传送前log文件
        sendClear(info);
        //写传送后log文件
    }
};
 

方法三是指明被调用的函数位于base class中,但该方法对于virtual函数的表现不好,应该尽量少用,改用前两种方法.

template<class Company>
class LoggingMsgSender : public MsgSender<Company> {
  public:
    using MsgSender<Company>::sendClear;
    void sendClearMsg(MsgInfo const& info) {
        //写传送前log文件
        MsgSender<Company>::sendClear(info);
        //写传送后log文件
    }
};

尽管使用上述三种方法确保编译器不会在这个阶段产生编译错误,使用上述三种方法都是向编译器保证base class template中存在sendClear.如果base class template中的确没有定义这个函数,那么讲导致新的问题.考虑给base class template一个特化版本.

class CompanyZ {};
 
template <>
class MsgSender<CompanyZ> {
  public:
    //Z公司不提供明文发送
    void sendSecret(MsgInfo const& info) {
    }
};
 
int main() {
    LoggingMsgSender<CompanyZ> zMsgSender;
    MsgInfo msgData;
    zMsgSender.sendClearMsg(msgData);
}

上述代码无法通过编译,因为base class template的偏特化版本中并未提供SendClear.

条款44:将与参数无关的代码抽离template

  • template生成的多个class和多个函数,任何template都不该和某个造成膨胀的template参数产生相依关系.
  • 因非类型模板参数造成的代码膨胀,往往可以消除,做法是以函数或class成员变量替换template参数.

template是节省重复代码的奇妙方法,但却可能导致代码膨胀,也就是说,cpp源码虽然看着很短小,但生成的目标码却很大.举个例子,你正在为一个固定尺寸的方阵编写一个template.

template <class T, size_t n>
class SquareMatrix {
  public:
      //...某些其他函数
    void invert();//矩阵逆运算
};
 
int main() {
    SquareMatrix<int, 5> sm1;
    SquareMatrix<int, 10> sm2;
    sm1.invert();
    sm2.invert();
}

上述调用会具现化两份invert,虽然这两份invert并非完全相同,但不同点却只有矩阵大小.这让我们本能的想到为他们建立一个带参数的invert函数以消除模板上对常数n的依赖.于是我们做出了如下修改:

template <class T>
class SquareMatrixBase {
  protected:
    void invert(size_t matrixSize);
};
 
template <class T, size_t n>
class SquareMatrix : private SquareMatrixBase<T> {
  private:
    using SquareMatrixBase<T>::invert;//避免遮掩base版本的invert,见条款33
 
  public:
    void invert() {
        this->invert(n);//防止编译器进入template base class查看是否具有invert函数
    }
};
 

目前为止一切都好,但一个棘手的问题是invert该如何知道它的数据在哪里?想必只有derived class才能知道,derived class如何联络其base class做invert操作?一个方法是为base class的invert函数添加一个指针参数,指向矩阵数据起始点,这个方法并不好,向invert这样需要获取矩阵数据的函数可能并非一个,还需要一一为其他函数添加这样的指针作为额外参数.另一个办法是为base添加一个指针成员.像这样:

template <class T>
class SquareMatrixBase {
  private:
    size_t size;
    T* pData;
 
  protected:
    SquareMatrixBase(size_t n, T* pMem) : size(n), pData(pMem) {
    }
 
    // 给pData重新赋值
    void setDataPtr(T* ptr) {
        this->pData = ptr;
    }
};

这允许derived class重新决定内存分配的方式,某个实现版本可能会决定将矩阵数据保存在SquareMatrix对象内部:

template <class T, size_t n>
class SquareMatrix : private SquareMatrixBase<T> {
  public:
    SquareMatrix() : SquareMatrixBase<T>(n, data) {
    }
 
  private:
    T data[n * n];
};

这种对象不需要动态分配内存,但对象自身非常大,另一种做法是把每个矩阵数据放进heap.

template <class T, size_t n>
class SquareMatrix : private SquareMatrixBase<T> {
  public:
    SquareMatrix() : SquareMatrixBase<T>(n, data) {
    }
 
  private:
    vector<int>* data;
};

条款45:运用成员函数模板接受所有兼容类型

使用member function template生成可接受所有兼容类型的函数.声明member template用于泛化copy构造或泛化赋值操作,还需要声明正常的copy构造函数和赋值操作符.

指针做的良好的一件事是,支持隐式转换.子类可以隐式转换为父类指针,指向non-const对象可以转换为指向const对象的指针等等.如果用户在自己实现的智能指针中完成上述转换则有点麻烦.比如用户有个SmartPtr的template class,对于这个template来说,SmartPtr和SmartPtr的亲密关系并不比vector和Widget更加亲密.为了能获得SmartPtr之间的转换能力,必须将他们明确的编写出来.我们利用模板可被无限量具现化的特点,为SmartPtr编写member template之一的构造函数模板.

template <class T>
class SmartPtr {
  public:
      //member template,为了生成copy构造函数模板
    template <class U>
    SmartPtr(SmartPtr<U> const& other);
};

上述代码的意思是,对于任何类型T和U,都可以根据SmartPtr<U>生成一个SmartPtr<T> ,这两个是template不同具现,我们称之为泛化copy构造函数.泛化copy构造函数并没有声明为explicit,这是蓄意的.因为原始指针类型之间的转换也是隐式转换.这个泛化copy构造函数能提供的东西比我们需要的更多.我们希望根据一个SmartPtr<Derived>创建一个SmartPtr<Base>,却不希望根据一个SmartPtr<Base>创建一个SmartPtr<Derived>,后者对于继承来说是矛盾的.假设SmartPtr也同std::shared_ptr一样提供一个get函数,我们可以再构造模板的实现代码中添加约束转换行为.

template <class T>
class SmartPtr {
  public:
      //member template,为了生成copy构造函数模板
    template <class U>
    SmartPtr(SmartPtr<U> const& other):heldPtr(other.get()) {
    }
 
    T* get() const {
        return heldPtr;
    }
 
  private:
    T* heldPtr;
};

member template不限于构造函数,他们常常扮演的另一个角色是支持赋值操作.std::shared_ptr支持所有来自内置指针,shared_ptr,weak_ptr等赋值操作,这些赋值操作的实现手段类似上述copy构造模板.

声明copy构造模板不会影响编译器自动生成copy构造函数,如果你想控制copy构造的行为,需要同时声明泛化copy构造函数和正常的copy构造函数.

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

  • 当编写一个class template时,他所提供的与次template相关的函数支持所有参数之隐式类型转换时,将那些函数定义为class template的friend函数.

条款24讨论过什么non-member函数有能力在所有实参上施行隐式转换,本条款将其涉及到的Rational class改为template.

template <class T>
class Rational {
  public:
      Rational(T const& numberator=0,T const& denominator=1);
      T const& numerator() const;
      T const& denominator() const;
};
template<class T>
Rational<T> const& operator*(Rational<T> const& lhs,Rational<T>const& rhs){
    //...
}
 
int main() {
    Rational<int> onHalf(1, 2);
    Rational<int> result = onHalf * 2;//在非模板中可以通过,但在此无法编译通过
}

上述失败表面,在模板化的Rational中某些东西和non-template版本不同.在调用operator*的时候,第一参数为onHalf,这是一个Rational类型,所以编译器将T推导为int,第二参数声明为Rational,但传递给第二参数的是一个int,编译器无法推算T的类型.也许你会认为编译器使用Rational的non-explicit构造函数将int转换为Rational,进而推导T为int,但编译器不会这么做.因为在template实参推导过程中从不将隐士类型转换纳入考虑.绝不会!这样的转换在函数调用过程中确实被使用,但在能够调用一个函数之前,必须知道那个函数存在.为了知道它的存在,必须为相关的function template推导出参数类型(然后才可以将适当的函数具现化出来).在template实参推导过程中并不考虑采纳“通过构造函数而发生的”隐式类型转换.

利用一个事实可以解决这个问题.Class template并不依赖template实参推导,template实参推导只施行于function template上,因此template总是能够在class template具现化时得知T.因此,令Rational声明适当的operator*为其friend函数,可以简化整个问题.

 
template <class T>
class Rational {
  public:
    Rational(T const& numberator = 0, T const& denominator = 1);
    T const& numerator() const;
    T const& denominator() const;
    friend Rational const operator(Rational const& lhs, Rational const& rhs);
};
 
template <class T>
Rational<T> const operator*(Rational<T> const& lhs, Rational<T> const& rhs) {
    //...
}
 
int main() {
    Rational<int> onHalf(1, 2);
    Rational<int> result = onHalf * 2;
}

现在,oneHalf被声明为一个Rational,Rational被具现化出来,作为其一部分的friend函数被自动声明出来,接受Rational参数.现在这个函数已经是一个函数而非函数模板了.因此编译器调用它时可以使用隐式转换.但是,这段代码无法通过编译.首先谈谈Rational声明operator的语法.在一个class template中,template名称可被用来作为“template和其参数”的简称,因而在Rational内可以只写Rational而不用写Rational.现在来看一下当前的主要问题.混合计算的代码通过了编译,因为编译器知道我们要调用哪个函数,那是接受两个Rational的operator,但那个函数只被声明于Rational内,并没有被定义出来.我们意图在class外部提供operator* template的定义式,但这行不通.如果在class template内声明一个函数,就有责任定义那个函数,既然没有提供定义式,连接器当然找不到他.最简单的方式就是将operator*的定义式移动到class template内部.

这种方法的一个有趣的点是,我们用了friend函数,然后却与使用friend的传统用途毫不相干.为了让类型转换发生在所有参数上,我们需要一个non-member函数.为了让这个函数被自动具现化,我们需要将它声明在class内部.在class内部声明一个non-member的唯一办法就是friend.在class内部定义的函数都暗自称为inline.你可以让inline声明带来的冲击最小化,方法是friend函数什么都不做,只是调用一个定义于class外部的辅助函数.

template <class T>
class Rational;
 
 
template <class T>
Rational<T> doMultiply(Rational<T> const& lhs, Rational<T> const& rhs){
}
template <class T>
class Rational {
  public:
    Rational(T const& numberator = 0, T const& denominator = 1);
    T const& numerator() const;
    T const& denominator() const;
 
    friend Rational const operator*(Rational const& lhs, Rational const& rhs) {
        doMultiply(lhs,rhs);
    }
};

许多编译器会强迫把所有的template定义式放进头文件,所以或许需要在头文件内定义doMultiply.doMultiply作为template自然也不支持混合乘法,然而调用它的operator却支持.本质上operator支持了类型转换的所有东西,再将转换好的对象传给一个适当的具现化的doMultiply.

条款47:请使用traits classes表现类型信息
traits class使得类型相关信息在编译期可用.以template和template的特化实现.整合重载技术后traits class有可能在编译期对类型执行ifelse测试.

template<class IterT,class DistT>
void advance(IterT& iter,DistT d){
 
}

STL中有不同的迭代器,input_iterator,output_iterator,forward_iterator,bidirectional_iterator,random_access_iterator
不同的iterator有着不同的能力,我们希望根据iterator的能力实现不同advance的版本.

template <class IterT, class DistT>
void advance(IterT& iter, DistT d) {
    if(iter is random access iterator){
        iter+=d;
    }else{
        while循环一次前进一个
    }
}

方法是使用iterator_traits<>.
先在迭代器内或自定义的迭代器内类编写
typedef random_access_iterator_tag iterator_category;
iterator_traits只是鹦鹉学舌的相应iterator_class的嵌套式typedef.

template<class IterT>
struct iterator_traits{
    typedef typename Iter::iterator_category iterator_category;
};
 

对于指针,应该给一个该template的偏特化版本.

template <class IterT>
struct iterator_traits<IterT*> {
    typedef random_access_iterator_tag iterator_category;
};
 
template <class IterT, class DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) {
    // 根据不同的tag填充不同的内容
}
 
template <class IterT, class DistT>
void advance(IterT& iter, DistT d) {
    doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category);
}
 

条款48:认识template元编程

  • template metaprogramming可将工作由运行期移动到编译器,因而得以实现早期错误侦测和更高的执行效率.
template没有迭代,只有递归.循环藉由模板具现化的递归完成.
template <int n>
struct Factorial {
    enum { value = n * Factorial<n - 1>::value };
};
 
template <>
struct Factorial<0> {
    enum { value = 1 };
};
 
int main(){
    cout<<Factorial<12>::value;
}
 

条款49:了解new-handler的行为

  • set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用.
  • 当operator new抛出异常反映一个未获满足的内存需求之前,会先调用一个客户指定的错误处理函数,一个所谓的new-handler.客户需要调用声明于的标准库函数set_new_handler来指定用以处理内存不足的函数.
class X {
  public:
    static void outOfMemory();
};
 
class Y {
  public:
    static void outOfMemory();
};
 
int main() {
    X* p1 = new X; // 分配不成功希望调用X::outOfMemory
    Y* p2 = new Y; // 分配不成功希望调用Y::outOfMemory
}

C++不支持上面的这种专属的new-handler,但可以自己实现.只需令每个class提供自己的set_new_handler和operator new.

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

有许多理由编写一个自定的new和delete.
怎么会有人想要替换编译器提供的new和delete?有三个常见理由:

  • 检测运用上的错误.自定一个new可以超额分配内存,额外空间可以放置特定的签名,delete得以检查上述签名是否原封不动,以记录某些日志.
  • 强化效能.编译器自带的new和delete主要用于一般目的,他们不但被长期执行的程序接受,也被执行时间少于一秒的程序接受.他们需要处理一系列需求,大块内存的,小块内存的,混合大小内存的.必须接纳各种分配形态,各种声明周期的分配和归还.必须考虑内存碎片问题.编译器自带的new和delete需要满足各种各样的需求,采用中庸之道对每个工作都适度的好,不对特定工作有最佳表现.定制new和delete性能胜过缺省版本,最高50%.
  • 收集使用上的统计数据.分配区块的大小分布如何?寿命分布如何?使用FIFO还是LIFO次序分配与归还?
  • 增加分配和归还的速度.
  • 降低缺省内存管理器带来的空间额外开销.缺省内存管理器往往使用更多的内存.
  • 将相关对象聚簇.将一些数据聚簇,减少内存页错误,提升效率.

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

  • new应内含一个无限循环并在其中尝试内存分配,如果无法满足需求,调用new-handler.应该能够处理0byte申请.delete收到null时不做任何事情.

实现一致性new必须返回正确的值,内存不足时必须调用new-handling函数,必须对付零内存的准备,必须避免不慎遮掩正常形式的new.new的返回值十分单纯,如果它有能力提供内存,就返回一个指向那块内存的指针,否则,抛出bad_alloc异常.其实也没那么单纯,new不止一次尝试分配内存,每次失败后会尝试调用new-handling函数,其也许能释放出某些内存.只有new-handling函数指针是null,new才会抛出异常.operator new需要把0byte的申请视为1byte.

new内含一个无限循环,break这个循环的唯一办法就是内存被成功分配或new-handling函数做了一些事情:让更多内存可用,安装另一个new-handler,卸载new-handler或抛出bad_alloc异常,或承认失败直接返回.

delete的规矩更简单,保证删除null指针永远安全.被删除的对象来自某个base class的派生而没有virtual析构函数,C++传给operator delete的size_t
数值可能不正确.

条款52:写了placement new也要写placement delete

  • 当你写一个placement operator new,确保有对应的placement delete.否则程序可能引发内存泄露.
  • 在类中声明placement new 和placement delete,确定不要无意识的遮掩了正常版本的new和delete.

假设Widget有个带日志功能的placement new,一定要给他定义一个placement delete且要和对应placement new具有完全相同的参数.

class Widget {
  public:
    static void* operator new(size_t size, ostream& logStream) throw(bad_alloc) {
    }
 
    static void operator delete(void* pMemory) throw() {
    }
 
    static void operator delete(void* pMemory, ostream& logStream) throw() {
 
    }
};

如果没有对应的placement delete,当调用new(std::cerr) Widget时,假设分配内存的动作成功了,但构造函数的过程产生了错误,这块已经分配的内存只能由操作系统运行时来回收,运行时将会寻找同placement new具有相同参数的placement delete.所以,如果没有定义这样的placement delete,就什么都不会调用.

上述代码调用new Widget时会出错,因为内部的new遮掩了普通new.可以在内部声明普通版本的new和delete.

class Widget {
  public:
    static void* operator new(size_t size, ostream& logStream) throw(bad_alloc) {
    }
 
    static void operator delete(void* pMemory) throw() {
    }
 
    static void operator delete(void* pMemory, ostream& logStream) throw() {
    }
 
    static void* operator new(size_t size) throw(bad_alloc) {
        return ::operator new(size);
    }
 
    static void operator delete(void* pMemory) throw() {
        ::operator delete(pMemory);
    }
};

如果基类中没有声明普通版的new和delete,派生类也没法使用.需要在基类中声明普通new和delete,利用using声明式在派生类中阐明.

using Base::operator new;
using Base::operator delete;

条款53:不要轻忽编译器的警告

  • 严肃对待编译器的警告信息.努力取得无任何警告信息的荣誉.但不要过度依赖编译器的报警能力,不同编译器对事情的态度不相同.
class B {
  public:
    virtual void f() const;
};
 
class D : public B {
  public:
    virtual void f();
};

D中的f函数会遮掩B中的f,而不是重载,有的编译器可能会发出一个警告,有的并不会.

条款54:熟悉标准程序库

条款55:让自己熟悉Boost程序库

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值