丹麦计算机科学家,本贾尼·斯特劳斯特卢普(Bjarne Stroustrup)博士,于1979年为美国电报电话公司(AT&T)贝尔实验室工作期间,在C语言的基础上,引入了面向对象概念,发明了C++语言,制定了它的最初定义,并完成了它的第一个实现,被誉为C++之父。本贾尼博士选择并制定了C++的设计标准,设计了它的主要语言特性,开发并协助开发了早期标准库的许多内容,并在迄今为止长达数十年的时间里,持续负责处理C++标准化委员会中的扩展提案。
C++的设计目的是为程序组织提供类似Simula的语言特性,同时为系统程序设计提供C语言级别的效率和灵活性。Simula是C++抽象机制的最初来源。类、继承和虚函数等概念皆由Simula借鉴而来。模板和异常则在晚些时候才被引入C++,灵感的来源也有所不同。
C++的演化和它的使用相伴而行。本贾尼博士花费了大量时间倾听用户的意见,征求有经验的程序员的观点,同时编写了大量的实际代码。本贾尼博士在AT&T贝尔实验室的同事们为C++最初十年的发展做出了不可磨灭的贡献。
C++既不是一个不知名的匿名委员会的作品,也不是一个想象中的万能独裁者的作品,它是成千上万个有奉献精神、经验丰富、辛勤工作的个体的劳动结晶。
隶属于国际标准化组织(ISO)的C++语言国际标准化工作组的代号是WG21( C语言国际标准化工作组的代号是WG14),其大部分工作文档都可以在网上获得。
创造C++的工作始于1979年秋季,当时的名字是“C with Classes”,即带类的C。下面是其简要的大事年表:
年份 | 重要事件 |
---|---|
1979 | “C with Classes”工作开始。最初的特性集包括类、派生类、公有和私有访问控制、构造和析构函数、带实参检查的函数声明。最初的库包含非抢占式多任务和随机数发生器 |
1984 | “C with Classes”更名为C++。增加了虚函数、函数与操作符重载、引用。库增加了I/O流和复数 |
1985 | C++的第一个商业版本发布于当年的10月14日。当时的库已经包含了非抢占式多任务、I/O流和复数。《The C++ Programming Language》出版 |
1989 | 《The Annotated C++ Reference Manual》出版 |
1991 | 《The C++ Programming Language, Second Edition》出版。提出了基于模板的泛型编程、基于异常的错误处理和基于RAII(资源获取即初始化)的资源管理理念 |
1997 | 《The C++ Programming Language, Third Edition》出版。引入了ISO C++标准,包括命名空间、dynamic_cast和有关模板的很多改进。标准库增加了标准模板库(STL)框架,包括泛型容器和算法 |
1998 | 《ISO C++标准》发布,即C++98 |
2002 | 标准的修订工作开始,这个版本俗称C++0x |
2003 | 《ISO C++标准》的首个缺陷修正版发布,即C++03 |
2011 | 《ISO C++11标准》发布。增加了统一初始化语法、转移语义、基于初始化表达式的类型推导(auto)、基于范围的for循环语句、可变参数模板、匿名函数(lambda表达式)、类型别名、针对并发编程的内存模型等。标准库增加了线程、mutex、正则表达式、基于哈希表的无序容器、资源管理指针(unique_ptr和shared_ptr)等 |
2013 | 第一个完整的C++11实现出现。《The C++ Programming Language, Fourth Edition》出版。增加了C++11的新内容 |
2014 | 《ISO C++14标准》发布。实现了可变参数模板、数字分隔符、泛型匿名函数和一些针对标准库的改进。第一个C++14实现完成 |
2015 | 《C++ Core Guidelines》项目启动 |
2017 | 《ISO C++17标准》发布。提供了各种各样的新功能,如保证运算顺序、结构化绑定、折叠表达式、文件系统库、并行计算、variant和optional等。第一个C++17实现完成 |
2020 | 《ISO C++20标准》发布。增加了模块、概念、协程、范围库、printf风格的格式化库、日期时间库和许多小功能。第一个C++20实现完成 |
在C++标准的制定过程中,C++11曾一度被命名为C++0x。彼时的委员会乐观地认为,新标准的完成日期不会晚于2009年。这种事在大型项目的开发中并不鲜见。在这之后的C++14、C++17和C++20,都分别于2014、2017和2020年按时发布了。主要编译器提供商也都及时跟进了每一个标准的实现工作。
根据本贾尼博士的自述,他之所以有发明这样一种新语言的冲动,是因为希望在多处理器(多核)和局域网(集群)环境下发布UNIX的内核服务。为此,他需要开发一些事件驱动的仿真程序。Simula是编写这类程序的理想语言,但性能差强人意。同时,这样的程序还需要有直接访问硬件和支持高性能并发的能力。C语言也是个不错的选择,但它对模块化和类型检查的支持却很弱。最后,本贾尼博士决定将Simula风格的类机制加入到C中,结果就得到了“C with Classes”,也就是C++的前身。它的一些特性适合编写具有最小时空需求的程序,在一些大型项目的开发中,这些特性经受住了严峻的考验。当时的“C with Classes”还没有操作符重载、引用、虚函数、模板、异常等很多现代C++特性。C++第一次用于研究机构之外是在1983年7月。
C++,读作“C plus plus”,这个名字是由Rick Mascitti在1983年夏天创造的,取代了最初的“C with Classes”。这个名字体现了新语言的进化本质——它从C语言演化而来,其中的“++”是C语言的递增运算符。稍短的名字“C+”是一个语法错误,它也曾被用于命名另一种不相干的语言。一些C语言行家可能会认为C++不如++C更为贴切。新语言没有被命名为D的原因是,作为C的扩展,它并没有试图通过删除特性来解决已存在的问题,另一个原因是已经有好几个自称C语言继任者的语言被命名为D了。
C++最初的设计(当时还叫“C with Classes”)包含带实参类型检查和隐式类型转换的函数声明、具有接口和实现间public/private差异的类机制、派生类及构造和析构函数。早期的类型参数是用宏实现的,直到1980年底,模板作为一组新的语言特性才被提出,以支持一套完整的泛型化程序设计风格。构造函数和析构函数最早被称为new函数和delete函数,new函数为成员函数创建了执行环境,而delete函数则完成了相反的工作。这是C++资源管理策略的根源,并引发了对异常的需求,同时也是令代码更简洁、更清晰的关键技术。甚至直到今天,还没有哪种除C++以外的编程语言,支持能执行通用代码的多重构造函数,而析构函数则更是C++独一无二的发明。
C++的第一个商业版本发布于1985年的10月14日。那时的C++已经有了内联、const、函数重载、引用、操作符重载和虚函数等特性。彼时,基于虚函数的运行时多态曾引起过很大争议。在Simula中司空见惯的东西,在系统程序员看来却未必有多大价值。系统程序员总是对间接函数调用心存疑虑,而熟悉其它面向对象编程语言的人们则很难相信,虚函数调用的速度能快到足以应对系统级程序的开发。在当时,甚至直到现在,很多有着面向对象编程背景的程序员,很难接受这样一个理念——调用虚函数就是为了表达一个必须在运行时做出的选择。虚函数在当时受到很大阻力,可能还有另外一个原因,那就是很多人还没有意识到,基于程序设计语言所支持的更规范的代码结构,可以构建出更好的系统。尤其是C程序员,他们的信条是,好的系统只能源自彻底的灵活性和仔细地手工打造程序的每一处细节。而一个真正的C++程序员则认为,来自语言和工具的自动化能力,与手动的精雕细琢相比,同样重要甚至更有价值。编程实践告诉我们,系统的内在复杂性总是处于程序代码所能表达的边缘。
以前的人们是这样描述C++的:它是一种通用的编程语言,它是更好的C,它支持数据抽象,它支持面向对象编程。请注意,这里并没有说“C++是一门面向对象的编程语言”。这里的“数据抽象”指的是信息隐藏、非继承结构中的类,以及泛型编程。早期的C++对泛型编程的支持很差,只能通过宏来实现,模板和概念的出现要晚得多。
上世纪80年代末到90年代初的C++,重点解决了基于模板的泛型编程和基于异常的错误处理问题。在设计模板的过程中,为了与C风格代码竞争高要求系统的应用开发任务,在灵活性与效率面前,提早类型检查只能被放在更次要位置。这个问题直到C++20引入概念后才部分地得到解决。异常的设计重点在于异常的多级传播、将任意信息传递给异常处理程序,以及异常和资源管理融合等问题。其中的关键技术之一是将释放资源的工作交给带析构函数的局部对象完成,即所谓RAII(资源获取即初始化)。
C++继承机制的表现力非常丰富。它甚至支持从多个父类共同派生子类,即多重继承。当然多重继承很有难度,因此长期以来争议颇多。当前,绝大多数同时支持静态类型检查和面向对象设计的编程语言,都支持基于抽象基类(接口)的多重继承。
C++语言的演化与一些关键的库特性密切相关。早期的C++库已经有了复数类、动态数组类、栈类、I/O流类及操作符重载机制。后来又有了字符串和列表。并发特性早在1980年就已经是“C with Classes”的一部分,但直到2011年才正式进入C++11标准,并得到C++实现的支持。模板机制的引入催生了vector、list、map、sort等泛型容器和算法的设计和实现。
1998年,标准库最重要的革新是增加了标准模板库(STL)框架,其中包括泛型容器和算法。STL已经在C++社区和更大范围内产生了巨大的影响。
在C++的成长环境中,有着众多或成熟或实验性的编程语言,如Ada、Algol、ML等。它们对C++语言的发展构成了不同程度的影响。但是,真正起决定性作用的,还是实际的应用场景。这是一个需要深思熟虑的策略问题,它令C++的革新是问题驱动式的,而非模仿性的。
C++自诞生以来,其使用情况呈爆炸式地增长,语言本身也不可避免地会发生一些变化。1987年,事情逐步变得明朗,C++的正式标准化已成必然。C++的标准化工作需要语言发明者、编译器实现者和大量使用者共同参与。
AT&T贝尔实验室允许将《C++参考手册(修订版)》的草案向C++社区公开,其中不乏AT&T的竞争对手。AT&T贝尔实验室是一家开明的公司,其对C++社区的无私贡献,在很大程度上避免了C++作为一门编程语言的碎片化问题。来自数十家机构的大约100个人阅读了草案并提出了意见,使之成为被普遍接受的参考手册和后续标准化工作的基础文献。
C++语言美国国家标准化工作组X3J16,于1989年12月筹建,由HP公司发起,并于1991年6月成为C++语言国际标准化工作组WG21的一部分。最初标准草案的公众预览版于1995年4月发布。1998年,第一个ISO C++标准,以22票赞成0票反对,正式获得批准,代号ISO/IEC 14882-1998,简称C++98。此标准的缺陷修正版与2003年发布,即C++03,它与C++98的差别不大。
C++11曾经有很多年被称为C++0x,它是WG21的重要成果。委员会的工作流程严谨而繁重,这有助于产生更好也更严格的规范,但同时也限制了创新。这一版本标准草案的公众预览版于2009年发布。2011年8月,历史上第二份ISO C++标准,以21票赞成0票反对,正式获得批准,代号ISO/IEC 14882-2011,简称C++11。
造成C++11与C++98/03两个标准间隔如此漫长的原因是,大多数委员会成员,包括本贾尼博士本人,都对ISO的规则有一个错误印象,以为在上一版标准发布之后,下一版标准拟定之前,要有一个等待期。结果就是,有关新语言特性的标准化工作直到2002年才开始。其它原因包括现代语言及其基础库日益增长的规模。语言的规模增长了30%,而库的规模则增长了100%。规模的增长大部分来源于更详细的规范,而非新增加的功能。而且C++标准化的工作必须非常小心,不能发生由于不兼容而导致旧代码不能工作的问题。委员会不能破坏数十亿行正在被使用的C++代码。数十年如一日地保证稳定的兼容性,也是标准制定工作的重要特质。
C++11向标准库增加了非常多的工具和方法,并推动了语言特性集的完善,这都为了满足一种综合编程风格——在C++98/03中已被证明很成功的多种范型的总和。C++11标准制定工作的总体目标是:使C++成为系统编程和构建库的更好的语言,使C++成为更容易学习和使用的语言。
C++11标准制定的一项主要工作是实现并发系统程序设计的类型安全和可移植性。这包括一个内存模型和一组无锁编程特性,并在此基础上,增加了线程库。
在C++11之后,人们普遍认为相隔13年才推出新标准,时间实在是太长了。缩短标准发布之间的时间间隔,有助于减少夜长梦多的可能性,避免因有人提出“我恰好有个重要功能要添加”而导致时间拖延。最后采用的方案是每三年发布一个更新版本。三年一次的更新,可能会是小版本更新,也可能是大版本更新,或者二者交替出现。
C++14是一个旨在完善C++11的小版本更新。这反映了一个现实,在固定的发布日期下,一定会存在一些想要但无法按时交付的功能。人们会经常发现,新版本中的一些功能很完善,而另一些功能则更象是个半成品,甚至连半成品都算不上。
C++17本该是个大版本更新,即涉及软件架构、设计方法和思维模式等方面的更新。然而事实上,C++17顶多只能算是一个中等版本更新。它包括许多小的扩展,很多功能,如概念、模块、协程等,要么还没准备好,要么尚存争议,要么缺乏设计方向。结果就是C++17在每个方面都改善了一点点,但并不会显著改变那些,已经接受并正在实践C++11和C++14标准的程序员们的生活。
C++20提供了承诺已久其迫切需要的重大功能特性。如模块、概念、协程、范围和许多小功能。它同C++11一样,是对C++的重大升级。截止到2021年底,C++20已经广泛可用。
WG21现在大约有350名成员。在如此庞杂和多样化的群体中获得惊人的一致,是一件十分艰巨的工作。臃肿的功能、缺乏一致性的风格和短视的决策,无不阻碍着C++语言向更易于使用、更连贯的方向进化。委员会意识到了这一点,并在持续不断地反击。总的来讲是成功的,但仍不可避免地会有一些复杂性,从那些有用的小功能、流行时尚的语言特性和服务于罕见的特殊情况中滋生蔓延出来。
标准规定了什么是正确的,但并没有规定怎么才能做到正确。后者依赖于良好且有效率的实践。理解语言功能的技术细节是一回事,将它们与其它功能、库和工具有效地结合使用,以生成更好的软件是另一回事。这里所说的更好,是指更易于维护、更不易出错、更快地运行。委员会有责任开发、推广和支持一致的编程风格,同时支持旧代码向这些更现代、更有效和更一致的风格演变。
随着语言和库的发展,推广高效的编程风格,正在变得越来越重要。让程序员仅仅为了追求高效和卓越,而放弃那些现在还能运行的代码,及其困难。直到今天,仍然有人认为C++只是对C语言的一些小修小补,也仍然有人相信,兴盛于上世纪八九十年代的,那种基于大规模类层次结构的面向对象编程,才是发展的顶峰。许多人仍在为于大量旧C++代码中使用现代C++而斗争。另外,还有许多人过于热衷于使用新特性,他们甚至认为,只有使用了大量模板元编程的代码,才能算是真正的C++。
2015年,本贾尼博士会同大批来自世界各地,特别是来自Microsoft、RedHat和Facebook的人一起,启动了名为“C++ Core Guidelines”的项目,旨在实现完全的类型安全和资源安全,作为更简单、更快速、更安全,也更易于维护的代码基础。该项目将推动整个C++社区向前发展,以期从语言功能、库和工具的持续改进中受益。
时至今日,C++堪称被广泛使用的编程语言。其用户群从1979年的1人迅速增长到1991年的40万人。在这12年的时间里,它的用户量大约每7.5个月就会翻一翻。当然,自最初的急剧增长之后,用户量的增长速度必然会逐渐放缓。截止到2018年,全世界范围内,大约有450万名C++程序员。如果算到今天,这个数字会再增加至少120万。而且这种增长大部分发生在2005年以后。当时处理器速度的指数级增长停止了,因此语言的性能变得越来越重要。实现这种增长并没有依赖正式的市场营销或有组织的用户社区。
C++主要是一种工业语言,也就是说它在工业领域的应用,要远远超过其在教育或编程语言研究中的应用。C++诞生于贝尔实验室,它经受住了电信和系统编程(包括设备驱动、网络通信和嵌入式系统)中各种严苛需求的考验。从那时起,C++的使用就已经扩展到几乎所有行业:微电子、Web应用和基础设施、操作系统、金融、医疗、汽车、航空航天、高能物理、生物、能源、机器学习、视频游戏、图形图像、动画、虚拟现实,等等。在这些领域,C++相比于其它编程语言的优势在于,它既能高效地操作底层硬件,同时又能很好地控制上层复杂性。就目前看来,C++的应用领域还在不断地扩充。
可以将C++语言概括为一组相互支撑的设施:
静态类型系统,对内置类型和用户定义类型一视同仁
值语义和引用语义
系统和通用资源管理(RAII)
高效的面向对象编程
灵活的泛型编程
编译时编程
直接使用机器和操作系统资源
通过库提供并发编程,通常借助内部函数实现
标准库组件为这些高级目标提供了进一步的基本支持。
这里关注的重点是自C++98/03以后,历经C++11、C++14、C++17和C++20等数次版本更新,C++在语言特性和标准库方面的演进过程。
单纯地逐条罗列C++的各种语言特性容易让人感到困惑。需要记住,任何一种语言特性通常都不是单独使用的,特别是,大多数C++11中新增加的语言特性,如果离开了原有的旧特性所提供的框架,将变得毫无意义。
编号 | 新增特性 | 内容参见 |
---|---|---|
1 | 通过花括号列表,执行统一且通用的初始化 | 1.4.2 初始化 5.2.3 容器的初始化 |
2 | 借助auto关键字,从初始值设定项中推导出类型 | 1.4.2 初始化 |
3 | 避免类型窄化 | 1.4.2 初始化 |
4 | 更加通用且有保证的常量表达式constexpr | 1.6 常量 |
5 | 基于范围的for循环 | 1.7 指针、数组和引用 |
6 | 用关键字nullptr表示空指针 | 1.7.1 空指针 |
7 | 通过enum class定义带作用域的强类型枚举 | 2.4 枚举 |
8 | 返回类型后置语法 | 3.4.4 返回类型后置 |
9 | 通过noexcept说明符防止异常传播 | 4.4 错误处理的其它替代方式 |
10 | 编译时断言static_assert | 4.5.2 static_assert |
11 | 从花括号列表到std::initializer_list的语言映射 | 5.2.3 容器的初始化 |
12 | 通过override关键字显式指明覆盖 | 5.3 抽象类 |
13 | 通过default和delete关键字控制(对象成员的)默认实现 | 6.1.1 基本操作 |
14 | 类内成员的初始值设定项 | 6.1.3 成员初始值设定项 |
15 | 借助右值引用表达转移语义 | 6.2.2 转移容器 |
16 | 用户自定义字面量 | 6.6 用户自定义字面量 |
17 | 匿名函数 | 7.3.3 匿名函数 |
18 | 为类型和模板定义别名 | 7.4.2 别名 |
19 | 可变参数模板 | 8.4 可变参数模板 |
20 | 原始字符串字面量 | 10.4 正则表达式 |
21 | 更简单、更通用的SFINAE(替换失败不是错误)规则 | 16.4.3 类型生成器 |
22 | 借助__func__宏获取当前函数名字符串 | 16.5 source_location |
23 | 内存模型 | 18.1 引言 |
24 | Unicode字符 | — |
25 | long long整数类型 | — |
26 | alignas和alignof对齐控制 | — |
27 | 借助decltype获取表达式的类型 | — |
28 | [[carries_dependency]]和[[noreturn]]属性 | — |
29 | 借助noexcept操作符在表达式中检测throw的可能性 | — |
30 | 源自C99的整数类型扩展(选择较长整数类型的规则)、 宽窄字符串编译时拼接、__STDC_HOSTED__、_Pragma(X)、 可变参数宏和空参数宏 | — |
31 | inline命名空间 | — |
32 | 继承构造函数 | — |
33 | 委托构造函数 | — |
34 | 显式类型转换操作符 | — |
35 | 借助extern关键字显式实例化模板 | — |
36 | 函数模板的默认模板参数 | — |
37 | 基于thread_local的线程本地存储 | — |
编号 | 新增特性 | 内容参见 |
---|---|---|
1 | 二进制字面量 | 1.4 类型、变量与运算 |
2 | 数字分隔符 | 1.4 类型、变量与运算 |
3 | 改进constexpr函数,允许使用循环 | 1.6 常量 |
4 | 函数的返回类型推导 | 3.4.3 返回类型推导 |
5 | 泛型匿名函数 | 7.3.3.1 匿名函数作为函数参数 |
6 | 模板变量 | 7.4.1 模板变量 |
7 | 更通用的匿名函数捕获表 | — |
8 | [[deprecated]]属性 | — |
9 | 其它小扩展 | — |
编号 | 新增特性 | 内容参见 |
---|---|---|
1 | 严格指定运算顺序 | 1.4.1 算术运算 |
2 | 带有初始值设定项的选择语句 | 1.8 检验 |
3 | 用底层类型的值初始化enum类型的变量 | 2.4 枚举 |
4 | 结构化绑定 | 3.4.5 结构化绑定 |
5 | 保证拷贝省略 | 6.2.2 转移容器 |
6 | 类模板参数的类型推导 | 7.2.3 模板参数推导 |
7 | 编译时if | 7.4.3 编译时if |
8 | 泛型值模板参数(auto模板参数) | 8.2.5 概念与auto |
9 | 折叠表达式 | 8.4.1 折叠表达式 |
10 | 十六进制浮点数字面量 | 11.6.1 流式格式化 |
11 | std::byte类型 | 16.7 位操作 |
12 | 超对齐类型的动态分配 | — |
13 | UTF-8字面量(u8) | — |
14 | constexpr匿名函数 | — |
15 | inline变量 | — |
16 | [[fallthrough]]、[[nodiscard]]和[[maybe_unused]]属性 | — |
17 | 其它小扩展 | — |
编号 | 新增特性 | 内容参见 |
---|---|---|
1 | 保证编译时求值的consteval函数 | 1.6 常量 |
2 | 保证静态(非运行时)初始化的constinit变量 | 1.6 常量 |
3 | 对带作用域的枚举使用using | 2.4 枚举 |
4 | 模块 | 3.2.2 模块 |
5 | 三向比较(宇宙飞船)操作符“<=>” | 6.5.1 比较(关系操作符) |
6 | 通过“[*this]”按值捕获当前对象 | 7.3.3 匿名函数 |
7 | 概念 | 8.2 概念 |
8 | 协程 | 18.6 协程 |
9 | 可指定的初始值设定项(C99功能的略微受限版本) | — |
10 | [[no_unique_address]]、[[likely]]和[[unlikely]]属性 | — |
11 | 在constexpr中使用new、union、try-catch、dynamic_cast和typeid | — |
12 | 其它小扩展 | — |
C++11以两种形式为标准库添加了新的内容,一种是全新的组件,如正则表达式匹配库,另一种是对C++98标准库的改进,如为容器增加了转移语义。
编号 | 新增组件 | 内容参见 |
---|---|---|
1 | 容器的initializer_list构造函数 | 5.2.3 容器的初始化 |
2 | 容器的转移语义 | 6.2.2 转移容器 13.2 使用迭代器 |
3 | 正则表达式(reg) | 10.4 正则表达式 |
4 | 单向链表(forward_list) | 12.4 forward_list |
5 | 哈希容器(unordered_map、unordered_multimap、 unordered_set和unordered_multiset) | 12.6 unordered_map 12.8 容器概述 |
6 | 容器的emplace操作 | 12.8 容器概述 |
7 | 更多算法,如move、copy_if和is_sorted等 | 13 算法 |
8 | 资源管理指针(unique_ptr和shared_ptr) | 15.2.1 unique_ptr和shared_ptr |
9 | 固定大小的连续序列容器array | 15.3.1 array |
10 | 元组(tuple) | 15.3.4 tuple |
11 | duration和time_point等时间工具 | 16.2.1 时钟 |
12 | 改进的函数适配器 | 16.3 函数适配 |
13 | 类型特征,如is_integral_v、is_base_of等 | 16.4.1 类型谓词 |
14 | 通过quick_exit放弃进程 | 16.8 退出程序 |
15 | 随机分布和随机引擎 | 17.5 随机数 |
16 | 整数类型别名,如int16_t、uint32_t、int_fast64_t等 | 17.8 类型别名 |
17 | thread | 18.2 任务和thread |
18 | 互斥量和锁 | 18.3.1 mutex和锁 |
19 | 低层并发支持(atomic) | 18.3.2 原子量 |
20 | 条件变量 | 18.4 等待事件 |
21 | 高层并发支持(packaged_thread、future、promise和async) | 18.5 任务间通信 |
22 | 拷贝和重新抛出异常 | 18.5.1 future和promise |
23 | 垃圾回收(后来被移除) | 19.2.9 移除或弃用的特性 |
24 | 借助system_error报告错误代码 | — |
25 | 广泛地使用constexpr函数 | — |
26 | 系统地使用noexcept函数 | — |
27 | string到数值的转换 | — |
28 | 带作用域的分配器 | — |
29 | 基于ratio的编译时有理数算术 | — |
30 | 其它小扩展 | — |
编号 | 新增组件 | 内容参见 |
---|---|---|
1 | 用户自定义字面量 | 6.6 用户自定义字面量 |
2 | 按类型寻址元组 | 15.3.4 tuple |
3 | shared_mutex、shared_lock和unique_lock | 18.3.1 mutex和锁 |
4 | 关联容器的异构查找 | — |
5 | 其它小扩展 | — |
编号 | 新增组件 | 内容参见 |
---|---|---|
1 | string_view | 10.3 字符串视图 |
2 | 文件系统 | 11.9 文件系统 |
3 | 多态分配器 | 12.7 分配器 |
4 | 并行算法 | 13.6 并行算法 17.3.1 并行数值算法 |
5 | variant | 15.4.1 variant |
6 | optional | 15.4.2 optional |
7 | any | 15.4.3 any |
8 | 特殊数学函数 | 17.2 数学函数 |
9 | scoped_lock | 18.3.1 mutex和锁 |
10 | 通过invoke函数调用任何可以为给定参数集调用的方法 | — |
11 | 基于to_chars和from_chars的基本字符串转换 | — |
12 | 其它小扩展 | — |
编号 | 新增组件 | 内容参见 |
---|---|---|
1 | 基于format和vformat的printf风格格式化 | 11.6.2 printf风格的格式化 |
2 | 范围、视图和管道 | 14.1 引言 |
3 | 用于读写连续数组的span | 15.2.2 span |
4 | 日期 | 16.2.2 日期 |
5 | 时区 | 16.2.3 时区 |
6 | source_location | 16.5 source_location |
7 | bit_cast | 16.7 位操作 |
8 | 位操作 | 16.7 位操作 |
9 | pi、log10e等数学常数 | 17.9 数学常数 |
10 | 对atomic的诸多扩展 | 18.3.2 原子量 |
11 | 通过barrier和latch等待多个线程 | — |
12 | 特性测试宏 | — |
13 | 更多标准库函数成为constexpr函数 | — |
14 | 在标准库中更多地使用“<=>”操作符 | — |
15 | 其它小扩展 | — |
世界上有数十亿行C++代码在运行。没有人确切知道,哪些场合使用了哪些特性。因此,尽管ISO委员会已持续警告多年,很多人仍不太愿意移除那些基于旧特性的代码。然而,一些以制造麻烦著称的特性确实应被移除或弃用。
弃用某个功能,意味着标准委员会会有计划地让该功能消失。但是,委员会无权立即移除那些被频繁使用的功能,无论它们是多么的多余或者危险。因此,弃用是一种强烈的暗示,提示用户尽量避免使用该功能,因为它可能在未来的某一天突然消失。已被弃用的特性列表位于语言标准的附录部分。当编译器发现用户使用了某个已弃用的功能时,通常会发出警告。但是,那些被标记为弃用的功能,仍然是现行标准的一部分。历史上,为了保证兼容性,它们往往会被支持相当长的时间。由于用户对实现者所施加的压力,即使是那些最终被移除的功能,也往往可以在编译器(至少是部分编译器)中继续使用。
下面这些特性已被C++20移除:
异常说明,如“void foo() throw(X, Y);”,在C++98中可用,现在会报错
异常规范的支持工具,如unexpected_handler、set_unexpected、get_unexpected和unexpected等,现在用noexcept替代
三字符组
auto_ptr,现在用unique_ptr替代
存储说明符register
对bool类型的变量使用“++”操作符
通过export关键字导出模板,现在export关键字仅用于模块
将字符串字面量赋值给“char*”类型的变量,现在需要使用“const char*”或者auto
与参数绑定有关的标准库函数对象和相关函数,现在用匿名函数或者function替代
垃圾回收
下面这些特性已被C++20弃用:
为具有析构函数的类自动生成拷贝构造函数和拷贝赋值操作符函数
将定义在不同enum中的枚举值相互比较,或将一个枚举值与浮点数进行比较
比较两个数组
下标中的逗号操作,如“a[i,j]”,依赖于用户定义的带有多个参数的下标操作符函数
在匿名函数中隐式捕获“*this”,现在需要显式使用“[=,this]”
strstream,现在用spanstream替代
C++11:垃圾回收(后来被移除)
除了少数特例外,C++可以看作是C的超集(这里指的是C++20和C11)。二者的大多数差异来自C++更强调类型检查。编写良好的C代码往往也是合法的C++代码。编译器可以诊断C++和C之间的每一个差异。C++20标准的附录部分列出了其与C11不兼容的地方。
为什么说C++是C的兄弟(而不是后裔)呢?下面是一个简化的语言族谱:
经典C有两个主要的后代,标准C和标准C++。多年来,这两种语言以不同的速度向不同的方向发展。结果就是,每种语言都以略有不同的方式,提供对传统C风格编程的支持。由此产生的兼容性问题,会给同时使用C和C++的程序员带来麻烦。无论是在一种语言中调用另一种语言的库的使用者,还用一种语言为另一种语言提供库的实现者,程序员的生活都可能变得十分悲惨。
上面图中的粗线表示继承了大量特性,细线表示借鉴了主要特性,虚线表示参考了次要特性。标准C和标准C++作为经典C的两个主要后裔,表现出一种兄弟般的并列关系。两者都带有经典C的关键特性,但又都不与经典C完全兼容。经典C实际上就是在K&R C(K=Brian W. Kernighan,R=Dennis M. Ritchie)的基础上增加了枚举和结构体赋值。
请注意,C和C++之间的差异,不一定是将对C的更改引入C++的结果。一些特性在C++中已被广泛使用多年,然后C又以不一致的方式引入了该特性,从而引发了兼容性问题。例如void*可被隐式转换为任意类型的指针,又如全局常量的链接。还有一些特性是在进入C++标准一段时间后,才被C标准所吸纳,这中间也会存在间断性的不兼容。例如对内联的定义。
C和C++之间存在许多细微的不兼容。所有这些问题都可能会给程序员带来麻烦,但如果仅从C++的角度来处理,其实并不难以解决。一般而言,任何纯粹C语言的代码片段都可以纯C的方式被编译,并通过extern "C"机制完成链接。
将C程序转换为C++程序可能会遇到下面这些问题:
次优的设计和编程风格
void*被隐式地转换为其它类型的指针
将一些C++关键字,如class、private等,用作标识符
分别按C和C++方式编译产生的目标文件,在链接时不兼容
显然,C程序是以C风格编写的,就象K&R在《The C Programming Language》一书中使用的风格。这意味着广泛地使用指针和数组,甚至还有随处可见的各种宏。这些设施很难在大型程序中被可靠地使用。资源管理和错误处理通常都是临时起意,不仅没有语言级的工具支持,而且连完整的文档和一致性规范都很欠缺。即便是将一段C程序,简单地逐行转换为对等的C++程序,编译器至少会执行更好地检查,进而发现更多的bug。当然,这样做并没有改变程序的基本结构,产生缺陷的根源没有得到彻底解决。如果在原始的C程序中既已存在不完整的错误处理、资源泄漏或者缓冲区溢出等问题,那么它们在简单翻译而成的C++程序中会依然存在,除非更改代码的主要结构,以获得来自C++的优势。以下是一些关于从C风格迁移到C++风格的建议:
不要将C++仅仅视为添加些许功能的C。C++的确可以那样使用,但那绝不是最好的方式。与C相比,要想真正从C++中获益,就要采用完全不同的设计理念和实现风格
将C++标准库作为新技术和新风格的榜样。注意与C标准库的区别,例如通过“=”复制字符串,而不要再使用strcpy函数
在C++中几乎不需要用到宏。可以用const、constexpr、enum或enum class定义常量或常量清单;用constexpr、consteval和inline避免函数调用开销;用template声明一系列函数和类型;用namespace避免名字冲突。这些在C中需要用宏的地方,在C++中都有更好的替代方案
不要在需要变量之前声明它,变量一经声明请立即初始化。变量的声明可以出现在任何能够使用语句的地方,例如for语句的初始值设定项或条件语句中
不要使用malloc和free分配和释放自由存储区中的内存,new和delete操作符可以更好地完成相同的任务。realloc的很多应用场景也可以被动态数组vector取代。不要只是将malloc和free简单替换为“裸”new和delete
避免使用void*、union和强制类型转换,除非在某些函数或类的实现深处。使用这些东西,一方面限制了类型系统所能提供的支持,另一方面也会导致性能下降。在大多数情况下,强制类型转换往往意味着设计的失误
如果必须使用显式类型转换,请使用语义更加明确的命名类型转换,如static_cast等,以表明转换的真实意图
尽量减少对数组和C风格字符串的使用。与传统C风格相比,C++标准库的string、array和vector通常更有助于编写简单且易于维护的代码。一般而言,非到万不得已,不要尝试自己构建标准库已经提供的东西
除非在专门的代码中,如内存管理器等,尽量避免使用指针算术
将连续序列,如数组等,作为span传递。这是在不额外添加测试案例的情况下,避免范围错误(缓冲区溢出)的好方法
对于简单的数组遍历,可以使用基于范围的for循环语句。与传统C风格的for循环相比,它更易于编写,速度更快,也更安全
使用nullptr表示空指针,即什么都不指向的指针,不要再用0或者NULL宏
不要假设用C风格(没有类、模板、异常等C++特性)费力编写的东西,一定比更精炼的替代方案(使用C++标准库)更高效,实际情况通常恰恰相反(当然也不绝对)
在C中,void*可用于为任意类型的指针赋值或初始化,但在C++中却不行。例如:
xxxxxxxxxx
11int* p = malloc(n * sizeof(int)); // 在C中正确,在C++中报错
这可能是最难处理的不兼容问题了。注意,从void*到不同类型指针的转换并非总是无害的。例如:
xxxxxxxxxx
41char ch;
2void* pv = &ch;
3int* pi = pv; // 在C中正确,在C++中报错
4*pi = 666; // 意外修改了ch后面三个字节中的数据
如果同时使用C和C++两种语言编写程序,应将malloc函数的返回值,显式转换为所期望的指针类型。例如:
xxxxxxxxxx
11int* p = (int*)malloc(n * sizeof(int)); // 在C和C++中都正确
如果只使用C++编写程序,应避免使用malloc函数。例如:
xxxxxxxxxx
11int* p = new int[n];
C和C++可以被实现为使用不同的链接规范,多数编译器也确实是这么做的。其基本原因是C++极为强调类型检查。另外,还有一个实现上的原因是C++支持重载,因此可能出现相同作用域内,有多个同名函数共存的情况。为此,C++链接器必须采取一些更为特殊的方法,以解决这个问题。
为了让一个C++函数也能够使用C的链接规范,从而使它可以被C程序调用,或者反过来,让一个C函数能够被C++程序调用,需要将其声明为extern "C"。例如:
xxxxxxxxxx
11extern "C" double sqrt(double);
这样,sqrt函数就可以被C或C++代码调用了,而其定义既可以作为C函数,也可以作为C++函数被编译。
在同一个作用域中,对于任何给定的名字,只允许一个具有该名字的函数使用C链接规范,因为C不允许函数重载。链接说明不影响类型检查,因此对于被声明为extern "C"的函数,仍要遵循C++的函数调用和参数检查规则。
《ISO C++标准》定义了C++
在为新项目选择风格或更新代码库时,请参考《C++ Core Guidelines》
学习C++时,不要孤立地关注语言特性
不要拘泥于几十年前的语言特性集和设计技术
在生产代码中使用新特性,最好先编写一个小程序试验一番,以验证该特性是否符合标准,性能能否满足要求
学习C++时,请使用能获得的最新的、最完整的标准C++实现
C和C++的公共子集,并非学习C++的最佳初始子集
避免强制类型转换
优先选择命名类型转换,如static_cast等,不要使用C风格的类型转换
将C程序改写为C++程序时,要给与C++关键字存在冲突的标识符换名
如果不得不使用C语言编写程序,出于可移植性和类型安全的考虑,尽可能使用C和C++的公共子集
将C程序改写为C++程序时,应将malloc函数的返回值强制转换为适当的类型,或者干脆改成new
在将malloc和free换成new和delete后,考虑用vector、push_back和reserve取代realloc
C++不允许从整数到枚举类型的隐式类型转换,如果必须转换,请使用显式类型转换
每个形如<xxx.h>的标准C头文件,都将名字定义在全局命名空间中,与之对应的C++头文件<cxxx>,则将相同的名字定义在std命名空间中
在C++中声明C函数时,请使用extern "C"链接说明符
优先使用string而非C风格字符串(以空字符结尾的字符数组)
优先使用iostream而非stdio
优先使用容器,如vector等,而非内置数组