2.1 Default Constructor 的构造操作

本文深入探讨C++中默认构造函数的合成条件与工作原理,包括成员类对象、基类、虚拟函数及虚拟基类四种情况。分析编译器如何在需要时合成构造函数,以及构造函数在初始化成员变量和调用基类构造函数的角色。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

“default constructors...在需要的时候被编译器产生出来“。被谁需要?做什么事情?看如下代码:

class Foo{
public:
	int val;
	Foo* pnext;
};

void foo_bar()
{
	//程序要求bar’s members都被清零
	Foo bar;
	if(bar.val || bar.pnext)
		//...do something
	//...
}

例子中,正确的程序予以是要求Foo有一个default constructor,可以将它的两个members初始化为0。上述只会隐式声明出不会初始化对象的default constructor,初始化对象是程序员的责任。

对于class X而言,如果没有任何user-declared constructor,那么会有一个default constructor被隐式(implicitly)声明出来,一个被隐式声明出来的default constructor将是一个trivial(无用)constructor。

C++ Standard然后会一一叙述什么情况下这个implicit default constructor会被视为他trival。一个nontrivial default constructor在ARM语义中就是编译器需要的那种,必须的话会被编译器合成。下面4小结分别讨论nontrivial default constructor的4种情况。

“带有Default Constructor”的Member Class Object

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

于是有个问题:在C++各个不同的编译模块中,编译器如何避免合成出多个default constructor(比如一个在A.C文件合成,一个B.C文件合成)呢?解决方法是把合成的default constructor、copy constructor、destructor、assignment copy constructor都以inline方法完成。一个inline函数有静态连接,不会被文件以外者看到。如果函数太复杂,不适合做inline,就会合成一个explicit non-inline static实例。

如下例子,下面的程序片段,编译器为class Bar合成一个default constructor:

class Foo{
public:
	Foo();
	Foo(int);
	...
};

class Bar
{
public:
	Foo foo;
	char* str;
};

void foo_bar
{
	Bar bar;//Bar::foo必须在此处出书哈
	if(str)
	{
		
	}
	...
}

被合成的Bar 的default constructor内含必要的代码,能够调用class Foo的default constructor来处理member object Bar::foo,但它不会产生任何代码来处理Bar::str,将Bar::foo初始化时编译器的责任,将Bar::str初始化时程序员的责任。被合成的default constructor看起来像这样:

Bar::Bar()
{
	//C++伪代码
	foo.Foo::Foo();
}

被合成的default constructor只满足编译器的需要,而不是程序的需要。如果程序员在default constructor中初始化了str,编译器还是要初始化member object foo。由于default constructor已经被显示定义出来了,编译器无法合成第二个。

编译器采取的行动是“如果class A内含一个或一个以上的member class object,那么class A的每一个constructor必须调用每一个member classes的default constructor”。编译器会扩张已存在的constructors,在其中安插代码,使得user code被执行之前,先调用必要的default constructor。

如果有多个class member objects都要求constructor初始化操作,C++语言要求“member objects 在class中的声明顺序”来调用各个constructors。这一点由编译器你完成,它为每一个constructor安插代码,以“member声明顺序”调用每一个member所关联的default constructors。这些代码将被安插在explicit user code之前。

如果像上面的有explicit default constructor,程序员只初始化了内置的成员,那么编译器还是要在explicit default constructor中安插初始化member object的代码,按照它们声明的顺序来安插、

“带有Default Constructor”的Base Class

类似的道理,如果一个没有任何constructors的class派生一个“带有default constructor”的base class,那么这个derived class的default constructor会被视为nontrivial,并因此需要被合成出来。它将调用上层的base classes的default constructor(根据它们的声明顺序)。对一个后继派生的class而言,这个合成的constructor和一个“被显示提供的default constructor”没有什么差异。

如果设计者提供多个constructors,但其中没有default constructor,编译器会扩展每一个现在的constructor,将“用以调用所有必要之default constructors”的程序代码加进入。它不会合成一个新的default constructor,因为其他“由user所提供的constructors”存在的缘故。如果同时亦存在“带有default constructor”的member class objects,那么default constructor也会被调用——在所有base classconstructor都被调用之后。

“带有一个Virtual Function”的Class

另有两种情况,也需要合成default constructor:

  1. class声明(或继承)一个virtual function。

  2. class派生自一个继承串链,其中有一个或更多的virtual base classes。

不管哪种情况,由于缺乏由user声明的constructors,编译器会详细记录合成一个default constructor的必要信息。如下的程序片段:

class Widget
{
public:
	virtual void flip() = 0;
	//...
};

void flip(const Widget& widget)
{
	widget.flip();
}

//假设Bell和Whistle都派生自Widget
void foo()
{
	Bell b;
	Whistle w;
	
	flip(b);
	flip(w);
}

下面两个扩张会在编译器间发生:

  1. 一个virtual function table会被编译器产生出来,内放class的virtual function地址上。

  2. 在每一个class object中,一个额外的pointer member(也就是vptr)会被编译器和好处能出来,内含相关的vtbl的地址。

此外,widget.flip()的虚拟调用操作(virtual invocation)会被重写。以使用widget的vptr和vtbl的flip()条目:

//widget.flip()的虚拟调用操作的转变
(*widget.vptr[1])(&widget);
  • 1表示flip()所在的virtual table中的固定索引;
  • &widget代表要交给“被调用的某个flip()函数实例”的this指针。

为了让这个机制发挥功效,编译器必须为每一个Widget(或其派生类的)object的vptr设定初值,放置适当的virtual table地址。对于class所定义的每一个constructor,编译器会安插一些代码来做这些事情。对于那些未声明的constructor的classes,编译器会为它们合成一个default constructor,以便正确初始化每一个class object的vptr。

“带有一个Virtual Base Class”的Class

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

class X
{
public:
	int i;
};

class A: virtual public X
{
public:
	int j;
};

class B: virtual public X
{
public:
	double d;
};

class C: public A,public B
{
public:
	int k;
};

//无法在编译器决定(resolve)出pa->X::i的位置
void foo(const A* pa)
{
	pa->i = 1024;
}

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 = 1024;
}

其中__vbcX表示编译器所产生的指针,指向virtual base class X。

正如你臆想的那样,__vbcX(或编译器所做出的某个什么东西)是在class object建构期间完成的。对于class所定义每一个constructor,编译器会安插那些“允许每一个virtual base class的执行期存取操作”的码。如果class没有声明任何constructors,编译器必须为它合成一个default constructor。

总结

有四种情况,会导致”编译器必须为未声明的constructor之classes合成一个default constructor“。C++ Stardand把那些合成物称为implicit nontrivial default constructors。被合成出来的constructor只能满足编译器(非程序)的需要。它之所以能完成任务,是借着”调用member object或base class的default constructor“或是”为每一个object初始化其virtual function机制或virtual base class机制“而完成。至于没有存在那四种情况又没有声明任何constructor的classes,我们说它们拥有的是implicit trivial default constructors,它们实际上并不会被合成出来。

在合成的default constructor中,只有base class subobjects和member class objects会被初始化。所有其他的nonstatic data member,如整数、整数指针、整数数组等等不会被初始化。这些初始化操作对程序而言或许是需要的,但是对编译器并非必要。如果程序需要一个”把某指针设为0“的default constructor,那么提供它的人应该是程序员。

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

  1. 任何class如果没有定义default constructor,就会被合成一个出来。
  2. 编译器合成出来的default constructor会明确设定”class 内每一data member 的默认值“。

如你所见,没一个是真的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值