构造函数语义学:默认构造函数合成时机

2. 构造函数语义学

2.1 默认构造函数

一般情况下,很多人会有如下的两种理解:1. 任何类若没有定义默认构造函数,则会被编译器合成一个。2. 编译器合成出来的默认构造函数会显式的为类里的每一个数据成员指定默认值。而这两种说法,其实都是错的。如下所示的代码即可以否定上两点:
首先通过编译结果可看虽然在主函数里使用了Foo类,但是编译器在右侧的编译结果中并没有生成任何构造函数。且通过执行的返回值可以看到,编译器并不会为a,b指定任何的默认值,其所使用的值都是a,b被分配内存时,内存块里的遗留值。所以编译器在本质上只是为foo分配了相应的内存空间,虽然这里也可以说编译器为类Foo生成了构造函数,但是这个构造函数是implicit trival的,也就是不仅是隐式的,而且是无用的,从编译结果来看也是不存在的。
image.png
之所以这样,是因为为a,b分配指定的初始值,不是编译器的工作,而应该是程序员显式定义相应的构造函数来完成。而在一些情况下,编译器就会合成相应的构造函数,这些函数是隐式的,但是是trival的。下面分别叙述编译器会合成构造函数的四种时机:

  1. 含有被显式定义默认构造函数的类成员

如果一个类没有任何的构造函数,但是它内部含有一个类成员对象,且这个成员对象类型含有一个默认构造函数,则这个类的隐式默认构造函数就是有用的,编译器必须为这个类合成一个默认构造函数。且这个构造函数的合成只有在被调用时才会发生。
例如如下编译结果,只有当16行的Bar bar调用时,编译器会为Bar合成一个默认的构造函数,这个构造函数会在内部调用Foo的默认构造函数,但是注意它不会产生任何代码来初始化id,因为将对象foo初始化是编译器的责任,所以编译器需要调用foo(),但是为id初始化是程序员的责任,编译器不会将其初始化。
image.png
所以程序员应该写一个Bar的构造函数用于给id赋予指定值,这里对Bar类在10行新增了默认构造函数(缺省构造函数)。这时编译器的操作时:当一个类含有类对象成员时,编译器会为此类的每一个显式的构造函数插入一些代码,一般都是在本类的构造函数被调用之前,会先调用成员类的构造构造。
例如这里和上述相比可以发现会同样在16行调用成员类的默认构造函数,只不过和上版本相比,其紧接着会多17-18行代码用于执行id=0的操作。
image.png

上述经过编译器操作后,Bar的显式构造函数有可能如下代码块所示。

Bar() : foo(Foo()) { id = 0; }

同时若本类含有多个类成员,则编译器为按照这些类成员对象被声明的顺序,依次调用每一个member所关联的默认构造函数。注意若有一个类成员被显式的调用了构造函数,则编译器不会对其再生成调用函数。

  1. 基类含有默认构造函数

当一个没有显式定义任何构造函数的类派生自一个带有默认构造函数的基类时,编译器会为这个子类合成相应的默认构造函数,这个构造函数会调用基类的默认构造函数。其特殊情况的分析也和第一种情况一样:

  • 当子类继承自多个基类时,编译器会按照其继承的顺序来调用基类的默认构造函数。
  • 对于子类的构造函数,若在其内部没有显式的调用基类的构造函数,则编译器也会在这些构造函数的初值列(initialization list)或花括号的起始部分增加基类构造函数的调用。
  1. 带有虚函数的类

当一个类声明或继承了一个虚函数,则编译器需要为这个类合成默认构造函数。如下的weight抽象类被bell类继承,bell类不含有任何构造函数。这时编译器会为其生成默认的构造函数,用于完成虚表和虚指针的构造:

  1. 在合成的构造函数中,一个虚表会被编译器生成出来,虚表内存储虚函数的地址。
  2. 对于每一个类对象,其也会包含一个额外的虚指针,指向类虚表的地址。

除此之外,虚函数的调用操作也会被重写,用于实现运行多态。例如widget.filp()的调用会被编译器重写为(*widget.vptr[1])(&widget),即在运行时动态根据widget的实际类型来查找实际的虚表,实现多态。

image.png

4. 继承自虚基类的类
虚基类的含义与作用可以参考如下:

可以看到,为了实现虚继承,必须定义相应的虚继承表用于指向虚基类在继承类中的偏移量,虚基类表与虚基类指针的设定都是在编译阶段由编译器生成相应的默认构造函数完成的。
虚函数表的作用:

总结:
在上述四种情况下,编译器必须为没有声明构造函数的类合成一个默认构造函数。这些构造函数被称为是implicit nontrival的,但是注意这些合成出来的构造函数只能满足编译器的需要,它的目的借助着调用类成员对象或类继承对象的构造函数来完成自己的构造,或是为类初始化虚函数表或虚继承表指定初始值。
除了上述四种情况外,没有声明任何构造函数的类,它们拥有的是implicit trival的构造函数,而且实际上它们并不会被合成出来。同时注意对于编译器合成出来的任何构造函数,它只会有效的完成自己的工作,例如初始化虚函数表、虚继承表、调用类对象构造函数、调用基类构造函数这样的行为。对于类内具有的整数、指针等,其是不会对其执行默认初始化的,对其进行默认初始化应该是程序员的任务。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值