目录
- 统计速览
- 概念框架
- 默认初始化 default-initialization
- 值初始化 value-initialization
- 零初始化 zero-initialization
- 直接初始化 direct-initialization
- 拷贝初始化 copy-initialization
- 列表初始化 list-initialization
- 聚合初始化 aggregate-initialization
- 圆括号聚合初始化(C++20)
- 指定初始化器 designated-initializer
- 引用初始化 reference-initialization
- 成员初始化器列表 member initializer list
- 委托构造函数 delegating constructor
- 类模板参数推导 CTAD
- 退化副本:auto(x) 与 auto{x}(C++23)
- 静态与线程局部变量初始化
- 标准库辅助设施
- 综合示例:一次看全所有初始化方式
- 附录:高频面试题
- 参考资料
最近在浏览博文的时候无意间看到了一个信息“C++ 的初始化方式有20多种”,当时就给我震惊到了。
出于好奇,于是借助AI对这个信息做了进一步的了解与统计,发现这个信息是真的!
对于一个学习并使用C++十多年的我来说也是带来了不小的震撼的。
我对这些信息进行了整理汇总,因为我觉得你也可能会与我一样感到震惊!
统计速览
- 基线标准:C++26(ISO/IEC 14882:2026,最新发布标准)。
- 初始化方式总数:28 种,覆盖 C++11 至 C++26,分为标准形式、成员与构造函数机制、静态与线程局部存储期、模板推导与新机制、声明与捕获类机制五类,标准库辅助初始化另计。
- 代码实例:19 段(含文末综合示例),均经 g++ -std=c++23 实测通过。
概念框架
C++ 的初始化行为由 ISO/IEC 14882 的 [dcl.init] 一节统一规定。标准给出的初始化种类之间并非平行关系:一部分描述对象最终得到什么值,如 zero、default、value,另一部分描述书写语法,如:direct、copy、list。
聚合初始化、指定初始化器、引用初始化、成员初始化器列表、委托构造函数、类模板参数推导,以及静态与线程局部变量的初始化,分别在不同语境下被触发。
理解每种方式的触发条件与结果,是规避未定义行为与性能陷阱的前提。
图1:C++ 初始化方式五类分类(28 种 · C++11–C++26)
C++26(ISO/IEC 14882:2026,最新发布标准)使更多标准容器与适配器可用于常量求值,其初始化可发生在常量求值期。
默认初始化 default-initialization
当变量声明不带初始化器、new 表达式不带初始化器,或成员初始化器列表中省略某成员(且该成员没有默认成员初始化器)时,该实体被默认初始化。
对类类型,调用其默认构造函数。
对数组,每个元素被默认初始化。
对除类与数组之外的类型(标量),在具有自动存储期(即普通局部变量的存储期)时不执行任何初始化,对象持有不确定值;读取该值在 C++26 之前属于未定义行为(程序可能崩溃或得到任意值),自 C++26(P2795R5)起属于错误行为(erroneous behavior,新分类,介于未指定与未定义行为之间,标准允许实现以诊断或陷阱替代崩溃,程序仍视为良构)。
一个常见风险是最扰人的语法分析(most vexing parse,编译器把本可理解为对象定义的语句按函数声明来解析),如:T x(),被解析为函数声明而非对象定义。
C++11 起可用空花括号 T x{},得到值初始化的对象。
#include <string>
void demo() {
// 块作用域中未带初始化器的内置类型持有不确定值
int x;
// 读取 x 在 C++26 之前属于未定义行为,自 C++26 起属于错误行为
// 类类型调用其默认构造函数
std::string s;
}
// 圆括号写法被解析为函数声明,而非对象定义
int misleading();
// C++11 起用空花括号得到值初始化的对象
int safe{};
规则见 cppreference default-initialization 与 ISO/IEC 14882 [dcl.init]。
值初始化 value-initialization
值初始化在以下情形发生。
以空括号对或空花括号对(C++11 起)创建对象,包含 T()、new T()、T{}、new T{},以及成员初始化器列表中的空括号对或空花括号对。
需要注意:当 T 是聚合类型时,空花括号触发聚合初始化而非值初始化。
值初始化的效果取决于类型。
对没有用户提供的默认构造函数的类类型,对象先被零初始化,再被默认初始化。
对具有用户提供的默认构造函数的类类型,直接默认初始化,不做前置的零初始化。
对数组,每个元素被值初始化。
对其它类型,对象被零初始化。
关键陷阱在于”是否由用户提供”这一判定。
若类提供了用户提供的默认构造函数,值初始化会跳过零初始化,未在该构造函数中赋值的成员将持有不确定值。
#include <vector>
// 空圆括号与空花括号触发值初始化
int a = int();
int b{};
// 动态对象用空括号对值初始化为零
int* p = new int();
// 容器元素被值初始化为零
std::vector<int> v(10);
struct WithUserCtor {
int mem;
// 用户提供默认构造函数
WithUserCtor() {}
};
// mem 不会被置零,持有不确定值
WithUserCtor w{};
规则见 cppreference value-initialization 与 ISO/IEC 14882 [dcl.init]。
图2:T{} 与 T() 触发值初始化后的决策流与四个出口
零初始化 zero-initialization
零初始化通常不是一个可由用户直接写出的语法,而是作为其它初始化的子步骤出现。对具有静态或线程局部存储期的变量,若在常量初始化阶段未被处理,则先被零初始化。
对值初始化中的相应类类型与非类类型,也先执行零初始化。
零初始化的结果:算术类型置为 0,bool 置为 false,指针置为空指针,成员指针置为空成员指针。
引用不参与零初始化,因为它不是对象。
// 静态存储期变量在常量初始化之前先被零初始化
int global_counter;
// 值初始化时类对象先被零初始化,再默认初始化
// 成员 a、b、c 先被置为 0、0.0、false
struct S { int a; double b; bool c; };
S s{};
规则见 cppreference zero-initialization 与 ISO/IEC 14882 [dcl.init]。
直接初始化 direct-initialization
直接初始化由圆括号 T obj(args) 或自 C++11 起的花括号 T obj{args} 触发,也出现在 new 表达式与函数风格转换中。
对类类型,重载决议考虑全部构造函数,包含 explicit 构造函数。
对内置类型,执行相应的隐式转换。
与拷贝初始化不同,直接初始化可以成功调用 explicit 构造函数。
#include <string>
#include <vector>
// 圆括号直接初始化,explicit 构造函数可参与
std::string s(5, 'x');
// 花括号直接初始化(C++11 起)
std::vector<int> v{1, 2, 3};
规则见 cppreference direct-initialization 与 ISO/IEC 14882 [dcl.init]。
拷贝初始化 copy-initialization
拷贝初始化由等号语法 T obj = other; 触发,也出现在函数实参传递、返回值与抛出异常等语境中。
对类类型,重载决议考虑用户定义转换序列,但 explicit 构造函数不被考虑。
这也是 std::vector
在等号语法的拷贝初始化中,即便最佳匹配是 explicit 构造函数,程序同样不合法。
#include <string>
// 等号语法触发拷贝初始化
std::string s = "hello";
// explicit 构造函数在拷贝初始化中不被考虑
// 下列写法错误:尺寸构造函数为 explicit
// std::vector<int> v = 10;
// 函数实参传递也是拷贝初始化语境
void take(std::string arg);
规则见 cppreference copy-initialization 与 ISO/IEC 14882 [dcl.init]。
列表初始化 list-initialization
列表初始化(含 C++11 起提出的统一初始化 uniform-initialization)由花括号触发:T obj{args}; 或 T obj = {args};。它试图用一套统一的语法覆盖此前各不相同的初始化写法。
对类类型,匹配按 [dcl.init.list] 规定的阶段进行。这里的 std::initializer_list 是标准库提供的只读数组视图,用来把花括号里的元素整体传给构造函数;后备数组指编译器先把花括号元素放进一个隐藏的常量数组,再让 initializer_list 指向它。若 T 是聚合类型,执行聚合初始化;若 T 是 std::initializer_list 的特化,按后备数组构造;否则先考察接受 std::initializer_list 的构造函数,若无匹配再考察其余构造函数,且只允许非窄化转换。
窄化转换在列表初始化中被禁止,包含浮点到整数、浮点向精度不更高的浮点类型、整数或无作用域枚举到浮点、整数到无法表示其全部值的整数类型、指针或成员指针到 bool 等;当源是常量表达式且结果能被目标类型精确表示时,多数类别可以豁免。
#include <vector>
// 花括号触发列表初始化,优先匹配 initializer_list 构造函数
// 得到两个元素:10 与 20
std::vector<int> a{10, 20};
// 圆括号调用 (size, value) 重载,得到十个值为 20 的元素
std::vector<int> b(10, 20);
// 窄化转换在列表初始化中被禁止
// 下列写法错误:浮点到整型的窄化
// int c{3.14};
规则见 cppreference list-initialization 与 ISO/IEC 14882 [dcl.init.list]。
图3:列表初始化按序尝试聚合、initializer_list 与常规构造三阶段
聚合初始化 aggregate-initialization
聚合类型指数组,或满足特定条件的类。这些条件随标准演进而逐步放宽。
C++11 至 C++14 要求类没有用户提供的构造函数、没有私有或保护的非静态数据成员、没有基类、没有虚函数。C++14 起允许存在默认成员初始化器。C++17 允许类带有公有基类,同时把 explicit 构造函数与继承的构造函数一并列为失格条件。C++20(P1008R1)把判据由用户提供的构造函数收紧为用户声明或继承的构造函数:只要类声明了任一构造函数(无论是否 explicit、是否给出函数体),该类即不再是聚合;换言之 explicit 在 C++20 不再作为单独判据,因为任何用户声明的构造函数都会使类失格。
聚合初始化按成员声明顺序用花括号填充每个元素,嵌套聚合使用嵌套花括号。省略的成员优先按其默认成员初始化器初始化,没有默认成员初始化器时按空花括号初始化(对标量类型效果同值初始化,即为零);引用成员若未显式初始化则程序非良构。一旦类提供了用户提供的构造函数(C++11 至 C++17)或用户声明的构造函数(C++20),它就不再是聚合类型。
struct Point { int x; int y; };
// 按声明顺序用花括号初始化聚合
Point p{1, 2};
// C++17 起含公有基类的类也是聚合
struct WithBase : Point { int z; };
WithBase wb{{3, 4}, 5};
// 省略的成员被值初始化为零
// 此处 y 被值初始化为 0
Point q{1};
规则见 cppreference aggregate-initialization 与 ISO/IEC 14882 [dcl.init.aggr]。
圆括号聚合初始化(C++20)
C++20(P0960)起,聚合也可用圆括号初始化,与花括号得到相同结果。在 C++20 之前,聚合只能用花括号初始化,圆括号写法会被解析为函数声明或触发普通构造函数匹配。该特性由特性宏 __cpp_aggregate_paren_init(201902L)标识。
struct Pixel { int x; int y; };
// C++20 起聚合可用圆括号初始化,与花括号等价
Pixel p(1, 2);
// C++20 之前下列写法无法初始化聚合,会被解析为函数声明
// Pixel bad();
C++20 还为聚合启用了类模板参数推导(聚合 CTAD)。
指定初始化器 designated-initializer
自 C++20 起,聚合可用成员名显式赋值。每个指示符必须命名 T 的直接非静态数据成员,且各指示符必须按成员的声明顺序出现。
乱序、嵌套、与常规初始化器混用,以及对数组使用指定初始化器,在 C++ 中均不被允许。联合体只能指定一个成员。被指定的成员按对应初始化器初始化;未指定的成员优先按其默认成员初始化器初始化,没有默认成员初始化器时按空花括号初始化(对标量类型效果同值初始化,即为零),且窄化转换同样被禁止。
struct Rect {
int x;
int y;
int w;
int h;
};
// 指定初始化器按成员名赋值,必须遵循声明顺序
Rect r{.x = 1, .y = 2, .w = 3, .h = 4};
// 跳过部分成员时,被跳过的成员被值初始化
// 此处 y 与 w 被值初始化为零
Rect r2{.x = 1, .h = 4};
// 顺序错乱将导致编译错误
// 下列写法错误:顺序与声明不一致
// Rect bad{.y = 2, .x = 1};
规则见 cppreference aggregate-initialization 的 Designated initializers 小节与 ISO/IEC 14882 [dcl.init.aggr]。
引用初始化 reference-initialization
引用声明带初始化器时执行引用初始化。引用绑定到同类型的左值或右值,也可绑定到基类子对象或函数。
非 const 的左值引用不能绑定到右值或临时对象,这是 int& r = 10; 不合法的原因。const 左值引用与右值引用可以绑定到临时对象,并将临时对象的生命周期延长至引用的生命周期结束。一个例外出现在用圆括号直接初始化聚合内的引用成员时,临时对象的生命周期只到包含该初始化器的完整表达式结束,可能留下悬垂引用。
#include <string>
// 左值引用必须绑定到同类型左值
std::string s = "hi";
std::string& r = s;
// const 左值引用可绑定到临时对象并延长其生命周期
const std::string& cr = std::string("tmp");
// 非 const 引用不能绑定到右值
// 下列写法错误:不能将非 const 左值引用绑定到右值
// int& bad = 10;
规则见 cppreference reference-initialization 与 ISO/IEC 14882 [dcl.init.ref]。
成员初始化器列表 member initializer list
构造函数的定义可在冒号之后携带成员初始化器列表,用于在构造函数体执行之前初始化基类和成员。
const 成员、引用成员,以及没有默认构造函数的类类型成员,必须在成员初始化器列表中初始化。与在构造函数体内对成员赋值相比,列表方式避免了先默认构造再赋值的额外开销。成员按其在类中声明的顺序初始化,与列表中的书写顺序无关;若成员间存在初始化依赖,书写顺序不当可能读到未初始化的值。
#include <string>
struct Widget {
int id;
std::string name;
// 成员初始化器列表在构造函数体之前初始化成员
Widget(int i, const char* n) : id(i), name(n) {}
};
规则见 cppreference constructor 与 ISO/IEC 14882 [class.base.init]。
委托构造函数 delegating constructor
自 C++11 起,成员初始化器列表可以把初始化工作委托给同一个类的另一个构造函数。
被委托的目标构造函数先执行,随后执行委托者自己的函数体。
一个构造函数要么委托给其它构造函数,要么初始化自己的成员,二者不能同时存在。
委托链不能形成环,否则程序不合法。
#include <string>
struct Logger {
std::string prefix;
// 目标构造函数
Logger(const std::string& p) : prefix(p) {}
// 委托构造函数把初始化交给目标构造函数
Logger() : Logger("default") {}
};
规则见 cppreference constructor 与 ISO/IEC 14882 [class.ctor]。
类模板参数推导 CTAD
自 C++17 起,在类模板的变量声明、new 表达式与函数风格转换中,只要不提供模板实参列表,编译器就从初始化器推导实参。
推导机制基于推导指南(告诉编译器如何由构造实参反推模板实参的规则):编译器从每个构造函数隐式生成虚构函数模板,并始终加入一个复制推导候选。
用户也可用 -> 语法提供用户定义指南,并可将其声明为 explicit,此时该指南在拷贝初始化中被忽略。
列表初始化通常优先匹配 std::initializer_list 构造函数,但当初始化器是单个同类型特化对象时,首阶段被跳过,复制推导优先于包装(对应提案 P0702R1,该提案作为缺陷报告回溯适用于 C++17),因此 std::vector copy = src,推导出 std::vector
#include <vector>
#include <tuple>
// 编译器从初始化器推导类模板实参
// 下列推导为 std::vector<int>
std::vector v{1, 2, 3};
// 下列推导为 std::tuple<int, double, char>
std::tuple t(1, 2.0, 'c');
// 单个同类型对象初始化时复制推导优先于包装
// 复制推导,不会变成 vector<vector<int>>
std::vector<int> src{1, 2};
std::vector copy = src;
对聚合类模板,C++20(P1816)会合成隐式推导指南,因此聚合也能从花括号初始化器推导实参。
// 聚合类模板,没有用户提供的构造函数
template <class T>
struct Agg { T a; T b; };
// C++20 起聚合也触发 CTAD,推导为 Agg<int>
Agg a{1, 2};
规则见 cppreference class-template-argument-deduction 与 ISO/IEC 14882 [temp.deduct.guide]、[over.match.class.deduct]。
退化副本:auto(x) 与 auto{x}(C++23)
C++23(P0849)引入 auto(x) 与 auto{x} 作为退化副本表达式,产生实参经退化(数组转指针、函数转函数指针、移除顶层 cv 限定)之后的副本,而非原类型。它与拷贝初始化不同:拷贝初始化保留原类型,退化副本会先把类型退化再复制。
// 数组在退化副本中退化为指针
int arr[3]{};
// p 的类型是 int*,而非 int[3]
// 数组已退化为指向首元素的指针
auto p = auto{arr};
// 圆括号形式效果相同
auto q = auto(arr);
静态与线程局部变量初始化
具有静态或线程局部存储期的变量先经历静态初始化,再视情况进行动态初始化。
静态初始化在常量初始化与零初始化二者中取其一:初始化器是常量表达式时执行常量初始化,否则执行零初始化;两者合称静态初始化,先于一切动态初始化完成。
常量初始化保证在程序中任何动态初始化开始之前完成,它并不保证先于其它对象的常量初始化。适用对象包括:初始化器整体是常量表达式的变量,绑定到静态存储期对象、临时对象或函数的引用,以及可调用 constexpr 构造函数的对象。
同一翻译单元(一次编译的一个源文件及其包含的全部头文件)内,非局部变量按声明顺序初始化。
跨翻译单元的初始化顺序未被标准规定:各单元的动态初始化属于不确定顺序(indeterminately sequenced)。若某个单元的变量动态初始化依赖另一单元的变量,且该变量的动态初始化尚未执行,这即静态初始化顺序问题(static initialization order fiasco)。此时目标对象已完成零初始化:若它是标量类型,读到的是零初始化后的值,该值未指定而非未定义行为;若它是类类型,其对象生命周期尚未开始,访问其成员属于未定义行为。
函数内的静态局部变量在首次调用时初始化,自 C++11 起该初始化是线程安全的。
这一惯用法常被用来规避静态初始化顺序问题。
// 具有静态存储期的变量先被零初始化,再视情况常量或动态初始化
int counter = 0;
// 常量初始化在全部动态初始化之前完成
const int kBase = 100;
// 线程局部变量在每个线程首次使用时初始化
thread_local int tls_id = 0;
// 用函数内静态局部变量规避跨单元的初始化顺序问题
struct Config { /* ... */ };
Config& instance() {
// 首次调用时初始化,且线程安全
static Config cfg;
return cfg;
}
constinit 说明符(C++20,__cpp_constinit 201907L)断言具有静态或线程存储期的变量经历常量初始化(属静态初始化,先于一切动态初始化完成)。若初始化器不是常量表达式,程序非良构。它不改变变量的初始化方式,只强制常量初始化的保证,借此规避动态初始化阶段的不确定性。
// 常量表达式函数,可用于常量初始化
constexpr int compute() { return 7; }
// constinit 断言 g 经历常量初始化
// 若 compute() 不是 constexpr,程序非良构
constinit int g = compute();
// 与静态初始化的关系:constinit 强制对象落在常量初始化阶段
// 因此不会进入动态初始化,不受跨单元顺序问题影响
规则见 cppreference constant-initialization、initialization 与 ISO/IEC 14882 [basic.start.static]、[basic.start.dynamic]、[basic.stc.static]、[basic.stc.thread]。
图4:静态与线程局部变量三段式初始化与跨单元顺序问题
标准库辅助设施
std::to_array(C++20,
#include <array>
// 从内建数组构造 std::array,推导元素类型与大小
int raw[]{1, 2, 3};
auto a = std::to_array(raw);
// a 的类型为 std::array<int, 3>
// 从字符数组构造,避免退化为指针
char text[] = "hi";
auto b = std::to_array(text);
// b 的类型为 std::array<char, 3>
std::ranges::to(C++23,
std::expected(C++23)是一种结果类型,用来表达一个操作要么得到成功值、要么得到错误;单子式(monadic)指它提供 and_then、or_else 等接口,可把多个可能失败的操作链式组合。它经 std::expected<T, E>{val} 或 std::unexpected{e} 构造,用于表示可能失败的初始化结果。
综合示例:一次看全所有初始化方式
把上文分散的各节写法收进同一段程序,编号对应篇首统计速览的 28 种形式;编译验证环境为 g++ 13.3 -std=c++23。
#include <array>
#include <memory>
#include <string>
#include <vector>
#include <expected>
// 以下示例在同一翻译单元内依次演示全部初始化方式。
// 全局/命名空间作用域的对象具有静态存储期,标量在此被零初始化,不触发未定义行为。
// 1. 默认初始化:类类型调用隐式默认构造函数(标量在静态存储期被零初始化)
struct DefaultInit { int m; };
DefaultInit d1;
// 2. 值初始化:空花括号先零初始化(标量)再默认初始化(类类型)
int v_zero{};
DefaultInit d2{};
// 3. 零初始化:静态存储期变量在静态初始化阶段被置零
static int s_zero;
// 4. 常量初始化:初始化器为常量表达式,落在常量初始化阶段
constexpr int k_const = 42;
constinit int g_const = 42;
// 5. 拷贝初始化:以等号从同类型对象拷贝构造
DefaultInit d3 = d2;
// 6. 直接初始化:以括号调用匹配的构造函数
std::string s_dir(5, 'x');
DefaultInit d4(d2);
// 7. 列表初始化:优先匹配 std::initializer_list 构造函数
std::vector<int> v_list{1, 2, 3};
// 8. 聚合初始化:按成员顺序用花括号逐成员初始化
struct Point { int x; int y; };
Point p_agg{1, 2};
// 9. 引用初始化:引用必须绑定到同类型对象
int ref_target = 10;
int& r = ref_target;
// 10. 字符串字面量初始化字符数组:数组长度含结尾 '\0'
char buf[] = "hello";
// 11. 成员初始化器列表、12. 默认成员初始化器(NSDMI)、13. 委托构造函数:
// 构造函数用初始化器列表赋值;b 带 NSDMI;无参构造委托给带参构造
struct Widget {
int a;
int b = 1;
Widget() : Widget(0) {}
Widget(int x) : a(x), b(x) {}
};
// 14. CTAD:编译器从初始化器推导类模板实参
std::vector v_ctad{1, 2, 3};
// 15. 聚合类型 CTAD(C++20):聚合模板同样可推导
template <class T>
struct Agg { T x; T y; };
Agg agg_ctad{1, 2};
// 16. 指定初始化器(C++20):按名字赋值,须按声明顺序
Point p_desig{.x = 3, .y = 4};
// 17. 圆括号聚合初始化(C++20 P0960)
Point p_paren(5, 6);
// 18. constinit:强制变量经历常量初始化,不进入动态初始化阶段
constinit static int ci = 5;
// 19. auto(x) 与 auto{x} 退化副本(C++23 P0849)
int av = 7;
auto av_copy = auto(av);
auto av_copy2 = auto{av};
// 20. 保证复制消除(C++17):从纯右值初始化,不发生拷贝/移动
std::vector<int> v_elide = std::vector<int>{1, 2, 3};
// 21. 静态初始化已涵盖于 s_zero(零初始化)与 g_const(常量初始化)
// 22. 动态初始化-有序:同一翻译单元内按声明顺序
int dyn_ordered = ref_target + 1;
// 23. 动态初始化-部分有序(C++17,inline 变量)
inline int iv_ordered = ref_target;
// 24. 动态初始化-无序(C++14,变量模板)
template <class T>
int vt_unordered = ref_target;
// 25-26. 提前/延迟动态初始化是上述静态变量动态阶段的顺序选择,无独立语法:
// 动态初始化先于 main 称为提前,按需首次 odr-use 称为延迟。
// 线程局部存储期:先静态初始化,再视情况动态初始化
thread_local int tl = 0;
// 27. 结构化绑定(C++17):绑定到聚合的子对象
auto [px, py] = p_agg;
// 28. lambda 初始化捕获(C++14):在闭包内直接初始化数据成员
auto lam = [captured = ref_target * 2]() { return captured; };
// 标准库辅助初始化
// std::to_array(C++20):从内建数组构造 std::array
int raw[]{1, 2, 3};
auto arr = std::to_array(raw);
// std::expected(C++23):表达成功值或错误的初始化结果
std::expected<int, int> ex = std::expected<int, int>{42};
// std::construct_at(C++20):在原生存储上原地构造
alignas(int) char raw_mem[sizeof(int)];
int* cp = std::construct_at(reinterpret_cast<int*>(raw_mem), 9);
// std::ranges::to 与 std::from_range 需 GCC 14+(libstdc++ 13 未提供),
// 标准库辅助设施一节已说明,此处从略。
int main() {
(void)d1; (void)v_zero; (void)d2; (void)s_zero;
(void)k_const; (void)g_const; (void)d3; (void)s_dir; (void)d4;
(void)v_list; (void)p_agg; (void)r; (void)buf; (void)p_desig;
(void)p_paren; (void)ci; (void)av_copy; (void)av_copy2; (void)v_elide;
(void)dyn_ordered; (void)iv_ordered; (void)vt_unordered<int>; (void)tl;
(void)px; (void)py; (void)lam(); (void)arr; (void)ex; (void)cp;
return 0;
}
至此,示例从默认初始化一路覆盖到 C++23 的退化副本与标准库辅助设施,每种写法的触发条件与结果均可在前述各节对照。
附录:高频面试题
【问题一】默认初始化和值初始化有什么区别,为什么 int x; 和 int x{}; 结果不同?
【解题思路】区分二者触发语法与对内置类型的结果。关键在标量类型于块作用域中默认初始化不执行任何动作,而值初始化会先零初始化。
【参考答案】int x; 不带初始化器,对自动存储期的标量执行默认初始化,对象持有不确定值,读取在 C++26 之前属于未定义行为,自 C++26(P2795R5)起属于错误行为。int x{}; 的空花括号触发值初始化,对除类与数组之外的类型先执行零初始化,因此 x 得到 0。类类型在用户提供了默认构造函数时,值初始化会跳过零初始化,这一点也常被追问。依据见 [dcl.init] 与 cppreference default-initialization、value-initialization。
【问题二】std::vector
【解题思路】这是列表初始化与直接初始化的差异,核心在 std::initializer_list 构造函数的优先级。
【参考答案】圆括号触发直接初始化,匹配单参 (size_type) 构造,得到十个值为 0 的元素。花括号触发列表初始化,优先匹配接受 std::initializer_list 的构造函数,得到仅含一个元素 10 的向量。该差异正是窄化与 initializer_list 优先规则带来的典型陷阱。依据见 [dcl.init.list] 与 cppreference list-initialization。
【问题三】聚合初始化的条件在 C++ 不同版本中有哪些变化,C++20 之后还有改动吗?
【解题思路】按 C++11、C++14、C++17、C++20 逐步回忆限制项的增减,重点在构造函数、基类与默认成员初始化器;并补充 C++20 圆括号聚合初始化与聚合 CTAD,说明 C++23、C++26 未对聚合资格引入新的初始化相关限制。
【参考答案】C++11 要求类无用户提供的构造函数、无私有或保护的非静态数据成员、无基类、无虚函数。C++14 起允许存在默认成员初始化器。C++17 允许类带公有基类。C++20 将限制改为无用户声明或继承的构造函数,explicit 构造函数不再单独使类失去聚合资格,并且 C++20 还引入圆括号聚合初始化(P0960),使聚合可用圆括号如 Pixel p(1, 2); 初始化,此前聚合只能用花括号。C++23 未对聚合资格本身引入新限制;C++26 仅以缺陷报告形式做了几处澄清(P3106R1 未知边界数组、P2752R3 花括号初始化器的存储期),不涉及新的失格条件。依据见 [dcl.init.aggr] 与 cppreference aggregate-initialization。
【问题四】为什么列表初始化能防止窄化转换?
【解题思路】说明窄化转换在列表初始化中被整体禁止,并举几类被禁转换。
【参考答案】列表初始化的重载决议阶段只允许非窄化转换,被禁止的包含浮点到整数、浮点向精度不更高的浮点类型、整数或无作用域枚举到浮点、整数到无法表示其全部值的整数类型、指针或成员指针到 bool;当源是常量表达式且结果能被精确表示时多数类别可豁免。因此 int c{3.14}; 在编译期报错,而同等写法用圆括号则通过。依据见 [dcl.init.list] 与 cppreference list-initialization。
【问题五】什么是静态初始化顺序问题,应如何规避?
【解题思路】说明跨翻译单元动态初始化顺序未指定,以及函数内静态局部变量的惯用法。
【参考答案】具有静态存储期的非局部变量在同一翻译单元内按声明顺序初始化,但跨翻译单元的顺序未被标准规定。若某单元的变量在动态初始化时依赖另一单元尚未完成动态初始化的变量,且该目标为标量类型,会读到其经零初始化后的值,该值未指定(各单元动态初始化属不确定顺序),而非未定义行为;若目标为类类型且其生命周期尚未开始,访问其成员属于未定义行为。常用规避手段是把跨单元共享的对象改为函数内的静态局部变量(Meyers 单例),使其在首次调用时初始化且自 C++11 起线程安全。依据见 [basic.start.dynamic] 与 cppreference initialization。
【问题六】值初始化在什么时候会先执行零初始化,什么时候不会?
【解题思路】围绕“是否为用户提供的默认构造函数”这一判定展开。
【参考答案】对没有用户提供的默认构造函数的类类型,值初始化先零初始化再默认初始化;对具有用户提供的默认构造函数的类类型,直接默认初始化,跳过零初始化;对数组逐元素值初始化;对其它类型零初始化。是否为用户提供的构造函数是区分的关键,类内 = default 通常不被视为用户提供的构造函数。依据见 cppreference value-initialization 与 [dcl.init]。
【问题七】什么是 CTAD,编译器如何推导 std::vector v{1, 2, 3};?
【解题思路】说明 C++17 的推导指南机制与列表初始化下 initializer_list 的优先。
【参考答案】CTAD 即类模板参数推导,在变量声明、new、函数风格转换且未写模板实参列表时,编译器从初始化器推导实参。其机制是隐式从构造函数生成虚构函数模板并加入复制推导候选,用户也可用 -> 提供用户定义指南。对 std::vector v{1, 2, 3};,列表初始化优先匹配接受 std::initializer_list 的构造函数,元素类型为 int,故推导为 std::vector
【问题八】成员初始化器列表与构造函数体内赋值有何区别,委托构造函数有什么限制?
【解题思路】说明列表在构造函数体前初始化,避免先默认构造再赋值;并说明委托构造函数的互斥与成环限制。
【参考答案】成员初始化器列表在构造函数体执行前初始化成员,const 成员、引用成员和无默认构造函数的成员只能在此初始化;在构造函数体内赋值则先默认构造再赋值,存在额外开销。委托构造函数通过成员初始化器列表把工作交给同类另一构造函数,目标构造函数先执行;一个构造函数不能同时委托和拥有其它成员初始化器,且委托链不能成环。依据见 [class.base.init]、[class.ctor] 与 cppreference constructor。
参考资料
cppreference 各初始化页面(en.cppreference.com/w/cpp/language/):
default_initialization、value_initialization、zero_initialization、direct_initialization、copy_initialization、list_initialization、aggregate_initialization、reference_initialization、class_template_argument_deduction、constant_initialization、constructor、initialization、constinit、to_array、ranges::to(及 vector 范围构造)、expected。
ISO/IEC 14882 标准条款:[dcl.init]、[dcl.init.list]、[dcl.init.aggr]、[dcl.init.ref]、[class.base.init]、[class.ctor]、[temp.deduct.guide]、[over.match.class.deduct]、[basic.start.static]、[basic.start.dynamic]、[basic.stc.static]、[basic.stc.thread]。
《C++ Primer》(Stanley B. Lippman 等著)第 3 章:字符串、向量和数组,涵盖变量定义与初始化基础。