C语言中初始化列表和列表初始化如何改成?
摘要:深入理解 C++ 中初始化列表与列表初始化的区别:前者解决对象生命周期与内存模型问题,后者统一初始化语法并提供类型安全保障。
目录初始化列表(Member Initializer List)初始化 vs. 赋值为什么要用初始化列表?必须使用的场景(物理限制)初始化的顺序(内存布局决定)列表初始化(List Initialization)统一初始化语义底层机制:std::initializer_list安全特性:防止窄化转换(Narrowing Conversion)解决 “最令人头秃的解析”(Most Vexing Parse)两者的 “撞车”如何选择 {} 与 ()?{} 的写法直接列表初始化(Direct List Initialization)拷贝列表初始化(Copy List Initialization)直接列表初始化和拷贝列表初始化的选择explicit 关键字的影响真的应该所有情况都用 {} 吗?陷阱 1:std::vector 的构造陷阱 2:auto 类型推导的歧义{} 的特性:默认值初始化(Value Initialization)
本文首发于我的个人博客:Better Mistakes
版权声明:本文为原创文章,转载请附上原文出处链接及本声明。
由于技术迭代较快,文章内容可能随时更新(含勘误及补充)。为了确保您看到的是最新版本,并获得更好的代码阅读体验,请访问:
🍭 原文链接:https://bfmhno3.github.io/note/initialization-in-cpp/
很多初学者容易混淆初始化列表(Member Initializer List)和列表初始化(List Initialization),因为它们的名字很像,但它们实际上解决的是完全不同的问题:
初始化列表:解决的是对象生命周期与内存模型的问题(“什么时候赋初值”)
列表初始化:解决的是类型系统的统一性与安全性的问题(“用什么语法赋初值”)
初始化列表(Member Initializer List)
它的形式出现在构造函数参数列表之后,函数体大括号之前,以 : 开头。
class A {
int x;
public:
A(int val) : x(val) {} // 初始化列表
};
初始化 vs. 赋值
在 C++ 的对象模型中,“初始化”(Initialization)和 “赋值”(Assignment)是两个截然不同的物理过程。
内存分配(Allocation):在栈上或堆上划出一块内存
初始化(Initialization):在这块内存上构建对象,使其成为一个合法的实例
赋值(Assignment):对象已经存在了,擦出旧值,填入新值。
构造函数的执行时间线:
进入构造函数之前:编译器必须确保所有成员变量都已经 “出生”(初始化完成)
进入构造函数体({...}):这已经是 “出生后” 的世界了,这里面写的代码都是 “赋值” 操作。
为什么要用初始化列表?
如果你不用初始化列表,而是写在函数体内:
class Person {
std::string name;
public:
// 写法 1:在函数体内赋值
Person(const std::string& n) {
name = n;
}
};
底层发生了什么?
隐式初始化:在进入 { 之前,编译器发现 name 还没有初始化,于是强行用 std::string 的默认构造函数。name 变成了一个空字符串 ""
赋值操作:进入 { 后,执行 name = n;。调用 std::string 的赋值运算符,把刚才那个空字符串的内容丢弃,拷贝 n 的内容
这就类似于,你先建了一个空房子,然后立即把它拆了重建成你想要的样子,这就是双倍的开销。
正确的写法(初始化列表):
// 写法 2:使用初始化列表
Person(const std::string& n) : name(n) {}
底层发生了什么?
直接构造:直接调用 std::string 的拷贝构造函数,用 n 来 “生出” name
函数体为空,无操作。收益:省去了一次默认构造和一次赋值操作。
必须使用的场景(物理限制)
有些东西必须 “出生时” 就确定,生出来后再改就晚了。
