Effective C++ 1 构造/析构/赋值运算

05:了解C++默认编写并调用哪些函数

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

这一章有以下几个点需要注意:

  1. 一个空的类,编译器自动生成了哪些函数。
class empty{};

// 上述代码与下类似

class empty{
public:
	empty() {...}	// default构造函数
	~empty() {...}	// 析构函数
	empty(const empty& rhs) {...}	// 拷贝构造函数
		
	empty& operator=(const empty& rhs){...}	// 拷贝赋值函数
};
  1. 编译器产出得析构函数是一个non-vitual函数。
  2. 编译器创建的copy构造函数和copy assignment操作符,只是单纯的将源对象中的每一个non-static成员变量拷贝到目标对象。
  3. 在以下三种情况下,我们需要自定义copying assignment操作符。
    1. 如果打算在一个”内含reference成员“的class内支持赋值操作,必须自定义。
    2. 如果类中包含const成员变量,那么编译器不会生成默认copying assignment操作符,因此需要自定义。
    3. 如果base class将copy assignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。

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

为驳回编译器自动提供的机能,可将相应的函数声明为private并且不予实现。或者使用Uncopyable这样的base class。

这一章提供了两种拒绝编译器自动生成函数的方法。

**方法一:**这种方法就是将这些函数声明为private,并且不实现他们。

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

这样的话,当客户试图拷贝HomeForSale,那么编译器就会报错。如果在friend 函数或者member函数内试图拷贝HomeForSale,那么链接器会报错。(这里如果有疑问,可以读一下《程序员的自我修养》)。

**方法二:**方法一中,针对friendmember函数中拷贝对象,会在链接环节出现错误。如果我们将这类错误提前到编译期间,也算是一种代码的优化。因此出现以下方法:

class Uncopyable{
protected:
	Uncopyable() {}		// 允许derived对象构造和析构
	~Uncopyable(){}
private:
	Uncopyable(const Uncopyable&);	// 阻止copying
	Uncopyalbe& operator=(const Uncopyable&)
};

那么当我们将HomeForSale继承于该类后,就能将friendmember函数内拷贝的错误提前到编译器。

class HomeForSale : private Uncopyable{
	....
};

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

  • 带多态性质的base class应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
  • Classes的设计目的如果不是作base class,或者不是为了实现多态性质,那么就不应该声明virtual 析构函数

这一章讲述了为什么我们需为多态基类什么virtual析构函数、以及什么时候不需要virtual析构函数。这章需要注意以下几点:

  1. 我们先来看下以下代码会出现什么问题?
class TimeKeeper{
public:
	TimeKeeper();
	~TimeKeeper();
	....
};

class AtomicClock: public TimeKeeper{....}

// 工厂函数,在Go语言中由于没有构造函数,因此该类函数随处可见
// 返回一个指针,指针指向TimeKeeper派生类的动态分配对象
TimeKeeper* GetTimeKeeper();	

// 注意GetTimeKeeper()返回的动态类型是子类对象指针
TimeKeeper* ptk = GetTimeKeeper();
delete ptk;

当我们执行delete ptk时,就会出现问题。因为试图通过base class指针去删除一个derived class对象,并且父类析构函数为non-virtual。那么就会出现未定义的情况——对象的derived部分没有被销毁。为了解决上述问题,我们只需将析构函数设置为virtual即可。

  1. 如果一个类,我们不将它作为base class,那么就不需要将析构函数设置为virtual。这是因为,虚函数会带来内存消耗,比如虚指针、虚函数表。
  2. 标准string、stl容器vector、list、set均没有virtual析构函数,因此,我们在日常开发中,不应该设置类继承于他们;
  3. 为一个抽象的class声明一个pure virtua析构函数,并且提供该函数的定义。
class AWOV{
public:
	virtual ~AWOV() = 0; 	// 声明pure virtual析构函数
};

AWOV::~AWOV() {}	// 定义pure virtual析构函数
  1. 析构函数的运作方式:最深层的析构函数先被调用,然后其base class被调用。因此,TODO 4中我们需要给纯虚析构函数定义,如果没有,那么当最后调用AWOV纯虚构函数时会出现链接器报错。

08:别让异常逃离析构函数

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们或者结束程序。
  • 如果客户端需要对操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数执行该操作。

这一章讲述了为什么不能将异常从析构函数中逃离?我们首先来看以下例子:

class Widge{
public:
	....
	~Widget() {...}		// 假设这个会吐出一个异常
};

void doSomething() {
	std::vector<Widget> v;
	....		// v在这里自动销毁
}

对于上述代码,当v中的第一个Widget销毁的时候出现异常,那么第二个又出现异常。如果这个时候不结束程序执行,就会导致不明确的行为。

我们再来看一个数据库连接的例子,

class DBConnection{
public:
	...
	static DBConnection create();		// 这个也是一个工厂函数,生成一个DBConnection对象
	void close();
};

为了防止客户忘记执行close()操作,因此,我们利用对象来管理资源,代码如下:

class DBConn{	// DBConn class 来管理DBConnection对象
public:
	...
	~DBConn(){			// 确保数据连接总是会被关闭
		db.close();
	}
private:
	DBConnection db;
};

因此,客户可能写出以下代码:

{
DBConn dbc(DBConnection::create());
....			// 这一段通过dbc操作DBConnection
				// 当离开这个作用域时,dbc就会调用析构函数,从而调用close()函数
}

如果,dbc顺利调用了close() 函数,那到一切完美。但该调用导致异常,DBConn析构函数传播该异常,就会造成不可预知的错误。以下讲述了两种解决该问题的方法:

  1. 如果close()抛出异常就结束程序,可通过abort()完成:
DBConn::~DBConn(){
	try{
		db.close();
	} catch(...){
		// 制作运转记录,记下对close的调用失败
		std::abort();
	}
}
  1. 吞下调用close()而发生的异常:

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

    这种方法,也不是最佳选择,因为,它不能对"导致close抛出异常"的情况做出反应。

  2. 一个比较好的方法是,重新设计DBConn接口,使客户能够对可能出现的情况做出反应:

class DBConn{	// DBConn class 来管理DBConnection对象
public:
	...
	void close{
		db.close();
		closed = true;
	}
	~DBConn(){			// 确保数据连接总是会被关闭
		if (!closed){
			try{
				db.close();
			} catch(...) {
				// 制作运转记录,记下对close的调用失败
			}
		}
		
	}
private:
	DBConnection db;
	bool closed;
};

最后一种方法,也就是提供了一个双保险,也就是,可以客户调用close函数,也可依赖析构函数。当析构函数出现异常时,客户可调用close接口,从而释放资源。

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

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

这一章讲述为啥不能在构造和析构过程中调用virtual函数?我们首先看一个例子:

// Base 类
class Transaction{
public:
	Transaction();
	virtual void LogTransaction() const = 0;
	....
};

Transaction::Transaction(){
	...
	logTransaction();
}

// Derived 类
class BuyTransaction : public Transaction{
public:
	virtual coid logTransaction() const;
};

// 执行以下代码会出现什么????
BuyTransaction b;

这里,当我们定义一个BuyTransaction对象时,首先会执行Base类的构造函数,在该构造函数内,会执行Base类的LogTransaction函数,该函数不会下降到derived 类。这样就出现和我们意图不一致的想法。出现这种情况的原因是:在derive class对象的base class构造期间,对象类型是base class 而不是 derived class。因此,virtual 函数、运行期类型信息也会把对象视为base class类型。

同理,析构函数也是样的,因为,derived class析构函数开始执行,对象内的derived class成员变量先呈现未定义 值。这样,进入base class析构函数时,就视为一个base对象了,因此,其中的virtual function就不会下降到derived class,产生和我们想法不一致的现象。

还有一个问题就是,上述代码中在抽象类中调用未提供定义的pure virtual function,连接器会报错。

那我们如何修改上述代码呢??看下面两种解决方法!!!

// Base 类
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);
}

// Derived 类
class BuyTransaction : public Transaction{
public:
	BuyTransaction(params) : Transaction(createLogString(parameters));	// 将log信息
	{....}											// 传给base class构造函数
	
private:
	static std::string createLogString(parameters)
};

// 执行以下代码会出现什么????
BuyTransaction b;

看上述代码,我们无法使用virtual函数从base class向下调用,那么在构造期间,可以通过“令derived class将必要的构造信息向上传递到base class构造函数替换加以弥补。”注意上述代码,我们将derived class 中的createLogStringg设置为static函数。这就可避免“初期未成熟的BuyTransaction对象内尚未初始化的成员变量。” (在单例模型中,也可通过static避免多线程竞争问题)

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

令赋值操作符返回一个reference to *this.

这一章节讲述为啥operator=应该返回一个reference to *this.

我们想来看一个日常开发中,我们会经常遇到的案例。

int x, y, z;
x = y = z = 5;

这一段代码的意思就是,15先赋值给z,然后将更新后的z赋值给y,在将更新后的y赋值给x;

为了实现上述的 连锁赋值, 赋值操作必须返回一个reference指向操作符左侧的实参。这个协议不仅适用于以上的标准赋值形式,也适用于所有 赋值相关运算。

比如:

class Widget{
public:
	...
	Widget& operator=(const Widget& rhs) {		// 返回类型是个reference
		....	
		return *this;							//返回左侧对象
	}
	
	Widget& operator+=(const Widget& ths) {
		...
		return *this;
	}
	
	Widget& operator=(int rhs){
		...
		return *this;
	}
};

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

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

这一章首先讲述,我们日常开发中存在哪些隐形的自我赋值,其次,讲述我们该如何应对自我赋值这种情况。

隐形赋值有哪些???

  • a[i] = a[j],当i = j时,这便是自我赋值;
  • *px = *py,当px,py指向同一个东西,也是自我赋值;
  • 继承体系下,也容易出现自我赋值,比如
class Base{};
class Derived: public Base{};
void doSomething(const Base& rb, Derived* pd); 	// rb和*pd有可能其实是同一对象

我们该如何规避自我赋值陷阱,看下面这个例子

class Bitmap{...};
class Widget{
	...
private:
	Bitmap* pb;			// 指针,指向一个从heap分配而得得对象
};

Widget& Widget::operator=(const Widget& rhs) {
	delete pb;
	pb = new Bitmap(*ths.pb);
	return *this;
}

上述代码中rhs*this可能是同一个对象。 那么delete pb就会将rhs也删除删除掉。

阻止这种错误,传统做法是在operator=最前面加一个 “证同测试”达到“自我赋值”的目的。代码如下:

Widget& Widget::operator=(const Widget& rhs) {
	if (this == &ths) return *this;			// 证同测试,如果是自我赋值,就不做任何事情。
	delete pb;
	pb = new Bitmap(*ths.pb);
	return *this;
}

虽然上述代码具备 “自我赋值安全性”,但是不具备 “异常安全性”。比如说,new失败了,那么Widget最终会持有一个指针,指向一块被删除的Bitmap;为了阻止这种异常,我们可以写出以下代码:

Widget& Widget::operator=(const Widget& rhs) {
	Bitmap* pOrig = pb;		// 记住原来的pb
	pb = new Bitmap(*ths.pb);	// 令pb指向*pb的一个复制(副本)
	delete pOrig;	// 删除原来的pb
	return *this;
}

上述代码具有“自我赋值安全性”且具有“异常安全性”。 处理步骤如下:

  1. 对原始Bitmap做一个复制
  2. 指向新复制的那个副本
  3. 删除原bitmap

上述代码的一个替代方案是:copy and swap技术。 我们可以来看看下述代码:

class Widget{
	...
	void swap(Widget& rhs);		// 交换rhs和*this的数据;
};

Widget& Widget::operator=(const Widget& rhs){
	Widget temp(rhs);	// 为rhs制作一个副本
	swap(temp);			// 将*this数据和上述复制数据交换
	return *this;
}

这里我们进一步可以将代码写成以下形式,这两个代码作用其实是一样的。

// 为rhs制作一个副本,这个过程在传值过程就发生了
Widget& Widget::operator=(const Widget rhs){
	swap(rhs);			// 将*this数据和上述复制数据交换
	return *this;
}

第二种方案相对于第一种牺牲了一部分清晰性,但是将“copying”动作从函数本体移动到“函数参数构造阶段”可以令编译器有时生成更加高效的代码。

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

  • copying函数应该确保复制”对象内所有成员变量“及”所有base class成分“。

  • 不要尝试以某个copying函数实现另一个copying函数。应该将共同技能放进放进第三个函数,由两个copying函数共同调用。

这一章讲述了,哪种情况我们需要自定义copying函数。以下几点需要注意:

  1. 我们称copying函数包含:copy构造函数和copy assignment操作符。
  2. 如果我们自定义copying函数,那么当copying函数的实现出现错误时,编译器却不会报错。

我们来看一个例子,

void logCall(const std::string& funcName);  //制造一个log entry
class Customer{
public:
	...
	Customer(const Customer& rhs);
	Customer& operator=(const Customer& rhs);
private:
	std::string name;
};

// 拷贝复制函数实现
Customer::Customer (const Customer& rhs) : name(rhs.name){
	logCall("Customer copy constructor");
}
// 复制操作符实现
Customer& Customer::operator=(const Customer& rhs) {
	logCall("Customer copy assignment operator");
	name = rhs.name;
	return *this;
}

对于上述类来说,当我们执行拷贝或赋值操作,没有任何问题。也就是以下操作:

Customer c1;
Customer C2(c1);	// 拷贝构造函数
Customer c3;
c3 = c2;			// 赋值操作符

但是,当我们在该类中,加入一个自定义对象,也就是组合一个对象,代码如下:

class Date{...};		// 日期
class Customer{
public:
	...
private:
	std::string name;
	Date lastTransaction;
};

如果copying函数的实现没有改变,执行上述赋值或拷贝操作,那么就会出现局部数据拷贝,但是此时:编译器却不会报错。 因此,当我们加入一个新成员变量时,我们需要修改copying函数。

我们再来看一个继承的例子:

class PriorityCustomer : public Customer{
public:
	...
	PriorityCustomer(const PriorityCustomer& rhs);
	PriorityCustomer& operator=(const PriortyCustomer& 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函数忽略了,继承与base class的成员变量。为了解决这个问题,应该使用下述写法:

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs): Customer(rhs), priority(rhs.priority) {
	logCall("priorityCustomer copy constructor");
}

PriorityCustomer& PriorityCustomer::operator(const PriorityCustomer& rhs){
	logCall("priorityCustomer copy assignment operator");
	Customer::operator=(rhs);			// 对base class成分进行赋值动作
	priority = rhs.priority;
	return *this;
}

注意Customer::operator=(rhs)的调用;

除了需要注意上述点,还有以下两点需要注意:

  1. 当编写一个copying函数,确保(1)复制所有local成员变量,(2)调用所有base class内的适当copying函数。
  2. 如果copy assignment 操作符和copying 构造函数两者有共同的操作,那么我们应该定义一个init函数,然后两个函数调用它;

参考

  • efffective C++
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值