Effective C++ 02 构造、析构、赋值运算符

本文详细阐述了C++中构造、析构、赋值运算符的相关规则。条款涵盖编译器自动为类生成的函数、拒绝编译器生成的函数、多态基类的virtual析构函数、异常安全的析构函数、赋值运算符的返回类型、处理自我赋值和复制对象的所有成员。强调了在构造和析构过程中避免调用virtual函数以及确保复制操作的正确性和异常安全性的重要性。
摘要由CSDN通过智能技术生成

2. 构造、析构、赋值运算符

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

当 C++ 处理空类时,如果你没有声明,那么编译器就会为它声明一个拷贝构造函数、一个拷贝赋值运算符和一个析构函数。此外如果美声生命任何构造函数,编译器还会声明一个 default 构造函数。所有这些函数都是 public 且 inline(见条款 30)。

例如:

class Empty { };

等价于

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

只有当这些函数被调用时,它们才会被编译器创建出来。

注意:编译器合成的析构函数是个 non-virtual(见条款 07),除非这个 class 的 base class 自身有 virtual 析构函数。

如果要在内含 reference 成员或 const 成员的 class 内支持赋值操作,就必须自定义 copy assignment 运算符。更改 const 成员是不合法的,所以编译器不知道如何在合成的赋值函数里处理它们。

如果某个 base class 将 copy assignmen 运算符声明为 private,编译器会拒绝为其 derived class 生成一个 copy assignment 运算符。

请记住:

  • 编译器可以自行为 class 创建 default 构造函数、copy 构造函数、copy assignment 运算符以及析构函数。

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

方法一

可以将 copy 构造函数或 copy assignment 运算符声明为 private。通过明确声明一个成员函数,阻止编译器合成其专属版本;而零这些函数为 private,使其可以阻止人们调用它。

但是这样的做法并不是绝对安全的,因为其成员函数和友元函数还是可以调用 private 函数。但是也可以通过只声明不定义的方式,那么那些不慎调用的人会获得一个连接错误,例如:

class HomeForSale {
private:
	HomeForSale(const HomeForSale&);  // 声明而不定义
	HomeForSale& operator=(const HomeForSale);
};

有了上述的声明,当客户企图拷贝 HomeForSale 对象,编译器会阻挠他。如果在成员函数或友元函数调用时,在连接期也会发生错误。

方法二

也可以将连接期错误转移到编译期(这是更好的做法,错误越早检测到越好),可以专门设置一个为了阻止 copying 动作而设计的 base class,这个 base class 很简单:

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

为了阻止 HomeForSale 对象被拷贝,我们只需要继承 Uncopyable 即可:

class HomeForSale : public Uncopyable {
	...
};

此时,只要任何人尝试拷贝 HomeForSale 对象,编译器会试着生成一个 copy 构造函数和一个 copy assignment 运算符,如条款 12 所说,这些函数会尝试调用其 base class 对应的操作,但那些尝试会被编译器拒绝,因为其 base class 的拷贝函数是 private 的。

请记住:

  • 为驳回编译器自动提供的机能(合成函数),可将相应的成员函数只声明不定义为 private 的。也可以使用像 Uncopyable 这样的 base class。

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

如果 class 含 virtual 函数

参考如下例子:

class TimeKeeper {
public:
	TimeKeeper();
	~TimeKeeper();
};

// 继承
class AtomicClock : public TimeKeeper { ... };
class WaterClock : public TimeKeeper { ... };
class WristWatch : public TimeKeeper { ... };

// 返回一个 base class 指针,指向一个 TimeKeeper 派生类的动态分配对象
TimeKeeper* getTimeKeeper();

// 从 TimeKeeper 继承体系获得一个动态配分的对象
TimeKeeper* ptk = getTimeKeeper();
delete ptk;  // 释放

getTimeKeeper 返回的指针指向一个 derived class 对象,而那个对象却经由一个 base class 指针被删除,而目前的 base class 有个 non-virtual 析构函数,这个结果是未定义的。实际上对象的 derived 成分并没有被销毁,而 derived 的析构函数并没有执行(执行的是基类的析构函数)。这就可能会造成资源泄露等后果。

消除这个问题的做法很简单:给 base class 一个 virtual 析构函数。这样就会执行 derived class 的析构函数:

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

virtual 函数的目的是允许 derived class 的实现得以客制化(见条款 34)。任何 class 只要带有 virtual 函数都几乎确定应该也有一个 virtual 析构函数。

如果 class 不含 virtual 函数

如果 class 不含 virtual 函数,通常表示它并不意图被作一个 base class。当 class 不企图被用作 base class,令其析构函数为 virtual 往往是一个馊主意。参考如下例子:

class Point {
public:
	Point(int xCoord, int yCoord);
	~Point();
private:
	virtual int x, y;
};

为了实现 virtual 函数,对象必须携带某些信息,主要用来在运行期间决定哪一个 virual 函数该被调用。这份信息通常是由一个所谓 vptr 指针指出。vptr 指向一个由函数指针构成的数组,成为 vtbl;每一个带有 vitrual 函数的 class 都有一个相应的 vtbl。当对象调用某一 virtual 函数,实际被调用的函数取决于该对象的 vptr 所指的那个 vtbl。

如果 Point class 内涵 virtual 函数,其对象的体积会增加:在 32-bit 计算机体系结构中将占用 64bits 至 96bits(两个 int 加上 vptr)。因此,为 Point 添加一个 vptr 会增加其对象的大小,Point 对象不再能够塞进一个 64-bit 缓存器,而 C++ 的 Point 对象也不再和其他语言内的相同声明有一样的结构(其他语言并没有 vptr),因此也就不再可能把它传递给其他语言所写的函数(除非明确补偿 vptr),因此也不再具有移植性。

因此,无端的将所有 class 的析构函数声明为 virtual,就像从未声明它们为 virtual 一样,都是错误的。许多人的心得是:只有当 class 内含至少一个 virtual 函数,才将它声明为 virtual 析构函数。

析构函数的运作方式是,最深层派生的那个 class 其析构函数最先被调用,然后是其每一个 base class 的析构函数被调用。

请记住:

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

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

C++ 并不禁止析构函数吐出异常,但它不鼓励你这样做。考虑如下代码:

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

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

当 vector v 被销毁,它有责任销毁其内含的所有 Widget。假设 v 在析构第一个元素期间,有个异常被抛出。其他的 Widget 还是要被销毁(否则它们保存的资源会发生泄露),因此 v 应该调用它们各个析构函数。但假设在那些调用期间,第二个 Widget 析构函数有抛出异常。在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确行为。C++ 不喜欢析构函数吐出异常!

如果析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,参考如下例子:

class DBConn {
public:
	~DBConn() {  // 确保数据库连接总是会被关闭
		db.close(); 
	}
private:
	DBConnection db;  // 负责连接数据库的 class
};

DBConn dbc(DBConnection::creat());

如果 close 调用导致异常,DBConn 析构函数会传播该异常,也就是允许它离开这个析构函数,这样就会造成问题。

有两个办法可以避免这一问题,DBConn 的析构函数可以:

  • 如果 close 抛出异常就结束程序。通常通过调用 abort 完成:
DBConn::~DBConn() {
	try {
		db.close();
	}
	catch (...) {
		// ...
		std::abort();
	}
}

强迫结束程序可以阻止异常从析构函数传播出去(那会导致不明确的行为)。也就是说调用 abort 可以抢先制 不明确行为 于死地。

  • 吞下印调用 close 而发生的异常
DBConn::~DBConn() {
	try {
		db.close();
	}
	catch (...) {
		// ...
	}
}

一般而言,将异常吞掉是一个坏主意,然而有时候吞下异常也比负担“草率结束程序”或“不明确行为带来的风险”好。

这两者的问题在于都没有对“导致 close 抛出异常的情况做出反应”。一个较佳的册罗是重新设计 DBConn 接口,使其客户有机会对可能出现的问题作出反应,虽然最后可能还是会面临同样的问题。这样就把调用 close 的责任从 DBConn 析构函数的手上移到 BDConn 客户手上。

class DBConn {
public:
	void close() {  // 供客户使用的新函数
		db.close();
		closed = true;
	}
	~DBConn() {
		if(!closed) {
			try {  // 如果客户没有关闭连接的话
				db.close();
			}
			catch (...) {
				// ...
			}
		}
	}
private:
	DBConnection db;
	bool closed;
};

由客服自己调用 close 并不会对他们带来负担,而是给他们一个处理错误的机会,否则他们可能没机会响应。

请记住:

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

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

参考下面的例子:

class Transaction {  // 所有交易的 base class
public:
	Transaction();
	virtual void logTransaction() const = 0;  // 因类型不同而不同的日志记录
}
Transaction::Transaction() {
	...
	logTransaction();  // 最后动作志记这笔交易
}
class ButTransaction : public Transaction {
public:
	virtual void logTransaction() const;  // log 此类型交易
}

当以下被执行时:

ButTransaction b;

会有一个 ButTransaction 构造函数被调用,但首先 Transaction 构造函数一定会更早被调用;derived class 对象内的 base class 成分会在 derived class 自身成分被构造之前先构造妥当。Transaction 构造函数的最后一行调用 virtual 函数 logTransaction。这时候被调用的 logTransaction 是 Transaction 内的版本,而不是 ButTransaction 内的版本。也就是说,base class 构造期间 virtual 函数绝不会下降到 derived class 阶层,或者说在 base class 构造期间,virtual 函数不是 virtual 函数。

在 derived class 对象的 base class 构造期间,对象的类型是 base class 而不是 derived class。不只 virtual 函数会被编译器解析至 base class,若使用运行期类型信息(例如 dynamic_cast(见条款 27) 和 typeid),也会把对象是为 base class 类型。相同的道理也适用于析构函数。

请记住:

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

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

关于赋值,可以把它们写成连锁形式:

int x, y, z;
x = y = z = 15;  // 赋值连锁形式

由于赋值采用右结合率,所以上述连锁赋值被解析为:

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

为了实现连锁赋值,赋值操作符必须返回一个 reference 指向操作符的左侧实参。这是 class 实现赋值操作符时应该遵守的协议:

class Widget {
public:
	Widget& operator=(const Widget &rhs) {  // 返回类型是引用,指向当前对象,这个协议不仅适用于标准赋值形式,也适用于所有赋值相关运算如,+=、-=、*= 等等
		...
		return *this;  // 返回左侧对象
	}
};

这只是个协议,并无强制性,如果不遵循它,代码一样可以通过编译。
请记住:

  • 令赋值运算符 返回一个 reference to *this。

条款 11:在 operator= 中处理"自我赋值"

”自我赋值“,即将对象赋值给自己:

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

假设如果你尝试自行管理资源,可能会发生在停止使用资源之前意外释放了它的情况。例如:

class Bitmap { ... };
class Widget {
private:
	Bitmap* bp;
};
// operator= 实现代码,虽然看起来合理,但是自我赋值时并不安全
Widget& Widget::operator=(const Widget &rhs) {
	delete pb;  // 停止使用当前的 bitmap
	pb = new Bitmap(*rhs.pb);  // 使用 rhs 的 bitmap 副本
	return *this;  // 见条款 10
}

自我赋值的问题是,operator= 函数内的 *this 和 rhs 有可能是同一个对象。若是如此, delete 就不只是删除当前对象的 bitmap,它也销毁了 rhs 的 bitmap。最后它的指针指向一个已经被删除的对象。

欲阻止这种错误,传统的做法是在 operator= 最前面加上一个”证同测试“,以达到检验:

Widget& Widget::operator=(const Widget &rhs) {
	if (this == &rhs) return *this;  // 证同测试,如果是在我赋值,就不做任何处理
	
	delete pb;  // 停止使用当前的 bitmap
	pb = new Bitmap(*rhs.pb);  // 使用 rhs 的 bitmap 副本
	return *this;
}

虽然完成了正同测试,但是这个版本仍然存在异常方面的问题,也就是说缺少”异常安全性“。如果 “new Bitmap” 发生了异常,Widget 最终会吃有一个指针指向一块被删除的 Bitmap。这个指针无法安全地删除,甚至无法安全的读取。

让 operator= 具被异常安全性时,往往会自动获得自我赋值安全的回报。例如:

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

在 operator= 函数内,确保代码不但异常安全而且自我赋值安全,的一个替代方案是,使用所谓的 copy and swap 技术。这个技术和异常安全性有密切关系,详细见条款 29。其具体如下:

class Widget {
	void swap(Widget& rhs);  // 交换 *this 和 this 的数据
};
Widget& Widget::operator=(const Widget& ths) {
	Widget temp(rhs);  // 为 ths 数据制作一个副本
	swap(temp);  // 交换数据
	return *this;
}

请记住:

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

条款 12:复制对象的每一个成分

情形一:添加成员函数

考虑如下例子,用一个 class 表现顾客,自定义 copying 函数,使得外界对它们的调用会被记录(logged)下来:

void logCall(const std::string& funcName):
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;
}

当添加一个新成员时:

class Date { ... };
class Custromer {
private:
	std::string name;
	Date lastTransaction;
};

这时候 copying 函数的确复制了顾客的 name,但没有复制新添加的 lastTransaction。大多数编译器对此不会发出任何反应,即使在最高警告级别中(见条款 53)。如果你自定义写成 copying 函数,即使代码不完全,编译器也不会有所提示。结论很明显:如果你为 class 添加一个新成员变量,必须同时修改 copying 函数。

情形二:继承

考虑下面例子:

class PriorityCustomer : public Customer {  // 一个 derived class
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;
}

PriorityCustomer 的 copying 函数貌似复制了 PriorityCustomer 内所有成员,但是,实际上它们仅复制了 PriorityCustomer 声明的成员变量,而没有复制 base class 内的成员变量。

如果是 copy 构造函数 base class 内的成员会执行 default 默认构造函数(如果没有,则无法通过编译)初始化。如果是 copy assignment 操作符上,它不会修改其 base class 的成员变量。

任何时候只要你承担起为 derived class 撰写 copying 函数的责任,就必须复制其 base class 成分。但这些成分往往是 private,所以你无法直接访问它们,你应该让 derived class 的 copying 函数调用相应的 base class 函数

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) 
: Customer(rhs),  // 调用 base class 的 copy 构造函数
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;
}

本条款所说的复制每一个成分就是,当你编写一个 copying 函数,请确保

  1. 赋值所有 local 成员变量;
  2. 调用所有 base class 内适当的 copying 函数。

如果你发现你的 copy 构造函数和 copy assignment 运算符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是 private 而且常被命名为 init。这个册罗可以安全消除 copy 构造函数和 copy assignment 运算符之间的代码重复。

请记住:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值