跳到主要内容

C++ 初始化列表

“初始化列表”在 C++ 中常指两种相关但不同的东西:构造函数的成员初始化列表,以及用花括号进行的列表初始化。std::initializer_list 则是标准库提供的一种类型,常用于接收一组只读元素。它们名字相近,使用场景也有关联,但不能互相替代。

构造函数的成员初始化列表​

成员初始化列表写在构造函数参数列表之后、函数体之前,用来直接初始化基类和成员:

class Rectangle {
public:
Rectangle(int width, int height)
: width_(width), height_(height) {}

private:
int width_;
int height_;
};

冒号后面的 width_(width) 和 height_(height) 分别初始化对应成员。也可以使用花括号形式:

Rectangle(int width, int height)
: width_{width}, height_{height} {}

为什么要用成员初始化列表​

成员初始化列表会在构造函数体执行前初始化成员。若在函数体中赋值,成员会先被初始化,再执行赋值:

class Example {
public:
Example(int value) {
value_ = value; // value_ 先默认初始化,然后再赋值
}

private:
int value_;
};

下面的写法则直接用参数初始化 value_:

class Example {
public:
Example(int value) : value_(value) {}

private:
int value_;
};

这些成员必须在对象构造时完成初始化。const 成员之后不能再赋值,引用也必须在创建时绑定到对象;而没有默认构造函数的成员无法先被默认构造、再在构造函数体中赋值。因此,若没有其他初始化来源,就要在构造函数的成员初始化列表中初始化它们:

class Record {
public:
Record(int id, int& source)
: id_(id), source_(source) {}

private:
const int id_;
int& source_;
};

也可以用类内默认成员初始化器提供初始化:

int global_source = 0;

struct NonDefault {
explicit NonDefault(int) {}
};

class Record {
public:
Record() = default;

private:
const int id_{0};
int& source_ = global_source;
NonDefault item_{1};
};

构造函数没有在成员初始化列表中指定这些成员时,会采用对应的类内默认成员初始化器;若在构造函数初始化列表中显式指定,则使用显式指定的初始化。例子中的 item_ 没有默认构造函数,因此它的类内初始化器必须提供可用的构造参数。换言之,成员初始化列表不是唯一的初始化方式,但这些成员必须在构造阶段得到初始化,不能留到构造函数体里再赋值或绑定。

初始化顺序​

基类先于派生类成员初始化;同一个类的成员则严格按照它们在类中声明的顺序初始化,而不是按照成员初始化列表里的书写顺序:

class Pair {
public:
Pair(int value) : second_(value), first_(second_) {}

private:
int first_;
int second_;
};

上例中 first_ 会先于 second_ 初始化,所以 first_(second_) 读取的是尚未初始化的 second_,结果不可靠。应让声明顺序和依赖关系一致,并按同样顺序书写初始化列表:

class Pair {
public:
Pair(int value) : first_(value), second_(first_) {}

private:
int first_;
int second_;
};

委托构造​

一个构造函数可以把初始化工作委托给同一个类的另一个构造函数:

class Point {
public:
Point() : Point(0, 0) {}
Point(int x, int y) : x_(x), y_(y) {}

private:
int x_;
int y_;
};

委托构造函数的初始化列表中只能指定另一个构造函数,不能同时再列出数据成员。

花括号列表初始化​

C++11 起,可以用花括号初始化对象和变量:

int count{3};
Point origin{0, 0};

花括号初始化会拒绝可能丢失数据的窄化转换:

int value = 3.14; // 可以编译,但会丢失小数部分
// int other{3.14}; // 编译错误:窄化转换

花括号可能改变构造函数的选择​

如果类型提供了接收 std::initializer_list 的构造函数,花括号初始化会优先考虑这类构造函数。这会让容器的括号和花括号写法含义不同:

#include <vector>

std::vector<int> filled(3, 7); // 3 个元素,每个都是 7
std::vector<int> values{3, 7}; // 两个元素:3 和 7

选择括号还是花括号时,应确认目标类型的构造函数重载和预期含义。

std::initializer_list​

std::initializer_list<T> 是标准库类型,表示一组类型为 T 的只读元素。它常用于编写支持花括号传参的构造函数:

#include <initializer_list>

class IntList {
public:
IntList(std::initializer_list<int> values) {
for (int value : values) {
// 在这里处理每个 value
}
}
};

IntList numbers{1, 2, 3};

传入的元素不能通过 std::initializer_list 修改。若需要保存这些元素,通常要在构造函数中把它们复制到自己的容器中。

小结​

  • 构造函数的成员初始化列表负责直接初始化基类和成员,位于构造函数体之前。
  • 成员的实际初始化顺序由类中的声明顺序决定。
  • 花括号列表初始化能检查窄化转换,但也可能优先选择 std::initializer_list 构造函数。
  • std::initializer_list<T> 是一种只读元素列表类型,不等同于成员初始化列表。