【Effective C++】构造/析构/赋值运算

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/daaikuaichuan/article/details/85109271

一、了解C++默默编调用了哪些函数

1、编译器默认调用的函数

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

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

  当需要这些函数时,这些函数会被编译器创建,这几个是最常见的、也是用的最多的。那么编译器创建的这些函数都是做了什么?

  首先,如果没有构造函数,编译器将会创建一个默认构造函数,由它来调用基类和non-static成员变量的构造函数。

  析构函数是否是虚函数,继承基类,如果没基类,那么默认是non-virtual。析构函数会调用基类和non-static成员变量的析构函数。编译器创建的拷贝构造函数和赋值构造函数是个浅拷贝

  在编译器创建的复制构造函数和赋值操作符中,给成员变量初始化或赋值,会调用成员变量的赋值构造函数和赋值操作符

2、请记住

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


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

1、拒绝编译器自动生成的函数

  所有编译器产出的函数都是public。为了阻止这些函数被创建出来,你得自己声明它们,但这里并没有什么需求使你必须将它们声明为public。有的时候拷贝构造函数和赋值操作符是不应该使用的,所以可以将copy构造函数或copy assignment操作符声明为private或者使用delete

class HomeForSale
{
public:
	/....../
private:
	HomeForSale(const HomeForSale&);
	HomeForSale& operator=(const HomeForSale&);
};
// C++11中可以使用delete关键字
class HomeForSale
{
public:
	HomeForSale(const HomeForSale&) = delete;
	HomeForSale& operator=(const HomeForSale&) = delete;
};

  将错误提前到编译阶段是最好的,毕竟越早出现错误越好。可以通过继承来实现,设计一个不可以复制的类。

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

class HomeForSale:private Uncopyable
{
   /......./ // class 不再声明copying函数
};

2、请记住

  为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现或者使用delete关键字。使用像Uncopyable这样的base class也是一种做法。

扫描二维码关注公众号,回复: 4739693 查看本文章

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

1、普通的基类的指针指向派生类的对象

#include <iostream>
using namespace std;

class Virtualbase
{
public:
    void Demon() { cout << "this is Virtualbase class" << endl; };
    void Base() { cout << "this is farther class" << endl; };
};
class SubVirtual :public Virtualbase
{
public:
    void Demon() 
    {
        cout << "this is SubVirtual!" << endl;
    }
    void Base() 
    {
        cout << "this is subclass Base" << endl;
    }
};
void main()
{
    Virtualbase* inst = new SubVirtual();
 	// 如果析构函数不是虚函数,将不会调用当前指针指向对象的函数。
    inst->Demon();
    inst->Base();
    /....../
}
/* 
运行结果:
this is Virtualbase class
this is farther class
*/
#include <iostream>
using namespace std;

class Virtualbase
{
public:
    virtual void Demon() { cout << "this is Virtualbase class" << endl; };
    virtual void Base() { cout << "this is farther class" << endl; };
};
class SubVirtual :public Virtualbase
{
public:
    void Demon() 
    {
        cout << "this is SubVirtual!" << endl;
    }
    void Base() 
    {
        cout << "this is subclass Base" << endl;
    }
};
void main()
{
    Virtualbase* inst = new SubVirtual();
 	// 如果析构函数是虚函数,就会调用当前指针指向对象的函数了。
    inst->Demon();
    inst->Base();
    /....../
}
/* 
运行结果:
this is SubVirtual!
his is subclass Base
*/

2、virtual析构函数的作用

  当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义——实际执行时通常发生的是对象的derived成分没被销毁,而其base class成分通常会被销毁,于是造成一个诡异的“局部销毁”对象。
  这是因为在使用这些类时,往往是通过基类指针或者引用使用的(类的实例在堆上),通过delete指针析构对象时,这时如果析构函数不是虚函数,将不会调用当前指针指向对象的析构函数。这是多态的原理。同理可知,要实现多态的函数,在基类也要声明为虚函数。

【Note】:
(1)当一个类不用做基类时,如果把其析构函数声明为虚函数是个馊主意。因为虚构函数是通过虚函数表调用的,在调用虚函数时多一步指针操作;除此之外,其对象占用的内存空间也会多一个虚函数指针

3、请记住

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

  • Classes的设计目的如果不是作为base class使用,或不是为了具备多态性(如条款06-2中的基类Uncopyable),就不该声明virtual析构函数


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

1、析构函数中发生异常有什么问题?

class DBConnection {
public : 
    /....../
    static DBConnection create(); // 这个函数返回DBConnection 对象
    void close(); //关闭联机;失败则抛出异常。
};

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

  如果调用close成功,则一切都美好。但是如果出现异常,DBConn会抛出异常,也就是允许这个异常离开析构函数,这样会传播异常。

  一个比较好的策略是重新设计DBCoon接口,是客户能对可能出现的异常做出反应。例如DBConn可以自己提供一个close函数,可以给客户一个机会来处理“因该操作而发生的异常”。DBConn也可以追踪其所管理的DBConnection是否已经关闭,并在答案为否的情况下由其析构函数关闭,这样可以防止遗失数据库连接。但是如果DBConnection的析构函数调用close失败,问题又回到了起点。

class DBConn {
public:
   .....
   void close()        //供客户使用的新函数
   {
       db.close();
       closed = true;
    }
    ~DBConn()
    {
       if (!closed) {
            try { db.close(); }
            catch( ... ){
                   // 日志
                   ....
                }
         }
    }
private:
    DBConnection db;
    bool closed;
};

2、请记住

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。

  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。


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

1、构造和析构过程中调用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;
     /....../
};
//考虑下面语句
BuyTransaction b;

  由于base class 构造函数的执行更早于derived class构造函数,当base class构造函数执行时derived class的成员变量尚未初始化。在derived class 对象的base class构造期间,对象的类型是base class而不是derived class。

  解决方案:在base class 内将virtual函数改为non-virtual,然后要求derived class构造函数传递必要信息给base class构造函数,而后base class 构造函数就可以安全的调用non-virtual函数了。如下:

class Transaction 
{
public:
	  //单参数构造函数,最好使用explicit禁止其进行隐式类型转换
     explicit Transaction (const std::string& logInfo);   
     //non-virtual函数
     void logTransaction (const std::string& logInfo) const;     
     /....../
};
Transaction::Transaction (const std::string& logInfo)
{
     /....../
     logTransaction (logInfo);  //non-virtual调用
}
class BuyTransaction :public Transaction 
{
public:
	// 将log信息传给base class 构造函数
    BuyTransaction(parameters)
    :Transaction(createLogString(parameters))  
    {   /......./   }
    /....../
private:
	 // 函数为static
     static std::string createLogString(parameters);  
};

2、请记住

  • 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。这就是所谓的:virtual函数在构造/析构期间的“失常表现”,也即,在此期间,virtual函数不是virtual函数。

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

1、连锁赋值

int x, y, z;
x = y = z = 15;    // 赋值连锁形式
// 赋值采用右结合律,所以上述连锁赋值被解析为:
x = (y = (z = 15));

  这里15先被赋值给z,然后其结果(更新后的z)再被赋值给y,然后其结果(更新后的y)再被赋值给x。为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧。

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

2、请记住

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

七、在operator=里处理"自我赋值"

1、正确进行自我赋值

class Widget
{
public:
	void swap(const Widget& rhs);//交换rhs和this
	Widget& operator=(const Widget& rhs)
	{
		Widget tmp(rhs); // 赋值一份数据
		swap(tmp) // 交换
		return *this; // 临时变量会自动销毁
	}
	int *p;
};

2、请记住

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

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


八、复制对象时务忘其每一个成分

1、正确复制对象

  在一个类中,有两个函数可以给复制对象:拷贝构造函数和赋值操作符,统称为copying函数。如果我们自己不编写者两个函数,编译器会帮我们实现这两个函数,编译器生成的版本会将对象的所有成员变量做一份拷贝。编译器生成的copying函数的做法通常是浅拷贝。如果我们自己实现了copying函数,编译器就不再帮我们实现。但是编译器不会帮我们检查copying函数是否给对象的每一个变量都赋值。

class PriorityCustomer : public Cutsomer
{
public:
	PriorityCustomer()
	{
		cout<<"PriorityCustomer Ctor"<<endl;
	}
	PriorityCustomer(const PriorityCustomer& rhs)
		:priority(rhs.priority)
	{
		cout<<"PriorityCustomer Copy Ctor"<<endl;
	}
	PriorityCustomer& operator=(const PriorityCustomer& rhs)
	{
		cout<<"PriorityCustomer assign operator"<<endl;
		priority=rhs.priority;
		return *this;
	}
private:
	int priority;
};
PriorityCustomer(const PriorityCustomer& rhs)
		:Cutsomer(rhs),priority(rhs.priority)
{
	cout<<"PriorityCustomer Copy Ctor"<<endl;
}
PriorityCustomer& operator=(const PriorityCustomer& rhs)
{
	cout<<"PriorityCustomer assign operator"<<endl;
	Cutsomer::operator=(rhs);
	priority = rhs.priority;
	return *this;
}

2、请记住

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

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

猜你喜欢

转载自blog.csdn.net/daaikuaichuan/article/details/85109271