深度探索C++对象模型第2章 构造函数语义学

默认构造函数

两个误区:

1 任何class如果没有定义默认构造函数,就会被合成一个出来:只有在某些情况下被合成

2 编译器合成出来的默认构造函数会明确设定class中每一个数据成员的默认值 :默认值是程序员该做的事情。

         默认构造函数会在需要的时候被编译器产生出来,要注意字眼“需要的时候”,例如如下代码:

class Bat{  
public:  
    int val;  
    Bat* pNext;  
    void bat_yell();  
};  
  
void Bat::bat_yell()  
{  
    Bat batMan;  
    if(batMan.val || batMan.pNext)  
        // ...do something  
    // ...        
}  


        按照程序员的本意,在bat_yell()方法中,batMan会调用默认构造函数将val和pNext初始化为0,但是实际结果并不是这样,程序员的需要并不代表编译器需要,当然,程序员的责任也不该被编译器一起所承担,归根结底说来,上述代码片段中,并不会有默认构造函数被合成。退一步讲,即便是有默认的构造函数被合成出来了,也不会将上述的数据成员初始化为0。也就是说此时的默认构造函数是trivial (无用的,不重要的),有四种情况编译器会产生nontrivial(重要的,有用的)的默认构造函数:

a. 带有默认构造函数的类成员对象:

         

        Bar的类成员对象foo含有默认构造函数,所以编译器会为类Bar合成默认构造函数,可能看起来像下面这样:

        值得注意的是,即便是编译器为我们合成了默认构造函数,但是并不会产生任何代码来初始化Bar的str成员变量,这个是程序员的责任。那么程序员如果提供了构造函数(默认或者带参数的),编译器会做些什么呢?答案是编译器会扩展以后的构造函数,在每一个构造函数中安插代码,代码会被安插到程序员代码的前面,使得每一个构造函数调用每一个带有默认构造函数的成员变量的默认构造函数,有一点绕,看个具体例子:

       为什么基类的默认构造函数被安插在派生类的构造函数的最前面呢?

        在派生类的构造函数中可能会调用基类数据成员,因此需要先对基类数据成员初始化。

       C++语言要求以“member objects 在class中的声明顺序”来调用各个constructors。这一点由编译器完成,它为每一个constructor安插程序代码,以“member声明顺序”调用呢每一个member所关联的default constructors。这些代码将安插在explicit user code之前。举个例子
 

class Dopey { pulic:Dopey();...)};
class Sneezy { public:Sneezy(); Sneezy(int);...};
class Bashful { public:Bashful();...};
class Snow_White {
private:
  Dopey dopey;
  Sneezy sneezy;
  Bashful bashful;
  int mumble;  
};

        如Snow_White没有定义default constructor,就会有一个nontrivial constructor被合成出来,依次调用Dopey,Sneezy,Bashful的default constructors。然而如果Snow_White定义了下面这样的default constructor

Snow_White():Sneezy(1024)
{
    mumble = 2048;
}

// 编译器扩张的default constructor 伪码
Snow_White():Sneezy(1024)
{
    // 插入member class object
   // 调用其 constructor
    dopey.Dopey::Dopey();
    sneezy.Sneezy::Sneezy(1024);
    bashful.Bashful::Bashful();
    // explicit user code
    mumble = 2048;
}

         

初始化列表的作用:   

        如 Rectangle()中,首先,编译器已经插入了string类的默认构造函数 ,然后又手动对name=“default”赋值,相当于下面//的两句,定义了一个临时对象temp,再调用string的赋值运算符。效率不高。

        如果用下面的初始化成员列表的方式,则仅仅调用string类的拷贝构造函数,不会调用默认构造函数;

        当然对于基本数据类型int ,char则不存在默认构造函数等,不存在这个问题。

        

b. 第二种情况是基类中由默认构造函数

        类似于第一种情况,如果一个没有定义默认构造函数的类继承(派生)于一个带有默认构造函数的基类,那么这个子类的默认构造函数会被合成出来,在其中以按照声明顺序分别调用基类们的构造函数。如果类设计者提供了多个构造函数,但是没有默认构造函数,编译器会扩张已有的构造函数,而不会合成一个新的默认构造函数。如果此类还包含第一种情况:含有存在默认构造函数的成员类对象,那么他们的默认构造函数会在调用基类默认构造函数之后调用。


c. 带有虚函数的类

        由于虚函数的存在,会产生虚函数表和虚表指针,而这些必须在对象构造期间安排好,所以对于类所定义的每一个构造函数,编译器会安插一些代码来做这样的事情,对于未声明任何构造函数类,编译器会为他们产生一个默认的构造函数,以便正确的初始化每一个类对象的虚表指针。


d. 继承自虚基类的类

        Virtual base class的实现法在不同的编译器之间有极大的差异。然而,每一种实现法的共同点在于必须使virtual base在其每一个derived class object中的位置,能够于执行期准备妥当。

        总之,以上四种情况编译器会合成一个隐式的重要的默认构造函数(implicit nontrivial default constructor)来满足编译器(而非程序)的需要:调用成员类或者基类的默认构造函数,或者完成每一个对象初始化其虚函数机制或者虚基类机制。对于其他情况并且没有声明任何构造函数的类,他们拥有的是隐式的不重要的默认构造函数(implicit trivial default constructor),实际上并不会被合成出来。
一定要注意,合成出来的默认构造函数只会初始化编译器认为的必要的部分(基类对象,类对象),其他数据成员并不会被初始化。

拷贝构造函数

        拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量

        它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:

  • 通过使用另一个同类型的对象来初始化新创建的对象。

  • 复制对象把它作为参数传递给函数。

  • 复制对象,并从函数返回这个对象。

  C++ Standard将copy constructor(拷贝构造函数)分为trivial (不需要)和nontrivial(需要)两种:只有nontrivial的实例才会被合成于程序之中。决定一个copy constructor是否是nontrivial的,则是由classs是否具有 bitwise copy semantics(位拷贝),在以下四种情况下:class 不具有bitwise copy semantics,如果一个已经声明的类缺乏copy constructor ,编译器为了正确处理“以一个 class object 作为另一个class object的初值”,编译器会为class合成一个copy constructor。

  • 当class内含一个member object(成员对象),而这个member object的class声明中有一个copy constructor(拷贝构造函数)(不论是用户explicitly define还是编译器合成的)
  • 当class继承自一个base class,而这个bass class 存在 copy constructor(无论是用户explicitly define  or 编译器合成的)
  • 当classs声明了一个或多个virtual function
  • 当类派生自一个继承串链,其中有一个或多个 virtual base class时

        对于以上四种情况,类不再具有bitwise copy semantics(逐位次拷贝语义学)时,而且默认构造函数若未被声明的话,default constructors会被视为nontrival.(可以对比默认构造函数,类似)

一、bitwise copy semantics:

比如下面这个例子就展示了 bitwise copy semantics:

class Word{
public:
    Word (const char*) ;
   ~Word() {delete [] str;}
private:
   int cnt;
   char *str;
};

        当定义:

Word d1;
Word d2 = d1;//仅仅发生位拷贝,将cnt ,str 逐个字节拷贝,=》会造成浅拷贝问题

 而下面这个例子Word类就不再展现出bitwise copy semantics,编译器必须合成出一个copy constructors,以便调用类成员对象String的拷贝构造函数 


class Word{
public:
    Word(const String&);
   ~Word(); 
private:
   int cnt;
   String str;   
};

//其中String声明了一个explicit copy construcotr
class String{
public:
   String(const char*); //显示声明的默认构造函数
   String(const char &);//explicit copy constructors
   ~String();  
    
};

         则针对上述情况,编译器为类Word合成一个copy consructor,如下所示:注意正如我们期待的一样:在被合成的copy constructor中,如整数、指针、数组等nonclass members也会被复制。

//C++伪代码
inline Word::Word(const Word& wd)
{
 str.String::String(wd.str);
 cnt = wd.cnt;
}

二、下面介绍三种会以一个object的内容作为另一个class object初值的情况:

 第一种情况就是对一个object做显示的初始化操作:

class X{...};
X x;

//显示的用一个object的内容作为另一个class object的初值
X xx = x;
extern void foo(X x);
void bar()
{
 X xx;
//以xx作为foo()第一个参数的初值(隐式的初始化操作)
foo(xx);
}

 第三种情况就是当函数传回一个class object时:

X foo_bar()
{
   X xx;
  return xx;
}

 当class object以相同class的另一个object作为初值时,其内部是以所谓的default memberwise initialization(逐成员初始化)完成的:即把每一个内建或派生的data member的值,从一个object拷贝到另一个object上,不过他并不会拷贝其中的class object,而是递归的实行member initializaiton.

三、下面我们讨论四种情况的第三种当classs声明了一个或多个virtual function

  当class声明了一个或多个virtual function时,编译器会发生以下扩张行为:

  • 增加一个virtual function table(vtbl),内含每个有作用的virtual function地址
  • class objet增加一个指向vtbl的指针(vptr).

当编译器导入一个vptr(是新导入一个vptr,例如将base class object以derived class的object内容初始化时,重要!!!)时,class就不再展现bitwise copy semantics啦,所以编译器需要合成一个copy constructor,来让vptr正确的初始化:

来看一个例子:

class ZooAnimal{
public:
   ZooAnimal();
   virtual ~ZooAnimal();
   virtual void animate();
   virtua void draw();

private:
   //ZooAnimal的内建数据...
};

class Bear : public ZooAnimal{
  Bear();
  void animate(); //virtual function
  void draw() ; //虚函数
  virtual void dance();
private:
   //Bear的内建函数

};

 当以一个ZooAnimal object作为另一个ZooAnimal object的初值 或 以一个Bear object作为另一个Bear object的初值时,此时并不是新导入一个vptr,virtual function table仍然是原有的表,所以此时具有bitewise semantics,此时编译器要产生的拷贝构造函数是trivial(无意义的),根本不会合成。

举个例子如下:

Bear yogi;
Bear whii = yogi;

 经过上述操作之后yogi oobject和whii object中分别含有的vptr指向的vtbl是相同的,如下图所示:

 而如果一个base class object以一个derived class object的初值初始化时,则此时base class 就不再具有bitwise semantics

举个例子:

 Bear yogi;
 ZooAnimal whii = yogi;

此时编译器需要合成一个nontrivial的copy structor来显示设定base object的vptr指向base class的vptr,而不是直接从右边derived class object中将vptr的值拷贝出来。 如下图所示:


 四、下面我们讨论四种情况的第三种:处理virtual Base class实例

       bitwise copy semantics失效发生在一个class object以其derived object作为初值时,而不是发生在一个class object 以一个同类的object作为初值时,(后者使用bitwise copy拷贝绰绰有余)

 举个例子如下:

//定义一个浣熊类
class Raccoon : public virtual ZooAnimal {
public:
   Raccoon() {   } 
   Raccoon(int val) {   }
private:
   //Raccon的内建数据

};

//定义一个大熊猫类 继承自浣熊
class RedPanda : public Raccoon{
public:
    RedPanda() {   }
    RedPanda(int val) {  }
private:
   //RedPand的内建数据
};

 记住浣熊才是这个具有虚基类的类,需要编译器合成trivial的 copy constructor就发生在当 浣熊以大熊猫作为初值时。因为每个编译器对于虚拟继承的承诺,都代表必须让 “derived class object中的virtual base class subobject位置”在执行期就准备妥当,即如下情况:

RedPanda little_red;
Raccoon  little_critter = little_red;

在这种情况下,编译器必须合成一个copy constructor,安插一些代码以设定virtual base class的初值(或只简单的确定它没有被抹除),对每一个members执行必要的memberwise初始化操纵,以及执行其他的内存相关操作。

参考:

深度探索C++对象模型之第二章:构造函数语意学之成员初始值列表 - 三只猫- - 博客园

 深度探索C++对象模型(4)——对象(4)——拷贝构造函数语义_波波鱼的博客-CSDN博客

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值