05:了解C++默默编写并调用哪些函数
1:一个空类,如果你自己没声明,编译器就会为它声明(编译器版本的)一个copy构造函数、一个copy assignment操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会为你声明一个default构造函数。所有这些函数都是public且inline的。
2:只有当这些函数被调用时,它们才会被编译器创建出来。
3:编译器生成的default构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,比如用base classes和non-static成员变量的构造函数和析构函数。至于copy构造函数和copy assignment操作符,编译器创建的版本只是单纯的将来源对象的每一个non-static成员变量拷贝到目标对象。
4:编译器创建的析构函数是个non-virtual,除非这个class的base class自身声明有virtual析构函数。
5:以下情况下,编译器会拒绝为class生出operator=:
a、类中具有引用成员或者const 常量成员:
template<class T> class NamedObject { public: NamedObject(std::string& name, const T& value); ... private: std::string& nameValue; // this is a reference const T objectValue; // this is const };
引用和常量必须在定义时进行初始化,不支持赋值操作。因此编译器拒绝为这样的类创建operator=函数;
如果你打算在一个包含reference成员或const成员的class内支持赋值操作,你必须自己定义copy assignment操作符。
b、如果某个base class将copy assignment操作符声明为private,编译器也拒绝为其derived class生成一个copy assignment操作符。
06:若不想使用编译器自动生成的函数,就应该明确拒绝
1:如果不希望class支持复制初始化或者赋值操作,因为不定义copy构造函数和copy assignment操作符,编译器会自动创建一个,因此不定义这俩函数达不到这个目的。
2:因为编译器创建的函数都是public的,为了阻止这些函数被创建出来,可以将copy构造函数或copy assignment操作符声明为private。这样便阻止了编译器创建这些函数,而且类的用户也无法调用它们。
3:上面的做法还是有漏洞,因为类的成员函数和友元函数还是可以调用你的private函数。这种情况下,可以仅仅声明而不去定义它们。这种情况下,如果有成员函数或友元函数调用它们的话,将会产生一个连接错误。
因此,将复制构造函数和赋值操作符声明为private且不去定义它们,当类的用户企图拷贝时,编译器会阻止他;如果在成员函数或友元函数中这么做,连接器会发出抱怨。
4:将连接期错误移至编译期是可能的(而且那时好事,毕竟越早侦测出错误越好),只要定义一个Uncopyable类,并将自己的类继承该类就好:
class Uncopyable { protected: // allow construction Uncopyable() {} // and destruction of ~Uncopyable() {} // derived objects... private: Uncopyable(const Uncopyable&); // ...but prevent copying Uncopyable& operator=(const Uncopyable&); };
任何人(包括成员函数或友元函数)尝试拷贝Uncopyable类的派生类对象时,编译期便试着生成一个copy构造函数和一个copy assignment操作符。这些函数的编译器生成版会尝试调用其base class的对应函数,那些调用会被编译器拒绝,因为其base class的拷贝函数是private。
07:为多态基类声明virtual析构函数
1:当derived class对象经由一个base class指针删除,而该base class带着一个non-virtual析构函数,则其结果是未定义的。实际执行时通常发生的是对象的derived成分没被销毁,而其base class成分通常会被销毁,于是造成一个诡异的“局部销毁”对象。
2:任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。
3:如果class不含virtual函数,通常表示它并不愿意被用做一个base class。当class不企图被当作base class时,令其析构函数为virtual往往是个馒主意。
欲实现出virtual函数,对象必须携带某些信息,主要用来在在运行期决定哪一个virtual函数该被调用。这份信息通常是由一个所谓vptr ( virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,称为vtbl ( virtual table );每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtb----编译器在其中寻找适当的函数指针。
因此,无端的将某个class的析构函数声明为virtual,会增加对象的体积(vptr)。因此许多人的心得是:只有当class内含至少一个virtual函数,才为它声明virtual析构函数。
4:标准string,以及所有STL容器如vector,list,set,map等等,都不virtual析构函数,因此,不应该将它们当做base class。
5:如果抽象基类声明了virtual析构函数,则必须为它提供一份定义:
class AWOV { public: virtual ~AWOV() = 0; // declare pure virtual destructor }; AWOV::~AWOV() {} // definition of pure virtual dtor
这是因为,派生类继承该抽象基类后,派生类对象销毁时,会首先调用派生类的析构函数,然后是基类的析构函数。因此编译器会在AWOV的derived classes的析构函数中创建一个对~AWOV的调用,所以必须提供一份定义,否则会连接错误。
08:别让异常逃离析构函数
C++并不禁止析构函数吐出异常,但它不鼓励这么做。如果某个类的析构函数有可能抛出异常,则要么:抛出异常时直接调用abort退出程序;要么抛出异常时吞下异常,仅记录日志。
09:绝不在构造和析构过程中调用virtual函数
1:不要再构造函数和析构函数中,调用virtual函数。
2:在base class构造期间,如果构造函数中调用了virtual函数,即使当前正在构造derived class(构造派生类对象时,需要首先构造其基类部分),virtual函数也是base class中的版本。也就是说;在base class构造期间,virtual函数不是virtual函数。
在derived class对象的base class构造期间,对象的类型是base class而不是derived class。不只virtual函数会被编译器解析至(resolve to)base class,若使用运行期类型信息(runtime type information,例如dynamic_cast和typeid),也会把对象视为base class类型。
3:相同道理也适用于析构函数。一旦derived class析构函数开始执行,对象内的derived class成员变量便呈现未定义值,所以C++视它们仿佛不再存在。进入base class析构函数后对象就成为一个base class对象。
4:确定你的构造函数和析构函数都没有调用virtual函数,而它们调用的所有函数也都要服从这一约束。
10:令operator=返回一个reference to *this
1:赋值时,可以将其写成连锁形式:x = y = z = 15;赋值采用右结合律,因此这个表达式等价于:x = ( y = ( z = 15 ) );
2:为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。这是你为classes实现赋值操作符时应该遵循的协议:
Widget& operator=(const Widget& rhs) // return type is a reference to { // the current class ... return *this; // return the left-hand object }
3:这个协议不仅适用于标准的赋值形式,也适用于所有赋值相关运算,比如+=。
11:在operator=中处理“自我赋值”
1:“自我赋值”发生在对象被赋值给自己时,不要认定客户绝不会那么做,而且自我赋值并不总是那么可被一眼辨识出来,例如:a[i] = a[j];这条语句中,如果i和j相同,这便是自我赋值;再比如:*px = *py;如果px和py恰巧指向相同,这也是自我赋值。
2:“自我赋值”时,可能会掉进“在停止使用资源之前意外释放了它”的陷阱。比如:
Widget& Widget::operator=(const Widget& rhs) { delete pb; pb = new Bitmap(*rhs.pb); return *this; }
这里的自我赋值问题是,operator=函数内的*this和rhs有可能是同一个对象。果真如此delete就不只是销毁当前对象的bitmap,它也销毁rhs的bitmap。
欲阻止这种错误,传统做法是藉由operator=最前面的一个“证同测试(identity test )”达到“自我赋值”的检验目的:
Widget& Widget::operator=(const Widget& rhs) { if (this == &rhs) return *this; // identity test: if a self-assignment, // do nothing delete pb; pb = new Bitmap(*rhs.pb); return *this; }
3:这个新版本仍然存在异常方面的麻烦。更明确地说,如果”new Bitmap”导致异常(不论是因为分配时内存不足或因为Bitmap的copy构造函数抛出异常),Widget最终会持有一个指针指向一块被删除的Bitmap。
令人高兴的是,让operator=具备“异常安全性”往往自动获得“自我赋值安全”的回报。因此愈来愈多人对“自我赋值”的处理态度是倾向不去管它,把焦点放在实现“异常安全性”上。例如,我们只需注意在复制pb所指东西之前别删除pb:
Widget& Widget::operator=(const Widget& rhs) { Bitmap *pOrig = pb; // remember original pb pb = new Bitmap(*rhs.pb); // make pb point to a copy of *pb delete pOrig; // delete the original pb return *this; }
现在,如果”new Bitmap”抛出异常,pb保持原状。即使没有证同测试,这段代码还是能够处理自我赋值,因为我们对原bitmap做了一份复件、删除原bitmap、然后指向新制造的那个复件。它或许不是处理“自我赋值”的最高效办法,但它行得通。
4:在operator=函数内确保代码不但“异常安全”而且“自我赋值安全”的一个替代方案是,使用所谓的copy and swap技术。
它是一个常见而够好的operator=撰写办法:
Widget& Widget::operator=(const Widget& rhs) { Widget temp(rhs); // make a copy of rhs's data swap(temp); // swap *this's data with the copy's return *this; }
或者,可能更常见的是下面这种写法:
Widget& Widget::operator=(Widget rhs) { swap(rhs); return *this; }
12:复制对象时勿忘其每一个成分
1:如果自己写复制构造函数或赋值操作符而不使用编译器的版本,则需要注意的是:如果你为class添加一个成员变量,你必须同时修改复制构造函数和赋值操作符函数(你也需要修改class的所有构造函数,以及任何非标准形式的operator=(比如+=))。如果你忘记,编译器不太可能提醒你。
2:任何时候只要你承担起“为derived class撰写copying函数”的重责大任,必须很小心地也复制其base class成分。那些成分往往是private,所以你应该让derived class的copying函数调用相应的base class函数。