Effective C++(条款1-10)

条款1-4省略。

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

如果一个类中没有任何函数,c++在创建该类时会默默编写并调用哪些函数呢?

  首先如果类中没有没有任何函数,那么编译器会为该类声明一个拷贝构造函数、复制操作符、default构造函数、和析构函数;函数类型都是public且inline;

ps:

Class Empty{};

   就相当于写下了以下代码

   Class Empty{
   public:
       Empty(){}                                                              //default 构造函数
       Empty(const Empty& rhs){}                                  //拷贝构造函数
       ~Empty(){}                                                           //析构函数
       Empty& operator=(const Empty& rhs)                  //复制操作符

   };

那么有没有什么时候编译器是不会自动编写这些函数的吗?

1.如果编译器自动生成的默认的构造函数/析构函数不能满足该类的需求时,那么就需要在自己创建类时需要编写一个自己需要构造函数/析构函数,编译器就不会在为该类创建构造函数/析构函数;
2.如果类中有对象成员或者const成员,那么编译器会拒绝生成赋值函数;

拷贝构造函数和赋值函数的用法:
ps:

 String a("hello");    String b("world");    String c = a;//这里c对象被创建调用的是拷贝构造函数                 //一般是写成 c(a);这里是与后面比较    c = b;//前面c对象已经创建,所以这里是赋值函数

编译器生成的拷贝函数和赋值函数的缺点:
如果不主动编写的话,编译器将以“位拷贝”的方式自动生成缺省的函数。在类的设计当中,“位拷贝”是应当防止的。倘若类中含有指针变量,那么这两个缺省的函数就会发生错误。这就涉及到深复制和浅复制的问题了。

 

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

如果不希望class支持某一项特定机能机能,那么只要不声明相对应的函数就可以了,但是因为拷贝构造函数和赋值函数是可以被编译器
自动编写的并且是public的,所以正常一个calss是不能屏蔽拷贝机能的;

那么如果想写一个不支持拷贝类的需要怎么做呢?

1.首先想到的就是自己重写一个私有的拷贝构造函数和赋值函数,使编译器不会在自动编写这两个函数;也能阻止外部调用这两个函数;
   但是这样的做法不能完全保证不会被调用到,因为成员函数和友元函数还是可以调用到你的私有函数;
   怎么解决友元函数和成员函数的调用问题呢?
   可以将函数只进行声明不实现该函数,因为这个时候如果有函数调用你没有实现的函数连接器会报错;
   这样就能保证该class没有拷贝机能;

  ps:

  class NoCopy{
   public:
      .....
   private:
     NoCopy(cons tNoCopy& res);
     NoCopy& operator=(cons tNoCopy& res)

  }

2.上述办法只能在连接期发现错误,本着错误发现越早越好的想法,能不能将错误移到编译期呢?
   设计一个专门阻止拷贝动作的base 类;将base类中的拷贝构造函数和赋值函数设置为私有的;
   ps:

   class NoCopyBase{   public:
      NoCopyBase(){}
      ~NoCopyBase(){}
   private:
     NoCopyBase(cons tNoCopy& res);
     NoCopyBase& operator=(cons tNoCopy& res)

  }

  为了防止对象被拷贝我们只需要做的就是继承base类;
  ps:

  class NoCopyChild:privte NoCopyBase{
  //不在声明 拷贝构造函数和赋值函数;
  }


  这样只要任何人进行拷贝行为时编译器都会去调用base类中的拷贝赋值函数,因为base中的函数是私有的所以编译器会报错;

 

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

如果基类的的析构函数不是虚函数的话,会造成什么影响,首先来看一段代码;

ps:

class Father
{
public:
  Father(){cout<<"contructor Father!"<<endl;};
  ~Father(){cout<<"destructor Father!"<<endl;};
};

class Son:public Father
{

public:
  Son(){cout<<"contructor Son!"<<endl;};
  ~Son(){cout<<"destructor Son!"<<endl;};
};

int main()
{
Father *pfather=new Son;
delete pfather;
pfather=NULL;
return 0;
}

运行结果:
contructor Father!
contructor Son!
destructor Father!
通过运行结果来看子类的构造函数已经被调用,但是在释放时并没有调用子类的析构函数;所以子类中成员变量也没有被销毁,造成了“局部销毁”;这样就会造成内存泄漏;

 

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

如果在析构函数中执行可能会抛出异常的操作,操作全部成功那么没有任何问题。
如果操作出现异常那么就是让这个异常逃离了析构,将这个异常抛了出去,再往下执行时可能会因为此次的异常造成不确定的风险,而且出现问题时很难调查出错误的原因;

解决办法:
1.如果程序碰到该异常,还能继续可靠的运行下去,可以在析构函数出现操作异常时,将该异常吞掉;
2.将析构函数的异常操作行为重新提供一个函数,使其调用者有机会在(析构函数之前)执行该操作出现异常时,可以处理该异常;(推荐使用)

 

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

先看一段代码;

// 所有交易的基类
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对象的时候,首先Transaction的构造函数会被调用,从而其virtual函数也被调动,这里就是引发惊奇的起点。这时候被调用的logTransaction是Transaction的版本,而不是派生类BuyTransaction的版本。
所以并没有实现多态的功能;

切记!!
在基类的构造过程中,虚函数调用从不会被传递到派生类中。派生类对象表现出来的行为好象其本身就是基类型。不规范地说,在基类的构造过程中,虚函数并没有被”构造”。也就是说,在派生类对象的基类子对象构造期间,调用的虚函数的版本是基类的而不是子类的。
对上面这种看上去有点违背直觉的行为可以用一个理由来解释-因 为基类构造器是在派生类之前执行的,所以在基类构造器运行的时候派生类的数据成员还没有被初始化。如果在基类的构造过程中对虚函数的调用传递到了派生类, 派生类对象当然可以参照引用局部的数据成员,但是这些数据成员其时尚未被初始化。这将会导致无休止的未定义行为。沿类层次往下调用尚未初始化的对象的某些部分本来就是危险的,所以C++干脆不让你这样做。
在对象的析构期间,存在与上面同样的逻辑。一旦一个派生类的析构器运行起来,该对象的派生类数据成员就被假设为是未定义的值,这样以来,C++ 就把它们当做是不存在一样。一旦进入到基类的析构器中,该对象即变为一个基类对象,C++中各个部分(虚函数,dynamic_cast运算符等等)都这样处理。

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

注意:
这只是个协议,并无强制性。若不遵循它,代码一样可通过编译。

令operator=返回一个reference to *this的目的:实现连锁赋值;

在代码中我们经常发现会出现以下类似的代码:

int x,y,z;
x=y=z=15

由于赋值是右结合律,所以a=b=c------->a=(b=(c = 15));
相当于先用c给b赋值,然后在用b给a赋值。
试想一下如果赋值函数写成
operator = (const ***& rhs){}
不会返回reference to *this;
那么上述的连续赋值操作将不被允许;
因为第一次赋值操作时没有返回对象指向左侧操符左侧的实参,所以在进行第二次操作赋值时,没有办法调用赋值函数,因为没有合适参数传入到赋值函数中;

如果自己定义的类也想实现连锁赋值那么也必须需要遵守该协议;

即:返回引用本体,而不能是一个副本,不然无法实现a=b=c这样的连锁赋值

posted @ 2020-09-16 00:16  吉尔加斯  阅读(213)  评论(0)    收藏  举报