C++ 初始化

最后更新: 2022-06-23 04:19:15 +0800 +0800
原文以英文撰写。您正在阅读AI翻译版本。

初始化为实体提供其第一个值,或建立引用绑定。赋值会修改已经存在的对象。由于构造函数只参与初始化,而赋值运算符只在生命周期开始后参与,因此二者之间的区别会影响重载决议、不变量和性能。

C 背景 #

C 声明包含声明符,每个声明符后面都可以跟随 = initializer。初始化器可以是赋值表达式或花括号初始化器列表;嵌套花括号用于初始化嵌套聚合体。

c
struct point { int x; int y; } points[] = {
    [0] = {.x = 1},
    [2] = {.y = 4},
};

C 聚合体初始化会将省略的元素视为具有静态存储期进行初始化:算术值变为零,指针值变为 null 指针,嵌套聚合体则递归初始化。相反,没有初始化器的自动存储期标量具有不确定值,在存储某个值之前不得读取。

C++ 形式与语法 #

C++ 允许声明符携带等号初始化器、花括号初始化器或圆括号初始化器。因此,以下声明属于不同形式:

cpp
T copy = expression;      // 复制初始化
T direct(expression);     // 直接初始化
T list{expression};       // 直接列表初始化
T copy_list = {expression}; // 复制列表初始化

braced-init-list 不是表达式,也没有类型。目标类型和初始化形式决定重载决议、聚合体规则和窄化检查。在声明中,T object() 是函数声明,而不是空对象;要声明对象,应使用 T object{};。

基本类别 #

零初始化 #

零初始化会将标量值设置为相应的零值,将指针值设置为 null 指针;数组则逐个元素处理。零初始化会作为静态存储期和线程存储期初始化的一部分发生,也会出现在某些值初始化路径中。它不是引用的独立写法,因为每个引用都必须绑定到对象或函数。

cpp
static int requests;      // 在动态初始化之前进行零初始化。
thread_local void* cache; // 在动态初始化之前为 null 指针。

默认初始化 #

不带初始化器的声明会对其对象进行默认初始化。类会调用所选的默认构造函数;数组会对每个元素进行默认初始化。具有自动存储期或动态存储期的标量不会获得初始化,因此在赋值之前不能安全地读取。

cpp
Widget widget; // 调用 Widget::Widget()。
int count;     // 没有初始值。

值初始化 #

值初始化在 T{} 或 T() 等上下文中使用空初始化器。标量会变为零。对于类类型,会选择默认构造函数;如果该构造函数不是用户提供的,则会在构造函数体执行前进行零初始化。数组会对每个元素进行值初始化。

cpp
int count{};
std::array<int, 4> flags{};

复制初始化、直接初始化和列表初始化 #

复制初始化允许隐式转换,但不会选择 explicit 构造函数。直接初始化可以选择 explicit 构造函数。直接列表初始化同样可以选择它,而如果复制列表初始化所选的构造函数是 explicit,则程序非良构。

cpp
struct Port {
    explicit Port(int);
};

Port a(443);     // 直接初始化。
Port b{443};     // 直接列表初始化。
// Port c = 443; // 错误:排除 explicit 构造函数。

列表初始化会拒绝窄化转换。它首先优先考虑可行的 std::initializer_list 构造函数,因此可能有意地产生与圆括号不同的结果。

cpp
std::vector<int> repeated(3, 7); // 7, 7, 7
std::vector<int> elements{3, 7}; // 3, 7
// int truncated{3.5};           // 错误:窄化转换。

当初始化器列表优先级不会造成语义不匹配时,应使用花括号来检查窄化转换。当类型有意区分数量/值构造函数与值列表时,应使用圆括号。

聚合体、数组和存储期 #

数组是聚合体。花括号聚合体初始化按声明顺序进行;省略的元素根据适用的空初始化器规则进行初始化。

cpp
int values[5]{1, 2}; // 其余元素为零。

struct Endpoint {
    std::string host;
    int port;
};
Endpoint local{.host = "localhost", .port = 443}; // C++20 指定初始化。

C++20 也允许使用圆括号进行聚合体初始化。与花括号聚合体初始化不同,圆括号形式允许窄化转换,并且不会延长绑定到引用元素的临时对象的生命周期。C++ 指定初始化器比 C 的限制更多:它们必须按声明顺序指定直接的非静态数据成员;C++ 没有数组索引指定器。

具有静态存储期或线程存储期的对象在声明满足条件时会进行常量初始化;否则,静态初始化会将其进行零初始化。所有静态初始化都会在动态初始化之前完成。constinit 要求静态初始化,但不会使对象成为 const;当满足其他要求时,constexpr 声明一个可用于常量求值的值。这些是存储和求值保证,并不是普通对象初始化的替代方案。

引用与生命周期 #

引用初始化会绑定一个别名;它不会创建第二个对象。绑定形式和值类别决定临时对象的生命周期是否会延长。

cpp
int value = 7;
int& writable = value;
const int& extended = 42; // 临时对象的生命周期与 `extended` 一样长。

通过 writable 进行赋值会修改 value,但不会重新绑定引用。应将初始化视为对象不变量开始生效的时点,并将赋值视为单独规定的状态转换。

评论

在 GitHub 上阅读或评论(发表评论需要登录)