首先梳理一下目前C++的初始化有哪几种。

按被初始化的变数的类型分:

  1. 引用
  2. 字元数组
  3. 聚合类型(aggregate type;数组,没有构造函数的struct和union ←_← 一般这种类型不声明为class)
  4. 类类型(struct、class、union)
  5. 内建对象类型和enum

这些虽然不算简单(尤其是考虑类型转换函数operator T),但毕竟是和类型对应的。不同类型有不同的初始化规则也算正常。

按初始化的语法分:

  1. 默认初始化(default-initialization,例如T t;
  2. 直接(非列表)初始化(direct-(non-list-)initialization,例如T t(args...);
  3. 复制(非列表)初始化(copy-(non-list-)initialization,例如T t = init;
  4. 直接列表初始化(direct-list-initialization,例如T t{ args... };
  5. 复制列表初始化(copy-list-initialization,例如T t = { args... };

这就麻烦多了。同一个场合常常有多种初始化语法可以选择,甚至各种语法常常有相同的效果,但是它们有时又有不同的语义,所以不能无脑选择。这显然是过度复杂了。

这几种语法中,#1、#3和最初的#5是从C语言抄的。在C语言里,三者的分工算是比较明确:#1用于没有初始值的情况,#3用于类似赋值的初始化,#5用于其他情况(主要是逐个初始化结构体成员和数组元素,但有{ 0 }这个对所有对象都能用的「万能初始化」)。

#2大概是C++最初就有的。这种圆括弧语法似乎看起来很符合直觉,比如new Complex(1, 0)Complex(1, 0)能让人一看就想到构造函数调用(虽然有严重的语法二义性问题)。

C++标准化以前,#2大概已经可以用于没有构造函数的类型,包含了#1和#3的功能。

C++标准化时,发现如果#2和#3语义相同,会导致

// 假设直接初始化和复制初始化具有相同语义
std::vector& a(42); // 如果这是合法的...
int f(std::vector&);
int x = f(42); // ...那么这也是合法的

于是引入了explicit关键字,它使得#2和#3的重载决议(overload resolution)流程有微妙的不同(也有别的原因)。然而因为过于微妙,出现了难以预料的后果:在#3合法时,#2可能不合法(反过来倒是符合直觉);在#2和#3都合法时,二者可能有不同的语义。这就造成了不合理而且难以控制的复杂性。(expression SFINAE使得合法性差别可以转换为语义差别,这也增加了此处的复杂性。)

C++标准化时,没有参数的#2也被多加了一步:#2会给没有构造函数的对象赋初始值,#1不会。现在虽然也有相关争议,但争议主要是,要不要删除或改进#1。所以这个多加的一步大概是受欢迎的。

C++03中,大概就是这样的状况:#1、#2、#3都可以用于没有构造函数的类型,也可以调用构造函数,但具体有微妙的不同,不能互相替代。#5不能调用构造函数,但和#3也有重叠。

#4和改进版#5试图解决这一混乱,让所有类型都可以用花括弧初始化。如果它和原有的语义保持一致,可能会是不错的改进。可惜它引入了完全崭新的语义(initializer_list),导致#2和#4都合法时,二者有明显不同的语义。

std::vector& a(42);
std::vector& b{ 42 }; // 等价于下一行,不等价于上一行
std::vector& c = { 42 };

initializer_list并不是唯一的新语义,但是被认为是最大的问题。

#4和#5受explicit影响,也不能互换(虽然这次设计成了explicit不影响重载决议过程,只影响结果是否合法)。结果没有一种语法真正做到哪都能用。

因为与原有语义不同,虽然#4和#5号称「统一的初始化」,但是连标准库都不敢用。

于是成了五种都可以调用构造函数,也都可以用于没有构造函数的类型,但具体规则各不相同的初始化方式,而且没有一种占绝对优势。

这还没完。为了让标准库更好地支持聚合类型(原本只能用#4或#5),C++20让聚合类型能用#2的语法初始化——当然,规则和#4与#5都是不一样的。


  1. 事无巨细的底层控制
  2. 不容忍任何无意义开销
  3. 向后兼容性

那么,代价是什么呢?复杂!


为什么C++的初始化规则这么复杂?


不知道你说的是什么规则复杂?是C++初始化的「语法规则」复杂吗?还是指C++初始化的「编译(实现)规则」复杂?

如果是前者,「一切语法问题和帝国主义都是纸老虎」,需要的只是你的耐心、细心、熟悉而已。推荐两个「万金油」初始化语法给你:

  1. int i = {}, j = { 123 };
  2. int i {}, j { 123 };

此两个形式可以应对C++里面所有的初始化,一个不行换另一个准行,包括复杂的自定义类对象、各种容器对象的初始化。

如果是后者,需要你对「显式/隐式构造」、「隐式类型转换」、「复制构造/移动构造」、「复制赋值/移动赋值」、「全局存储/静态存储」、「动态(堆)存储 / 局部(栈)存储」、、、尤其是「静态初始化顺序」等概念有一个大致的认识,并且加以实践,就能熟练运用了。


最后回答:「为什么C++的初始化规则这么复杂?

1.C++是这个世界上,到目前为止(2020/06/28),唯一有可能(至少是理论上)实现任何软体(只要人类可以构想得出)的编程语言!

是的,唯一!

从嵌入式系统、武器/航天的实时系统、到绚丽多彩的游戏、非线性视频编辑、动画、AI、加解密、操作系统、驱动程序、手机软体、JVM、各种虚拟机、浏览器、Office、PS、、、请问,其它还有哪个语言能做得到?至少我学电脑30年了还没听说过。很多追求极端性能和专业化的软体,最后都是靠C/C++实现(OS Kernel、Windows、Photoshop、AutoCAD、Office、JVM、、、)。

所以它承载了太多太多的需求、限制、期待、惯例、历史包袱,所以它必须包罗万象、面面俱到、海纳百川,而且目标代码还要快如闪电!!!它就是「零成本抽象」的唯一典范(也许有瑕疵缺憾,也许不是最佳,但绝对是唯一)!

它就是整个CS(计算机科学)知识的载体,如果你把C++里面的问题全部弄懂了,基本上也就明白了CS里面绝大多数的问题了!它的进化、演变也是人类探索信息技术的一部历史、一部方法学。

所以,它怎么可能不复杂?!

2.见过了太多人,发太多的帖子说C++的某个方面复杂、反人类,但是槽点基本都在一些比较简单、初级的层面上。如:「模板编程好复杂」、「指针编程好复杂」、「头文件处理好复杂」、、、说明什么?说明很多人基本都在还没看到真正的「复杂」之前就放弃了。真正提得出点「好问题」的人,他们不是觉得C++复杂,是觉得「C++好玩儿」!因为你最终会发现只有在C++里面,你才能自由地解决那些「真正好玩儿的问题」本身,而不是去大费周章地绕开语言平台强加给你的限制。


C++的初始化只有两类。

{}用于顺序数据结构的初始化。如数组、结构体。

()用于初始化函数初始化,初始化参数写在()里。

C++的特点就是事无巨细,初始化函数能有多奇葩,()里的参数就有多奇葩。


因为c++什么规则都复杂


老婆早上问:早饭吃啥?

你:随便吧!

老婆中午问:午饭吃啥?

你:你看著办!

老婆下午问:晚饭吃啥?

你正欲说话,看见了老婆手里的菜刀果断的闭上了嘴。

醒醒,你没有老婆!


推荐阅读:
相关文章