ModernC++/欢迎回到C++ 现代C++
欢迎回到C++ -现代C++
Welcome back to C++ - Modern C++
自从它被创建,C++逐渐成为世界上使用最广泛的语言之一。正确编写的 C++ 程序快速、高效。 相对于其他语言,该语言更加灵活:它可以在最高抽象级别上工作,也可以在芯片级(硅级别)上工作。C++ 提供高度优化的标准库。它允许访问低级硬件功能,以最大限度地提高速度并最大限度地减少内存需求。C++可以创建继续所有种类的应用:游戏、设备驱动、HPC、云、桌面、嵌入式和移动应用程序等等。甚至其他编程语言的库和编译器也是用 C++ 编写的。
C++的一个原始需求是向下兼容C语言。因此,C++总是允许以C风格编程,使用原始指针、数组、以空字符结尾的数组字符串<span class=”hint–top hint–rounded” aria-label=”及用数组存储的字符串,以 \0
结束。例const char *str = "Hello World\0";
“>[1]以及其他功能。这或许能够拥有高效的表现,但也可以出现报错并让程序出现复杂性。C++的发展强调了一些特性,这些特性大大减少了对使用C风格语法的需要。当您需要他们时,旧式C编程工具仍然存在。但是,在现代 C++ 代码中,您应该越来越少地需要它们。现代 C++ 代码更简单、更安全、更优雅,并且仍然和以往一样快。
以下部分概述了现代 C++ 的主要特性。除非另有说明,此处列出的功能在 C++11 及更高版本中可用。在 Microsoft C++ 编译器中,您可以设置 /std 编译器选项来指定要用于您的项目的标准版本。
资源和智能指针
Resource and smart pointers
C风格编程中的一类主要bug是内存泄漏(确实如此,三天写代码,两天找bug)。泄漏通常是由于对使用 new 分配的内存调用 delete 失败引起的。现代 C++ 强调“资源获取即初始化”(RAII) [2]原则。 其理念很简单。 资源(堆内存、文件句柄、套接字等)应由对象“拥有”。 该对象在其构造函数中创建或接收新分配的资源,并在其析构函数中将此资源删除。 RAII 原则可确保当所属对象超出范围时,所有资源都能正确返回到操作系统。
为了支持对简单采用RAII的原则,C++基本库(STL)提供了三个智能指针类型: std::unique_ptr
、std::shared_ptr
以及std::weak_ptr
。智能指针拥有申请和删除内存的控制句柄(智能指针处理它所拥有的内存的分配和删除)[3]。下面的示例演示了一个类,其中包含一个数组成员,该成员是在调用 make_unique()
时在堆上分配的。 对和的 new
调用 delete
由 unique_ptr
类封装。 当 widget
对象超出范围时,将调用 unique_ptr 析构函数,此函数将释放为数组分配的内存。
1 |
|
请尽可能使用智能指针管理堆内存(确实如此,如果没有追求极致效率的需求,使用智能指针是最方便且安全的)。 如果必须 new
显式使用和 delete
运算符,请遵循 RAII 原则。 有关详细信息,请参阅对象生存期和资源管理 (RAII)。
std::string 和 std::string_view
std::string and std::string_view
C风格字符串是错误产生的另一个重要来源。通过使用std::string
和 std::wstring
,您可以排除几乎所有和C风格字符串有联系的错误。此外,你还可以通过其成员方法获得更多字符串操作上的便利,例如搜索,追加,前缀等。两者都针对速度进行了高度优化[4]。将字符串传递给只需要只读访问权限的函数时,在 C++17 中,您可以使用 std::string_view 以获得更大的性能优势。
std::vector向量和其他标准库容器
std::string and other Standard Library containers
标准库容器都遵循 RAII 原则。 它们为安全遍历元素提供迭代器。 此外,它们对性能进行了高度优化,并且已充分测试正确性。 通过使用这些容器,可以消除自定义数据结构中可能引入的 bug 或低效问题。 使用 vector
替代原始数组,来作为 C++ 中的序列容器。
1 |
|
使用 map(而不是 unordered_map),作为默认关联容器。 对于退化和多案例,使用 set、multimap 和 multiset。
1 |
|
需要进行性能优化时,请考虑以下用法:
- 例如当重要的数据被嵌入时,将std::array类型作为类成员。
- 使用无序的关联容器,例如 unordered_map。 它们的每个元素的开销较低,并且具有固定时间查找功能,但正确高效地使用它们的难度更高。
- 使用std::vector时需要排序。有关详细信息,请参阅算法。
不要使用 C 风格的数组。对于需要直接访问数据的旧 API,请使用访问器方法,例如f(vec.data(), vec.size());
有关容器的更多信息,请参阅 C++ 标准库容器。
标准库算法
在假设需要为程序编写自定义算法之前,请首先查阅C++ 标准库中的算法。 标准库包含许多常见操作(如搜索、排序、筛选和随机化)的算法分类,且这些分类的算法库还在不断增加。 譬如<math>中的内容涵盖的很广泛。 自 C++17 起,变提供了许多算法的并行版本。
这里列举以下内容,比较重要。
for_each
:默认遍历算法(基于范围的 for 循环)。transform
:用于容器元素的非就地修改[5]。find_if
:默认搜索算法。sort
,lower_bound
:排序、在一个范围内找到搜索元素的下标。以及其他排序和搜索算法。
如果要写一个比较函数,可以使用lambda表达式以及’<’符号
1 |
|
使用auto关键字代替显示类型名称
auto instread of explicit type names
C++11引入了auto关键字,以便在变量、函数和模板的声明。auto关键字会指示编译器推导对象的类型,这样您即可无需显示键入。当对象是嵌套模板时,使用auto进行声明尤其有用。
1 |
|
基于范围的for循环
Range-based for loops
对数组和容器的C风格迭代容易出现索引错误,且重复的键入过程单调乏味。要消除这些错误并使您的代码更具可读性,请使用基于范围的 for 循环以及标准库容器和原始数组。有关详细信息,请参阅请参阅基于范围的 (https://docs.microsoft.com/zh-cn/cpp/cpp/range-based-for-statement-cpp?view=msvc-170)语句。
1 |
|
译注:
使用for_each虽然可以极大的提高for效率,但是如果在处理极大数据时或极长文本时请谨慎使用。因为其会将内容复制一份进行遍历。
用 constexpr
表达式替代宏
C 和 C++ 中的宏是在编译前由预处理器处理的一种标记。在编译代码之前,编译器会将使用宏定义的地方替换成宏所定义的值。C 样式编程通常使用宏来定义编译时常量值。 但宏容易出错且难以调试。 在现代 C++ 中,应优先使用 constexpr
变量定义编译时常量:
1 |
|
统一初始化
在现代 C++ 中,可以使用任何类型的括号初始化。 在初始化数组、矢量或其他容器时,这种初始化形式会非常方便。在下面的这个例子中声明了一个类S,三个均为std::vetor(向量)类型的变量v1,v1,v3并用不同方式进行初始化。
1 |
|
若要了解详细信息,请参阅括号初始化。
移动语义
现代 C++ 提供了移动语义,此功能可以避免进行不必要的内存复制。 在此语言的早期版本中,在某些情况下无法避免复制。 移动操作会将资源的所有权从一个对象转移到下一个对象,而不必再进行复制。 一些类拥有堆内存、文件句柄等资源。 实现资源所属的类时,可以定义此类的移动构造函数和移动赋值运算符。 在解析重载期间,如果不需要进行复制,编译器会选择这些特殊成员。 如果定义了移动构造函数,则标准库容器类型会在对象中调用此函数。 有关详细信息,请参阅移动构造函数和移动赋值运算符 (C++)。
Lambda表达式
在使用C语法编程中,一个函数可以通过返回函数指针的方式传递给另一个函数,这不方便位于和理解。它们引用的函数可能在源代码的其他地方定义,而不是从调用它的位置定义的。而且他们不是类型安全的。现代C++提供了对函数对象、类的运算符重写,从而使它们可以像函数一样进行调用。创建函数对象的最简便方法是使用内联 lambda 表达式。 下面的示例演示如何使用 lambda 表达式传递函数对象,然后由 for_each
函数在vector的每个元素中调用此函数对象:
1 |
|
Lambda表达式的形式为[](){}
,这里的是[=](int i) { return i > x && i < y; }
。此函数有一个int类型的形参,并返回一个bool类型的值,指示该参数是否大于 x
且小于 y
。这里使用的x,y是lambda之前声明的两个变量。行为上看似是[=]接受了上文的变量,实际上是lambda接受了值的副本。有关更详细的内容请看相关专题。
异常
Exceptions
现代 c + + 强调异常,而不是错误代码,作为报告和处理错误条件的最佳方式。 有关详细信息,请参阅现代 C++ 处理异常和错误的最佳做法。
std::atomic
对线程间通信机制使用 C++ 标准库 std::atomic
结构和相关类型。
std::variant(C++17)
在以C风格编程程序时通过使用联合体(共用体)使不同类型的成员占据同一个内存位置而达到节约内存的目的。但这并不是安全的,并容易导致编译错误。 C++17 引入了更加安全可靠的 std::variant
类,来作为联合体(共用体)的替代项。可以使用 std::visit
函数以类型安全的方式访问 variant
类型的成员。
译者有话说
本页的上述的示例代码为微软参考手册的简单案例,本人会在下方附上个人理解的完整代码。此部分代码也可以移步至CvRain/oh-modern-cpp: Code example of Microsoft modern C + + vs2022 translation (github.com)进行阅览,请原谅本人代码水平不精,谢谢。
1 |
|
- 及用数组存储的字符串,以
\0
结束。例const char *str = "Hello World\0";
↩ - RAII,Resouce Acquisition Is Initialization:资源获取即初始化 ↩
- 智能指针说白了就是一个模板类来控制一个原始指针的内存操作。此部分可以翻阅其他书籍。 ↩
- 但实际上std::string 在某些方面使用性能并不优越,比如复制字符串。这里可参阅《C++性能优化指南》 ↩
std::stransform
不保证按顺序适用unary_op或binary_op若要按顺序将函数应用于序列或应用修改序列元素的函数(在指定的范围内应用于给定的操作,并将结果存储在指定的另一个范围内)。 ↩