前言

继前两篇总结了C++对象模型及其内存布局后,我们继续来探索一下C++对象的默认构造函数。对于C++的初学者来说,有如下两个误解:

  • 任何class如果没有定义default constructor,就会被合成出来
  • 编译器合成出来的default constructor会显示设定“class内每一个data member的默认值“

如果读者对这两句话理解颇深,了解里面的陷阱,那么可以不必阅读下去;倘若你有一点点疑惑,那非常好,跟着我一起继续下去!

无用(trivial)的构造函数

一如前两篇的风格,先以一个例子来抛出一些疑惑,让读者带着问题来阅读,想来会事半功倍。如下面的程序代码,我定义了一个Cat类,里面包含两个参数,一个int型变量age和一个char型指针name。

class Cat{
public:
    int age;
    char* name;
};

然后,在运行如下的测试代码:

int main(){
    Cat cat;
    cout<<“age=”<<cat.age<<endl;
    cout<<"name="<<(long long*)cat.name<<endl;
}

上述测试代码输出如下:

age=4196864
name=0x400800

我们来看看输出,age和name输出的都是一些随机值。在分析输出之前,先理解如下两个概念

  • ”程序员的需要“:程序员希望默认的构造函数能初始化类实例中所有的data members
  • “编译器的需要”:保证在创建类的实例的时候,存在一个构造函数,使得实例能创建,程序能正常运行。

如果按照程序员的需要,age和name会被初始化,可是编译器并没有那么智能,猜不透程序员的想法,它给定的输出就是一堆随机值。

类似于这种编译器并没有按照”程序员的需要“对每个成员变量进行初始化,而是仅仅满足了”编译器的需要“,而默认生成的构造函数就被称为trivial constructor。

那么,编译器会在什么时候能按照”程序员的需要“,对类里面的data members初始化呢?请继续往下看。

有用(non-trivial)的构造函数

所谓的non-trivial constructor就是:编译器在合成构造函数的时候会对类中的data mambers初始化,从而满足”程序员的需求”。还是那个疑问,什么时候编译器会自动合成一个这样的构造函数呢?下面分四种情况来一一说明。

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

如果一个类没有任何constructor,但它内含一个member object,后者还有一个default constructor,那么,编译器就会自动合成一个non-trivial constructor函数,不过这个合成操作只有在构造函数真正需要被调用的时候才合成。观察以下代码:

class Cat{
public:
    Cat():age(0){
        cout<<"Cat has been initialized"<<endl;
    }
    int age;
};
class Animal{
public:
    Cat cat;
};
int main(){
    Animal animal;
}

在Animal类中声明了一个Cat实例,Cat类中有自己的空构造函数。接着我们可以运行一下这段代码,输出如下:

Cat has been initialized //cat类对象被构造

从输出中可以看出,我们在main函数中声明了一个animal实例,然后animal类构造出了cat实例。可见,如果类的成员有其默认的构造函数,编译器就会合成一个non-trivial constructor,合成的空构造函数应该如下所示:

Animal(){
    Cat::Cat();
}

如果一个类内含一个或多个成员类对象,那么编译器为这个类合成的默认构造函数会一次调用成员类的空构造函数,其调用顺序是声明顺序决定的。那么,如果类自己定义了空的构造函数怎么办?很简单,编译器会将这些成员类的构造代码安插在空构造函数里面,其摆放顺序也是按照声明顺序。

如以下测试代码:

class Dog{
public:
    Dog(){
        cout<<"Dog has been initialized"<<endl;
    }
};
class Cat{
public:
    Cat():age(0){
        cout<<"Cat has been initialized"<<endl;
    }
    int age;
};
class Animal{
public:
    Animal(){weight=0;}//按照声明顺序,cat和dog的构造会在weight之前
    Cat cat;
    Dog dog;
    int weight;
};
int main(){
    Animal animal;
}

该测试代码的输出如下:

Cat has been initialized //先构造cat类对象,因为声明顺序在前
Dog has been initialized //再构造dog类对象

带有默认构造函数的基类

如果一个派生类没有任何自定义构造函数,但它的基类有默认的构造函数,那么派生类中默认合成的构造函数中会安插基类的构造函数代码(按照声明顺序)。

如下例所示:

class Animal{
public:
    Animal(){
        cout<<"Animal has been initialized"<<endl;
    }
};

class Cat : public Animal{
public:

};
int main(){
    Cat cat;
}

输出如下:

Animal has been initialized //父类构造函数的输出

输出结果显示子类合成的构造函数调用了父类的空构造函数。在继承关系中,我们知道构造顺序是先构造父类,如果有多个父类就按照声明的继承顺序依次构造,然后在构造子类。编译器在父类拥有空构造函数的情况下,会合成一个non-trivial constructor并在里面按顺序安插父类的空构造函数调用代码。

那如果子类声明了多个构造函数,唯独没有空构造函数怎么办呢?编译器会合成一个空的构造函数吗?答案是否定的,编译器不会为它合成空构造函数,而是将父类的构造函数或者类对象的空构造函数依次安插在每个声明的构造函数内。

上面一堆话可能比较绕口,举个小例子就很好理解了,请看下面的代码:

class Dog{
public:
    Dog(){
        cout<<"Dog has been initialized"<<endl;
    }
};

class Animal{
public:
    Animal(){
        cout<<"Animal has been initialized"<<endl;
    }
};

class Cat : public Animal{//Cat类继承于Animal类,并在类里面声明了dog类的实例
public:
    Dog dog;
    Cat(int a){//Cat类里面没有空构造函数
        age = a;
    }
private:
    int age;
};

int main(){
    //Cat cat;  //如果这样写的话,编译器会报错,因为编译器根本没有为Cat类合成空构造函数
    Cat cat(1);
}

上述代码的输出如下:

Animal has been initialized  //先构造基类
Dog has been initialized    //再构造类中声明的类对象

所以,我们可以理一下编译器安插的构造函数的顺序:

  • 如果有基类,且基类有空构造函数,就按照基类的继承顺序依次构造
  • 如果声明的成员变量有空构造函数,就按照声明顺序依次构造

带有一个虚函数的类

C++对象模型的那些事儿(一)中讲到,如果类含有虚函数,那么编译器会生成一个虚指针,虚指针指向含有虚函数地址的虚表。因此,如果一个类还有虚函数的话,那么编译器自然会生成一个空的构造函数,用来初始化虚指针。

于是,下面两个扩张行动就会在编译期间发生:

  • 一个虚函数表会被编译器产生出来,里面存放这虚函数的地址
  • 在每一个类对象中,一个额外的虚表指针会被编译器合成出来,其指向上述生成的虚表
typedef void(*Func)(void);
class Animal{
public:
    virtual void eat(){
        cout<<"Animal eat"<<endl;
    }
};

int main(){
    Animal animal;//调用空构造函数
    Fun pfun1 = (Fun)*((long long*)*(long long*)(&animal));
    pfun1();//执行函数,输出Animal eat
}

在上例中,main函数中定义一个Animal类的对象animal,调用了编译器合成的空构造函数,接下来我们通过强制类型转换,首先提取虚表指针的地址,然后提取虚表的地址,将虚表的第一个函数赋给一个函数指针,最后运行该函数,输出Animal eat。因此,编译器确实按照上述两个步骤合成了一个non-trivial的空构造函数。

带有一个虚基类的子类

虚基类的实现方法有点复杂,我们首先来看看下面这个例子:

class Animal{
public:
    int weight;
};

class Livestock : public virtual Animal{
public:
    int color;
};

class Canidae : public virtual Animal{
public:
    int age;
};
class WatchDog :  public  Canidae , public  Livestock{
public:
    int breed;
};

其中,Animal是一个基类,Livestock和Candiae类虚继承于Animal类,WatchDog类继承于Livestock和Candiae类。基于上述继承关系,我们执行一下测试代码;

void setWeight(const Animal* a){//运行时绑定。在编译期间无法决定a->Animal::weight的位置
    a->weight = 100;
}
int main(){
    setWeight(new Livestock);
    setWeight(new WatchDog);
}

针对上述测试代码,编译器无法固定setWeight()中的a->weight的实际偏移位置,因为a的真正类型都没有确定,所以必须是在确定传入的指针类型之后才能确定,也称为运行时绑定。

所有经由引用或者指针来存取一个虚基类的操作都可以通过指针来完成,那么,这个虚基类中weight变量的位置是如何确定的呢?想必你心里已有答案,没错

  • 对于类中所有定义的每一个构造函数,编译器会安插那些“允许每一个虚基类的执行器存取操作”(如上述的a->weight)的代码
  • 如果类没有任何构造函数,编译器必须为其合成一个空构造函数

总结&结束语

本文通过测试代码一一测试了编译器在什么时候会产生trivial和non-trivial构造函数。现做如下整理。

生成non-trivial构造函数的情况:

  • 带有默认构造函数的成员类对象
  • 带有默认构造函数的基类
  • 带由虚函数的类
  • 带有虚基类的类

除这四种情况外,并且没有声明任何构造函数的类,编译器会在“需要的时候”为其构造一个隐式的trivial构造函数,它们实际上并不会被合成出来。

这里的“需要的时候”只需要用到空构造函数的时候,如调用空构造函数构造一个类对象。如果没有用到空构造函数的时候,是不会被合成出来的

在合成的空构造函数中,只有基类子对象和成员类对象会被初始化。所有其他的nonstatic data member(如int,int*,int数组等)都不会被初始化。这些初始化虽然对程序而言或许有需要,但对编译器则非必要。

所以,请注意!如果你需要把某指针初始化为null这类操作,最好老老实实自己写构造函数来初始化,不要指向编译器能帮你干这些事!

现在!请回过头去看前言中的两句话,是不是突然就意识到没有一个是对的。

About Me

由于本人也是初学,在写作过程中,难免有错误的地方,读者如果发现,请在下面留言指出。

最后,如有疑惑或需要讨论的地方,可以联系我,联系方式见我的个人博客about页面,地址:About Me

另外,本人的第一本gitbook书已整理完,关于leetcode刷题题解的,点此进入One day One Leetcode

欢迎持续关注!Thx!

C++对象模型的那些事儿之三:默认构造函数的更多相关文章

  1. C++对象模型的那些事儿之四:拷贝构造函数

    前言 对于一个没有实例化的空类,编译器不会给它默认生成任何函数,当实例化一个空类后,编译器会根据需要生成相应的函数.这类函数包括一下几个: 构造函数 拷贝构造函数 析构函数 赋值运算符 在上一篇博文C ...

  2. C++对象模型——默认构造函数的合成

    最近在学习C++对象模型,看的书是侯捷老师的<深度探索C++对象模型>,发现自己以前对构造函数存在很多误解,作此笔记记录. 默认构造函数的误解 1.当程序猿定义了默认构造函数,编译器就会直 ...

  3. 【C++对象模型】构造函数语意学之一 默认构造函数

    默认构造函数,如果程序员没有为类定义构造函数,那么编译器会在[需要的时候]为类合成一个构造函数,而[需要的时候]分为程序员需要的时候和编译器需要的时候,程序员需要的时候应该由程序员来做工作,编译器需要 ...

  4. C++对象模型(一):The Semantics of Constructors The Default Constructor (默认构造函数什么时候会被创建出来)

    本文是 Inside The C++ Object Model, Chapter 2的部分读书笔记. C++ Annotated Reference Manual中明确告诉我们: default co ...

  5. C++对象模型的那些事儿之五:NRV优化和初始化列表

    前言 在C++对象模型的那些事儿之四:拷贝构造函数中提到如果将一个对象作为函数参数或者返回值的时候,会调用拷贝构造函数,编译器是如何处理这些步骤,又会对其做哪些优化呢?本篇博客就为他家介绍一个编译器的 ...

  6. C++ 合成默认构造函数的真相

    对于C++默认构造函数,我曾经有两点误解: 类如果没有定义任何的构造函数,那么编译器(一定会!)将为类定义一个合成的默认构造函数. 合成默认构造函数会初始化类中所有的数据成员. 第一个误解来自于我学习 ...

  7. C++默认构造函数的一点说明

    大多数C++书籍都说在我们没有自己定义构造函数的时候,编译器会自动生成默认构造函数.其实这句话我一直也是 深信不疑.但是最近看了一些资料让我有了一点新的认识. 其实我觉得大多数C++书籍之所以这样描述 ...

  8. C++编译器会对没有构造函数的类生成默认构造函数吗?(有必要的时候才生成,要看情况。有反汇编验证)

    之前在上C++的课的时候,印象中有那么一句话:如果一个类没有任何构造函数,那么编译器会生成一个默认的构造函数 今天在看<深度探索C++对象模型>的第二章:“构造函数语意学”的时候发现之前听 ...

  9. C++的默认构造函数与构造函数

    今天看书,忽然发现自己对默认构造函数/构造函数的理解很模糊,在实际项目中写类时,这些细节问题并没有涉及到.因此,就专门对着<C++ Primer Plus>将默认构造函数/构造函数这一块简 ...

随机推荐

  1. 51Nod 1753 相似子串

    题目大意: 两个字符串相似定义为: 1.两个字符串长度相等 2.两个字符串对应位置上有且仅有至多一个位置所对应的字符不相同 给定一个字符串,每次询问两个子串在给定的规则下是否相似.给定的规则指每次给出 ...

  2. POJ 3045 Cow Acrobats

    Description Farmer John's N (1 <= N <= 50,000) cows (numbered 1..N) are planning to run away a ...

  3. HNOI2002 营业额统计(Splay Tree)

    题目:http://www.lydsy.com/JudgeOnline/problem.php?id=1588 题意: Tiger最近被公司升任为营业部经理,他上任后接受公司交给的第一项任务便是统计并 ...

  4. [BZOJ]4650 优秀的拆分(Noi2016)

    比较有意思的一道后缀数组题.(小C最近是和后缀数组淦上了?) 放在NOI的考场上.O(n^3)暴力80分,O(n^2)暴力95分…… 即使想把它作为一道签到题也不要这么随便啊摔(╯‵□′)╯︵┻━┻ ...

  5. 浅谈java中内置的观察者模式与动态代理的实现

    一.关于观察者模式 1.将观察者与被观察者分离开来,当被观察者发生变化时,将通知所有观察者,观察者会根据这些变化做出对应的处理. 2.jdk里已经提供对应的Observer接口(观察者接口)与Obse ...

  6. 软件测试人员在工作中如何运用Linux

    从事过软件测试的小伙们就会明白会使用Linux是多么重要的一件事,工作时需要用到,面试时会被问到,简历中需要写到. 对于软件测试人员来说,不需要你多么熟练使用Linux所有命令,也不需要你对Linux ...

  7. YOLO: 3 步实时目标检测安装运行教程 [你看那条狗,好像一条狗!]

    封面图是作者运行图,我在 ubuntu 环境下只有文字预测结果. Detection Using A Pre-Trained Model 使用训练好的模型来检测物体 运行一下命令来下载和编译模型 gi ...

  8. String,StringBuilder,StringBuffer三者的区别

    参考   String,StringBuilder,StringBuffer三者的区别 这三个类之间的区别主要是在两个方面,即运行速度和线程安全这两方面. 1.运行速度 首先说运行速度,或者说是执行速 ...

  9. WebService接口与HTTP接口的联系

    1 WebService有很多协议,为什么HTTP比较流行? WebService是个很重型的规范,它的应用协议是SOAP(简单对象访问协议),它所依赖的下层通信方式不单单是HTTP,也有SOAP o ...

  10. JS多个对象添加到一个对象中

    var obj1 = {"qq":10}; var obj2={"mm":2,"nn":3}; var obj3={"xx&quo ...