博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Conclusion for Constructors,Destructors,and Assignment Operators
阅读量:4215 次
发布时间:2019-05-26

本文共 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
v; }//v在这里被销毁
每次析构Widget都会抛出异常,在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确的行为。

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构造函数,因为这就像试图构造一个已经存在的的对象。反过来也不行,那样仿佛给一个尚未构造好的对象赋值。

较好的做法是建立一个新的成员函数给两者调用。

你可能感兴趣的文章
lua学习笔记之五(Lua中的数学库)
查看>>
dos: tree命令生成目录结构
查看>>
Managing Projects from the Command Line(android官网文档)
查看>>
Android项目自动生成build.xml,用Ant打包
查看>>
CCLayer注册lua回调函数setTouchPriority失效
查看>>
cocos2dx左下角三行数值意义
查看>>
LUA modue require package 区别
查看>>
package.loaded
查看>>
cocoStudio: Button设置锚点问题
查看>>
vld 使用
查看>>
MAC下安装多版本JDK和切换几种方式
查看>>
java.util.concurrent详解
查看>>
java事务大总结(一) 先理解数据库的事务以mysql为例
查看>>
java事务大总结(二) 理解JDBC事务的工作机制
查看>>
java事务大总结(三) 理解学习 JTA(Java Transaction API)
查看>>
java事务大总结(四)spring事务相关大总结
查看>>
驴妈妈管理的一点经验总结
查看>>
IOS开发学习的好资料大搜藏
查看>>
SSH的认证终结(无需密码的git操作或者ssh链接无需密码)
查看>>
Jetty 的工作原理以及与 Tomcat 的比较
查看>>