当前位置: 首页 > 编程语言 > C++ > 正文

如何正确理解C++默认构造函数

时间:2017-02-15

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

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

合成默认构造函数会初始化类中所有的数据成员。

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

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

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

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

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

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

正文

什么是默认构造函数?

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

没有带明显形参的构造函数。

提供了默认实参的构造函数。

类设计者可以自己写一个默认构造函数。编译器帮我们写的默认构造函数,称为“合成的默认构造函数”。强调“没有带明显形参”的原因是,编译器总是会为我们的构造函数形参表插入一个隐含的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被初始化以便可以输出。然而这种需要并不会促使编译器合成默认构造函数。惟有被编译器所需要时,编译器才会合成默认构造函数。那怎样的类才是编译器需要合成默认构造函数的呢?

总结:

合成默认构造函数总是不会初始化类的内置类型及复合类型的数据成员。

分清楚默认构造函数被程序需要与被编译器需要,只有被编译器需要的默认构造函数,编译器才会合成它。