public 函数_构造函数语意学:构造函数

“关于C++,最常听到的一个抱怨就是,编译器背着程序员做了太多事情。

C++ Annotated Reference Manual(ARM)告诉我们:“默认构造函数在需要的时候被编译器产生出来”。什么是“被需要”?被谁需要?干什么?

下面这段代码来看一下:

class Base{ public: int val; Base *pnext; };void Test(){    Base base;    if(base.val && base.pnext){......}}

这段程序要求在两个members都不为0时,做某些事情。正确的调用应该是在Base的构造函数里将其初始化为0。上述代码是不是ARM曾说的“在需要的时候”?并不是。要求两个members都不为0才执行任务是程序的需要,实现它是程序员的责任,但编译器却没有这样的要求,上述程序片段并不会为Base合成一个默认构造函数。

那么,到底什么时候才需要合成默认构造函数呢?在编译器需要的时候,此外,被合成出来的构造函数也只会执行编译器所需要的动作,换句话说,合成的默认构造函数函数并不会把两个members初始化为0.

C++ Standard[ISO-C++95] 的Section 12.1如是说:

对于 class X,如果没有任何的用户声明的构造函数,那么会有一个默认构造函数被隐式(implicit)声明出来......一个被隐式声明的默认构造函数将是一个浅薄无用的(trivial)构造函数

什么时候需要non-trivial的默认构造函数?下面分四种情况来说明下。

01

内含[带有默认构造函数的类]的类

内含指的是一个类的实例化对象,作为另一个类的数据成员,这种情况就叫做内含。不直观?来看下图:

5389ffa79fb382b43634464b9f238596.png

对应的代码形式为:

class Foo { public: int a; };class Bar { public: Foo foo; };

如果是多重内含的话,代码可以长这样:

class B1 { public: B1() { cout << "B1() Called" << endl; } };class B2 { public: B2() { cout << "B2() Called" << endl; } };class B3 { public: B3() { cout << "B3() Called" << endl; } };class Derived {public:    B1 b1;    B2 b2;    B3 b3;    Derived() { cout << "Derived() Called" << endl; }};

就拿上面的代码为例,如果Derived类内含member class object(即b1、b2、b3),那么Derived类将在每一个版本的构造函数中,调用每一个member class object的默认构造函数。编译器将扩张已经存在的,即显式的(explicit)构造函数,在用户代码前,插入隐式的(implicit)member class默认构造函数。

编译器扩张后的效果是什么样子的?

// 伪代码,不一定能编译通过class Derived {public:    B1 b1;    B2 b2;    B3 b3;    Derived() {        /* Compiler's implicit code begin*/        b1.B1::Dopey();        b2.B2::Sneezy();        b3.B3::Bashful();        /* Compiler's implicit code end*/        cout << "Derived() Called" << endl;    }};

Derived类的显式默认构造函数将隐式调用三个成员的显式默认构造函数。来看一下运行效果是不是这样:

void Test(){    cout <"**********Test() Called**********" <endl;    Derived d;    cout << "**********Test() Ended**********" << endl;}int main(){    Test();    return 0;}

7da557c8a12d5720e79552540f65a11c.png

实验证实了上述的结论。

02

带有默认构造函数的基类

基类与内含就是两个不同的概念了,基类说明两个类之间的关系是继承。它又分为四种情况:

一、若子类和基类都没有任何显式构造函数,则编译器将为子类和基类自动生成隐式构造函数

class B1 {};class B2 {};class B3 {};class Derived :  public B1, public B2, public B3 {};

来进行一下调用:

void Test() {    cout << "**********Test() Called**********" << endl;    Derived d;    cout << "**********Test() Ended**********" << endl;}int main() {    Test();    return 0;}

OK,什么也不会输出。这是因为,编译器在这种情况下,隐式地为四个类生成了默认构造函数。但没有东西输出,这是因为像上个程序一样输出函数调用信息,那是程序员的需求,却不是编译器成功实例化对象的需要。

二、若子类没有显式构造函数,派生自含有显式默认构造函数的基类,编译器将为子类合成隐式默认构造函数,并按照声明顺序调用基类显式默认构造函数。

如果类之间的关系满足上述约束,那么这个Derived类的默认构造函数会被视为nontrivial,因此需要由编译器进行合成。

class B1 { public: B1() { cout << "B1() Called" << endl; } };class B2 { public: B2() { cout << "B2() Called" << endl; } };class B3 { public: B3() { cout << "B3() Called" << endl; } };class Derived : public B1, public B2, public B3 {};void Test() {    cout << "**********Test() Called**********" << endl;    Derived d;    cout << "**********Test() Ended**********" << endl;}int main() {    Test();    return 0;}

来看一下运行结果:

19339cd7671460f1646d0ce9a63a4a92.png

为什么要强调“按照声明顺序”?因为C++类里初始化顺序只与声明顺序有关

看一下我把继承顺序调换一下:

class Derived : public B3, public B1, public B2 {};void Test() {    cout << "**********Test() Called**********" << endl;    Derived d;    cout << "**********Test() Ended**********" << endl;}

看一下运行结果:

f86a95acfebeabdc3acdb28cedcac0fe.png

很好,按照继承的声明顺序调用基类们的默认构造函数。

三、若子类有显式构造函数,但没有默认构造函数,派生自含有默认构造函数的基类,编译器将为子类的每一版构造函数隐式插入基类的默认构造函数。

class B1 { public: B1() { cout << "B1() Called" << endl; } };class B2 { public: B2() { cout << "B2() Called" << endl; } };class B3 { public: B3() { cout << "B3() Called" << endl; } };class Derived :  public B1, public B2, public B3 {public:    Derived(int) { cout << "Derived(int) Called" << endl; }};void Test() {    cout << "**********Test() Called**********" << endl;    Derived d(12);    cout << "**********Test() Ended**********" << endl;}

同样道理,如果设计者提供了多个构造函数,但是其中没有默认构造函数的话,编译器会扩张每一个显式构造函数,来调用所有必要的基类默认构造函数。

但是有一点:编译器不会再为子类合成一个新的隐式默认构造函数,因为设计者定义的构造函数已经可以满足需要。

来运行一下看看:

ff5665d171dc506abd79ee0f13877847.png

很好,满足结论。

四、若子类有显式构造函数,但是基类没有默认构造函数,则不能通过编译

代码如下:

class B1 { public: B1(int a) { cout << "B1(int) Called" << endl; } };class B2 { public: B2(int a) { cout << "B2(int) Called" << endl; } };class B3 { public: B3(int a) { cout << "B3(int) Called" << endl; } };class Derived :  public B1, public B2, public B3 {public:    Derived() { cout << "Derived() Called" << endl; }    // no default constructor exists for class "Dopey"};

结合第三点来看,这是因为,基类里面设计者已经提供了显式的带有int参数的构造函数,虽然没有默认构造函数,但是完全足够对基类进行实例化。这不满足手册提到的“在必要的时候生成隐式构造函数”的约束。

看一下编译结果:

acebb4bd549039bc9c2a5ec9d1c3b452.png

编译器说没有和B3::B3()匹配的构造函数,这也侧面印证了如果不显式调用基类的构造函数,那么将默认自动隐式调用基类的默认构造函数。

但是在这个例子里,基类没有默认构造函数,编译器也没有为基类进行自动合成隐式构造函数。所以编译不通过。

编译错误提示我们,应该传入含有一个参数的基类构造函数进行初始化,再次印证了我们说的“不满足'在必要的时候生成隐式构造函数'的约束”。

03

带有虚函数的类

若类含有显式构造函数,编译器为每个构造函数隐式安插vptr向虚函数表

若没有显式构造函数,编译器将隐式合成默认构造函数对vptr行初始化。

上述两个扩张行动,将在编译期间发生。

04

带有虚基类的类

编译器必须使virtual base class其每一个派生类对象中的位置能够在执行期准备妥当。例如下列代码中:

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; };// 编译时期无法reslove出 pa->X::i 的位置void foo(A *pa) { pa->i = 1024;}

上述代码中,真正指向的类型是可变的。因为C类也是一个A类。

编译器可能会进行如下转变:

void foo(A *pa) { pa->__vbcX->i = 1024; }

__vbcX是编译时期产生的指向virtual base class X的指针,在class object构造时期被完成。如果class没有显式声明构造函数,编译器要为其合成一个隐式默认构造函数,安插进允许每一个virtual base class得执行期存取操作的代码。

05

总结

有上述四种情况,会造成编译器必须为未声明构造函数的类合成一个隐式构造函数。。C++ Standard把这些合成出来的称为implicit nontrivial default constructors。被合成的构造函数只能满足编译器,而非程序的要求。至于不在这四种情况中又没有声明任何构造函数的类,它拥有的是implicit trivial default constructors,实际上并不会被合成出来。在合成的default constructors中,只有base class subobjects和member class objects会被初始化,其他的非静态数据成员均不会被初始化。这也导致了常有的两个误区:
  1. 任何class如果没有定义显式默认构造函数,都会被合成出来一个。

  2. 编译器合成的隐式默认构造函数会显式设定class里每一个数据成员的默认值。

上面的两个说法都是错的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值