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_ptrstd::shared_ptr以及std::weak_ptr。智能指针拥有申请和删除内存的控制句柄(智能指针处理它所拥有的内存的分配和删除)[3]。下面的示例演示了一个类,其中包含一个数组成员,该成员是在调用 make_unique() 时在堆上分配的。 对和的 new 调用 delete 由 unique_ptr 类封装。 当 widget 对象超出范围时,将调用 unique_ptr 析构函数,此函数将释放为数组分配的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <memory>
class widget
{
private:
std::unique_ptr<int> data;
public:
widget(const int size) { data = std::make_unique<int>(size); }
void do_something() {}
};

void functionUsingWidget() {
widget w(1000000); // lifetime automatically tied to enclosing scope
// constructs w, including the w.data gadget member
// ...
w.do_something();
// ...
} // automatic destruction and deallocation for w and w.data

请尽可能使用智能指针管理堆内存(确实如此,如果没有追求极致效率的需求,使用智能指针是最方便且安全的)。 如果必须 new 显式使用和 delete 运算符,请遵循 RAII 原则。 有关详细信息,请参阅对象生存期和资源管理 (RAII)

std::string 和 std::string_view

std::string and std::string_view
C风格字符串是错误产生的另一个重要来源。通过使用std::stringstd::wstring,您可以排除几乎所有和C风格字符串有联系的错误。此外,你还可以通过其成员方法获得更多字符串操作上的便利,例如搜索,追加,前缀等。两者都针对速度进行了高度优化[4]。将字符串传递给只需要只读访问权限的函数时,在 C++17 中,您可以使用 std::string_view 以获得更大的性能优势。

std::vector向量和其他标准库容器

std::string and other Standard Library containers
标准库容器都遵循 RAII 原则。 它们为安全遍历元素提供迭代器。 此外,它们对性能进行了高度优化,并且已充分测试正确性。 通过使用这些容器,可以消除自定义数据结构中可能引入的 bug 或低效问题。 使用 vector 替代原始数组,来作为 C++ 中的序列容器。

1
2
vector<string> apples;
apples.push_back("Granny Smith");

使用 map(而不是 unordered_map),作为默认关联容器。 对于退化和多案例,使用 set、multimap 和 multiset。

1
2
3
map<string, string> apple_color;
// ...
apple_color["Granny Smith"] = "Green";

需要进行性能优化时,请考虑以下用法:

  • 例如当重要的数据被嵌入时,将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
2
3
4
5
6
auto comp = [](const widget& w1, const widget& w2)
{ return w1.weight() < w2.weight(); }

sort( v.begin(), v.end(), comp );

auto i = lower_bound( v.begin(), v.end(), widget{0}, comp );

使用auto关键字代替显示类型名称

auto instread of explicit type names
C++11引入了auto关键字,以便在变量、函数和模板的声明。auto关键字会指示编译器推导对象的类型,这样您即可无需显示键入。当对象是嵌套模板时,使用auto进行声明尤其有用。

1
2
map<int,list<string>>::iterator i = m.begin(); // C-style
auto i = m.begin(); // modern C++

基于范围的for循环

Range-based for loops
对数组和容器的C风格迭代容易出现索引错误,且重复的键入过程单调乏味。要消除这些错误并使您的代码更具可读性,请使用基于范围的 for 循环以及标准库容器和原始数组。有关详细信息,请参阅请参阅基于范围的 (https://docs.microsoft.com/zh-cn/cpp/cpp/range-based-for-statement-cpp?view=msvc-170)语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <vector>

int main(){
std::vector<int> v {1,2,3};

// C-style
for(int i = 0; i < v.size(); ++i){
std::cout << v[i];
}

// Modern C++:
for(auto& num : v){
std::cout << num;
}
}

译注:
使用for_each虽然可以极大的提高for效率,但是如果在处理极大数据时或极长文本时请谨慎使用。因为其会将内容复制一份进行遍历。

用 constexpr 表达式替代宏

C 和 C++ 中的宏是在编译前由预处理器处理的一种标记。在编译代码之前,编译器会将使用宏定义的地方替换成宏所定义的值。C 样式编程通常使用宏来定义编译时常量值。 但宏容易出错且难以调试。 在现代 C++ 中,应优先使用 constexpr 变量定义编译时常量:

1
2
3
4
#define SIZE 10 // C-style
constexpr int size = 10; // modern C++
constexpr unsigned int number = 114514;

统一初始化

在现代 C++ 中,可以使用任何类型的括号初始化。 在初始化数组、矢量或其他容器时,这种初始化形式会非常方便。在下面的这个例子中声明了一个类S,三个均为std::vetor(向量)类型的变量v1,v1,v3并用不同方式进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <vector>

struct S
{
std::string name;
float num;
S(std::string s, float f) : name(s), num(f) {}
};

int main()
{
// C-style initialization
std::vector<S> v;
S s1("Norah", 2.7);
S s2("Frank", 3.5);
S s3("Jeri", 85.9);

v.push_back(s1);
v.push_back(s2);
v.push_back(s3);

// Modern C++:
std::vector<S> v2 {s1, s2, s3};

// or...
std::vector<S> v3{ {"Norah", 2.7}, {"Frank", 3.5}, {"Jeri", 85.9} };

}

若要了解详细信息,请参阅括号初始化

移动语义

现代 C++ 提供了移动语义,此功能可以避免进行不必要的内存复制。 在此语言的早期版本中,在某些情况下无法避免复制。 移动操作会将资源的所有权从一个对象转移到下一个对象,而不必再进行复制。 一些类拥有堆内存、文件句柄等资源。 实现资源所属的类时,可以定义此类的移动构造函数和移动赋值运算符。 在解析重载期间,如果不需要进行复制,编译器会选择这些特殊成员。 如果定义了移动构造函数,则标准库容器类型会在对象中调用此函数。 有关详细信息,请参阅移动构造函数和移动赋值运算符 (C++)

Lambda表达式

在使用C语法编程中,一个函数可以通过返回函数指针的方式传递给另一个函数,这不方便位于和理解。它们引用的函数可能在源代码的其他地方定义,而不是从调用它的位置定义的。而且他们不是类型安全的。现代C++提供了对函数对象、类的运算符重写,从而使它们可以像函数一样进行调用。创建函数对象的最简便方法是使用内联 lambda 表达式。 下面的示例演示如何使用 lambda 表达式传递函数对象,然后由 for_each 函数在vector的每个元素中调用此函数对象:

1
2
3
4
5
6
7
std::vector<int> v {1,2,3,4,5};
int x = 2;
int y = 4;
auto result = find_if(begin(v), end(v), [=](int i) {
return i > x && i < y;
}
);

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <memory>
#include <string>

class Student{
public:
    Student() = delete;
    Student(const std::string &name, const std::string &id)
        : stu_name(name), stu_id(id){
    }
    std::string GetName() const {
        return stu_name;
    }
    std::string GetId() const {
        return stu_id;
    }
    std::string GetCollege() const {
        return stu_college;
    }
    void SetCollage(const std::string college) {
        stu_college = college;
    }
private:
    const std::string stu_name;
    const std::string stu_id;
    std::string stu_college{};
};

int main() {
    auto student = std::make_unique<Student>("田所浩二", "114514");
    std::cout << student->GetId() << std::endl;
    std::cout << (*student).GetName() << std::endl;
    auto ptr_stu = student.get(); // if smart pointer is released, this raw pointer will be null
    ptr_stu->SetCollage("computr network");
    std::cout << ptr_stu->GetCollege() << std::endl;

    auto share_stu = std::make_shared<Student>("114", "1919810");
    auto share_stu_1(share_stu);
    share_stu_1->SetCollage("communication engineering");
    std::cout << share_stu->GetCollege() << std::endl;
}



// make_unique (C++14) creates a unique poniter that manages a new object
// make_unique_for_overwrite (C++20)

  1. 及用数组存储的字符串,以 \0结束。例const char *str = "Hello World\0";
  2. RAII,Resouce Acquisition Is Initialization:资源获取即初始化
  3. 智能指针说白了就是一个模板类来控制一个原始指针的内存操作。此部分可以翻阅其他书籍。
  4. 但实际上std::string 在某些方面使用性能并不优越,比如复制字符串。这里可参阅《C++性能优化指南》
  5. std::stransform 不保证按顺序适用unary_op或binary_op若要按顺序将函数应用于序列或应用修改序列元素的函数(在指定的范围内应用于给定的操作,并将结果存储在指定的另一个范围内)。

ModernC++/欢迎回到C++ 现代C++
http://cvrain.cloudvl.cn/2022/08/08/Cpp/Welcome back to C++/
作者
ClaudeRainer
发布于
2022年8月8日
许可协议