最近在学习C++对象模型,看的书是侯捷老师的《深度探索C++对象模型》,发现自己以前对构造函数存在很多误解,作此笔记记录。

默认构造函数的误解##

1.当程序猿定义了默认构造函数,编译器就会直接使用此默认构造函数####

来一个简单的栗子

class Student;
class School
{
public:
School(){}
...
Student students;
};

我们知道,一个对象,在定义的时候就一定会调用其构造函数。而在我们上面的默认构造函数,明显没有调用students的构造函数,所以,编译器绝对没有直接使用我们的默认构造函数。具体细节,请看下文,这里只提问题。

2.当程序猿没有定义构造函数的时候,编译器就会帮我们定义默认构造函数####

接下来,就让我们带着这些错误的看法来进入正文。

为什么要帮我们定义默认构造函数##

再来个简单的栗子

class Student
{
public:
Student(){} //有定义
void Study(); //只给出了声明,没有定义
...
};
void main()
{
Student stu;
//stu.Study(); //调用没有定义的函数
}

上面是一个可以编译,连接,运行的例子完整代码。其中,Study()函数只有声明,但是没有定义,但是却通过了编译?为什么呢?因为你没有用到它。即使,你将注释的那行代码取消注释,它也不会在编译期出错,只会等到连接的时候编译器才会提示错误。具体可以参考这篇博客,依样画葫芦。你也可以先不看,记住这样的话:编译器没有具体需要用到某个函数时(上面是因为代码中没有调用Study函数),这个函数可以没有实现。所以,你可以在你的代码中很不负责任地声明很多没有用的函数并且不对其中的任何一个进行实现。

回到我们的内容,“为什么要帮我们定义默认构造函数”,答案就是编译器要用到,但是你却没有给出明确定义。注意,这里不是程序需要,而是编译器需要。程序需要用到,是指我们希望class中的成员,基类等能够正常地值初始化。而编译器需要,是指,没有这个函数,编译连接工作就没办法正常地进行。

那么问题就来了,编译器具体什么时候有这个需求。

在四个需求下,编译器需要一个默认构造函数##

第一个需求####

如果一个class没有任何constructor,但它内含一个member object,而后者有default constructor,那么这个class的implicit default constructor就是“nontrivial",编译器需要为该class合成一个default constructor。不过,这个合成操作只有在constructor真正需要被调用时才会发生。

这里引用了书里的话。nontrivial的意思就是有用的。举个例子说明一下。

class Student
{
public:
Student(){}
...
};
class School
{
Student students; //不是继承,是内含
char* name;
...
};
void main()
{
int a;
School school; //合成操作在这里发生
}

上面的例子中,编译器为School类合成了一个default constructor,因为School中包含的Student具有默认的构造函数,而我们在构造School的时候,需要调用Student的默认构造函数,所以编译器就帮我们合成了一个大概样子如下的默认构造函数。

School::School()
{
students.Student();
}

注意:School::name初始化是程序的需求,而不是编译器的需求。所以,合成的构造函数不会完成对name的初始化。时刻分清,编译器的需求与程序的需求不是一回事。

回到上面的程序,编译器在main中的第二行代码中才进行了合成。还记得在上一部分中我们提到的那些只声明没有定义的函数,无独有偶,假如我们在上面的代码中没有实例化School,那么这个合成操作永远不会进行,因为编译器不需要!!!只有当需要用到这个默认构造函数的时候,编译器才会进行合成。

这里还有一个问题,假如我们自己定了构造函数,却没有调用内部对象的构造函数时,编译器还会合成一个新的构造函数吗?否。编译器只会在已有的构造函数里面插入”编译器需要“的代码。再来个简单的栗子。

class Student
{
public:
Student(){}
...
};
class School
{
public:
School(){name = NULL} //没有初始化students
Student students; //不是继承,是内含
Student students2;
char* name;
...
};
//编译器插入自己需要的代码,最后的构造函数类似如下
School::School()
{
//在用户自己的代码前插入,确保所有的对象在使用前已经初始化
students.Student();
students2.Student(); //存在多个对象,按照声明的顺序进行插入
name = NULL;
}

第二个需求####

如果一个没有任何constructor的class派生自一个"带有default constructor"的base class,那么这个derived class的default constructor会被视为nontrivial,并因此需要被合成出来。

这一点与第一个需求很相似。需要记住的有以下几点。

1.在derived class的constructor(已有或者合成)中,编译器除了插入member class object的constructor外,还会插入base class constructor。

2.base class constructor的调用时间在member class object之前。

第三个需求###

class声明(或继承)一个virtual function,当缺乏程序猿声明的constructor,编译器合成一个default constructor。

我们知道,virtual function在调用的过程中,具体的函数是在编译器是不可知的。比如

class Base
{
public:
Base();
virtual void Print();
};
class Derived:public Base
{
public:
Derived();
virtual void Print();
};
void Print(Base *para)
{
para->Print();
}
void main()
{
Base a;
Derived b;
Print(a); //调用Base::Print();
Print(b); //调用Derived::Print();
}

编译器如何得知调用哪一个Print()呢?当class中含有virtual function的时候,编译器会在class中插入“虚表指针",并在构造函数中进行初始化。虚表指针指向了虚函数表,里面记录了各个虚函数的地址。程序之所以能够实现多态,实际上就是因为调用虚函数的时候,动态地使用了这个表里面的地址。

回归一下正题,在这里要强调的是,当存在虚函数的时候,编译器需要构造虚表指针。所以,假如我们没有任何构造函数的时候,编译器就会合成一个默认构造函数,里面满足除了前面“第一个需求,第二个需求”外,还会在在Base class完成构造之后,完成虚表指针的初始化。假如我们已经定义了构造函数,那么就会在base class constructors之后,member initialzation list之前完成虚表指针的初始化。

第四个需求###

class派生自一个继承串链,其中有一个或更多的virtual base classes,当没有程序猿定义的constructor的时候,编译器就会合成一个默认构造函数。举个例子

class X
{
public:
int i;
};
class A:public virtual X{};
class B:public virtual X{};
class C:public A,public B{};
void Foo(const A *pa)
{
pa->i = 1024;
}
void main()
{
Foo(new A);
Foo(new C);
}

编译器没办法确切地知道“经由pa"而存取的X::i的实际偏移位置,因为pa的真正类型可以改变。编译器必须改变”执行存取操作“的那些代码,使X::i可以延迟至执行期才决定下来。对于class定义的每个constructor,编译器会安插那些”允许每一个virtual base class“执行期存取操作的代码。如何class没有声明任何constructor,编译器必须为它合成一个default constructor。

最后这一点的具体实现讲解起来是一个长的过程,限于个人目前理解不透彻,暂时先搁着。有兴趣的读者可以参考《深度探索C++对象模型》5.2节的内容。

C++对象模型——默认构造函数的合成的更多相关文章

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

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

  2. C++中默认构造函数中数据成员的初始化

    构造函数的任务是初始化数据成员的,在类中,如果没有显示定义任何构造函数,编译器将为我们创建一个构造函数,称为合成的默认构造函数,合成的默认构造函数使用与变量初始化相同的规则来初始化成员.即当类中的数据 ...

  3. C++对象模型的那些事儿之三:默认构造函数

    前言 继前两篇总结了C++对象模型及其内存布局后,我们继续来探索一下C++对象的默认构造函数.对于C++的初学者来说,有如下两个误解: 任何class如果没有定义default constructor ...

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

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

  5. C++关于编译器合成的默认构造函数

    有两个常见的误解: 1.任何类如果没有定义默认构造函数,就会被合成出一个来. 2.编译器合成的默认构造函数会显式地设定类内每一个数据成员的默认值. 对于第一个误解,并不是任何类在没有显式定义默认构造函 ...

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

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

  7. 合成的默认构造函数定义为delete的一种情况(针对C++11标准)

    1. 默认初始化 如果定义变量时没有指定初值,则变量会被默认初始化,此时变量被赋予了"默认值". 对于类类型的变量来说,初始化都是依靠构造函数来完成的.因此,即使定义某个类的变量( ...

  8. 【C++对象模型】构造函数语意学之二 拷贝构造函数

    关于默认拷贝构造函数,有一点和默认构造函数类似,就是编译器只有在[需要的时候]才去合成默认的拷贝构造函数. 在什么时候才是[需要的时候]呢? 也就是类不展现[bitwise copy semantic ...

  9. [C++]默认构造函数

    默认构造函数(default constructor)就是在没有显示提供初始化式时调用的构造函数.它由不带参数的构造函数,或者为所有的形参提供默认实参的构造函数定义.若个定义某个类的变量时没有提供初始 ...

随机推荐

  1. 《网络对抗》——逆向及Bof基础实践

    <网络对抗>--逆向及Bof基础实践 原理 利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数. 手工修改可执行文件,改变程序执行流程,直接跳转到g ...

  2. c++builder ZIP文件解压与压缩(ZLIB DLL调用)(转载 )

    转载:http://blog.csdn.net/goodai007/article/details/7414512 头文件:ZipAndFile.h //----------------------- ...

  3. 《Python程序设计(第3版)》[美] 约翰·策勒(John Zelle) 第 2 章 答案

    判断对错1.编写程序的好方法是立即键入一些代码,然后调试它,直到它工作.2.可以在不使用编程语言的情况下编写算法.3.程序在写入和调试后不再需要修改.4.Python 标识符必须以字母或下划线开头.5 ...

  4. Python3基础 file open 打开txt文件并打印出全文

             Python : 3.7.0          OS : Ubuntu 18.04.1 LTS         IDE : PyCharm 2018.2.4       Conda ...

  5. BZOJ 3339 && luogu4137 Rmq Problem / mex(莫队)

    P4137 Rmq Problem / mex 题目描述 有一个长度为n的数组{a1,a2,-,an}.m次询问,每次询问一个区间内最小没有出现过的自然数. 输入输出格式 输入格式: 第一行n,m. ...

  6. Visio 画图去掉页边距(图形四周的空白区域)的解决办法

    步骤如下: 1.打开Visio对象后在[文件]菜单[选项]菜单项上单击,然后单击[自定义功能区]. 2.在[自定义功能区]的主选项卡中找到[开发工具]复选框,打上勾,单击确定. 3.在Visio对象的 ...

  7. [bzoj 1260][CQOI 2007]涂色paint

    Description 假设你有一条长度为5的木版,初始时没有涂过任何颜色.你希望把它的5个单位长度分别涂上红.绿.蓝.绿.红色,用一个长度为5的字符串表示这个目标:RGBGR. 每次你可以把一段连续 ...

  8. window 环境下jdbc访问启用kerberos的impala

    最近,公司生产集群添加kerberos安全认证后,访问集群的任何组件都需要进行认证,这样问题来了,对于impala,未配置kerberos安全认证之前通过impala的jdbc驱动(impala-jd ...

  9. 利用JSP中的过滤器解决中文乱码问题

    首先我们创建过过滤器: package com.gbx; import java.io.IOException; import javax.servlet.Filter; import javax.s ...

  10. Linux环境下的定时任务(转载)

    今天做了个数据库的备份脚本,顺便系统得学习一下Linux下定时执行脚本的设置.Linux下的定时执行主要是使用crontab文件中加入定制计划来执行,设置比Windows稍微复杂一些(因为没有图形界面 ...