最近在学习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. Zigbee学习

    (一)Zigbee简介和开发环境快速建立(IAR) 1.我不是很清楚控制链条,对于Zigbee不是太清楚 答案:CC2530 芯片上集成了 8051 内核(增强型) 2.性能特点:低速率远距离,这造就 ...

  2. 关于python环境的一些安装设置

    操作系统Redhat Linux,自带python2.6.Python程序的运行其实相当简单,只需在操作系统中安装并配置好python环境即可,和运行java需要配置jre一样(哪里简单,真简单就不会 ...

  3. ubuntu下如何修改时区和时间

    1.修改时区 sudo tzselect (按提示选择即可) sudo cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 2. 修改时间 sudo ...

  4. JavaScript:正则表达式 前瞻 找位置

    js中全部都是顺序环视 顺序环视匹配过程 对于顺序肯定环视(?=Expression)来说,当子表达式Expression匹配成功时,(?=Expression)匹配成功,并报告(?=Expressi ...

  5. UVA 10462 Is There A Second Way Left?(次小生成树&Prim&Kruskal)题解

    思路: Prim: 这道题目中有重边 Prim可以先加一个sec数组来保存重边的次小边,这样不会影响到最小生成树,在算次小生成树时要同时判断次小边(不需判断是否在MST中) Kruskal: Krus ...

  6. Is it bad to rely on foreign key cascading? 外键 级联操作

    Is it bad to rely on foreign key cascading? I'll preface前言 this by saying that I rarely delete rows ...

  7. 【命令】Linux常用命令

    常用指令 ls 显示文件或目录ls -f 查看目录中的文件 ls -l 列出文件详细信息l(list) ls -a 列出当前目录下所有文件及目录,包括隐藏的a(all)ls *[0-9]* 显示包含数 ...

  8. CSS3 动画的一些属性

    定义式 @keyframes 动画名称{ from{ } to{ } } 调用式 动画类似函数,只定义不调用是没效果的,所以要配合调用式使用. animation: 动画名称 动画时间 延时 时间曲线 ...

  9. jerichotab 初始化页面显示tab页中的第一个

    tab初始化默认显示第一个内容,但是tab标签显示最后一个. 源代码: $.fn.initJerichoTab({ renderTo: '#consumable', uniqueId: 'jerich ...

  10. C# ashx接收ContentType="text/xml"类型值

    public void ProcessRequest(HttpContext context) { context.Response.ContentType = "text/plain&qu ...