十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++11详解(一):统一初始化——列表初始化与 initializer_list

C++11详解(一):统一初始化——列表初始化与 initializer_list C11详解一统一初始化——列表初始化与 initializer_list 星恒随风个人主页❄️ 个人专栏《指针合集》《C语言基础》《数据结构》《机器学习导论》《前端基础》《python基础》《C从入门到入土》《Linux的学习之旅》✨ 数据即知识压缩即智能前言C11为什么要重新设计初始化1. C98 中初始化方式比较零散在 C98 中不同类型往往使用不同方式初始化。数组inta[]{1,2,3,4,5};结构体structPoint{intx;inty;};Point p{1,2};普通变量intx10;对象Dated(2025,1,1);容器vectorintv;v.push_back(1);v.push_back(2);v.push_back(3);可以看到数组 → {} 普通变量 → 对象 → () 容器 → 一个一个插入初始化语法并不统一。C11 的一个重要目标就是尽可能提供一套统一的初始化方式{}这就是列表初始化 List Initialization一、C11 的列表初始化1. 内置类型也可以使用 {}C11 中可以写intx{10};doubled{3.14};charch{A};也可以保留等号intx{10};因此intx1{10};intx2{10};都是合法的。第二种intx2{10};通常称为直接列表初始化二、自定义类型也支持列表初始化1. 普通类定义#includeiostreamusingnamespacestd;classDate{public:Date(intyear1,intmonth1,intday1):_year(year),_month(month),_day(day){}private:int_year;int_month;int_day;};以前通常Dated1(2025,1,1);C11 可以Date d2{2025,1,1};甚至Date d3{2025,1,1};编译器会根据2025 1 1寻找能够匹配的构造函数。三、列表初始化并不是“简单换了一对括号”这是一个很容易产生误解的地方。Date d{2025,1,1};虽然表面看起来只是() → {}但列表初始化在 C 语言中有自己完整的一套重载决议与类型检查规则。尤其需要注意initializer_list 窄化转换 构造函数选择这些问题。四、列表初始化禁止很多窄化转换这是{}相比传统初始化非常重要的特点。所谓窄化转换可以简单理解为把一个值转换成表达能力更弱、可能损失信息的类型。例如doubled3.14;intxd;传统写法通常允许3.14 ↓ 3但是intx{d};编译器通常会直接报错。1. 一个简单例子doubled3.14;intx1d;// 可以可能丢失小数// int x2{d}; // 错误窄化转换因此{}能够帮助我们在编译阶段发现一部分潜在的数据丢失问题。五、为什么现代 C 经常推荐 {}例如intx{10};string s{hello};vectorintv{1,2,3};这样有几个明显优点初始化语法更加统一 能够发现一部分窄化转换 方便初始化容器 方便构造临时对象 可以直接调用对象构造函数但这并不意味着任何时候{}都一定比()更好。因为一旦类提供initializer_list构造函数{}的重载选择可能和()不一样。后面就会看到这一点。六、容器初始化为什么仍然是一个问题假设我们希望创建vectorint并直接放进去1 2 3 4 5如果没有专门机制理论上可能需要vectorintv;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);那么能不能vectorintv{1,2,3,4,5};当然可以。支持它的重要机制之一就是std::initializer_list七、什么是 std::initializer_list需要#includeinitializer_list它是 C11 引入的一个标准库类模板std::initializer_listT可以把它理解成一个轻量级对象用来访问编译器为{x1,x2,x3...}创建的一组只读元素。例如std::initializer_listintil{10,20,30,40};此时il ↓ 10 20 30 40八、initializer_list 可以遍历例如#includeiostream#includeinitializer_listusingnamespacestd;intmain(){initializer_listintil{10,20,30,40};for(autoe:il){coute ;}coutendl;return0;}输出10 20 30 40也可以for(autoitil.begin();it!il.end();it){cout*it ;}九、initializer_list 中的元素为什么不能修改initializer_listT访问的是const T因此initializer_listintil{1,2,3};不能//*il.begin() 100;可以把它理解为initializer_list 负责“描述这一组初始化元素” 而不是一个真正可修改的 vector它本身也不会拥有类似capacity push_back erase这种容器能力。十、initializer_list 并不是 vector两者很容易混淆。1. vectorvectorintv{1,2,3};vector真正拥有动态内存 size capacity insert erase push_back2. initializer_listinitializer_listintil{1,2,3};更接近一个只读视图 / 轻量代理它只是帮助函数或构造函数接收一组数量可变的同类型初始化元素十一、vector 为什么能使用 {1,2,3,4,5}标准容器提供了类似vector(initializer_listvalue_typeil);这样的构造函数。可以简化理解为templateclassTclassvector{public:vector(initializer_listTil){for(constautoe:il){push_back(e);}}// ...};所以vectorintv{1,2,3};本质过程可以理解为{1,2,3} ↓ initializer_listint ↓ vector 的 initializer_list 构造函数 ↓ 构造 vector十二、list、map 等容器同样支持例如listintlt{1,2,3,4};以及mapstring,intm{{apple,3},{banana,5},{orange,2}};这里实际上组合了两层列表初始化外层 {} ↓ 初始化 map 内层 {} ↓ 初始化 pairconst string,int十三、map 初始化过程怎么理解例如mapstring,intm{{apple,3},{banana,5}};可以抽象理解成apple, 3 ↓ pair banana, 5 ↓ pair 两个 pair ↓ initializer_listvalue_type ↓ map 构造函数这也是 C11 列表初始化非常方便的地方。十四、initializer_list 也可以支持赋值例如vectorintv{1,2,3};v{10,20,30,40};对应容器提供operator(initializer_listT);因此不只是构造重新赋值也可以使用{}。十五、自己给类增加 initializer_list 构造例如设计一个简单容器#includeinitializer_list#includeiostreamusingnamespacestd;classNumbers{public:Numbers(initializer_listintil){for(autoe:il){coute ;}coutendl;}};intmain(){Numbers n{1,2,3,4,5};return0;}输出1 2 3 4 5也就是说initializer_list并不是 STL 容器专属机制。我们自己的类型也可以使用。十六、一个非常重要的坑initializer_list 构造优先级考虑#includeinitializer_list#includeiostreamusingnamespacestd;classTest{public:Test(intx,inty){coutTest(int,int)endl;}Test(initializer_listintil){coutinitializer_listendl;}};执行Testt1(1,2);会更自然地匹配Test(int,int)但是Test t2{1,2};如果initializer_list构造函数可匹配列表初始化的重载决议会非常偏向initializer_list 构造函数所以{} 和 ()不能机械地认为完全等价。十七、vector 中最经典的例子观察vectorintv1(5,1);它表示5 个元素 每个元素都是 1即1 1 1 1 1而vectorintv2{5,1};表示两个元素 5 1所以() 和 {}在这里具有完全不同的语义。十八、auto 和列表初始化C11 还经常看到autoil{10,20,30};这里il会被推导成类似initializer_listint因此可以for(autoe:il){couteendl;}但是不要因此形成所有使用 {} 的 auto 都一定推导为 initializer_list这种过度简化。实际推导规则与具体语法形式有关。十九、列表初始化在函数调用中的应用假设classDate{public:Date(intyear,intmonth,intday){}};voidPrintDate(constDated){}调用可以直接PrintDate({2025,1,1});编译器利用{2025,1,1}构造Date临时对象再绑定到参数。二十、在容器插入中的应用例如vectorDatedates;dates.push_back({2025,1,1});相比Dated(2025,1,1);dates.push_back(d);前者更加紧凑。后面学习移动语义和emplace_back之后还会继续解决如何减少临时对象和拷贝的问题。二十一、列表初始化真正统一了什么可以把 C11 之前的数组初始化 对象构造 结构体初始化 容器初始化 临时对象构造统一成非常相似的Type obj{...};例如intx{10};Point p{1,2};Date d{2025,1,1};vectorintv{1,2,3};mapstring,intm{{apple,1},{banana,2}};代码在形式上更加一致。二十二、常见错误1. 认为 {} 与 () 完全一样错误。vectorintv1(5,1);vectorintv2{5,1};语义完全不同。2. 认为 initializer_list 是 vector不是。它没有push_back erase capacity等能力。3. 认为 initializer_list 中元素可以修改元素视为const T不能直接修改。4. 忽略窄化转换例如doubled3.14;// int x{d};列表初始化会阻止这种潜在信息损失。5. 认为 initializer_list 的底层布局是标准固定的不要依赖类似“一定两个指针” “一定在栈上”这样的实现细节。程序应该只使用begin()end()size()等标准接口。总结C11 列表初始化解决的核心问题是让不同类型拥有更加统一的初始化语法最重要的语法Type obj{args...};而std::initializer_listT则让函数和类能够方便地接收{x1,x2,x3,...}这一组同类型初始化元素。需要重点记住四件事情1. {} 可以初始化内置类型和自定义类型 2. 列表初始化会阻止很多窄化转换 3. STL 容器通过 initializer_list 支持 vectorint{1,2,3} 4. 如果类存在 initializer_list 构造函数 {} 与 () 的构造函数选择可能不同理解这一部分以后C11 后面的右值引用 移动构造 emplace会逐渐把“对象怎么构造、怎么传递”这件事继续向前推进。
返回列表