C++进阶_Effective_C++第三版(二) 构造/析构/赋值运算 Constructors,Destructors,and Assignment Operators

构造/析构/赋值运算Constructors,Destructors,and Assignment Operators

5、了解C++默默编写并调用那些函数

Know what functions C++ silently writes and calls.
声明一个empty class空类,经过C++处理后,就会变成一个非empty class,编译器就会为它声明一个default构造函数,一个copy构造函数,一个copy assignment操作符和一个析构函数的类,所有的这些函数都是public且inline。如下:
编码版本:

class Empty { };

编译器处理后版本:

class Empty { 
public:
	Empty() {}					//default构造函数
	Empty(const Empty& rhs) {}		//copy构造函数
	~ Empty() {}					//析构函数
	Empty& operator=(const Empty& rhs) {} //copy assignment操作符
};

但是这些函数只有被需要(被调用)时,才会被编译器创建出来。

Empty e1;     //default构造函数,析构函数
Empty e2(e1);  //copy构造函数
e2 = e1;       //copy assignment操作符

对于default构造函数,析构函数主要是给编译器一个地方放置“藏身幕后”的代码,像是调用base classes和non-static成员变量的构造函数和析构函数。copy构造函数和copy assignment操作符编译器创建的版本只是单纯地将来源对象的每一个non-static成员变量拷贝到目标对象。
如下一个NamedObject template:

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

由于其声明了构造函数,编译器不再为它创建default构造函数,这就意味如果设计一个class,其构造要求实参,不用担心编译器默认添加一个无实参构造函数而覆盖自己定义版本。由于没有声明copy构造函数和copy assignment操作符,编译器会默认创建。如下用法:

NamedObject<int> no1(“Smallest”, 2);
NamedObject<int> no2(no1);

此时编译器生成的copy构造函数,对于no2.nameValue的初始化方式为调用string的copy构造函数并以no1.nameValue为实参。对于no2.objectValue由于为int是内置类型,会以拷贝no2.objectValue内的每一个bits来完成初始化。而对于copy assignment操作符的操作和copy构造函数类似。
但是如果nameValue为std::string& nameValue时,如果对于两个对象进行赋值操作,此时由于两个对象的nameValue为引用,但是C++不允许让reference改指向不同对象,此时编译器会拒绝此赋值动作。

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

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

Explicitly disallow the use of compiler-generated functions you do not want.
如果不想让某个类的对象通过赋值的方式创建或者对象被赋值,此时需要阻止编译器生成默认的copy构造函数和copy assignment操作符,此时可以将copy构造函数和copy assignment操作符声明为private。如此编译器无法默认创建其专属版本,而这些函数为private,是其他人无法调用它。但是此做法并不是绝对安全,因为member函数和friend函数还是可以调用此private函数。此时可以通过只声明不实现copy构造函数和copy assignment操作符,如此如果有函数调用它们则会提示连接错误(linkage error)。如下:

class HomeForSale {
public:private:HomeForSale(const HomeForSale&);  //只有声明
	HomeForSale& operator=(const HomeForSale&);
};

另一种更好的办法是设计一个专门为阻止copying动作的base class,然后private继承它即可。如下:

class Uncopyable {
protected:     //允许派生(derived)对象构造和析构
	Uncopyable() {}
	~Uncopyable() {}
private:
	Uncopyable(const Uncopyable&);   //阻止copying
	Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale: private Uncopyable {};
  • 为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base
    class也是一种做法。

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

Declare destructors virtual in polymorphic base classes.
对于有如下例子,记录时间,基类TimeKeeper,三个具体实现类继承与TimeKeeper:

	class TimeKeeper {
	pubilc:
		TimeKeeper();
		~ TimeKeeper();};
class AtomicClock:public TimeKeeper{};   //原子钟
class WaterClock:public TimeKeeper{};    //水钟
class WristClock:public TimeKeeper{};     //腕表

对于正常使用的话,由于不想操作具体细节,可以利用factory(工厂)函数,返回一个基类的指针,指向新生成的派生类(derived class)对象,返回通过此函数获得一个动态分配的对象,在使用后,释放它,避免资源泄漏:

TimeKeeper *getTimeKeeper();  //返回一个指针,指向TimeKeeper派生类动态分配对象
TimeKeeper *ptk = getTimeKeeper();  //获得一个动态分配对象//使用它
delete ptk;						//释放

由于获得的指针指向一个派生类对象,但是由一个基类指针而删除,由于基类中有一个non-virtual析构函数,其结果未有定义—实际执行时通常发生的是对象的派生成分没被销毁。这样造成了一个诡异的“局部销毁”对象。为了清除这个问题:给基类一个virtual析构函数,此后删除派生类对象就会销毁整个对象,包括派生类部分:

class TimeKeeper {
	pubilc:
		TimeKeeper();
		virtual ~ TimeKeeper();};

对于TimeKeeper这样的基类一般除了析构函数外,通常还有其他的virtual函数,因为virtual函数的目的是允许派生类的实现得以定制化。例如TimeKeeper就可能拥有一个virtual getCurrentTime,它在不同的派生类中有不同的实现。任何class只要带有virtual函数都几乎确定也有一个virtual析构函数。

  • polymorphic(带多态性质的)基类应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
  • Classes的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。

8、别让异常逃离析构函数

Prevent exceptions from leaving destructors.
C++并不禁止析构函数抛出异常,但它不建议这样做。例子如下:

class Widget{
pubilc:
	~Widget() {}   //假设这个可能抛出一个异常
};
Void do Something()
{
	std:vector< Widget > v;}  //v在这里被自动销毁

当vector v被销毁时,它需要销毁其内所有的Widgets。而在析构第一个Widget期间,有个异常被抛出,但其他的还应该被销毁,因此v应该调用它们各个析构函数,如果第二个Widget析构函数又抛出异常,现在有两个同时作用的异常,对C++而言太多了,此时程序若不结束执行就是导致不明确的行为。
但是如果析构函数必须执行一个动作,而这个动作可能会在失败时抛出异常,如下例子,使用一个class负责数据库连接:

class DBConnection {
public:
	static DBConnection create();  //返回DBConnection对象
	void close();    //关闭连接,失败抛出异常
};

为了确保数据库连接总能被关闭,在析构函数上调用close()函数:

class DBConn {   //管理DBConnection对象
public:~ DBConn()	{		db.close();	} //确保数据库连接总是被关闭
private:
	DBConnection db;
};
可以如下使用数据库连接:
{
	DBConn dbc(DBConnection::create());}

但是如果调用导致异常,Dbconn析构函数会传播该异常,也就是允许它离开这个析构函数。为了避免这个问题,可以使用如下两个办法:
如果close抛出异常就结束程序。通过调用abort完成:

DBConn::~ DBConn()
{
	try{ db.close(); }
	catch () { //制作运转记录,记下对close的调用失败;
		std::abort();
	}
}

吞下因调用close而发生的异常:

DBConn::~ DBConn()
{
	try{ db.close(); }
	catch () { 
		//制作运转记录,记下对close的调用失败;
	}
}

但是这这种办法都无法对导致close抛出异常的情况做出反应。一个更好的办法是重新设计DBConn接口,是其客户有机会对可能出现的问题作出反应。例如DBConn自己可以提供一个close函数,因而赋予客户一个处理异常的机会。DBConn也可以追踪其所管理的DBConnection是否已被关闭,并在未关闭时由其析构函数关闭。这可以防止遗失数据库连接。但是如果DBConnection析构函数调用close失败,然后还是使用强迫程序结束或吞下异常。新类如下:

class DBConn {   //管理DBConnection对象
public:void close() { db.close(); closed = true;}
	~ DBConn()	
	{
		if(!closed) {
			try { db.close();}
			catch() 
			{  //根据运行记录,记下对close的调用失败
			}
		}
	}
private:
	DBConnection db;
bool closed;
};
  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕获任何异常,然后吞下它们(不传播)或结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

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

Never Call virtual functions during construction or destruction.
假设有个class继承体系,用来模拟股市交易如买入、卖出的订单等,然后每个交易在升级日志中都需要创建一笔记录:

class Transaction {    //所有交易的基类
public:
	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的构造函数被调用,但是首先基类Transaction的构造函数一定会更早调用,派生类对象的基类成分会在派生类自身成分被构造之前先构造完成。但是Transaction构造函数的最后一行调用virtual函数logTransaction,这时候调用的logTransaction是Transaction内的版本,不是BuyTransaction内的版本,因为基类构造期间virtual函数绝对不会下降到派生类阶层。某些编译器会为此发出一个警告信息,即使没有警告,因为logTransaction函数在Transaction内是个pure virtual。除非它被定义(但基本不可能),否则程序无法连接,因为连接器找不到必要的Transaction::logTransaction的实现代码。
相同的道理也适用于析构函数。一旦派生类析构函数开始执行,对象内的派生类成员变量便呈现未定义值。
对于上述例子,在class Transaction内将logTransaction改为non-virtual,然后要求派生类构造函数传递必要的信息给Transaction构造函数,而后那个构造函数便可以安全地调用non-virtual logTransaction函数,如下:

class Transaction {    
public:
	explicit Transaction(const std::string& logInfo);  
	void logTransaction(const std::string& logInfo) const; //non-virtual函数.
};
Transaction::Transaction(const std::string& logInfo) 
{logTransaction(logInfo);          // 调用non-virtual函数
}
class BuyTransaction:public Transaction{  //派生类
public:
	BuyTransaction(parameters) : Transaction(createLogString(parameters))
	{}   //将log信息传递给基类构造private:
		static std:string createLogString(parameters);
};

由于无法使用virtual函数从基类向下调用,在构造期间可以派生类将必要的构造信息向上传递给基类构造函数。需要注意private static函数createLogString的使用,比使用初值列更加方便也可读性更高。令此函数为static,也就不肯能意外指向初期未成熟的BuyTransaction对象内尚未初始化的成员变量。
 在构造和析构期间不用调用virtual函数,因为这类调用从不下降至派生类(比起当前执行构造函数和析构函数的那层)。

10、令operator=返回一个reference to *this

*Have assignment operators return a reference to this.
C++中可以将赋值写成连锁形势

int x,y,z;
x = y = z = 15; 

赋值采用右结合,所以被被解析为

x = (y =( z = 15));

为了实现此类操作,赋值操作符必须返回一个reference指向操作符的左侧的实参:

class Widget{
public:
	…
	Widget& operator=(const Widgets& rhs)
	{return* this;
	}
};

同样上述方法也适用于所有赋值相关运算:

class Widget{
public:
	…
	Widget& operator+=(const Widgets& rhs)  //适用于+=、-=、*=等等
	{return* this;
	}
	Widget& operator=(int rhs)   //即使参数类型不同也可以
	{return* this;
	}
};

所有内置类型和标准程序库提供的类型如string,vector,complex,tr1:shared_ptr都提供此支持。

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

11、在operator=中处理“自我赋值”

Handle assignment to self in operator=.
“自我赋值”发生在对象被赋值给自己时:

class Widget {};
Widget w;
…  
w = w; //赋值给自己

看起来有点异常,但是是合法的,而且有可能会这么做,而且很多时候很难识别,例如:

a[i] = a[j];   //潜在的自我赋值,如果i=j
*px = *py   //潜在的自我赋值,如果指向同一个东西

这些并不明显的自我赋值,是别名(aliasing)带来的结果。一般而言如果某段代码操作pointers或references而它们被用来“指向对个相同类型的对象”,就需要考虑这些对象是否为同一个。两个对象如果来自同一个继承体系,不需要声明为相同类型就可能操作“别名”,因为一个基类的引用或指针可以指向一个派生类对象。为了阻止这种错误,让operator=具备“异常安全性”,为了考虑效率可以把证同测试(identity test)加在起始处。如下

Widget& Widget::operator=(const Widget& rhs)
{
	if(this == &rhs) return *this;		//证同测试
	Bitmap* pOrig = pb;			//记住原先的pb
	pb = new Bitmap(*rhs.pb);		//令pb指向*pb的一个副本
	delete pOrig;					//删除原先的pb
	return *this;
}

另一个方案是使用copy and swap技术,如下:

class Widget {
	void swap(Widget& rhs);  //交换*this和rhs数据};
Widget& Widget::operator=(const Widget& rhs)
{
	Widget temp(rhs);   //为rhs数据制作一根副本
	swap(temp);		//将*this数据和上述副本的数据交换
	return *this;	
}
  • 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

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

Copy all parts of an object.
设计良好的面向对象系统(OO-system)会将对象的内部封装起来,只保留两个函数负责对象拷贝(复制),即copy构造函数和copy assignment操作符,合称为copying函数。
如果自定义了copying函数,然后编译器对于其中的错误很难捕捉到。如下一个class顾客顾客,提供自定义copying函数,使得外界对它们的调用会被记录下来:

void logCall(const std::string& funcName); //记录log函数
class Customer {
public:Customer(const Customer& rhs);
	Customer& operator=( const Customer& rhs);
private:
	std::string name
};
Customer::Customer(const Customer& rhs):name(rhs.name) //复制rhs的数据
{	
	logCall(“Customer copy constructor”);
}
Customer::Customer& operator=( const Customer& rhs)
{
	logCall(“Customer copy assignment constructor”);
	name = rhs.name;      //复制rhs的数据
	return *this;
}

但是如果加入一个新的成员lastTransaction,但是目前的copying函数执行的是局部拷贝,而且此时出现的问题,编译器很难捕获到:

class Date {};
class Customer 
{
public:private:
	std::string name;
	Date lastTransaction;
};

如果发生继承,会出现更难发现的问题,如下例子只是复制了PriorityCustomer内的成员变量,但每个PriorityCustomer内还包含所继承的Customer成员变量副本,而那些成员变量却未被复制:

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

对于此情况,如果基类为自定义copying函数,则需要为派生类编写自定义copying函数,此时需要很小心的复制其基类成分,对于private的成分,无法直接访问它们,应该让派生类的copying函数调用相应的基类的函数:

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
:Customer(rhs), priority(rhs.priority) //调用基类的copy构造函数
{	
	logCall(“PriorityCustomer copy constructor”);	
}
PriorityCustomer::PriorityCustomer& operator=(const PriorityCustomer& rhs)
{
	logCall(“PriorityCustomer copy assignment operator);
	Customer::operator=(rhs);  //对基类成分进行赋值动作
	priority = rhs.priority;
	return *this;
}

当编写一个copying函数,确保赋值所有local成员变量,调用所有基类内的适当的copying函数。如果copying构造函数和copy assignment操作符有很多相近的代码,消除重复代码的正确做法为建立一个新的成员函数给两者调用。通常是private的init函数。

  • Copying函数应该确保复制“对象内的所有成员变量”及“所有基类成分”。
  • 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

上一篇: C++进阶_Effective_C++第三版(一) 让自己习惯C++ Accustoming Yourself to C++
下一篇: C++进阶_Effective_C++第三版(三) 资源管理 Resource Management

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风华一叶知秋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值