欢迎回到 C++ - 现代 C++

自创建以来,C++ 即已成为世界上最常用的编程语言之一。 正确编写的 C++ 程序快速、高效。 这种语言比其他语言更灵活:它既可以工作在最高的抽象层次,也可以直达硅层面。

C++ 提供高度优化的标准库。 它支持访问低级别硬件功能,从而最大限度地提高速度并最大程度地降低内存需求。 C++ 几乎可以创建任何类型的程序:游戏、设备驱动程序、High-Performance 计算、云、桌面、嵌入式和移动应用等。 即使是其他编程语言的库和编译器也以 C++ 编写。

C++ 的原始要求之一是与 C 语言向后兼容。 因此,C++ 允许使用原始指针、数组、以 null 结尾的字符串和其他功能进行 C 样式编程。 它们可以实现出色的性能,但也会产生 bug 和复杂性。

C++ 的演变强调了大幅减少使用 C 样式成语的需求的功能。 如果需要,你仍可以使用旧的 C 编程设施。 但是,在新式 C++ 代码中,对上述设施的需求会越来越少。 现代 C++ 代码更加简单、安全、美观,而且速度仍像以往一样快速。

下面几个部分概述了现代 C++ 的主要功能。 此处列出的功能在 C++11 及更高版本中可用,除非另有说明。 在 Microsoft C++ 编译器中,可以设置 /std 编译器选项,指定要用于项目的标准版本。

资源和智能指针

C 样式编程的一个主要 bug 类型是内存泄漏。 泄漏通常是由于未对使用 new 分配的内存调用 delete 而导致的。 现代 C++ 强调“资源获取即初始化”(RAII) 原则。

其理念很简单。 资源(如堆内存、文件句柄和套接字)应归对象 所有 。 该对象在其构造函数中创建或接收新分配的资源,并在其析构函数中将此资源删除。 RAII 原则可确保当所属对象超出范围时,所有资源都能正确返回到操作系统。

为了支持轻松采用 RAII 原则,C++ 标准库提供了三种智能指针类型:

智能指针处理其拥有的内存的分配和删除。 下面的示例演示了一个类,其中包含一个数组成员,该成员是在调用 make_unique() 时在堆上分配的。 类 unique_ptr 封装对 newdelete. 的调用。 widget当对象超出范围时,unique_ptr将调用析构函数,并释放为数组分配的内存。

#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

请尽可能地使用智能指针管理堆内存。 如果必须显式使用 newdelete 运算符,请遵循 RAII 原则。 有关详细信息,请参阅对象生存期和资源管理 (RAII)

std::stringstd::string_view

C 样式字符串是 bug 的另一个主要来源。 通过使用 std::stringstd::wstring,几乎可以消除与 C 样式字符串关联的所有错误。 你还可以享有成员函数带来的便利,例如搜索、追加、前置追加等。 两者都对速度进行了高度优化。 将字符串传递到仅需要只读访问权限的函数时,在 C++17 中,可以使用 std::string_view,以便提高性能。

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

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

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

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

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

需要性能优化时,请考虑使用:

  • 使用无序的关联容器,例如 unordered_map。 这些容器的单个元素开销更低,并且支持常量时间查找,但要正确且高效地使用它们,难度也更大。
  • 已排序vector。 有关详细信息,请参阅算法

请勿使用 C 样式数组。 对于需要直接访问数据的旧 API,请改用 f(vec.data(), vec.size()); 等访问器方法。 有关容器的详细信息,请参阅 C++ 标准库容器

标准库算法

在假设需要为程序编写自定义算法之前,请查看 C++ 标准库 算法。 标准库包含许多常见操作(如搜索、排序、筛选和随机化)的算法分类,这些分类在不断增长。 数学库的内容很广泛。 在 C++17 及更高版本中,提供了许多算法的并行版本。

以下是一些重要示例:

  • for_each:默认遍历算法以及基于 for 范围的循环。
  • transform:用于非原位修改容器元素。
  • find_if:默认搜索算法。
  • sortlower_bound 和其他默认的排序和搜索算法。

若要编写比较运算符,请使用严格的 <,并尽可能使用命名 lambda

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 替代显式类型名称

C++11 引入了 auto 关键字,以便可将其用于变量、函数和模板声明中。 auto 会指示编译器推导对象的类型,这样你就无需显式键入类型。 当推导出的类型是嵌套模板时,auto 尤其有用:

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

基于范围的 for 循环

对数组和容器的 C 样式迭代容易引发索引错误,而且键入过程单调乏味。 若要消除这些错误,并提高代码的可读性,可使用基于范围的 for 循环,此循环包含标准库容器和原始数组。 有关详细信息,请参阅基于范围的 for 语句

#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;
    }
}

constexpr 表达式替代宏

C 和 C++ 中的宏是指编译之前由预处理器处理的标记。 在编译文件之前,宏标记的每个实例都将替换为其定义的值或表达式。 C 样式编程通常使用宏来定义编译时常量值。 但宏容易出错且难以调试。 在现代 C++ 中,应优先使用 constexpr 变量定义编译时常量:

#define SIZE 10 // C-style
constexpr int size = 10; // modern C++

统一初始化

在现代 C++ 中,可以使用任何类型的括号初始化。 在初始化数组、矢量或其他容器时,这种初始化形式会非常方便。 在下面的示例中,v2 使用三个 S 实例进行初始化。 v3 使用三个 S 实例进行初始化,而这三个实例本身也是使用大括号初始化的。 编译器基于 v3 声明的类型推断每个元素的类型。

#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++ 提供了函数对象,即重载了 operator() 运算符的类,因此可以像函数一样被调用。 创建函数对象的最简便方法是使用内联 lambda 表达式。 以下示例演示如何使用 lambda 表达式传递函数对象,该 find_if 函数在向量中的每个元素上调用该对象:

    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 的单个参数并返回一个布尔值来表示此参数是否大于 x 并且小于 y 的函数”。请注意,可在 lambda 中使用来自周围上下文的 xy 变量。 [=] 指定这些变量按值 捕获。 换句话说,lambda 表达式具有其自己的这些值副本。

异常

与错误代码相比,新式 C++ 更注重异常,将其作为报告和处理错误条件的最佳方法。 有关详细信息,请参阅现代 C++ 处理异常和错误的最佳做法

std::atomic

对线程间通信机制使用 C++ 标准库 std::atomic 结构和相关类型。

std::variant (C++17)

在 C 风格编程中,联合体通常用于通过使不同类型的成员共享同一内存位置来节省内存。 联合不是类型安全的,并且容易出现编程错误。 C++17 引入了更加安全可靠的 std::variant 类,来作为并集的替代项。 可以使用 std::visit 函数以类型安全的方式访问 variant 类型的成员。

另请参阅