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

  

对于C++默认构造函数,我曾经有两点误解:

  • 类如果没有定义任何的构造函数,那么编译器(一定会!)将为类定义一个合成的默认构造函数。
  • 合成默认构造函数会初始化类中所有的数据成员。

  第一个误解来自于我学习C++的第一本书 《C++ Primer》,在书中392页:“只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数”。

实际上这句话也没有说错,它说明了默认构造函数定义的必要非充分条件,然而却给当时初学C++的我造成了一定的误解。

  第二个误解依旧来自于Primer中的一句话:“合成的默认构造函数使用与变量初始化相同的规则来初始化成员。具有类类型的成员通过运行各自的默认构造函数来进行初始化”。然而这也是我理解的片面,因为Primer也说到了:“如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数”,言下之意就是合成的默认构造函数并不会初始化内置或复合类型的成员。

  总结了我有这些误解的原因,第一是初学时知识体系没形成,对Primer中所说的内容没有真正的理解,第二就是Primer在某种程度上的确不是C++初学者能看懂的书,或许看时觉得懂了,却是遗漏了很多知识。也说明了Primer 是座宝库,常常回顾将会有新的感悟。

  让我对上面两个观点产生疑惑,是在看《Effective C++》时,条款05《了解C++默认编写并调用哪些函数》中说到“….惟有当这些函数被需要(被调用),它们才会被编译器创建出来。” (“这些函数“指的是编译器版本的复制构造函数、赋值操作符和析构函数,还包括了默认构造函数。)也就是说,默认构造函数“被需要”的时候编译器才会帮我们合成,那什么情况才是默认构造函数”被需要“呢?这个问题《Effective C++》并没有给出答案,直到看了《深度探索C++对象模型》,才明白了编译器何时才会帮我们合成一个默认构造函数。

  我写这篇文章的目的是给和我有同样误解或疑惑的C++初学者看的,如果你对合成默认构造函数已有充分的认识,请忽略本文的内容。

正文

  • 什么是默认构造函数?

  默认构造函数是可以不用实参进行调用的构造函数,它包括了以下两种情况:

  1. 没有带明显形参的构造函数。
  2. 提供了默认实参的构造函数。    

  类设计者可以自己写一个默认构造函数。编译器帮我们写的默认构造函数,称为“合成的默认构造函数”。强调“没有带明显形参”的原因是,编译器总是会为我们的构造函数形参表插入一个隐含的this指针,所以”本质上”是没有不带形参的构造函数的,只有不带明显形参的构造函数,它就是默认构造函数。

  • 默认构造函数什么时候被调用?

   如果定义一个对象时没有提供初始化式,就使用默认构造函数。例如:

  

class A
{
public:
    A(bool _isTrue= true, int _num=10){ isTrue = isTrue; num = _num; }; //默认构造函数
    bool isTrue;
    int num;

};
int main()
{
    A a; //调用类A的默认构造函数
}
  • 理解“被需要”这三个字

  前面提到在《Effective C++》中指出惟有默认构造函数”被需要“的时候编译器才会合成默认构造函数。关键字眼是”被需要“。被谁需要?做什么事情?像下面这段代码,默认构造函数”被需要“了吗?

class A
{
public:
    bool isTrue;
    int num;

};
int main()
{
    A a;
    if (a.isTrue)
        cout << a.num;
    return 0;
}

  你可能认为这里定义类对象a的时候没有提供参数且A没有定义默认构造函数,编译器肯定是合成了一个默认构造函数并调用它来初始化A的数据成员,实则不是。当你试图查看合成默认构造函数把数据成员num初始化为什么值的时候,你会发现编译器甚至都让你运行不了程序:

  当类只含有内置类型或复合类型的成员时,编译器是不会为类合成默认构造函数的,这种类并不符合”被需要“的条件,甚至当类满足“被需要”条件,编译器合成了默认构造函数时,类中内置类型与复合类型数据成员依然不会在默认构造函数中进行初始化。Primer中也有提到:“如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数“。

  上面代码中,默认构造函数”被需要“是对程序来说的,程序需要isTrue被初始化以便可以进行条件判断,需要num被初始化以便可以输出。然而这种需要并不会促使编译器合成默认构造函数。惟有被编译器所需要时,编译器才会合成默认构造函数。那怎样的类才是编译器需要合成默认构造函数的呢?

  总结:

  1.   合成默认构造函数总是不会初始化类的内置类型及复合类型的数据成员。
  2.    分清楚默认构造函数被程序需要与被编译器需要,只有被编译器需要的默认构造函数,编译器才会合成它。
  •  何时默认构造函数才会被编译器需要?

  以下四种情况的类,编译器总是需要默认构造函数完成某些工作:

1. 含有类对象数据成员,该类对象类型有默认构造函数。

  如果一个类没有任何构造函数,但是它含有一个类对象数据成员,且该类对象类型有默认构造函数,那么编译器就会为该类合成一个默认构造函数,不过这个合成操作只有在构造函数真正需要被调用的时候才会发生。举个例子,编译器将为类B合成一个默认构造函数:

  

class A
{
public:
    A(bool _isTrue=true, int _num = 0){ isTrue = _isTrue; num = _num; }; //默认构造函数
    bool isTrue;
    int num;

};
class B
{
public:
    A a;//类A含有默认构造函数
    int b;
    //...
};
int main()
{
    B b;    //编译至此时,编译器将为B合成默认构造函数
    return 0;
}

  被合成的默认构造函数做了什么事情?大概如下面这样:

B::B()
{
    a.A::A();
}

  被合成的默认构造函数内只含必要的代码,它完成了对数据成员a的初始化,但不产生任何代码来初始化B::b。正如上面所说,初始化类的内置类型或复合类型成员是程序的责任而不是编译器的责任。为了满足程序的需要,我们一般会自己写构造函数来对B::b进行初始化,像这样:

B::B()
{
    a.A::A(); //编译器插入的代码
    b = 0;      //显示定义的代码
}

    如果类中有多种类对象成员,则编译器按照这些类对象成员声明的顺序,在构造函数按顺序插入调用各个类默认构造函数的代码。

2.基类带有默认构造函数的派生类。

  当一个类派生自一个含有默认构造函数的基类时,该类也符合编译器需要合成默认构造函数的条件。编译器合成的默认构造函数将根据基类声明顺序调用上层的基类默认构造函数。同样的道理,如果设计者定义了多个构造函数,编译器将不会重新定义一个合成默认构造函数,而是把合成默认构造函数的内容插入到每一个构造函数中去。

3. 带有虚函数的类  

  类带有虚函数可以分为两种情况:

  1. 类本身定义了自己的虚函数
  2. 类从继承体系中继承了虚函数(成员函数一旦被声明为虚函数,继承不会改变虚函数的”虚性质“)。

  这两种情况都使一个类成为带有虚函数的类。这样的类也满足编译器需要合成默认构造函数的类,原因是含有虚函数的类对象都含有一个虚表指针vptr,编译器需要对vptr设置初值以满足虚函数机制的正确运行,编译器会把这个设置初值的操作放在默认构造函数中。如果设计者没有定义任何一个默认构造函数,则编译器会合成一个默认构造函数完成上述操作,否则,编译器将在每一个构造函数中插入代码来完成相同的事情。

4.带有虚基类的类

  虚基类的概念是存在于类与类之间的,是一种相对的概念。例如类A虚继承于类X,则对于A来说,类X是类A的虚基类,而不能说类X就是一个虚基类。虚基类是为了解决多重继承下确保子类对象中每个父类只含有一个副本的问题,比如菱形继承。如下图: 

于是,类A对象中含有一份类X对象,类C中也含有一份类X对象,当我们遇上如下代码时:

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, public B{ public: int k; };

void function(A *pa)
{
    pa->i = 1000;
}
int main()
{
    A *a= new A();
    C *c= new C();
    function(a);  //关注重点在这里
    function(c);     //关注重点在这里
    return 0;
}

  函数function参数pa的真正类型是可以改变的,既可以把A对象指针赋值给pa,也可以把对象指针赋值给pa,在编译阶段并无法确定pa存储的i是属于A还是C的虚基类对象。为了解决这问题,编译器将产生一个指向虚基类X的指针,使得程序得以在运行期确定经由pa而存取的X::i的实际存储位置。这个指针的安插,编译器将会在合成默认构造函数中完成,同样的,如果设计者已经写了多个构造函数,那么编译器不会重新写默认构造函数,而是把虚基类指针的安插代码插入已有的构造函数中。

  • 总结

  重新强调文章开篇所提,以下两个观点都是误解:

a)   任何类如果没有定义构造函数,则编译器会帮我们合成一个默认构造函数。

b)   合成默认构造函数会对类中的每一个数据成员进行初始化。

  只有在编译器需要默认构造函数来完成编译任务的时候,编译器才会为没有任何构造函数的类合成一个默认构造函数,或者是把这些操作插入到已有的构造函数中去。

编译器需要默认构造函数的四种情况,总结起来就是:

a)   调用对象成员或基类的默认构造函数。

b)   为对象初始化虚表指针与虚基类指针。

  PS:如果本文哪个地方阐述不清楚或者错误,十分期待指出,多谢!

时间: 2024-10-29 10:48:08

C++ 合成默认构造函数的真相的相关文章

C++编译器合成默认构造函数的条件(合成默认拷贝构造函数雷同)

当用户没有为类定义构造函数的时候,编译器并不是为每个类合都成默认的构造函数,只是在编译器需要的时候才合成默认构造函数.编译器合成默认的构造函数只是满足编译器的编译的需要,而不是满足程序员的需要(例如,成员变量的初始化工作还需要程序员来定义构造函数). 1.对象中包含带有默认构造函数的对象 class A{ public: A(); } class B{ public: A a; } 在组合中B需要编译器合成默认的构造函数,来调用A的默认构造函数,来定义(并不帮助程序员初始化)B自己的成员变量,内

C++编译器合成默认构造函数和复制控制成员(拷贝构造函数,赋值操作符,析构函数)的条件

(参考自<深入理解C++对象模型>) ”C++新手一般有两个常见的误解: 任何class如果没有定义default constructor,就会被合成一个出来. 编译器合成出来的default constructor会明确设定class 内每一个data member的默认值.” 现在主要解释第一条为什么是错误的,根据<深入理解C++对象模型>,”default constructor 在需要的时候被编译器产生出来”,以下就是4种”需要的时候”: 1). 该类含有一个成员对象而后者

编译器生成默认构造函数的情况

在类没有显示声明构造函数的情况下,编译器并不总是为我们自动生成默认构造函数,以下4种情况,编译器才会为我们自动生成默认构造函数: 1.类中有一个类成员含有默认构造函数的,编译器会为该类自动生成默认构造函数,自动插入代码,调用该成员的构造函数: 2.基类中含有默认构造函数,编译器会为该类自动生成默认构造函数,自动插入代码,调用基类的构造函数: 3.类中含有虚函数时,由于编译器要为该类生成虚函数表vtable,并为该类的对象生成指向该vtable的vptr,所以需要为该类合成默认构造函数: 4.虚继

默认构造函数的作用

// 首先定义一个No_default类. class  No_default { public: No_default (const string&);  // 声明了一个构造函数,但是却没有默认构造函数! private: int a; string b; }; // 定义第二个类,其中有一个No_default类型的成员. class  B { public: No_default   my_mem;   // 类B也没有构造函数! }; B  b;                    

C++的允诺 ---- 默认构造函数 真的如你所愿吗

首先,本篇文章只讲 "默认构造函数",即如你所知,默认构造函数是不带参数的构造函数. 编译器会在 适当的时候 为class合成一个默认构造函数 ~~ 先问以下两个问题: 编译器会为任何没有声明构造函数的class,合成默认构造函数?? 错!!! 合成的默认构造函数会显示设定class内的每一个data member的默认值?? 错!!! class Base { public: int x; int getX() const {return x;}; }; int main() { B

默认构造函数的构造操作

以下四种情况,编译器将会合成(即由编译器负责)一个nontrivial默认构造函数. 1.成员对象带有默认构造函数 例如: #include <iostream> using namespace std; class Foo { public: Foo() { cout << "Foo" << endl; } // Foo类含有默认构造函数 }; class Bar { public: Foo foo; int x; }; int main() {

【C++对象模型】构造函数语意学之一 默认构造函数

默认构造函数,如果程序员没有为类定义构造函数,那么编译器会在[需要的时候]为类合成一个构造函数,而[需要的时候]分为程序员需要的时候和编译器需要的时候,程序员需要的时候应该由程序员来做工作,编译器需要的时候则由编译器来做工作. C++中,全局变量 / 对象的内存会被清零(如果类对象没有程序员定义的构造函数的时候), 而堆heap 或 栈stack上的变量或对象则不会被清零,其内存只取决于上一次这段内存的值. 而编译器什么情况下会为类合成默认构造函数呢?会在以下四种情况合成默认的构造函数: 1.类

[C++11] 默认构造函数

类通过一个特殊的构造函数来控制默认初始化过程,这个函数就是默认构造函数,默认构造函数无需任何实参.我们可以显示的定义默认构造函数也可以让编译器为我们生成默认构造函数.默认构造函数以如下规则初始化累的数据成员: 如果存在类内初始值,用它来初始化成员. 否则,默认初始化该成员. class Sales_data { public: std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; }; 如上所示,因为Sales_da

【C++ Primer 第七章】 默认构造函数的作用

默认构造函数的作用 1.  默认构造函数 C++ 默认构造函数是对类中的参数提供默认值的构造函数,一般情况下,是一个没有参数值的空函数,也可以提供一些的默认值的构造函数,如果用户没有定义构造函数,那么编译器会给类提供一个默认的构造函数,但是只要用户自定义了任意一个构造函数,那么编译器就不会提供默认的构造函数,这种情况下,容易编译报错,所以正确的写法就是用户在定义构造函数的时候,也需要添加一个默认的构造函数,这样就不会造成编译报错. 如:用户自定义的默认构造函数 class Test { publ