本文共 7738 字,大约阅读时间需要 25 分钟。
条款05:
1.编译器默认生成的函数有哪些?
如果写下一个空类,经过C++处理过后,就不是一个空类了。
class Empty(){};
编译器生成的类如下:
class Empty{public: Empty(); // 缺省构造函数 Empty(const Empty&); // 拷贝构造函数 ~Empty(); // 析构函数 Empty& operator=(const Empty&); // 赋值运算符 Empty* operator&(); // 取址运算符 const Empty* operator&() const; // 取址运算符 const};所以编译器默认生成的函数有:default构造函数,copy构造函数,析构函数,copy assignment操作符,取地址运算符,取地址运算符 const。这些函数都是public且inline。
2.这些默认函数做些什么?
构造函数和析构函数主要是调用base classes和non-virtual成员变量的构造函数和析构函数。编译器产生的析构函数是个non-virtual,除非这个class的base class自身声明有virtual析构函数。
copy构造函数和copy assignment操作符,编译器创建的版本只是单纯地将来源对象的每一个non-static成员变量拷贝到目标对象。
3.有引用或者const成员变量,则需自己定义copy assignment,编译器拒绝为class生出operator=。
class Test{public: … Test(std::string& name);private: std::string& nameValue; //有一个引用的成员 };std::string dog(“dog”);std::string cat(“cat”);Test t1(dog);Test t2(cat);t1 = t2;如果赋值,那么其中的引用namevalue就可以更改了,就是说引用是可以指向不同的对象,那当然是不行的,所以编译器会拒绝这一行为,报错。同样如果类中有const成员,也无法通过编译器。
4.如果base class的copy assignment函数为private,那么derived class无法自动生成copy assignment函数,因为编译器为derived classes所生成的copy assignment操作符想象中可以处理base class成分,但他们无权调用derived class无权调用的成员函数。
条款06:
1.可以将copy构造函数或copy assignment操作符声明为private来阻止编译器来生成这两个函数。C++ iostream程序库中阻止copying行为就采用这种方法。
2.上面的做法也不是绝对安全,因为member函数和friend函数还是可以调用private函数。
3.可以专门为阻止copy而设计一个base class:
class Uncopyable{protected: Uncopyable(){} ~Uncopyable(){}private: Uncopyable(const Uncopyable&); Uncopyable& operator=(const Uncopyable&);};为了阻止HomeForSale对象被拷贝,我们唯一需要做的就是继承Uncopyable。
class HomeForSale :private Uncopyable{ //...};条款07:
1.又见工厂函数
用下面的基类和继承类作为不同的计时方法:
class TimeKeeper{public: TimeKeeper(); ~TimeKeeper(); //....};class AtomicClock : public TimeKeeper{ ... };class WaterClock : public TimeKeeper{ ... };class WristClock : public TimeKeeper{ ... };客户只想在程序中使用时间,不想操心时间如何计算等细节,可以设计factory函数,返回指针指向一个计时对象。factory函数会“返回一个base class指针,指向新生成之derived class对象”:
TimeKeeper * ptk = getTimeKeeper(); //用基类的指针指向派生类的对象2.factory函数的返回对象必须位于heap。因此为了避免泄露内存和其他资源,将factory函数返回的每一个对象适当的删除很重要。
delete ptk;但是依靠客户删除,基本上会带来错误倾向,见条款18.
现在问题来了,getTimeKeeper返回的指针指向一个继承类对象,而那个对象却经由一个base class指针被删除,而目前的base class有个non-virtual析构函数。
3.当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,实际执行时通常发生的是对象的derived成分没被销毁。但是其基类成分被销毁。4.解决3中问题的做法:给base class一个virtual析构函数,此后删除derived class对象就会销毁整个对象,包括所有derived class成分。
class TimeKeeper{public: TimeKeeper(); virtual ~TimeKeeper(); //....};class AtomicClock : public TimeKeeper{ ... };class WaterClock : public TimeKeeper{ ... };class WristClock : public TimeKeeper{ ... };任何class只要带有virtual函数,那几乎可以确定应该也有一个virtual析构函数。
5.如果class不含virtual函数,通常表示它并不意图被用作一个base class。当class不企图被当做base class,令其析构函数为virtual往往是个馊主意。
class Point{public: Point(int xCoord, int yCoord); ~Point();private: int x, y;};如果int是32bits,那么Point对象可塞入一个64-bit缓存器中。然而当Point的析构函数是virtual,形势起了变化。即需要额外的内存来存放vptr。 6.vptr(virtual table pointer)指向一个由函数指针构成的数组,称为vtbl(virtual table),每一个带有virtual函数的class都有一个相应的vtbl。vptr用来在 运行期指出哪一个virtual函数该被调用。
更多关于虚函数表的内容参考:http://blog.csdn.net/haoel/article/details/1948051/
注意:虚函数表在编译器建立,虚函数地址也在编译器加入到虚函数表。
7.如果Point class内含virtual函数,其对象的体积会增加:32位计算机体系将占用64-bit至96-bit(2个ints加上vptr);在64位计算机体系结构中可能占用64-128bits,因为指针在64-bit计算机中占64bits。其对象大小增加50%-100%。
8.即使完全不带virtual函数,还是可能被“non-virtual析构函数问题”困扰。
标准string不含任何virtual函数,但如果错误的把它当做基类,则可能出问题。
class SpecialString :public std::string{ //...};
如果在程序中无意将一个pointer-to-SpecialString转换为一个pointer-to-string,然后将转换所得的那个string指针delete掉:
SpecialString* pss = new SpecialString("Impending Doom");std::string* ps;//...ps = pss;//...delete ps; //现实中*ps的SpecialString资源会被泄露,因为其析构没被调用STL中的vector,list,set,等等,都是不带virtual析构函数的class,不要让这些类称为基类。 9.为希望称为抽象的那个class声明一个pure virtual析构函数。
class AWOV{public: virtual ~AWOV() = 0;};必须为这个pure virtual析构函数提供一份定义:
AWOV::~AWOV(){}析构函数的运作方式是:最深层派生的那个class其析构函数最先被调用,然后是其每一个base的析构函数被调用。(与构造顺序相反)
编译器会在AWOV的derived classes的析构函数中创建一个对~AWOV的调用动作,所以必须为这个函数提供一份定义。
条款08:
1.有经验的程序员通常会这样组织编写代码模块,如下:
void func(){ try{ //这里代码完成真正复杂的计算工作 //这里的代码在执行过程中可能抛出DataType1类型的异常 } catch(DataType1 &d1){ //检测到异常后的工作 }}2.C++析构函数吐出异常,会导致不明确行为。
class Widget{public:...~Widget(){...}//假设这个可能吐出一个异常};void doSomething(){ std::vector每次析构Widget都会抛出异常,在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确的行为。v; }//v在这里被销毁
3.不要让异常逃离析构函数
class DBConnection{ public: ... static DBConnection create(); //get a DBConnection omit parameter for simplicity void close(); //close DBConnection };这是一个负责连接数据库的类,为了确保客户不忘记调用close(),可以穿件一个类来管理连接。
class DBConn{ public: DBConn(DBConnection& db):db_(db){} ~DBConn(){ db_.close(); } ... private: DBConnection& db_; };就可以这样来用:
{ DBConn connection(*(DBConnection.create())); ... //自动析构时候调用close方法 }如果调用close导致异常,DBConn析构函数会传播该异常,也就是允许它离开这个析构函数。这样会抛出难以驾驭的麻烦。
4.如果close抛出异常就结束程序,可以调用abort完成:
DBConn::~DBConn(){ try{ db_.close(); }catch(...){ .... std::abort();//结束程序,可以强制"不明确行为"死掉 } }也可以吞下因调用close而发生的异常:
DBConn::~DBConn(){ try{ db_.close(); }catch(...){ //制作运转记录,记下对close的调用失败 } }5.一个较佳的策略是重新设计DBConn接口,使其客户有机会对可能出现的问题作出反应。
class DBConn{ public: DBConn(DBConnection& db):db_(db){} void close(){ db_.close(); isClosed_ = true; } ~DBConn(){ if(!isClosed_){ //使用以上两种解决方案之一来进行解决. } } ... private: DBConnection& db_; bool isClosed_; };条款09:
1.绝对不要在构造和析构过程中调用virtual函数
#include下面这句:#include using namespace std;class Transaction{public: Transaction(); virtual void logTransaction() const = 0;};Transaction::Transaction(){ cout << "Base Transaction" << endl; logTransaction();}void Transaction::logTransaction() const{ cout << "Base log" << endl;}class BuyTransaction : public Transaction{public: BuyTransaction(); virtual void logTransaction() const;};BuyTransaction::BuyTransaction(){ cout << "Buy Transaction" << endl;}void BuyTransaction::logTransaction() const{ cout << "Buy log" << endl;}class SellTransaction : public Transaction{public: SellTransaction(); virtual void logTransaction() const;};SellTransaction::SellTransaction(){ cout << "Sell Transaction" << endl;}void SellTransaction::logTransaction() const{ cout << "Sell log" << endl;}int main(){ BuyTransaction b; //SellTransaction s; return EXIT_SUCCESS;}
BuyTransaction b;先调用Transaction构造函数,该构造函数调用logTransaction是Transaction内的版本,而不是BuyTransaction内的版本--------即使目前即将建立的对象类型是BuyTransaction。 base class构造期间virtual函数绝对不会下降到derived classes阶层。 2.在继承类对象的基类构造期间,对象的类型是base class而不是derived class。对象在derived class构造函数开始执行前不会成为一个derived class对象。
相同道理也适用于析构函数,一旦derived class析构函数开始执行,对象内的derived class成员变量便呈现未定义值。进入base class析构函数后对象就成为一个base class对象。
3.构造函数调用的函数内部也不要调用virtual函数。
4.一种解决方法是将logTransaction函数改为non-virtual桉树,然后要求derived class构造函数传递必要信息给Transaction构造函数。
条款10:
1.为了实现连锁赋值,令operator=返回一个reference to *this
class Interger{ public: ... Interger& operator=(const Interger& rhs){ ... return *this; } };
条款11:
1.自我赋值可能会出现问题class Bitmap{ //...};class Widget{ //...private: Bitmap* pb; //指向一个从heap分配而得的对象};下面是operator=实现代码,自我赋值出现时并不安全:
Widget& Widget::operator=(const Widget& rhs){ delete pb; pb = new Bitmap(*rhs.pb); return *this;}最后自己持有的指针指向已经删除的对象。
2.为避免上述错误,可以在最前面加上“证同测试”,检测是否为自我赋值。
Widget& Widget::operator=(const Widget& rhs){ if (this == &rhs) return *this; delete pb; pb = new Bitmap(*rhs.pb); return *this;}但是上述代码存在异常安全性,想想如何改? 条款12:
1.如果你为class添加一个成员变量,你必须同时修改copying函数,以免复制对象时漏掉某个成分。
2.继承类编写复制函数时,调用所有base classes内的适当的copying函数。
3.不该令copy assignment操作符调用copy构造函数,因为这就像试图构造一个已经存在的的对象。反过来也不行,那样仿佛给一个尚未构造好的对象赋值。
较好的做法是建立一个新的成员函数给两者调用。