阿龙的学习笔记---Effective C++---第二章、构造 / 析构 / 赋值运算

  • 条款05:了解C++默默为class创建并调用哪些函数

    • 如果定义一个空类 class empty{}; C++会自动为其创建默认构造函数、一个non-virtual析构函数、copy构造函数、copy-assignment 拷贝赋值运算符这四个函数。

      class Empty{};
      //相当于
      class Empty{
      public:
      	Empty(){}							//default 构造函数
      	Empty(const Empty& rhs){···}		//copy构造函数
      	Empty& operater=(const Empty& rhs){···}		//拷贝赋值运算符
      	~Empty(){}							//析构函数
      }
      
    • 默认产生的析构函数non-virtual性质的,除非你的base-class有一个virtual析构函数。

    • 对于默认产生的copy构造函数copy运算符,只是将来源对象的每一个non-static成员拷贝到目标对象中。例如如果一个成员是string,则会用string类的copy构造函数拷贝到目标对象,如果是int之类的内置类型,则将每个bit拷贝到目标对象。

    • 对于copy运算符,也会有编译器无法默认生成的时候。比如下面这种情况:

      class test{
      public:
      	test(int b, string& k) :a(b), str(k){}
      private:
      	const int a;			//const成员
      	string &str;			//非const引用 string
      };
      void main(){
      	string a = string("thanks");
      	test my1(5,a);
      	test my2(my1);	//拷贝构造函数可用,因为这是在初始化阶段,而不是赋值
      	my2 = my1;		//这句会报错,这个类中“operator=”不可用
      
      }
      

      由于test类中的成员一个是const类型,const类型不可赋值;一个是reference引用,C++不准让reference改指向不同对象。所以编译器无法给出默认的copy运算符,但可以自己编写。

      还有一种情况是base-class基类 将其 operator= 函数声明为private,那么derived-class派生类中无法调用,编译器则无能为力。


  • 条款06:如果不想使用编译器生成的函数,应该明确拒绝

    • 如果希望某个类只能被构造,而不能被拷贝拷贝构造,那我们可以将其copy构造函数拷贝运算符声明为private。这样可以组织人们拷贝某个对象。

    • 但上述做法不完全安全,因为成员函数友元函数还可以调用。那你可以只声明而不定义,这样在被调用时会产生一个链接错误,这是链接器产生的。

      class myHome{
      publicmyHome(){}
      	~myHome(){}
      private:
      	myHome(const myHome&);						//只有声明,没有定义。这里没有参数名,因为用不到
      	myHome& operator=(const myHome&);
      }
      
    • 还有一种更好的办法能解决这个问题,设计一个base-class基类来实现这个问题。

      让基类的copy构造函数拷贝运算符声明为private,这样无论对于用户还是成员函数或者友元函数,在derived-class派生类中如果要copy构造,编译器会自动构建,但这需要调用基类的拷贝构造函数,而这部分在派生类中不可见。编译器会报错。

      //基类
      class unCopyable{
      publicunCopyable(){}
      	~unCopyable(){}
      private:
      	unCopyable(const unCopyable&);						//只有声明,没有定义
      	unCopyable& operator=(const unCopyable&);
      }
      //派生类private继承
      class myHome: private unCopyable
      {
      ···
      }
      

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

    • 在一个多态基类(大概是一个基类,会被多个不同派生类继承而实现不同功能),如果不将基类的析构函数声明为virtual的话,则有可能出现问题。比如:

      //基类non-virtual析构函数
      class base{
      	base(){};
      	~base(){};
      };
      class A: public base{···};
      class B: public base{···};
      //基类指针指向一个派生类对象
      base* p = new A;
      //delete作用于基类指针,但指向一个派生类对象
      delete p;
      

      这时,delete p;只会调用基类的析构函数~base(); 而A的析构函数并没有执行,A中的元素可能没有被销毁,产生内存泄漏等问题。

    • 解决办法就是给基类的析构函数加上virtual属性。(不知道具体原理是不是因为动态绑定,无论基类指针还时派生类指针都会调用派生类的析构函数)。这样无论如何都会先调用派生类的析构函数,再调用基类的析构函数。

    • 这样的用法在于基类有多态用途,其派生类中还需要自己定义,这样的类一般都会有其他virtual函数存在。但无端为所有的类都加上virtual析构函数也是不对的。(在类的大小上因为虚函数表等会有问题)

    • 对于STL库中的一些标准类string,vector,unordered_map,list,set等,都是non-virtual析构函数,所以尽可能不要对其继承。否则可能出现上述问题。

    • 对于完全作为继承而用的抽象类,为其添加一个纯虚(pure-virtual)析构函数即可获得一个抽象类。

      class abstract{
      	abstract();
      	virtual ~abstract() = 0;
      }
      

      此时别忘了一定要给这个析构函数一个定义,否则派生类析构时会调用,找不到会有链接错误。


  • 条款08:别让析构函数抛出异常

    • 原文有点深奥,我太菜还看别的文章科普了一下,看了这篇构造函数、析构函数抛出异常的问题,才有点领悟。

    • 抛出异常:即检测是否产生异常,在C++中,其采用throw语句来实现,如果检测到产生异常,则抛出异常。throw 表达式;

      如果在try语句块的程序段中(包括在其中调用的函数)发现了异常,且抛弃了该异常,则这个异常就可以被try语句块后的某个catch语句所捕获并处理,捕获和处理的条件是被抛弃的异常的类型与catch语句的异常类型相匹配。

      假如说A方法调用–>B方法,B调用–>C方法。 然后在B和C方法里定义了throws Exception。A方法里定义了Try Catch。那么调用A方法时,在执行到C方法里出现了异常,那么这个异常就会从C抛到B,再从B抛到A。在A里的try catch就会捕获这个异常,然后你就可以在catch写自己的处理代码。

    • 构造函数可以抛出异常,在对象的构造过程中,或许由于系统资源有限而致使对象需要的资源无法得到满足,从而导致异常的出现,这时可以交给用户去处理。

    • C++标准指明析构函数不能、也不应该抛出异常

      C++异常处理模型是为面向对象而服务的。那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常而失效的对象,并释放资源, 这通过调用析构函数来完成,所以从这个意义上说,析构函数已经变成了异常处理的一部分。

      如果析构函数中再抛出异常,则没人保证这个对象的清理以及资源的释放等问题。

    • Effective C++书中提出两点理由
      1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
      2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

    • 一个例子:假如使用一个class 负责数据库连接:

      class DBConnection{
      public: 
       	... 
      	static DBConnection create();  
      	void close(); //关闭联机 :失败则抛出异常
      }
      

      客户需要手动关闭,为确保客户不忘记在DBConnection对象身上调用close(),创建一个用来管理DBConnection资源的class,并在析构函数中自动调用close。

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

      这便允许客户简单的使用这个class,而不用手动调用close,以免忘记而出错:

      {   //开启一个区块  
      	DBConn dbc (DBConnetion::create()); //建立DBConnection对象并交给DBConn对象以便管理。
      	//区块结束后,DBConn对象被销毁,自动为DBConnection 对象调用close
      }
      

      这时,如果close调用异常,DBConn析构函数会抛出该异常,即没有处理这个异常,让它离开了这个析构函数,这就会有上述说到的那些问题!

    • 文中给出了三种处理方法

      1)方法一:结束程序
       如果close抛出异常就结束程序,通常调用abort直接强制结束…

      DBConn::~DBconn(){
          try {
              db.close(); }
          catch(...){
              abort();
          }
      }
      

        如果程序遭遇一个“于析构期间发生的错误”后无法继续执行,“强制结束程序”是个合理选项,毕竟它可以阻止异常从析构函数传播出去(那会导致不明确的行为)。也就是说调用abort可以抢先制“不明确行为”于死地。

      2)方法二:吞下因调用close而发生的异常

      DBConn::~DBConn{
          try{ db.close(); }
          catch(...) {
              //制作运转记录,记下对close的调用失败!
              //无论怎么样,让异常不跑出去,封在内部。
          }
      }
      

        一般而言,将异常吞掉是个坏主意,因为它压制了“某些动作失败”的重要信息!然而有时候吞下异常也比负担“草率结束程序”或“不明确行为带来的风险”好。为了让这成为一个可行方案,程序必须能够继续可靠的执行。

      3)方法三:重新设计DBConn接口,使用户自己来处理。

      class DBConn {
      public:
          ...
          void close() //供客户使用的新函数
          {
              db.close();
              closed = true;		
          }
          ~DBConn(){
              if(!closed) {
                  try {       //关闭连接(如果客户不调用DBConn::close)
                        db.close();
                  }
                  catch(...) { //如果关闭动作失败,记录下来并结束程序或吞下异常。
                      制作运转记录,记下对close的调用失败;
                      ...
                 }
              }
          }
      private:
          DBConnection db;
          bool closed;
      };
      

      采用了一个标志位来确保用户没有调用close()函数,析构函数中也会调用,起到了保险。同时,将大部分的情况下的异常处理交给客户。

      由客户自己调用close并不会对他们带来负担,而是给他们一个处理错误的机会。如果他们不认为这个机会有用(或许他们坚信不会有错误发生),可能忽略它,依赖DBConn析构函数去调用close。

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


  • 条款09:绝不在构造或析构函数中调用virtual函数

    • virtual函数主要实现多态性,在基类、各派生类中发挥不同的作用。但是考虑构造函数和析构函数这两个特殊的函数,在继承关系中,初始化派生类时会调用基类的构造函数,此时派生类还未出现,如果在这里,虚函数调用的则是基类的虚函数,这也最初的设想完全不同,导致了不可预期的后果。

      比如说以下的例子:以一个为交易记录而生的class,在基类的构造函数中加入了一个虚函数,记录日志。看似好像每个派生类在初始化时都会做记录日志这个动作,可是并不是想象的那样。

      //所有交易的base class
      class Transaction {         
      public:
          Transaction();
          virtual void logTransaction() const = 0;  //做出一份因为类型不同而不同的日志记录,目前是一个纯虚函数
          ...
      };
      Transaction::Transaction()  //base class的构造函数的实现
      {
          ...
          logTransaction();    //最后的动作是对这笔交易进行记录
      }
      
      class BuyTransaction: public Transaction {    //derived class
      public:
          virtual void logTransaction() const;   //对这种类型的交易进行记录(log)
          ...
      };
      
      class SellTransaction: public Transaction {   //derived class
      public:
          virtual void logTransaction() const;   //对这种类型的交易进行记录(log)
          ...
      };
      
    • 如果必须要以这样的结构来实现功能,那么一种办法是:

      在class Transaction内将logTransaction函数改为non-virtual,然后要求derived class构造函数传递必要的信息给Transaction构造函数,而后即使在基类的构造函数中也可以根据不同的信息调用non-virtual的logTransaction函数。

      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);    //此时,是一个non-virtual调用
      }
      
      class BuyTransaction: public Transaction {
      public: 
          BuyTransaction( parameters )
              : Transaction(createLogString( parameters )) //将log信息传递给base class构造函数
              { ... }
          ...
      private:
          static std::string createLogString( parameters );
      };
      

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

    • 一般的赋值都遵循"连续赋值":a=b=c=100。所以为一个class写operator=函数时也需要默认这个规则。
    • += 、 -= 、*=等符号也最好符合这个标准。
    widge& operator=(const widget& rhs){
    	···
    	return *this;
    }
    

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

    • 自我赋值是将自己赋给自己,虽然一般不会这么做,但是如果有这种情况,operator=函数也应该能够处理。
    • 自我赋值出现的问题可能会出现在资源管理上,假如有new分配的资源,自我赋值时可能会触发“在停止使用资源前意外释放”的trap。例如:
      class widget{
      	···
      private:
      	Bitmap* bp;
      }
      
      widget& widget::operator=(const widget& rhs){
      	delete this->pb;
      	this->pb = new Bitmap(*rhs.bp);
      	return *this;
      }
      
    • 传统做法可以在最开始加一个判断:
      widget& widget::operator=(const widget& rhs){
      	if(rhs == *this) 
      		return *this;
      	delete this->pb;
      	this->pb = new Bitmap(*rhs.bp);
      	return *this;
      }
      
      虽然自我赋值解决了,但这个函数如果在new分配出现异常时,pb已经被delete了,所以依旧有风险。
    • 一种更好的做法是:
      widget& widget::operator=(const widget& rhs){
      	Bitmap* pb_origin = pb;
      	pb = new Bitmap(*rhs.bp);
      	delete pb_origin;
      	return *this;
      }
      
      这样,无论是自我赋值,亦或是new抛出异常,都不会有致命的问题。
    • 还有一种做法与“异常安全性”相关,由条款29详细说明,所谓的“copy & swap”。是一个常见而且够好的 operator= 的写法。
         	class widget{
         	private:
         		Bitmap* pb;
         		void swap(widget& rhs); 		//用于交换 this->pb 和 rhs.pb
      	widget& widget::operator=(const widget& rhs){
      		widget temp(rhs);	//复制一份rhs
      		swap(temp);
      		return *this;
      	}
      	```
      
      

  • 条款12:复制对象时勿忘其每个部分

    • 对于自己编写operator=函数以及copy构造函数,那么必须要注意,要复制每个成员变量,如果有添加成员变量等,别忘了修改他们。(以及修改其他构造函数)。

    • 对于derived class派生类operator=函数以及copy构造函数,很容易忘记要复制base-class基类的那一部分。

      class customer{   		//基类
      	···
      private:
      	string name;
      }
      //派生类,copy构造函数 及 赋值运算符 的实现
      class VIPCustomer: public customer{
      public:
      	VIPCustomer(const VIPCustomer& rhs): priority(rhs.priority)		//只复制了派生类中的变量
      	{
      	}
      	VIPCustomer& operator=(const VIPCustomer& rhs)
      	{
      		priority = rhs.priority;		//只复制了派生类中的成员
      		return *this;
      	}
      private:
      	int priority;
      }
      

      对于copy构造函数来说,初始化列表中只对priority进行了初始化,而未对base-class初始化。无参数传递则会调用default的构造函数对customer的部分进行初始化,而违反了copy构造的初衷。

      operator=函数中也没有基类customer的部分,则operator=函数并不会复制customer的部分,保持原值,也违反了初衷。

    • 修改上述的两种情况,在两个函数中都加上base-class基类的部分:

      class VIPCustomer: public customer{
      public:
      	VIPCustomer(const VIPCustomer& rhs): customer(rhs)	, priority(rhs.priority) 	//customer的函数放前面
      	{
      	}
      	VIPCustomer& operator=(const VIPCustomer& rhs)
      	{
      		customer::operator=(rhs);		//调用基类的operator=函数
      		priority = rhs.priority;		//只复制了派生类中的成员
      		return *this;
      	}
      private:
      	int priority;
      }
      
    • 另一点提醒:虽然copy构造函数和operator=之间可能会有重复部分,但不可互相调用。减少代码重复性可以通过编写一个功能函数init()的方式。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值