0
点赞
收藏
分享

微信扫一扫

Effective C++改善程序与设计的55个具体做法

大自然在召唤 2022-02-28 阅读 45

改善程序与设计的55个具体做法——2、构造/析构/赋值运算


前言

几乎我们写的每一个class都会有一或多个构造函数、一个析构函数、一个copy assignment操作符。这些很难让你特别兴奋,毕竟它们是你的基本谋生工具,控制着基本操作,像是产出新对象并确保它被初始化、摆脱旧对象并确保它被适当清理、以及赋予对象新值。如果这些函数犯错,会导致深远且令人不愉快的后果,遍及我们的整个类classes。所以确保它们行为正确是生死攸关的大事。本章提供的引导可让我们把这些函数良好地集结在一起,形成classes的脊柱。


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

class Empty {

};
相当于以下类:
class Empty {
public:
	Empty(){}                          //default 构造函数
	Empty(const Empty&rhs){}           //默认拷贝构造函数
	~Empty(){}                         //默认析构函数
	                                   //virtual 见后
	Empty& operator=(const Empty&rhs){}//copy assignment操作符
};
class Empty {
public:
	Empty(){...}                          //default 构造函数
	Empty(const Empty&rhs){...}           //默认拷贝构造函数
	~Empty(){...}                         //默认析构函数
	                                      //virtual 见后
	Empty& operator=(const Empty&rhs){...}//copy assignment操作符
};
由于声明了一个构造函数,编译器于是不再为它创建默认的构造函数。这很重要
,意味着我们用心设计一个类class,其构造函数要求实参,那我们就无须担心编译
器会毫无挂虑地为你添加一个无实参构造函数而遮盖掉你的版本。
template<typename T>
class NameObject {
public:
	NamedObject(const char*name, const T&value);
	NamedObject(const std::string&name, const T&value);
private:
	std::string nameValue;
	T objectValue;
};
template<typename T>
class NamedObject {
public:
	//以下构造函数如今不再接受一个const名称,因为nameValue
	//如今是个reference-to-non-const string。先前那个char*构造函数
	//已经过去了,因为必须有个string可供指涉。
	NamedObject(std::string& name, const T&value);
	//...        假设未申明operator=
private:
	std::string& nameValue;   //这如今是个reference
	const T objectValue;      //这如今是个const
};
现在考虑下面会发生什么事:
template<typename T>
class NamedObject {
public:
	//以下构造函数如今不再接受一个const名称,因为nameValue
	//如今是个reference-to-non-const string。先前那个char*构造函数
	//已经过去了,因为必须有个string可供指涉。
	NamedObject(std::string& name, const T&value);
	//...        假设未申明operator=
private:
	std::string& nameValue;   //这如今是个reference
	const T objectValue;      //这如今是个const
};

std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2);  

NamedObject<int> s(oldDog, 36);
p = s;

请记住

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

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

所有编译器产出的函数都是public。为阻止这些函数被创建出来,我们自行声明
它们,但这里并没有什么需求使你们必须将它们声明为public。因此你可以将
copy构造函数或copy assignment操作符声明为priate。
class HomeForSale {
public:
...
private:
...
	HomeForSale(const HomeForSale&);          //只有声明
	HomeForSale& operator=(const HomeForSale&);
};
如果不慎在member函数或friend函数之内那么做,轮到连接器发出抱怨。
class Uncopyable {
protected:
	Uncopyable(){}          //允许derived对象构造和析构
	~Uncopyable(){}
private:
	Uncopyable(const Uncopyable&);     //但阻止copying
	Uncopyable& operator=(const Uncopyable&);
};

为求阻止HomeForSale对象被拷贝,我们唯一需要做的就是继承Uncopyable:
class HomeForSale :private Uncopyable {
...
};

请记住

  • 为驳回编译器自动提供的机能,可以将响应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class 也是一种做法。

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

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

class AtomicClock:public TimeKeeper{};   //原子钟
class WaterClock :public TimeKeeper{};   //水钟
class WristWatch :public TimeKeeper{};   //腕表
//TimeKeeper派生类的动态分配对象
TimeKeeper* getTimeKeeper();    //返回一个指针,指向一个
为遵守factory函数的规矩,被getTimeKeeper()返回的对象必须位于heap。
因此为了避免泄漏内存和其他资源,将factory函数返回的每一个对象适当
地delete掉很重要。
TimeKeeper* ptk = getTimeKeeper();       //从TimeKeeper继承体系
										 //获得一个动态分配对象运用它...
delete ptk;					 			 //释放它,避免资源泄漏
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到
子类的析构代码。

问题出于getTimeKeeper返回的指针指向一个derived class对象(例如AtomicClock),而那个对象却经由一个base class指针(例如一个TimeKeeper*指针)被删除,而目前的base class(TimeKeeper)有个non-virtual析构函数。

解决方式:将父类中的析构函数改为虚析构或者纯虚析构。
class TimeKeeper {
public:
	TimeKeeper();
	virtual~TimeKeeper();
	//...
};

TimeKeeper* ptk = getTimeKeeper();
delete ptk;		    //现在,行为正确
任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数
如果class不含virtual函数,通常表示它并不意图被用到一个base class。
class Point {
public:
	Point(int xCoord, int yCoord);
	~Point();
private:
	int x, y;
};

欲实现出virtual函数,对象必须携带某些信息,主要用来在运行期决定那一个virtual函数被调用。这份信息通常是由一个所谓vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,称为vtbl(virtual table);每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl----编译器在其中寻找适当的函数指针。

虚析构和纯虚析构共性:
可以解决父类指针释放子类对象
都需要有具体的函数实现
虚析构和纯虚析构区别:
如果是纯虚析构,该类属于抽象类,无法实例化对象
class AWOV {             
public:
	virtual ~AWOV() = 0;//声明纯虚析构函数
};
//必须为纯虚析构函数提供一份定义:
AWOV::~AWOV(){}       //纯虚析构函数的定义

请记住

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

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

C++并不禁止析构函数吐出异常,但它不鼓励这样做。
class Widget {
public:
	~Widget(){}            //假设这个可能吐出一个异常
};

void doSomething()
{
	std::vector<Widget> v; 
}						  //v在这里被自动销毁
当vector v被销毁,它有责任销毁其内含的所有widgets。假设v内含十个Widgets,
而在析构第一个元素期间,有个异常被抛出。其他九个Widgets还是应该被销毁,
因此v应该调用它们各个析构函数。但假设在哪些调用期间,第二个Widget析构又
抛出异常。现在有两个同时作用的异常,这对C++而言太多了。
class DBConnection {
public:
	static DBConnection create(); //这个函数返回DBConnection对象
	//为求简化暂略参数,关闭联机失败则抛出异常
	void close();
};
class DBConn {
public:
	~DBConn() {
		db.close();
	}
private:
	DBConnection db;
};
这便允许客户写出这样的代码:
{
	...
	DBConn  dbc(DBConnection::create());
}
只要调用close成功,一切都美好,但如果该调用导致异常,DBConn析构函数会
传播该异常,也就是允许它离开这个析构函数。那会造成问题,因为那就是抛出
了难以驾驭的麻烦。
--------------------------------------------------------------------
两个解决办法可以避免这一问题:
  • 如果close抛出异常就结束程序。通常通过调用abort完成:
DBConn::~DBConn() {
	try { db.close(); }
	catch (...) {
		//制作运转记录,记下对close的调用失败
		std::abort();
	}
}
  • 吞下因调用close而发生的异常:
DBConn::~DBConn() {
	try { db.close(); }
	catch () {
		//制作运转记录,记下对close的调用失败
	}
}
一般而言,将异常吞掉是个坏主意,因为它压制了"某些动作失败"的重要信息。
一个较佳的策略是重新设计DBConn接口,使其客户有机会对可能出现的问题作出反应。
class DBConn {
public:
	void close() {      //提供用户使用的新函数
		db.close();
		closed = true;
	}

	~DBConn() {
		if (!closed) {
			try {       //关闭连接
				db.close();
			}
			catch (...) {
				//制作运转记录,记下对close的调用失败
			}
		}
	}
private:
	DBConnection db;
	bool closed;
};

请记住

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

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

不该在构造函数和析构函数期间调用virtual函数,因为这样的调用不会带来所预想的结果。
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;
};

请记住

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

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

当形参和成员变量同名时,可用this指针区分
在类的非静态成员函数中返回对象本身,可是使用return *this
class Widget {
public:
	Widget& operator=(const Widget&rhs) {    //返回类型是个reference
											 //指向当前对象
		return *this;						 //返回左侧对象
	}
};

请记住

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

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

class Base{};
class Derived:public Base{};
void doSomething(const Base& rb, Derived* pd);//rb和*pd有可能其实是同一个对象

请记住

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

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

class Base{};
class Derived:public Base{};
void doSomething(const Base& rb, Derived* pd);//rb和*pd有可能其实是同一个对象

void logCall(const std::string& funcName);
class Customer {
public:
	Customer(const Customer&rhs);
	Customer& operator=(const Customer&rhs);
private:
	std::string name;
};
如果你为class添加一个成员变量,你必须同时修改coying函数。如果忘记,编译器不太可能提醒。

一旦发生继承,可能回造成此一主题最暗中肆虐的一个潜藏危机:
class Date{};
class Customer {
public:
private:
	std::string name;
	Date lastTransaction;
};


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;
}

base class name 和lastTransaction未被复制

任何时候只要我们承担其为"derived class撰写copying函数"的重责大任,
必须很小心地也复制其base class成分。哪些成分往往是private,所以
我们无法直接访问它们,应该让derived class的copying函数调用相应的
bass 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函数应该确保复制"对象内的所有成员变量"及"所有base class成分"。
  • 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用
举报

相关推荐

0 条评论