C++11特性详解

系列 -

C++11C++ 程序设计语言标准的一个新的版本,在 2011 年由 ISO 批准并发布。C++11 新标准从而代替了原来的 C++98C++03.。C++11 标准是对 C++ 的一次巨大的改进和扩充。在核心语法,STL 标准模板等方面增加众多新功能,新亮点。例如新增 autodeltypenullptr 等关键字,增加范围 for 循环,新增 lambda 表达式等。下面将对 C++ 增加的众多新特性进行总结。

  • long long 类型
  • unsigned long long 类型
  • char16_tchar32_t 类型
  • nullptr 类型

C++11扩大了用大括号括起的列表(初始化列表)的适用范围,使其可用于所有内置类型和用户定义的类型(即类对象)。使用初始化列表时,可添加等号(=),也可不添加:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int a1 = {10}; // 等价于 int a1{10};
int a2{10}; // C++11 新标准

int *arr = new int[4]{1, 2, 3, 4}; // C++11 新标准

/* 构造函数初始化列表 */
class Test {
public:
    Test(int a, int b) : m_a(a), m_b(b) {}
private:
    int m_a;
    int m_b;
};
  • 使用初始化列表相对与赋值列表,可以避免不必要的临时对象的产生,提高效率。
  • 初始化列表语法可防止缩窄,即禁止将数值赋给无法存储它的数值变量。常规初始化允许程序员执行可能没有意义的操作,初始化列表语法禁止的类型转换,编译器将检查变量的大小来进行隐式转换。

列表初始化构造函数 std::initallizer_list, 可以接受任意数量相同类型的参数,并且可以防止对参数进行不必要的拷贝。只需要实现一个接受 std::initializer_list 参数的构造函数即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Test {
public:
    Test(std::initializer_list<int> list) {
        for(auto i : list) {
            cout << i << endl;
        }
    }
};

Test t = {1, 2, 3, 4, 5}; // 1 2 3 4 5

编译器根据初始化表达式推断变量的类型。auto 关键字只能用于变量声明,不能用于函数声明。auto 关键字不能与 constvolatile 一起使用,但可以与 constvolatile 限定符一起使用,以推断变量的类型。

1
2
3
auto i = 10; // i 为 int 类型
auto d = 10.0; // d 为 double 类型
auto str = "hello"; // str 为 const char* 类型

常见应用场景

1
2
3
4
5
6
7
for (std:initializer list<double>:iterator p il.begin(); p!=i1,end(); p++)
```cpp

- 使用auto

```cpp
for(auto p=i1.begin();p!=i1.end();p++)

使用auto关键字 简化了冗长的类型声明,程序员不需要再写冗长的类型声明,编译器会根据初始化表达式自动推导出变量的类型。

decltype 关键字用于获取变量的类型,decltype 关键字返回表达式类型,而不计算表达式的值。decltype 关键字可以用于任何表达式,包括函数调用、类型转换等。

1
2
int a = 10;
decltype(a) b = 20; // b 为 int 类型

使用 decltype 关键字在定义模板时非常有用,因为只有等到模板被实例化时才能确定类型

1
2
3
4
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
    return t + u;
}

如果不使用 decltype 关键字,那么 add 函数的返回类型将无法确定,因为 TU 的类型只有在函数被调用时才能确定。

根据使用的表达式,指定的类型可以为引用和cosnt

1
2
3
4
5
6
7
int j = 3;
int &k = j;
const int &n =j;
decltype(n) i1; //il type const int &
decltype(j) i2; //i2 type int
decltype((j)) i3; //i3 type int&
decltype(k+1) i4; //i4 type int
  • (()) 用于引用类型,表示引用类型,而不是引用的值。

C++11新增了一种函数声明语法:在函数名和参数列表后面(而不是前面)指定返回类型:

1
auto add(int a, int b) -> int { return a + b; }

可以使用decltype来指定模板函数的返回值类型

1
2
3
4
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
    return t + u;
}

为什么后置?

在编译器遇到函数add的参数列表前,TU还不在作用域内,因此必须在参数列表后使用decltype。这种新语法使得能够这样做。

对于冗长或复杂的标识符,如果能够创建其别名将很方便。以前,C++为此提供了typedef

1
typedef std::vector<std::string> StringVec;

C++11引入了using关键字,它不仅能够完成typedef的工作,还可以完成其他任务:

1
using StringVec = std::vector<std::string>;

差别在于,新语法也可用于模板部分具体化,但typedef不能

1
2
template<typename T>
using StringVec = std::vector<std::string>;

空指针是不会指向有效数据的指针。以前,C++在源代码中使用0表示这种指针,但内部表示可能不同。这带来了一些问题,因为这使得0即可表示指针常量,又可表示整型常量。C++11新增了关键字nullptr,用于表示空指针;它是指针类型,不能转换为整型类型。为向后兼容,C++11仍允许使用0来表示空指针,因此表达式nullptr == 0true,但使用nullptr而不是0提供了更高的类型安全。例如,可将0传递给接受int参数的函数,但如果您试图将nullptr传递给这样的函数,编译器将此视为错误。因此,出于清晰和安全考虑,尽量使用nullptr

智能指针是C++中的一种特性,它提供了一种更安全、更方便的方式来管理动态分配的内存。智能指针的主要目的是自动释放不再使用的内存,以避免内存泄漏。

智能指针的原理源RAII 机制,RAII(Resource Acquisition Is Initialization)是通过利用对象出了作用域的自动销毁的机理,使得资源也具有了生命周期,有了自动销毁(自动回收)的功能。RAII要求,资源的有效期与持有资源的对象的生命期严格绑定,即由对象的构造函数完成资源的分配(获取),同时由析构函数完成资源的释放。在这种要求下,只要对象能正确地析构,就不会出现资源泄露问题。

所谓智能指针(smart pointer),就是智能/自动化的管理指针所指向的动态资源的释放。它其实是一个对象,当中存储指向动态分配(堆)对象的指针。

智能指针主要有以下几种:

  • std::shared_ptr:共享指针,内部有一个引用计数器,当智能指针通过复制操作传递给另一个对象,引用计数会加一(所有复制的shared_ptr发生浅拷贝,共享引用计数和对象指针),多个智能指针可以共享同一块内存,当最后一个智能指针被销毁时,内存才会被释放。
  • std::unique_ptr:独占指针,只能有一个智能指针拥有这块内存,当智能指针被销毁时,内存才会被释放。
  • std::weak_ptr:弱指针,它指向一个std::shared_ptr管理的对象,但不增加引用计数。弱指针可以用来避免循环引用问题。

智能指针的使用非常简单,只需要在声明指针时使用std::make_sharedstd::unique_ptr等函数即可。例如:

1
2
std::shared_ptr<int> p1 = std::make_shared<int>(new int(10));
std::unique_ptr<int> p2 = std::make_unique<int>(new int(20));

智能指针的销毁是自动的,当智能指针超出作用域时,会自动调用析构函数释放内存。因此,不需要手动释放内存,避免了内存泄漏的问题。

auto_ptr一样,C++编程社区的集体经验表明,异常规范的效果没有预期的好。因此,C++11摒弃的异常规范。然而,标准委员会认为,指出函数不会引发异常有一定的价值,他们为此添加了关键字noexcept

noexcept 关键字用于指示函数不会抛出任何异常。例如:

1
2
3
void func() noexcept {
    // 函数体
}

如果函数声明为noexcept,但实际在函数体内抛出了异常,编译器将生成一个uncaught_exception异常,该异常将终止程序。

nonexcept 用于希望函数出现异常时,程序能够立即终止,而不是进入异常处理机制。

传统的C++枚举提供了一种创建名称常量的方式,但其类型检查相当低级。另外,枚举名的作用域为枚举定义所属的作用域,这意味着如果在同一个作用域内定义两个枚举,它们的枚举成员不能同名。最后,枚举可能不是可完全移植的,因为不同的实现可能选择不同的底层类型。为解决这些问题,C++11新增了一种枚举。这种枚举使用classstruct定义:

1
2
enum class new1 { RED, GREEN, BLUE };
enum struct new2 { RED, GREEN, BLUE };

这种枚举被称为作用域内枚举,因为枚举成员的作用域为枚举类型的作用域。因此,上述两种枚举都是有效的,但new1::REDnew2::RED是不同的枚举成员。

为简化和扩展类设计,C++11做了多项改进。这包括允许构造函数被继承和彼此调用、更佳的方法访问控制方式以及移动构造函数和移动赋值运算符。

C++很早就支持对象自动转换。但随着编程经验的积累,程序员逐渐认识到,自动类型转换可能导致意外转换的问题。为解决这种问题,C++引入了关键字explicit,以禁止单参数构造函数导致的自动转换

例如:

1
2
3
4
5
6
class MyString {
public:
    explicit MyString(int n) {
        // 构造函数体
    }
};

上述代码中,MyString类有一个接受int参数的构造函数。如果使用explicit关键字修饰该构造函数,则不能使用隐式转换。例如,下面的代码将无法编译:

1
MyString str = 10; // 错误:不能隐式转换

C++98允许在类内初始化静态成员,但不允许在类内初始化非静态成员。C++11改变了这种状况,允许在类内初始化非静态成员。例如:

1
2
3
4
5
6
class MyString {
public:
    MyString(int n) : length(n) { }
private:
    int length = 0;
};

内成员函数可使用等号或大括号版本的初始化,但不能使用圆括号版本的初始化。

通过使用类内初始化,可避免在构造函数中编写重复的代码,从而降低了程序员的工作量、厌倦情绪和出错的机会。

如果构造函数在成员初始化列表中提供了相应的值,这些默认值将被覆盖,因此第三个构造函数覆盖了类内成员初始化。

为改善模板和标准模板库的可用性,C++11做了多个改进;有些是库本身,有些与易用性相关

C++11新增了一种基于范围的for循环,可以简化遍历容器的工作。例如:

1
2
3
4
std::vector<int> vec = {1, 2, 3, 4, 5};
for (int n : vec) {
    std::cout << n << std::endl;
}

上述代码中,for循环将遍历vec中的每个元素,并将当前元素赋给nn的类型由vec中的元素类型推断得出。

实现自定义类基于范围的for循环,需要在类内定义迭代器对象,迭代器重载*运算符,->运算符,++运算符和==运算符,自定义类,需要重载begin()end()函数。并返回迭代器对象例如:

 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
47
48
49
50
template <typename T> class Vector {
    template <typename U>
    friend std::ostream &operator<<(std::ostream &os, const Vector<U> &vec);

private:
    T *data;
    size_t len;
    size_t cap;
    static constexpr double GROWTH_FACTOR = 2.0;

public:
    Vector(size_t len = 0, size_t cap = 0);
    Vector(const Vector &other);
    Vector(std::initializer_list<T> init);
    ~Vector();

    T &operator[](size_t index);
    Vector &operator=(const Vector &other);

    void push_back(T value);
    void pop_back();
    void clear();
    void remove(size_t index);

    size_t size() const;
    size_t capacity() const;
    bool isEmpty() const;

    void resize(size_t newLen);
    void reserve();

    void insert(size_t index, T value);
    class Iterator {
    private:
        T *ptr;

    public:
        Iterator(T *ptr) : ptr(ptr) {}
        T &operator*() { return *ptr; }
        T *operator->() { return ptr; }
        Iterator &operator++() {
            ptr++;
            return *this;
        }
        bool operator!=(const Iterator &other) { return ptr != other.ptr; }
        bool operator==(const Iterator &other) { return ptr == other.ptr; }
    };
    Iterator begin() { return Iterator(this->data); }
    Iterator end() { return Iterator(this->data + this->len); }
};

C++11新增了unordered_mapunordered_set容器,分别对应std::mapstd::set的无序版本,以及forward_listforword_list是一种单向链表,只能沿一个方向遍历;与双向链接的list容器相比,它更简单,在占用存储空间方面更经济

C++11还新增了模板array,要实例化这种模板,可指定元素类型和固定的元素数

C++11新增了STL方法cbegin()cend()。与begin()end()一样,这些新方法也返回一个迭代器,指向容器的第一个元素和最后一个元素的后面,因此可用于指定包含全部元素的区间。另外,这些新方法将元素视为const。与此类似,crbegin()crend()rbegin()rend()const版本。

除传统的复制构造函数和常规赋值运算符外,STL容器现在还有移动构造函数和移动赋值运算符

  • 移动构造函数:Vector(Vector&& other)
  • 移动赋值运算符:Vector& operator=(Vector&& other)

移动构造函数和移动赋值运算符都接受一个右值引用参数(&&),并接管该参数的资源,从而避免复制。

valarrayC++标准库中的一个类模板,用于存储和操作数值数组,支持常见的数组操作和数学运算。

valarray的特点

  • 高效:valarray 被设计为在数值计算中具有高效的性能,尤其是在处理大规模数据时。
  • 简洁的语法:提供了简洁的语法来进行元素级的操作,例如加法、减法、乘法、除法等。
  • 专用操作:valarray 提供了一些专门的操作,如切片(slice)、掩码(mask)和移位
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
std::valarray<int> v1 = {1, 2, 3, 4, 5};
std::valarray<int> v2 = {5, 4, 3, 2, 1};

// 加法运算
std::valarray<int> sum = v1 + v2; //6 6 6 6 6

std::valarray<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

// 创建一个切片,从索引 1 开始,每隔 2 个元素取一个,共取 4 个元素
std::slice my_slice(1, 4, 2);
std::valarray<int> sliced_v = v[my_slice]; // 1 3 5 7
std::valarray<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

// 创建一个布尔数组作为掩码
std::valarray<bool> mask = (v % 2 == 0);

// 使用掩码进行筛选
std::valarray<int> masked_v = v[mask]; // 0 2 4 6 8

模板valarray独立于STL开发的,其最初的设计导致无法将基于范围的STL算法用于valarray对象。C++11添加了两个函数begin()end(),它们都接受valarray作为参数,并返回迭代器,这些迭代器分别指向valarray对象的第一个元素和最后一个元素后面。这让您能够将基于范围的STL算法用于valarray

C++98新增了关键字export,旨在提供一种途径,让程序员能够将模板定义放在接口文件和实现文件中,其中前者包含原型和模板声明,而后者包含模板函数和方法的定义。实践证明这不现实,因此C++11终止了这种用法,但仍保留了关键字export,供以后使用。

为避免与运算符»混淆,C++要求在声明嵌套模板时使用空格将尖括号分开,C++11不再这样要求

1
std::vector<std::vector<int>> vec2d; // >> ok in C++11

传统的C++引用(现在称为左值引用)使得标识符关联到左值。左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可获取其地址。最初,左值可出现在赋值语句的左边,但修饰符const的出现使得可以声明这样的标识符,即不能给它赋值,但可获取其地址

C++11新增了右值引用,这是使用&&表示的。右值引用可关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值。右值包括字面常量(C-风格字符串除外,它表示地址)、诸如x + y等表达式以及返回值的函数(条件是该函数返回的不是引用)。右值引用的主要用途是启用所谓的移动语义,这可减少不必要的复制,从而提高程序的效率。

1
2
std::vector<int> v1;
std::vector<int> v2 = v1; // v2 是 v1 的副本

v2v1的副本,副本的创建过程称为复制。C++库通过使用vector的复制构造函数来执行复制。

移动构造函数和移动赋值运算符都接受一个右值引用参数(&&),并接管该参数的资源,从而避免复制。

当用一个右值(临时对象)来初始化另一个对象时,编译器会尝试调用移动构造函数。例如:

1
2
std::string s1 = "hello world";
std:: s2 = s1 + "bye bye";

s1 + "bye bye" 返回了一个临时的右值对象,编译器会调用移动构造函数给s2复制,这避免的临时对象的拷贝。

1
2
3
4
5
std::string func() {
    return "hello";
}

std::string s = func(); // 调用移动构造函数

当函数返回一个对象时,编译器会尝试调用移动构造函数。如果函数返回一个局部对象的引用,则编译器会调用移动赋值运算符。

你明确地使用 std::move 来转换一个左值为右值时,也会调用移动构造函

1
2
std::vector<int> v1;
std::vector<int> v2 = std::move(v1); // v2 是 v1 的移动副本

std::move函数将v1转换为右值引用,从而允许vector的移动构造函数使用v1的资源,而无需复制它们。移动构造函数接管v1的资源,将v1重置为一个空容器,然后v2包含v1之前的数据。

若满足以下条件,编译器会自动生成移动构造函数,否则不会自动生成,此时若没有定义移动构造函数,会调用拷贝构造函数。

  1. 类中没有用户定义的拷贝构造函数、拷贝赋值运算符、移动赋值运算符和析构函数。
  2. 类中的所有非静态数据成员、基类和所有非静态数据成员的类类型都有可移动构造函数。

默认移动构造函数会完成以下工作,

1
2
3
4
5
6
7
MyClass(MyClass&& other) noexcept {
    // 调用内部成员的移动构造函数
    this->ptr = std::move(other.ptr);
    ...
    // 默认析构
    other.~Myclass();
}

如果你想编写代码查看编译器如何调用移动构造函数如何,请添加编译器选项-fno-elide-constructors 关闭返回值优化RVO

在原有4个特殊成员函数(默认构造函数、复制构造函数、复制赋值运算符和析构函数)的基础上,C++11新增了两个:移动构造函数和移动赋值运算符。这些成员函数是编译器在各种情况下自动提供的。

默认的移动构造函数和移动赋值运算符的工作方式与复制版本类似:执行逐成员初始化并复制内置类型。如果成员是类对象,将使用相应类的构造函数和赋值运算符,就像参数为右值一样。如果定义了移动构造函数和移动赋值运算符,这将调用它们;否则将调用复制构造函数和复制赋值运算符。

C++11让您能够更好地控制要使用的方法。假定您要使用某个默认的函数,而这个函数由于某种原因不会自动创建。例如,您提供了移动构造函数,因此编译器不会自动创建默认的构造函数、复制构造函数和复制赋值构造函数。在这些情况下,您可使用关键字default显式地声明这些方法的默认版本

另一方面,关键字delete可用于禁止编译器使用特定方法。例如,要禁止复制对象,可禁用复制构造函数和复制赋值运算符

关键字default只能用于6个特殊成员函数,但delete可用于任何成员函数。delete的一种可能用法是禁止特定的转换。

如果给类提供了多个构造函数,您可能重复编写相同的代码。也就是说,有些构造函数可能需要包含其他构造函数中已有的代码。为让编码工作更简单、更可靠,C++11允许您在一个构造函数的定义中使用另一个构造函数。这被称为委托,因为构造函数暂时将创建对象的工作委托给另一个构造函数。委托使用成员初始化列表语法的变种

1
2
3
4
5
6
7
8
class MyClass {
public:
    MyClass(int x) : MyClass(x, x) {}
    MyClass(int x, int y) : x(x), y(y) {}
private:
    int x;
    int y;
};

C++11新增了继承构造函数,这让派生类能够继承基类的构造函数。继承构造函数使用using关键字,然后指定要继承的构造函数。例如,如果基类MyClass有如下构造函数:

1
2
3
4
5
class MyClass {
public:
    MyClass(int x) {}
    MyClass(int x, int y) {}
};

派生类MyDerivedClass可以这样继承这些构造函数:

1
2
3
4
class MyDerivedClass : public MyClass {
public:
    using MyClass::MyClass;
};

现在,MyDerivedClass可以使用MyClass的构造函数来创建对象。

虚方法对实现多态类层次结构很重要,让基类引用或指针能够根据指向的对象类型调用相应的方法,但虚方法也带来了一些编程陷阱。例如,假设基类声明了一个虚方法,而您决定在派生类中提供不同的版本,这将覆盖旧版本

  • override:如果派生类方法使用override修饰符,但该方法没有覆盖基类中的任何方法,编译器将生成错误消息。这有助于捕获由于拼错方法名而导致的错误。
  • finalfinal修饰符用于停止方法覆盖。如果基类方法被声明为final,则任何尝试覆盖该方法的行为都将导致编译器错误。

此外,如果类被声明为final,则任何尝试继承该类的行为都将导致编译器错误。

Lambda函数是C++11新增的一项功能,它让您能够编写类似函数的对象,这些对象的行为类似于函数指针,但它们是类型安全的,且使用起来更方便。Lambda函数让您能够编写内联代码块,然后将其作为参数传递给算法或其他函数。

1
2
3
[] (int x, int y) -> int {
    return x + y;
}

Lambda函数的定义由[]开始,然后是一对括号,括号中是参数列表,然后是->,最后是函数体。

Lambda函数的参数列表可以是空的,也可以包含多个参数。函数体可以是单个语句,也可以是多个语句。如果函数体包含多个语句,则这些语句必须用花括号括起来。

Lambda函数的返回类型可以是自动推断的,也可以是显式指定的。如果函数体包含单个语句,则返回类型将自动推断为该语句的结果类型。否则,返回类型必须显式指定。

闭包函数

Lambda函数可以捕获其所在作用域中的变量,这些变量称为闭包变量。闭包变量可以是值或引用。如果闭包变量是值,则Lambda函数将创建该变量的副本,并在函数体中使用该副本。如果闭包变量是引用,则Lambda函数将使用该变量的当前值。

1
2
3
4
int x = 10;
auto f = [x] (int y) -> int {
    return x + y;
};

捕获所有变量

  • [&]:捕获所有变量,按引用捕获, 在函数体内的修改会影响外部变量
1
2
3
4
5
6
7
int x = 0;
auto f = [&] (int y) -> int {
    x += 10;
    return x + y;
};
f(10) // 20
// x =10
  • [=]:捕获所有变量,按值捕获,在函数体内的变量是一个副本,对函数体内 外部变量的修改不会影响外部变量
1
2
3
4
5
6
7
int x = 0;
auto f = [=] (int y) -> int {
    x += 10;
    return x + y;
};
f(10) // 20
// x =0
  • [&, x]:捕获所有变量,除了x,按引用捕获
  • [=, &x]:捕获所有变量,除了x,按值捕获

C++提供了多个包装器(wrapper,也叫适配器[adapter])。这些对象用于给其他编程接口提供更一致或更合适的接口。

  • stack: vector进行包装,使其行为类似于stack
  • queue: 对deque进行包装,使其行为类似于queue
  • priority_queue: 优先队列,对vector进行包装。

std::functionC++ 标准库中一个通用的函数包装器,可以用来存储、传递和调用任何可调用对象,包括普通函数、lambda 表达式、函数对象(functors)、成员函数指针等。std::function 提供了统一的接口来处理不同类型的可调用对象,从而简化了函数的管理和使用。

std::function 的主要特点

  • 多态性:可以存储不同类型的可调用对象,并通过同一个接口调用它们。
  • 类型安全:std::function 会检查存储的可调用对象是否符合指定的函数签名。
  • 复制和赋值:std::function 对象可以复制和赋值,存储的可调用对象也会随之复制。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>
#include <functional>

int main() {
    std::function<void(int)> func = [](int a) {
        std::cout << "Lambda called with: " << a << std::endl;
    };
    func(20);
    return 0;
}

C++11新增了可变模板参数,让您能够定义接受可变数量参数的模板。可变模板参数使用...表示,可以在模板参数列表中指定。例如,以下代码定义了一个可变模板参数的模板:

1
2
3
4
5
6
7
template<typename... Args>
class MyClass {
public:
    void doSomething(Args... args) {
        // do something with args...
    }
};

这个模板接受任意数量的参数,并将它们传递给doSomething方法。 您可以使用sizeof...运算符来获取可变模板参数的数量。例如,以下代码将打印可变模板参数的数量:

1
2
3
4
5
6
7
template<typename... Args>
class MyClass {
public:
    void doSomething(Args... args) {
        std::cout << sizeof...(args) << std::endl;
    }
};

C++11引入了多个用于并发编程的特性,包括std::threadstd::mutexstd::condition_variable等。这些特性使得在C++中编写多线程程序变得更加容易和安全。

std::thread类用于创建和管理线程。您可以使用std::thread类的构造函数来创建一个新的线程,并使用joindetach方法来等待线程结束或将其分离。例如,以下代码创建了一个新的线程,并在其中执行一个函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iostream>
#include <thread>

void printHello() {
    std::cout << "Hello, world!" << std::endl;
}

int main() {
    std::thread t(printHello);
    t.join();
    return 0;
}

std::mutex类用于保护共享数据,防止多个线程同时访问。您可以使用std::mutex类的lockunlock方法来锁定和解锁互斥锁。例如,以下代码使用互斥锁来保护一个共享变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int sharedVariable = 0;

void increment() {
    mtx.lock();
    sharedVariable++;
    mtx.unlock();
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();

    std::cout << "sharedVariable: " << sharedVariable << std::endl;
    return 0;
}

std::condition_variable类用于在线程之间进行同步。您可以使用std::condition_variable类的waitnotify_onenotify_all方法来等待条件变量被通知或通知其他线程。例如,以下代码使用条件变量来同步两个线程:

 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
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id(int id) {
    std::unique_lock<std::mutex> lck(mtx);
    while (!ready) cv.wait(lck);
    // ...
    std::cout << "Thread " << id << '\n';
}

void go() {
    std::unique_lock<std::mutex> lck(mtx);
    ready = true;
    cv.notify_all();
}

int main()
{
    std::thread threads[10];
    // spawn 10 threads:
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(print_id, i);

    std::cout << "10 threads ready to race...\n";
    go();                       // go!

    for (auto& th : threads) th.join();

    return 0;
}