Menu

C++ 构造函数:正确地初始化对象

构造函数是在对象创建时运行的特殊成员函数。学习默认构造函数、带参构造函数、拷贝构造函数和成员初始化列表,以及怎样避免对象只初始化了一半。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

构造函数是什么

在上一页中,你定义了类并给它们加上了成员变量。但一个刚创建的对象,如果你不设置它的成员,它们保存的就是那块内存里原有的垃圾值。构造函数解决了这个问题:它是一种特殊的成员函数,在对象创建的那一刻自动运行,它唯一的任务就是让对象处于有效且完全初始化的状态。

构造函数的名字和类名相同,并且没有返回类型,连 void 都没有。你从不直接调用它;每当一个对象被创建出来,编译器都会替你调用它。

没有参数的 Counter() 叫作默认构造函数,在你创建对象却不传任何实参时使用的就是它。

带参构造函数

不接受参数的构造函数没什么问题,但通常你会希望用特定的值来创建对象。带参构造函数接受实参,并用它们初始化成员。

一个类可以有多个构造函数,只要它们的参数列表不同就行,这就是把普通的函数重载用在了构造函数上。下面的 Point 既可以带坐标创建,也可以不带:

一个常见的坑:Point p(); 不会创建对象,编译器会把它理解为一个名为 p、返回 Point 的函数声明。要调用默认构造函数,写 Point p;(不加括号),或者用花括号写 Point p{};。

成员初始化列表

到目前为止,示例都是在构造函数体内部给成员赋值。对简单类型来说这行得通,但这不是正确的做法。等到函数体运行时,每个成员都已经被默认构造过了;函数体随后把这个结果丢掉,再赋一个新值上去。成员初始化列表会在函数体之前一步到位地直接初始化每个成员。

语法是在参数列表后面加一个冒号,然后写成对的 member(value):

对于 string 成员,这样还避免了先构造一个空字符串再赋值,初始化列表第一次就把它构造正确了。

初始化列表不只是一种优化;在下面三种情况下它是必需的,因为函数体只能赋值,不能初始化:

  • const 成员:const 一旦存在,你就不能再给它赋值。
  • 引用成员:引用在诞生时就必须绑定。
  • 类型没有默认构造函数的成员。
class Sensor {
    const int id;        // const member
    int& slot;           // reference member

public:
    Sensor(int sensorId, int& s) : id(sensorId), slot(s) {}
    // Trying to set id or slot in the body would not compile.
};

有一个细节需要知道:成员按照它们在类中声明的顺序初始化,而不是按照你在初始化列表里写的顺序。如果一个成员的初始化要读取另一个成员,起作用的是声明顺序;两者不一致,是使用尚未初始化的值的经典原因。

默认实参与委托构造函数

你并不总是需要写多个重载。默认实参让一个构造函数就能覆盖多种情况:省略某个实参,默认值就会补上:

把带默认值的带参构造函数和单独的 Point() 默认构造函数放在一起时要小心:对于 Point p;,编译器分不清该调用哪一个,会报告二义性。两种做法选一种就好。

如果你确实有多个构造函数共享同样的初始化步骤,委托构造函数(C++11)可以让一个构造函数调用另一个,而不必重复那段逻辑。把另一个构造函数写进初始化列表,就完成了“委托”:

拷贝构造函数

当你以另一个对象的副本来创建对象时,比如按值传递、作为返回值返回,或者写 Foo b = a;,运行的就是拷贝构造函数。它的签名接受同一类型的 const 引用:

ClassName(const ClassName& other);

如果你不写,编译器会生成一个默认的拷贝构造函数,逐个复制每个成员。对于只保存值(int、string、vector)的类,这正是你想要的,你不应该自己再写一个。

真正的大坑在下一章的内存部分:如果你的类拥有一个指向堆内存的裸指针,默认拷贝构造函数复制的是指针,而不是数据。于是两个对象最终指向同一块内存,两个都会尝试释放它。这就是 double free(重复释放)bug。经验法则是“三/五法则”(Rule of Three/Five):如果你写了自定义的析构函数,几乎肯定也需要自定义的拷贝构造函数(以及拷贝赋值运算符)。在现代 C++ 中,更干净的做法是持有一个 std::vector 或智能指针,这样编译器生成的拷贝就能直接正常工作。

还要注意,参数按引用传递是必需的,而不是可选的:如果拷贝构造函数按值接受实参,那么为了调用它自己,就得先复制这个实参,形成无限递归,根本无法编译。

下一步:析构函数

构造函数负责把对象建立起来;析构函数负责把它拆掉。当对象离开作用域或被 delete 时,它的析构函数会自动运行,这里正好用来释放对象持有的文件、网络连接或堆内存。下一页讲解析构函数如何工作、它到底在什么时候触发,以及它如何与构造函数配合,构成 C++ 强大的 RAII 模式。

常见问题

C++ 中的构造函数是什么?

构造函数是一种特殊的成员函数,名字和类名相同,没有返回类型。它在对象创建时自动运行,任务是在其他代码使用这个对象之前,让它处于一个有效且完全初始化的状态。

默认构造函数和带参构造函数有什么区别?

默认构造函数不接受参数,在你创建对象却不提供任何值时使用(Point p;)。带参构造函数接受参数,让调用者可以用指定的值初始化对象(Point p(3, 4);)。一个类可以同时拥有这两种构造函数,因为构造函数按参数列表重载。

C++ 中为什么要用成员初始化列表?

成员初始化列表(: name(n), age(a))在构造函数体运行之前直接初始化成员。对于 const 成员、引用成员以及没有默认构造函数的成员,它是必需的;它还避免了在函数体里赋值时那种先默认构造、再赋值的浪费。

Coddy 编程语言插图

和 Coddy 一起学编程

开始