我们通常会说当生命一个 class 时,如果我们不为该 class 指定一个 constructor,那么编译器会替我们实现一个 connstructor,那么这种说法一定对吗?

事实上,这是不对的。这个不是我说的,是深入理解C++对象模型说的,原话是:

C++新手一般有两个常见的误解:

  1. 任何 class 如果没有定义 default constructor,就会被合成出一个来。                                    
  2. 编译器合成出来的 default constructor 会明确设定 "class 内每一个 data member 的默认值。
 
首先解释第一点,只有四种情况才会导致 ”编译器必须为未声明 constructor 之 classes 合成一个 default constructor“。被合成出来的 constructor 只能满足编译器(而非程序)的需要。它们之所以能够完成任务,是借着 “调用 member object 或base class 的 default constructor” 或是 “为每一个 object 初始化其virtual function 机制或 virtual base class 机制”而完成。至于没有存在那四种情况,而又没有声明任何 constructor 的 classes,我们说它拥有的是 implicit trivial(浅薄而无能,没啥用的),它们实际上并不会被合成出来。

下面分别讨论上述四种合成 constructor 的情况:

情况一:一个 class 内含一个 member object,且其 member object 拥有 default constructor

class Foo { public: Foo(), Foo( int ) ... };
class Bar { public: Foo foo; char* str; }; //不是继承,是包含 void foo_bar(){
Bar bar; //Bar::foo 必须在此处初始化
//译注:Bar::foo是一个member object,而其 class Foo 拥有default constructor,符合本小节主题
if( str ) { } ...
}

此时 class Bar 内含一个 member object Foo,并且 Foo 有 default constructor Foo(),那么编译器就会为 Bar 合成一个 default constructor,看起来像这样:

//Bar 的 default constructor 可能会这样合成
//被 member foo 调用 class Foo 的 default constructor
inline
Bar::Bar(){
//C++伪码
foo.Foo::Foo();
}

再一次请你注意,被合成的 default constructor 只满足编译器的需要,而不是程序的需要,为了让这个程序片段能够正确执行,字符指针 str 也需要初始化,那么程序员可能会这么做:

Bar::Bar() { str = ; }  //程序员定义的 default constructor  

此时编译器还需要初始化 member object foo,但是由于 default constructor 已经被程序员明确定义出来了,编译器没办法合成第二个。编译器采取的行动是:“如果 class A 内含一个或一个以上的 member class object,那么 class A 的每一个 constructor 必须调用每一个 member classes 的 default constructor”,编译器会向用户程序员的 constructors 前面插入必要的 default constructor。插入后可能像这样:

//插入后的 default constructor
//C++伪码
Bar::Bar(){
foo.Foo::Foo(); //插入的 compiler code
str = ; //explicit user code
}

如果有多个 class member objects 都要求初始化操作,将如何做呢?C++会按照 “member objects 在 class 中的声明次序“ 来调用各个 consructors。这一点由编译器完成。并且如果某个 member object 的 default constructor 被程序员定义过了,它照样会被按顺序调用。如:

//程序员对 Snow_White 类所写的 default constructor,显示初始化一个 member object
Snow_White::Snow_White() : sneezy( ){
mumble =
}

它会被扩张为:

Snow_White::Snow_White() : sneezy( ) {
//插入 member class onject
//调用其 constructor
dopey.Dopey::Dopey();
sneezy.Sneezy::Sneezy();
bashfun.Bashfun::Bashful(); //explicit user code
mumble = ;
}

情况二:带有 “default constructor” 的 base class

类似的道理,如果一个没有任何 constructors 的 class 派生自一个 ”带有 default constructor“ 的 base class,那么这个 derived class 的 default constructor 会被是为 nontrivial,并且因此需要被合成出来,它将调用上一层 base classes 的 default constructor(根据它们的声明次序)。对一个后继派生的 class 而言,这个合成的 constructor 和一个 ”被明确提供的 default constructor” 其实没什么区别。
 
如果设计者提供多个 constructors(比如带 int 参数的 constructor),但其中都没有 default constructor 呢? 编译器会扩张现有的每一个 constructors,在前面插入 base class 的 default constructor。如果有成员 member object,同样按顺序调用。
 
 
 

情况三:带有一个 “virtual Function” 的 class

如果一个 class 声明或继承一个 virtual function,并且缺乏由用户声明的 constructors,编译器会详细记录合成一个 default constructor 的必要信息。如:

class Widget{
public:
virtual void filp() = ;
...
} void filp( const Widget& widget ) { widget.filp(); } //假设 Bell 和 Whistle 都派生自 Widget
void foo(){
Bell b;
Whistle w; filp( b );
filp( w );
}

下面两个扩张步骤在编译期间发生:

  1. 一个 virtual function table (在 cfront 中被称为 vtbl)会被编译器产生出来,内放 class 的 virtual functions 地址。
  2. 在每一个 class onject 中,一个额外的 pointer member(也就是 vptr)会被编译器合成出来,内含相关的 class vtbl 的地址。
此外,widget.filp() 的虚拟引发操作(virtual invocation)会被重新改写(实际上就是虚函数调用传入的 this 会变化),以使用 widget 的 vptr 和 vtbl 中的 filp() 条目:

//widget.filp() 的虚拟引发操作(virtual invocation)的转变
// (* widget.vptr[ 1 ] )( &widget )
其中:
  • 1 表示 filp() 在 virtual table 中的固定索引
  • &widget 代表要交给 “被调用的某个 filp() 函数实体” 的 this 指针
为了让这个机制发挥功效,编译器必须为每一个 Widget(或其派生类之)object 的 vptr 设定初始值,放置适当的 virtual table 地址,对于 class 所定义的每一个 constructor,编译器会插入类似前文所说的伪码来做这样的事情。对于那些什么都没有声明的 classes,编译器则就像本文主题,为它们合成一个 default constructor,以便正确的初始化每一个 class object 的 vptr。(你可以理解,vptr 必须赋初值,就像我们平时用的指针,不用是赋NULL一样)。
 
 

情况四:带有一个 ”virtual base class“ 的 class

virtual base class 的实现法在不同的编译器之间有极大差异。然后每一种实现法的共通点在于必须是 virtual base class 在其每一个 derived class object 中的位置,能够与执行期准备妥当。例如下面的代码中:

class X { public: int i; };
class A : public virtual X { public: int j; };
class B : public virtual X { public: double d; };
class C : public A, pulic B { public: int k; }; //无法在编译使其决定(resolve)出pa->X::i 的位置
void foo( const A* pa) { pa->i = ; } main()
{
foo( new A );
foo( new C );
// ...
}
编译器无法固定住 foo() 函数之中"经由 pa 而存取的 X::i“ 的实际偏移位置,因为 pa 的真正类型可以改边,编译器必须改变 ”执行存取操作“ 的那些码,使 X::i 可以延迟至执行期才决定下来。
做法通常是在 “derived class object 的每一个 virtual base classes 中安插一个指针”(相当于一个中间件)。所有“经由 reference 或 pointer 来存取一个 virtual base class” 的操作可以通过相关指针完成。在我的例子中,foo() 可以被改写如下,以符合这样的实现策略:

//可能的编译器转变操做,中间加了一层
void foo( const A* pa ) { pa->__vbcX->i = ; }
其中,__vbcX表示编译器产生的指针,指向 virtual base class X。__vbcX是在 class 构造期间完成的。对于 class 所定义的每一个 constructor,编译器会安插哪些 ”允许每一个 virtual base class 的执行期存取操作“ 的码。如果 class 没有声明任何 constructors,编译器必须为它合成一个 default constructor。(最后这点我的理解是,像这种多态指针不能固定下来,我们需要给它指一个方向,不能让它乱指,所以编译器会合成default constructor)。
 
 
好了,前面解释了唯有的四种编译器会为未声明 constructor 的 classes 合成一个 default constructor 的情况。现在解释一下这句:
  • 编译器合成出来的 default constructor 会明确设定 "class 内每一个 data member 的默认值
在合成的 default constructor 中,只有 base class subobject 和 member objects 会被初始化。所有其他的 nonstatic data member,如整数、整数指针、整数数组等等都不会被初始化,这些初始化操作对程序而言或许又需要,但对编译器则并非必要。如果一个程序需要 "把某指针设为 0” 的 default constructor,那么提供它的人应该是程序员。

c++ 2.1 编译器何时创建默认构造函数的更多相关文章

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

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

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

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

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

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

  4. C++默认构造函数的问题

    C++ defaul construct :缺省构造函数(默认构造函数) 定义:第一种   构造函数没有参数,即是 A()形式的 第二种   构造函数的全部参数由缺省值提供,A(int a=0,int ...

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

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

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

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

  7. C/C++ 关于默认构造函数

    前言: 在C++中,对于一个类,C++的编译器都会为这个类提供四个默认函数,分别是: A() //默认构造函数 ~A() //默认析构函数 A(const A&) //默认拷贝构造函数 A&a ...

  8. C++ //构造函数调用规则 //1.创建一个类,C++编译器会给每个类添加至少3个函数 //默认构造(空实现) //析构函数(空实现) //拷贝函数(值拷贝) //2.如果我们写了有参构造函数 编译器就不会提供默认构造函数 但是会提供拷贝构造函数 //3.如果我们写了拷贝函数 编译器就不再提供 默认 有参 构造函数

    //构造函数调用规则 #include <iostream> using namespace std; //1.创建一个类,C++编译器会给每个类添加至少3个函数 //默认构造(空实现) ...

  9. C++编译器何时为用户提供默认构造函数

    第一种是类成员中有成员是类对象,并且该成员的类含有默认构造函数,那么C++编译器会帮你给这个类也生成一个默认构造函数,用来调用其成员对象的构造函数,完成该成员的初始化构造.需要强调的是,如果这个成员的 ...

随机推荐

  1. js另类值交换

    当我们有a.b两个值,想要交换,通常是要声明第三个变量,但是我最近看到这样一种不用声明第三个变量的处理方法: var a=1,b=2; a=[b,b=a][0]; 其实还是在内存中开出了一个新的空间( ...

  2. 最近的阅读list

    fast rcnn 统一了sppnet和rcnn,将原来rcnn分stage的训练合为一个整体的stage,一次完成cls, regression的训练.引入两个loss函数,一个是用来进行cls的, ...

  3. 11、SpringBoot------定时任务

      开发工具:STS 代码下载链接:https://github.com/theIndoorTrain/Springboot/tree/52ef6c0c805913db1e66ed18671c322e ...

  4. SqlServer 连接GreenPlum问题处理

    一.SQL SERVER.GREENPLUM 1. SSIS安装 ▶ 安装SQL SERVER 2005 数据库之后,运行SSIS工具,建立包,建立完成之后,新建工作流,双击工作流之后无法进行编辑,并 ...

  5. CharSquence 接口的作用,多态以增强String

    CharSquence 接口的作用,多态以增强String,CharSquence 可以多态定义 String StringBuffer StringBuilder.

  6. 变量类型 ROWID 和 UROWID

    ROWID:    ROWID为该表行的唯一标识,是一个伪列,这个伪列可以用SELECT查看,但是不可以用INSERT, UPDATE来修改,不可以用DELETE来删除. UROWID: ROWID可 ...

  7. 解题:在下面画线的地方填任何代码,使得最终输出 'hello world',至少写五个不同思路的方案

    今天(已经好些天前了...),群里面(JS前端开发跳板6群[81501322])有个群友问了这样一个问题. 如题:在下面画线的地方填任何代码,使得最终输出 'hello world',至少写五个不同思 ...

  8. 牛客小白月赛2 E 是是非非 【尼姆博弈】

    链接:https://www.nowcoder.com/acm/contest/86/E来源:牛客网 题目描述 坎为水,险阳失道,渊深不测:离为火,依附团结,光明绚丽. 坎卦:水洊至,习坎:君子以常德 ...

  9. mysql5.6 配置 文件

    mysql 3306 主库配置文件 [client] port = 3306 default-character-set=utf8mb4 socket = /ssd/mysql/3306/tmp/my ...

  10. kickstart+pxe+tftp+ntp(http)自动化安装平台的搭建

    听说过"克隆"吗,对于这个科技术语20年前可能还很陌生,羊可以克隆,通过基因dna序列:机器同样可以克隆通过网络IP! 如下为自动化安装平台: PXE(preboot execut ...