最近在学习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. 写Java代码的一些小技巧

    写Java代码有三年多了,遇到过很多坑,也有一些小小的心得.特地分享出来供各位学习交流.这些技巧主要涉及谷歌Guava工具类的使用.Java 8新特性的使用.DSL风格开发.代码封装等技巧. 一.nu ...

  2. 一种新的技术,C++/CLI

    一.来源 在一个项目中,拿到了一个demo,看起来像是C#,又像是C++,部分截图如下 1.界面[C#的winform] 2.mian入口,是cpp 3.解决方案 二.猜测 一开始以为是C#工程,因为 ...

  3. Serv-U FTP服务器安装及使用图解教程

    Serv-U,是一种被广泛运用的FTP服务器端软件,支持3x/9x/ME/NT/2K等全Windows系列.可以设定多个FTP服务器.限定登录用户的权限.登录主目录及空间大小等,功能非常完备. 它具有 ...

  4. linux下有线网卡出现ADDRCONF(NETDEV_UP): eth0: link is not ready的解决方法

    一.背景 2018年5月24日,笔者的pc已经连续运转两天了,突然要使用有线网卡,却发现有线网卡无法正常工作,于是查看了一下内核日志: r8169 0000:05:00.0 eth0: link do ...

  5. centos7 + mysql5.7 tar包解压安装

    #卸载系统自带的Mariadb [root@hdp265dnsnfs ~]# rpm -qa|grep mariadb mariadb-libs--.el7.centos.x86_64 [root@h ...

  6. 关于使用jquery的Ajax结合java的Servlet后台判定用户名是否存在

    关于把AJAX加入到注册登录demo中去 2018年3月10日 19:21:23 第一次来SUBWAY真切地打代码. 这次的西红柿汤还是挺好喝的. index.jsp: <%@ page con ...

  7. mysql删除sql表添加别名及删除sql的注意事项

    本文为博主原创,未经允许不得转载: 根据平常的习惯,个人会将操作的表后面添加一个别名,无论是使用还是不使用的时候,均是为了 修改还是扩展sql的时候更加安全,方便,快捷. 今天在写删除的sql时,对表 ...

  8. 【转】Windows Server 2008 R2怎样设置自动登陆

    Windows Server 2008 R2是一款服务器操作系统,提升了虚拟化.系统管理弹性.网络存取方式,以及信息安全等领域的应用,Windows Server 2008 R2也是第一个只提供64位 ...

  9. JavaScript页面跳转的一些实现方法

    第一种 <script language=”javascript” type=”text/javascript”> window.location.href=”login.jsp?back ...

  10. Lua面向对象 --- 多态

    多态,简单的理解就是事物的多种形态.专业的术语说就是:同一个实现接口,使用不同的实例而执行不同的操作. 工程结构: BaseRoom.lua: BaseRoom = {} function BaseR ...