Effective C++

来源:互联网 发布:用户行为分析python 编辑:程序博客网 时间:2024/06/02 15:49

前言:如何有效运用C++,包括一般性的设计策略,以及带有具体细节的特定语言特性。知道细节很重要,否则如果疏忽几乎总是导致不可预期的程序行为(undefined behavior)。本文总结对于如何使用C++的一些建议,从而让你成为一个有战斗力的C++程序员。


    • 了解C默默编写并调用了哪些函数
    • 若不想使用编译器自动生成的函数就应该明确拒绝
    • 为多态基类声明virtual析构函数
    • 别让异常逃离析构函数
    • 绝不在构造和析构过程中调用virtual函数
    • 令operator返回一个reference to this
    • 在operator中处理自我赋值
    • 复制对象时勿忘其每一个成分

1 了解C++默默编写并调用了哪些函数

如果是一个空类,那么编译器可能会自动生成:
* copy构造函数
* copy assignment操作符
* 析构函数
* default构造函数

以上这些函数都是publicinline的。

class Empty {};// 等价于class Empty {public:    Empty()                       // default构造函数    {}            Empty(const Empty& rhs)       // copy构造函数    {}     ~Empty()                      // 析构函数(是否是virtual呢?)    {}    Empty& operator=(const Empty& rhs)  // copy assignment操作符    {}};

注意:
1. 惟有当这些函数被调用,它们才会被编译器创建出来。

Empty e1;       // default构造函数                // 析构函数Empty e2(e1);   // copy构造函数e2 = e1;        // copy assignment操作符
  1. 编译器生成的析构函数是个non-virtual,除非这个class的base class自身声明有virtual析构函数。
  2. copy构造函数和copy assignment操作符,编译器创建的版本只是单纯地将来源对象的每一个non-static成员变量拷贝到目标对象。
#include<iostream>#include<string>template<typename T>class NamedObject {public:#if 1    NamedObject(const char* name, const T& value) :        nameValue(name), objectValue(value)    {        std::cout << "NamedObject(const char* name, const T& value)\n";    }#endif    NamedObject(const std::string& name, const T& value) :        nameValue(name), objectValue(value)    {        std::cout << "NamedObject(const std::string& name, const T& value)\n";    }public:    std::string nameValue;    T objectValue;};int main(){    NamedObject<int> no1("gerry", 1);    NamedObject<int> no2(no1);         // 调用copy构造函数    NamedObject<int> no3("yang", 2);    no3 = no1;    std::cout << no3.nameValue << "\n";    return 0;}

NamedObject没有声明copy构造函数,也没有声明copy assignment操作符,所以编译器会创建这些函数当它们被调用的时候。编译器生成的copy构造函数必须以no1.nameValue和no1.objectValue为初值设定no2.nameValue和no2.objectValue。两者之中,nameValue的类型是string,而标准的string有个copy构造函数,所以no2.nameValue的初始化方式是调用stringcopy构造函数并以no1.nameValue为实参。另一个成员NameObject::objectValue的类型是int(对此template具现体而言T是int),是个内置类型,所以no2.objectValue会以拷贝no1.objectValue内的每一个bits来完成初始化。编译器为NamedObject所生成的copy assignment操作符,其行为基本上与copy构造函数一样。

请记住

编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。

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

Explicitly disallow the use of compiler-generated functions you do not want.

通常如果你不希望class支持某一特定机能,只要不声明对应函数就是了。但这个策略对copy构造函数copy assignment操作符却不起作用。因为,如果你不声明它们,而某些人尝试调用它们,编译器会为你声明它们。
如果你不声明copy构造函数copy assignment操作符,编译器可能会为你产出一份,于是你的class支持copying;如果你声明它们,你的class还是支持copying但这里的目标却是要阻止copying!

答案的关键是,所有编译器产出的函数都是public。为阻止这些函数被创建出来,你得自行声明它们,但这里并没有什么需求使你必须将它们声明为public。因此你可以将copy构造函数copy assignment操作符声明为private这样明确声明一个成员函数,可以阻止编译器暗自创建其专属版本;而令这些函数为private,使得成功阻止人们调用它。

一般而言,这个做法并不绝对安全。因为member函数friend函数还是可以调用你的private函数。除非你非常聪明不去定义它,那么如果有人不慎调用任何一个,会获得一个连接错误(linkage error)。

将成员函数声明为private而且故意不实现它们,这一伎俩是如此为大家接受,因而被用在C++ iostream程序库中阻止copying行为。

例子:

#include<iostream>#include<string>class HomeForSale;void copy_friend(HomeForSale& lhs, HomeForSale& rhs){    lhs = rhs;}class HomeForSale {    friend void copy_friend(HomeForSale& lhs, HomeForSale& rhs);public:    HomeForSale()     {    }    HomeForSale(const std::string& lhs) :        name(lhs)    {    }    HomeForSale& copy_ctor(HomeForSale& lhs)    {        // error LNK2019: 无法解析的外部符号 "private: __thiscall HomeForSale::HomeForSale(class HomeForSale const &)" (??0HomeForSale@@AAE@ABV0@@Z),该符号在函数 "public: class HomeForSale & __thiscall HomeForSale::copy_ctor(class HomeForSale &)" (?copy_ctor@HomeForSale@@QAEAAV1@AAV1@@Z) 中被引用        return HomeForSale(lhs);    }    void copy_assignment(HomeForSale& lhs)    {        // error LNK2019: 无法解析的外部符号 "private: class HomeForSale & __thiscall HomeForSale::operator=(class HomeForSale const &)" (??4HomeForSale@@AAEAAV0@ABV0@@Z),该符号在函数 "public: void __thiscall HomeForSale::copy(class HomeForSale &)" (?copy@HomeForSale@@QAEXAAV1@@Z) 中被引用        *this = lhs;    }    std::string name;private:    HomeForSale(const HomeForSale&);    HomeForSale& operator=(const HomeForSale&);};int main(){    HomeForSale h1("first");    HomeForSale h3(h1);     // error    HomeForSale h2 = h1;    // error    HomeForSale h4("fouth");    h4 = h1;                // error    HomeForSale h5("fifth");        // linkage error    h5.copy_ctor(h1);    HomeForSale h6("sixth");    h6.copy_assignment(h1);         // linkage error    std::cout << h6.name << "\n";    HomeForSale h7("seventh");    copy_friend(h7, h1);              // error?    return 0;}

档用户企图拷贝HomeForSale对象,编译器会阻挠他。如果你不慎在member函数或friend函数之内那么做,会轮到连接器发出抱怨。

另一种方法

将连接器错误移植编译器是可能的,而且那是好事,毕竟越早发现错误越好。方法是:在一个专门为了阻止copying动作而设计的base class内,将copy构造函数copy assignment操作符声明为private

因为,只要任何人,甚至是member函数或friend函数,尝试拷贝HomeForSale对象,编译器便试着生成一个copy构造函数和一个copy assignment操作符,这些函数的“编译器生成版”会尝试调用其base class的对应兄弟,那些调用会被编译器拒绝,因为其base class的拷贝构造函数是private

这种方法也有一个问题,由于它总是扮演base class,因此使用此项技术可能导致多重继承,因为你往往还可能需要继承其他class,而多重继承有时会阻止empty base class optimization

#include<iostream>#include<string>class Uncopyable{protected:    Uncopyable() {}      // 允许derived对象构造和析构    ~Uncopyable() {}private:    Uncopyable(const Uncopyable&);             // 阻止copying    Uncopyable& operator=(const Uncopyable&);};class HomeForSale : private Uncopyable{public:    HomeForSale()     {    }    HomeForSale(const std::string& lhs) :        name(lhs)    {    }    HomeForSale& copy_ctor(HomeForSale& lhs)    {        return HomeForSale(lhs);    }    void copy_assignment(HomeForSale& lhs)    {        *this = lhs;    }    std::string name;private:    //HomeForSale(const HomeForSale&);    //HomeForSale& operator=(const HomeForSale&);};int main(){    HomeForSale h1("first");    HomeForSale h3(h1);     // error    HomeForSale h2 = h1;    // error    HomeForSale h4("fouth");    h4 = h1;                // error    HomeForSale h5("fifth");        // complie err, not linkage error    h5.copy_ctor(h1);    HomeForSale h6("sixth");    h6.copy_assignment(h1);         // complie err, not linkage error    std::cout << h6.name << "\n";    return 0;}

请记住

为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。

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

C++指出,当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未定义 —— 实际执行时通常发生的是,对象的derived成分没被销毁。于是造成一个诡异的“局部销毁”对象,从而导致资源泄露。

base类没有使用virtual析构函数

#include <stdio.h>#include <iostream>using namespace std;class base {public:    base() {cout << "base()\n";}    ~base() {cout << "~base()\n";} // note, have no virtualprivate:    int v1;};class derived : public base {public:    derived() {cout << "derived()\n";}    ~derived() {cout << "~derived()\n";}private:    int v2;};int main() {    //derived obj;    base *b = new derived;    // do something    delete b;    return 0;}/*output:base()derived()~base() */

base类使用virtual析构函数

#include <stdio.h>#include <iostream>using namespace std;class base {public:    base() {cout << "base()\n";}    virtual ~base() {cout << "~base()\n";} // have virtualprivate:    int v1;};class derived : public base {public:    derived() {cout << "derived()\n";}    ~derived() {cout << "~derived()\n";}private:    int v2;};int main() {    //derived obj;    base *b = new derived;    // do something    delete b;    return 0;}/*output:base()derived()~derived()~base() */

观点1:任何class只要带有virtual函数,都几乎确定应该也有一个virtual析构函数。

观点2:如果class不含virtual函数,通常表示它并不意图被用做一个base class。当class不企图被当做base class,令其析构函数为virtual往往是一个馊主意。因为,欲实现出virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。这份信息通常是由一个所谓vptr(virtual table pointer)指针指出,vptr指向一个由函数指针构成的数组,称为vtbl(virtual table)。每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl(编译器在其中寻找合适的函数指针)。这样,如果base class内含virtual函数,那么其对象的体积会增加,在32-bits计算机体系结构中将多占用32bits(vptr大小);而在64-bits计算机体系结构中多占用64bits(指针大小为8字节)。

观点3:标准库string不含任何virtual函数,但有时程序员会错误地把它当做base class。那么,当你在程序任意某处无意间将一个pointer-to-specialstring转换为一个pointer-to-string,然后将转换所得的那个string指针delete掉,则立刻被流放到”不明确行为上”。很不幸C++没有提供类似Java的final classes禁止派生的机制。

请记住

  1. 从里向外构造(ctor),从外向里析构(dtor)
  2. polymorphic (带多态性质) base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
  3. classes的设计目的如果不是作为base classes使用,或不是为了具备多态性使用,此class就不该声明virtual析构函数。

4 别让异常逃离析构函数

C++并不禁止析构函数吐出异常,但它不鼓励你这样做。

#include <iostream>#include <exception>#include <vector>using namespace std;class Widget {public:    Widget()    {        cout << "Widget()\n";    }    ~Widget()    {        cout << "~Widget()\n";        throw std::runtime_error("~Widget()");    }private:    int v;};int main(){    vector<Widget> w_vec;    w_vec.resize(3);    return 0;}/*output:Widget()Widget()Widget()~Widget()libc++abi.dylib: terminating with unexpected exception of type std::runtime_error: ~Widget()Abort trap: 6 */

当vector对象被销毁,它有责任销毁其内含的所有对象。假设vector内含10个对象,而在析构第一个元素期间,有个异常抛出,其他9个对象还是应该被销毁,否则它们保存的任何资源都会发生泄漏。因此,应该调用它们各个析构函数。

正确的处理方法:在析构函数里捕获每一个异常

#include <iostream>#include <exception>#include <vector>using namespace std;class Widget {public:    Widget()    {        cout << "Widget()\n";    }    ~Widget()    {        // 析构函数里如果抛出异常,需要自己捕获处理,否则会资源泄漏        try {            cout << "~Widget()\n";            throw std::runtime_error("~Widget()");        } catch (std::runtime_error &e) {            cout << "catch exception at ~Widget()\n";        }    }private:    int v;};int main(){    try {        vector<Widget> w_vec;        w_vec.resize(3);    } catch (...) {        cout << "catch exception at main()\n";    }    return 0;}/*Widget()Widget()Widget()~Widget()catch exception at ~Widget()~Widget()catch exception at ~Widget()~Widget()catch exception at ~Widget()*/

请记住

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

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

你不该在构造函数和析构函数期间调用virtual函数,因为这样的调用不会带来你预期的效果。

例如:假设你有个class继承体系,用来塑模股市交易如买进、卖出的订单等等,这样的交易一定要经过审计,所以每当创建一个交易对象,在审计日志中也需要创建一笔适当记录。

#include<stdio.h>#include<iostream>class Transaction { // base classpublic:    Transaction();    // 做出一份因类型不同而不同的日志记录    virtual void logTransaction() const = 0;};Transaction::Transaction(){    std::cout << "Transaction()\n";    // 最后动作是记录日志    logTransaction();}class BuyTransaction: public Transaction { // derived classpublic:    virtual void logTransaction() const {        std::cout << "BuyTransaction::logTransaction()\n";    }};class SellTransaction: public Transaction { // derived classpublic:    virtual void logTransaction() const {        std::cout << "SellTransaction::logTransaction()\n";    }};int main(){    BuyTransaction bt;}/*g++ -o no_virtual_in_ctor_dtor no_virtual_in_ctor_dtor.cppno_virtual_in_ctor_dtor.cpp:17:2: warning: call to pure virtual member function 'logTransaction';      overrides of 'logTransaction' in subclasses are not available in the constructor of      'Transaction'        logTransaction();        ^no_virtual_in_ctor_dtor.cpp:9:2: note: 'logTransaction' declared here        virtual void logTransaction() const = 0;        ^1 warning generated. */

发现无法调用derived class的函数,在编译期间就报错了。把pure virtual去掉:

#include<stdio.h>#include<iostream>class Transaction { // base classpublic:    Transaction();    // 做出一份因类型不同而不同的日志记录    virtual void logTransaction() const {        std::cout << "Transaction::logTransaction()\n";    }};Transaction::Transaction(){    std::cout << "Transaction()\n";    // 最后动作是记录日志    logTransaction();}class BuyTransaction: public Transaction { // derived classpublic:    virtual void logTransaction() const {        std::cout << "BuyTransaction::logTransaction()\n";    }};class SellTransaction: public Transaction { // derived classpublic:    virtual void logTransaction() const {        std::cout << "SellTransaction::logTransaction()\n";    }};int main(){    BuyTransaction bt;}/*g++ -o no_virtual_in_ctor_dtor no_virtual_in_ctor_dtor.cpp./no_virtual_in_ctor_dtor Transaction()Transaction::logTransaction() */

这次可以编译过了,但是发现调用的并不是派生类的virtual函数。

原因分析

  1. 在创建派生类对象时,derived class对象内的bass class成分会在derived class自身成分被构造之前先构造妥当。Transaction构造函数调用virtual函数logTransaction,这时被调用的logTransaction是Transaction内的版本,不是BuyTransaction内的版本。base class构造期间virtual函数绝不会下降到derived classes阶层,在base class构造期间,virtual函数不是virtual函数。
  2. 由于base class构造函数的执行更早于derived class构造函数,当base class构造函数执行时derived class的成员变量尚未初始化。如果此期间调用的virtual函数下降至derived classes阶层,而derived class的函数使用的local成员变量尚未初始化,将导致不明确行为。
  3. derived class对象的的base class构造期间,对象的类型是base class而不是derived class。不只virtual函数会被编译器解析至base class,若使用运行期类型信息(runtime type information),例如dynamic_casttypeid,也会把对象视为base class类型。

相同的道理也适用于析构函数。一旦derived class析构函数开始执行,对象内的derived class成员变量便呈现未定义值,所以C++视它们仿佛不再存在,进入base class析构函数后对象就成为一个base class对象。

解决方法

如何确保每次一有Transaction继承体系上的对象被创建,就会有适当版本的logTransaction被调用呢?

一种做法
是在class Transaction内将logTransaction函数改为non-virtual,然后要求derived class构造函数传递必要信息给Transaction构造函数,而后那个构造函数便可安全地调用non-virtuallogTransaction。

#include <stdio.h>#include <iostream>#include <string>class Transaction { // base classpublic:    explicit Transaction(const std::string& logInfo);    // 做出一份因类型不同而不同的日志记录    void logTransaction(const std::string& logInfo) const {        std::cout << "Transaction::logTransaction(): " << logInfo << "\n";    }};Transaction::Transaction(const std::string& logInfo){    std::cout << "Transaction()\n";    // 最后动作是记录日志    logTransaction(logInfo);}class BuyTransaction: public Transaction { // derived classpublic:    BuyTransaction(const std::string &paras)        : Transaction(createLogString(paras)) // 将log信息传给base class构造函数    {}private:    static std::string createLogString(const std::string& paras);};std::string BuyTransaction::createLogString(const std::string& paras){    if (paras == "1") return "1+";    else if (paras == "2") return "2+";    else return "+";}int main(){    BuyTransaction bt1("1");    BuyTransaction bt2("2");}/*g++ -o no_virtual_in_ctor_dtor no_virtual_in_ctor_dtor.cpp./no_virtual_in_ctor_dtorTransaction()Transaction::logTransaction(): 1+Transaction()Transaction::logTransaction(): 2+  */

请记住

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

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

int x, y, z;x = y = z = 10;      // 赋值连锁形式x = (y = (z = 10));  // 赋值采用右结合律

为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。这是你为classes实现赋值操作符时应该遵循的协议。

#include <iostream>class Widget {public:    explicit Widget(int rhs) : a(rhs)    {    }    Widget& operator=(const Widget& rhs) // 返回类型是个reference,指向当前对象    {        a = rhs.a;        return *this;   // 返回左侧对象    }    Widget& operator=(int rhs) // 此函数也适用,即使此操作符的参数类型不符协定    {        a = rhs;        return *this;    }    Widget& operator+=(const Widget& rhs)    {        this->a += rhs.a;        return *this;    }    void print()    {        std::cout << a << "\n";    }private:    int a;};int main(){    Widget w(1);    w.print();    Widget w2(2);    w = w2;    w.print();    int i = 100;    w = 100;    w.print();    w += w2;    w.print();}/* ./operator 12100102 */

请记住

令赋值(assignment)操作符返回一个reference to *this。

7 在operator=中处理自我赋值

自我赋值发生在对象被赋值给自己时,这看起来有点愚蠢,但是它合法。所以不要认定客户绝不会那么做。此外自我赋值动作并不总是可以一眼看出来。

// 潜在的自我赋值a[i] = a[j];  *px = *py;

这些并不明显的自我赋值,是名带来的结果。实际上,两个对象只要来自同一个继承体系,它们甚至不需要声明为相同类型就可能造成别名,因为一个base class的reference或pointer可以指向一个derived class对象。

class Base { ... };class Derived: public Base { ... };// rb和*pb有可能其实是同一对象void doSomething(const Base& rb, Derived* pd);

因此,在处理自我赋值时应该注意保证:
1. 自我赋值安全问题
2. 异常问题

class Bitmap { ... };class Widget {public:    Widget& operator=(const Widget& rhs);private:    Bitmap* pb;};// 不安全的版本Widget& Widget::operator=(const Widget& rhs){    delete pb;    pb = new Bitmap(*rhs.pb);    return *this;}// 安全的版本,但不具备异常安全性// 如果new异常,Widget最终会持有一个指针指向一块被删除的BitmapWidget& Widget::operator=(const Widget& rhs){    if (this == &rhs) return *this;   // identity test    delete pb;    pb = new Bitmap(*rhs.pb);    return *this;}// 异常安全的版本,同时也是自我赋值安全的// 现在如果new异常,pb保存原状// 即使没有identity test,这段代码还是能够处理自我赋值,虽然不是最高效的方法,但是行得通Widget& Widget::operator=(const Widget& rhs){    Bitmap* pOrig = pb;    pb = new Bitmap(*rhs.pb);    delete pOrig;    return *this;}

对于第三个版本的补充说明:
如果你很关心效率,可以把identity test再次放回函数起始处。然而这样做之前先问问自己,你估计自我赋值的发生概率有多高?因为这项测试也需要成本,它会使代码变得大一些并导入一个新的控制流分支,而两者都会降低执行速度。Prefetching, caching和pipelining等指令的效率都会因此降低。

另一个替代方案是:使用copy and swap技术。此方法,为了伶俐巧妙而牺牲了清晰性。

请记住

  1. 确保当对象自我赋值时operator=有良好行为,其中技术包括,比较来源对象和目标对象的地址,精心周到的语句顺序,以及copy-and-swap。
  2. 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

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

  1. 设计良好的OO系统会将对象的内部封装起来,只留两个函数负责对象拷贝,copy构造函数和copy assignment操作符,我们称它们为copying函数。
  2. 编译器会在必要的时候为我们的class创建copying函数,并说明这些“编译器生成版”的行为是,将被拷对象的所有成员变量都做一份拷贝。 如果你声明自己的copying函数,意思就是告诉编译器你不喜欢缺省实现中的某些行为,编译器仿佛被冒犯似的,会以一种奇怪的方式回敬你,当你的实现代码出错时却不告诉你。
  3. 如果你为class添加一个成员变量,你必须同时修改copying函数,如果你忘记了,编译器也不会告诉你。
  4. 任何时候,只要你承担起为derived class撰写copying函数的责任,必须很小心地也复制其base class成分,那些成分往往是private,所以你无法直接访问它们,你应该让derived class的copying函数调用相应的base class函数。
  5. 如果你发现你的copy构造函数和copy assginment操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用,这样的函数往往是private而且常被命名为init。
// 调用base class的copy构造函数Derived::Derived(const Derived& rhs): Base(rhs), xxx(rhs.xxx){}Derived& Derived::operator=(const Derived& rhs){    Base::operator=(rhs); // 对base class成分进行赋值    xxx = rhs.xxx;    return *this;}

当你编写一个copying函数,请确保:
* 复制所有local成员变量
* 调用所有base classes内的适当的copying函数

请记住

  1. copying函数应该确保复制对象内的所有成员变量,及所有base class成分。
  2. 不要尝试以某个copying函数实现另一个copying函数,应该将相近的代码放在第三个函数中,并由两个copying函数调用。
0 0
原创粉丝点击