public 函数_构造函数语意学:构造函数
发布日期:2021-10-30 18:55:17 浏览次数:3 分类:技术文章

本文共 5572 字,大约阅读时间需要 18 分钟。

“关于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**********" 

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里每一个数据成员的默认值。

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

转载地址:https://blog.csdn.net/weixin_39849239/article/details/111159937 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:光线追踪技术 清华大学 pdf_实时光线追踪技术:发展近况与未来挑战
下一篇:绝对值编码器选型手册.pdf_IOLink领域全球市场引领者编码器系列进一步扩充

发表评论

最新留言

网站不错 人气很旺了 加油
[***.192.178.218]2024年04月12日 08时37分21秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章