值初始化为什么存在,以及它和默认初始化的关系
本文以 C++20 为基准。除特别说明外,示例中的变量都是函数内的普通局部变量。
文中说某个 int 没有初始化数值时,不要通过打印它来验证结果。
使用 std::string 的例子需要包含 <string>。
value-initialization(值初始化)是一套特定的规则,不是“给变量一个初始值”的统称。 default-initialization(默认初始化)和它也不是覆盖所有初始化情况的两个大类。
0. c++ 的高性能默认要求
首先,只要没有,就不会初始化的
利用 value initialization ,就可以显示的说明,哪些成员是需要默认清零的。
其实 value initialization 反而奇怪
1. 为什么需要值初始化
先看没有写初始化器时的行为:
void demo() {
int n; // 默认初始化:不初始化这个 int 的数值
std::string s; // 默认初始化:调用默认构造函数,得到空字符串
}
两句都采用默认初始化,但是结果依赖类型:类可以通过构造函数建立初始状态,
普通局部 int 则没有这样的动作。
这给泛型代码提出一个需求:不知道 T 是整数、指针还是类时,能否用同一种写法, 让整数得到零、指针得到空指针,同时让类执行自己的默认构造逻辑?
T value; 不能满足这个需求;T value = 0; 也不通用,因为类不一定接受 0。
值初始化提供了相应规则,典型入口是表达式 T():
template<class T>
T make_initial_value() {
return T(); // 对这里的目标类型执行值初始化
}
void demo() {
auto n = make_initial_value<int>(); // 0
auto p = make_initial_value<int*>(); // 空指针
auto s = make_initial_value<std::string>(); // 空字符串
}
这个接口仍要求 T() 合法,例如类必须有可用的默认构造函数。
它也不承诺任意类的所有成员都有确定值:用户提供的构造函数仍然要负责自己的成员。
规则定义见标准草案的 初始化通则。
历史上具体修复了什么问题
CWG 35 记录了旧规则中的不一致:
struct Numbers {
int x;
double y;
};
struct Record {
int x;
double y;
std::string name;
};
当时 Numbers() 保证数值成员为零;但加入 std::string 成员、成为非 POD 类型后,
Record() 虽然会构造字符串,却不再保证旁边的 x、y 为零。
这里讲的是旧规则的问题,不是当前 C++ 的行为。
CWG 178 通过独立的值初始化规则处理了这个问题,
并把规则用于 T()、new T()、构造函数成员初始化器中的 member() 等位置。
在当前规则下,上述两个类型的 T() 都会让数值成员得到零,字符串正常构造。
为什么没有直接让默认初始化总是清零?CWG 35 明确指出,局部对象写成 T obj;
时允许数值成员不被初始化,是有意保留的行为。从使用上看,这也保留了“随后直接覆盖数据,
无需先要求清零”的选择,例如函数内的 int buffer[4096];。
2. 它在初始化体系中的位置
cppreference 的 Initializers 章节 列出的四种形式,分类的是声明中的初始化器语法:
| 语境 | 不使用列表 | 使用列表 |
|---|---|---|
| Copy | T x = expr; |
T x = {args...}; |
| Direct | T x(args...); |
T x{args...}; |
圆括号一格指参数列表非空的对象定义;T x(); 是函数声明。
这四格不属于 value-initialization 的四个子类。
有没有初始化器、使用哪种语法、目标是什么类型,共同决定应用哪些规则。
void demo() {
int a; // 默认初始化
int b = 42; // 拷贝初始化,不涉及值初始化
int c(42); // 直接初始化,不涉及值初始化
int d = {42}; // 拷贝列表初始化,不涉及值初始化
int e{42}; // 直接列表初始化,不涉及值初始化
int f{}; // 直接列表初始化 → 值初始化 → 零初始化
int g = {}; // 拷贝列表初始化 → 值初始化 → 零初始化
}
b 到 e 都有初始值 42,但它们都没有进行值初始化。
f 则可以同时从不同层次描述:使用直接列表初始化,空列表在这里触发值初始化,
而对 int 的值初始化要求零初始化。
箭头表示规则的衔接,不表示创建了多个对象,也不表示执行了多次赋值。 具体分支见标准草案的 列表初始化。
同一句代码的不同部分也可以涉及不同规则:
int b = int();
右侧的 int() 执行值初始化,得到 0;变量 b 从这个表达式进行拷贝初始化。
不能只看整句出现了 = 或 (),就给所有部分贴同一个标签。
3. 默认初始化和值初始化分别做什么
下面概括本文涉及的类型;默认构造函数不可用等情况会使程序不合法。
| 目标类型 | 默认初始化 | 值初始化 |
|---|---|---|
int、double、指针等标量 |
不执行数值初始化 | 零初始化:整数为 0、浮点数为 0.0、指针为空指针 |
| 数组 | 各元素默认初始化 | 各元素值初始化 |
| 类 | 选择并调用默认构造函数 | 根据选中的默认构造函数是否为 user-provided,决定是否先零初始化;随后默认初始化 |
因此它们不是互斥的两条完整路径:类的值初始化会进一步执行默认初始化。 “默认初始化”也不代表一定获得一个可读取的默认数值。
零初始化是语言规定的操作,不等同于 memset:例如指针得到空指针值,
并不要求该值的存储表示一定是所有位为零。
4. 怎样判断 () 和 {}
先区分代码所处的位置,再判断目标类型。
| 代码 | 含义 |
|---|---|
int x; |
对象定义,默认初始化 |
int x(); |
函数声明,没有创建 int 对象 |
int() |
表达式,值初始化,得到 0 |
int x{}; |
对象定义,列表初始化进一步触发值初始化,得到 0 |
new int |
创建一个默认初始化的 int |
new int() |
创建一个值初始化的 int,得到 0 |
new int{} |
列表初始化进一步触发值初始化,得到 0 |
new int(7) |
直接初始化,得到 7 |
new int{7} |
直接列表初始化,得到 7 |
表中的 new 形式用于对比规则;实际使用时需要管理返回的指针并释放对象。
new 改变对象存储的取得方式,不会让所有初始化都变成值初始化。
空花括号要继续看类型
struct Point {
int x;
int y;
};
void demo() {
int n{}; // 列表初始化 → 值初始化 → 零初始化
std::string s{}; // 列表初始化 → 值初始化 → 默认初始化,调用默认构造函数
Point p{}; // 列表初始化 → 聚合初始化
}
p 的两个成员也都得到 0,但 p 本身采用聚合初始化。
这里没有给出成员的初始化器,成员也没有类内默认值,因此按空初始化列表初始化各成员,
再对这两个 int 应用值初始化和零初始化。
对于空列表,判断常见类型时可以按这个顺序理解:
- 聚合类型:应用聚合初始化。
- 非聚合类且有默认构造函数:应用值初始化。
- 非聚合类没有默认构造函数:继续检查列表构造函数等规则,也可能报错。
- 标量:应用值初始化。
这份顺序只用于上面这些常见对象类型,完整规则还包括引用等情况。
不能把“写了 {}”“值初始化”“最终数值为零”当成同一件事。
规则见 列表初始化 和
聚合初始化。
5. 类的边界:为什么有 user-provided 这条规则
对类的值初始化,可以先用下面的思路理解:如果类作者提供了默认构造函数, 就由该构造函数负责建立初始状态;否则先进行零初始化,再执行默认构造逻辑。 但这只是帮助理解的说法,精确边界由 user-provided 这个标准术语决定。
struct A {
int x;
A() = default;
};
struct B {
int x;
B() {}
};
void demo() {
A a; // 默认初始化:a.x 没有初始化数值
A av{}; // C++20 中 A 不是聚合体:值初始化先零初始化,av.x == 0
B b; // 默认初始化:b.x 没有初始化数值
B bv{}; // 值初始化调用 B(),不预先零初始化;bv.x 没有初始化数值
}
A() = default 写在首次声明处,不属于 user-provided;B() {} 属于 user-provided。
= default 与空函数体不等价。 空函数体只是函数体里没有语句,
不会替作者补上 x 的数值初始化。
还有一种必须单独记住的情况:
struct C {
int x;
C();
};
C::C() = default; // 首次声明之后才 default:属于 user-provided
函数内的 C c{}; 不会因为值初始化而先把 x 置零。
判断依据不是肉眼看到有没有函数体,而是构造函数是否符合标准的定义。
| 默认构造函数的来源或写法 | 是否 user-provided |
|---|---|
| 编译器隐式声明 | 否 |
首次声明处 A() = default; |
否 |
B() {} |
是 |
先声明 C();,再定义 C::C() = default; |
是 |
定义见标准草案的 Explicitly-defaulted functions。
写业务类型时,可以减少对这条差异的依赖
如果成员应该有确定初值,直接把要求写在成员或构造函数中:
struct Settings {
int retries = 3;
int timeout{};
Settings() {}
};
void demo() {
Settings a; // retries == 3,timeout == 0
Settings b{}; // retries == 3,timeout == 0
}
这样即使默认构造函数是 user-provided,成员的初始状态仍然明确。 值初始化是否额外进行零初始化,不再决定这两个成员的最终值。
6. 存储期是另一个维度
int global; // 静态存储期,零初始化后为 0
void demo() {
static int s; // 静态存储期,零初始化后为 0
int local; // 普通局部变量,没有初始化数值
}
三处都没有显式初始化器,但前两者还有静态初始化规则。
不能因为全局 int 默认为零,就认为默认初始化会清零;
也不能因为最终得到零,就反推发生了值初始化。
参见标准草案的 Static initialization。
本站所有文章转发 CSDN 将按侵权追究法律责任,其它情况随意。