《Effective C++》——构造/析构/赋值运算(item 5 ~ item 12)

37 阅读5分钟

Item 5:了解 C++ 默默编写并调用哪些函数

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

  • “C++ 不允许 “让 reference” 改指向不同对象。”

  • “如果你打算在一个 “内含 reference 成员” 或者 “内含 const 成员” 的 class 内支持赋值操作(assignment),你必须自己定义 copy assignment 操作符。如果没有自定义的 copy assignment 操作符,C++ 会拒绝编译赋值动作。”

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

  • 为驳回编译器自动(暗自)提供的机能,有两种做法:
    • ① 将相应的成员函数声明为 private 并且不予实现
    class HomeForSale{
    public:
        ...
    private:
        ...
        HomeForSale(const HomeForSale&);    // 只有声明
        HomeForSale& operator=(const HomeForSale&);
    };
    
    • ② 增加为了阻止 copying 动作而设计的 base class
    class Uncopyable{
    protected:              // 允许 derived 对象构造和析构
        Uncopyable() {}
        ~Uncopyable() {}
    private:                // 阻止 copying
        Uncopyable(const Uncopyable&);  
        Uncopyable& operator=(const Uncopyable&);
    };
    
    class HomeForSale : private Uncopyable{
        ...
    };
    

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

  • polymorphic(带多态性质的) base classes 应该声明一个 virtual 析构函数。如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数。”

    • 当 derived class 对象经由一个 base class 指针被删除,而该 base class 带着一个 non-virtual 析构函数,其结果未有定义——实际执行时通常发生的是对象的 derived 成分没被销毁
  • Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性(polymorphically),就不该声明 virtual 析构函数。

Item 8:别让异常逃离析构函数

  • 析构函数绝对不要吐出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。”
    • 如果 close 抛出异常就结束程序。通常通过调用 abort 完成:
    DBConn::~DBConn()
    {
        try {db.close();}
        catch(...)
        {
            // 制作运转记录,记下对 close 的调用失败
            std::abort();
        }
    }
    
    • 吞下因调用 close 发生的异常
    DBConn::~DBConn()
    {
        try {db.close();}
        catch(...)
        {
            // 什么都不做
            // 不会导致程序崩溃,但缺点是你会失去异常的上下文和细节。
        }
    }
    
  • “如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。”
class DBConn
{
public:
    ...

    void close()    // 供客户使用的新函数
    {
        db.close();
        closed = true;
    }

    ~DBConn() 
    {
        if(!closed)
        {
            try {
                db.close();
            }
            catch(...)
            {
                // 制作运转记录,记下对 close 的调用失败
                // 记录下来并结束程序或吞下异常
                ...
            }
        }
    }

private:
    DBConnection db;
    bool closed;
};

Item 9:绝不在构造和析构函数中调用 virtual 函数

  • “在构造和析构期间不要调用 virtual 函数,因为这类调用从不下降至 derived class(比起当前执行构造函数和析构函数的那层)。”

下面是一个例子:

class Transaction {
public:
	Transaction();
	virtual void logTransaction() const = 0;

	// 其他操作

};

Transaction::Transaction()
{
	// 其他操作


	logTransaction();	//	最后动作:记录这笔交易
}


class BuyTransaction : public Transaction {
public:
	virtual void logTransaction() const;

	// 其他操作


};

当执行 BuyTransaction b;时,调用的 logTransaction Transaction 内的版本

“如何确保每次一有 Transaction 继承体系上的对象被创建,就会有适当版本的 logTransaction 被调用呢?”

下面是一种解决方法:

class Transaction {
public:
	explicit Transaction(const std::string&logInfo);
	void logTransaction(const std::string& logInfo);

	// 其他操作

};

Transaction::Transaction(const std::string& logInfo)
{
	// 其他操作


	logTransaction(logInfo);	//	最后动作:记录这笔交易
}


class BuyTransaction : public Transaction {
public:
	// 将 log 信息传递给 base class 构造函数
	BuyTransaction(parameters) : Transaction(createLogString(parameters))
	{
		// ...
	}

	// 其他操作

private:
	static std::string createLogString(parameters);
};

即由于无法使用 virtual 函数从 base class 向下调用,在构造期间,你可以藉由 “令 derived class 将必要的构造信息向上传递至 base class 构造函数” 替换之而加以弥补。

Item 10:令 operator= (赋值操作符) 返回一个 reference to *this

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

  • “确保当对象自我赋值时 operator= 有良好的行为。其中技术包括比较 “来源对象” 和 “目标对象” 的地址、精心周到的语句顺序以及 copy-and-swap。”

  • “确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。”

观察下面的例子:

class Bitmap {...};

class Widget{
	...

private:
	Bitmap* pb;
};

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

在这里,*thisrhs 可能是同一个对象,所以 delete 不只是销毁了当前对象的 bitmap,也销毁了 rhs 的 bitmap,因此后续操作会出现问题。

第一次改进:增加证同测试

Widget& Widget::operator=(const Widget& rhs)
{
	if (this == &rhs) return *this;	// 证同测试
	delete pb;
	pb = new Bitmap(*rhs.pb);
	return *this;
}

证同测试具有 “自我赋值安全性”,但是不具备 “异常安全性”。 即,当 “new Bitmap” 导致异常(不论是因为分配时内存不足或因为 Bitmap 的 copy 构造函数抛出异常),Widget 最终会持有一个指针指向一块被删除的 Bitmap。这样的指针有害。

第二次改进:

// 复制 pb 所指东西前别删除 pb
Widget& Widget::operator=(const Widget& rhs)
{
	Bitmap* pOrig = pb;
	pb = new Bitmap(*rhs.pb);
	delete pOrig;
	return *this;
}

替代方案:copy and swap 技术

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

// ①
Widget& Widget::operator=(const Widget& rhs)
{
	Widget temp(rhs);
	swap(temp);
	return *this;
}

// ②
Widget& Widget::operator=(Widget rhs)
{
	swap(temp);
	return *this;
}

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

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

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