C++新特性

Channing Hsu

C++11 引入了许多新特性,以改进语言的语法、功能和标准库。以下是一些主要的新特性:

一、语法的改进

(1) 统一的初始化方法

C++11 引入了统一的初始化语法 {},可以用于变量初始化、数组初始化、类成员初始化等。

1
2
int x{5};
std::vector<int> vec{1, 2, 3, 4, 5};

(2) 成员变量默认初始化

类成员变量可以在声明时直接初始化。

1
2
3
4
class MyClass {
int x = 0;
double y = 1.0;
};

(3) auto 关键字

auto 允许编译器自动推断变量的类型,减少类型声明的冗余。

1
2
auto x = 5; // x 被推断为 int 类型
auto y = 3.14; // y 被推断为 double 类型

(4) decltype 关键字

decltype 用于推断表达式的类型。

1
2
int x = 0;
decltype(x) y = 1; // y 被推断为 int 类型

(5) 智能指针

C++11 引入了智能指针 std::shared_ptrstd::unique_ptr,用于自动管理动态分配的对象的生命周期。

1
2
3
4
#include <memory>

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

(6) 空指针 nullptr

C++11 引入了 nullptr 作为空指针的明确表示,替代传统的 NULL

1
int* p = nullptr;

(7) 基于范围的 for 循环

简化遍历容器元素的语法。

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

(8) 右值引用和移动语义

引入右值引用和移动构造函数,允许高效地将资源从一个对象移动到另一个对象,提高性能。

1
2
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);

二、标准库扩充

(9) 无序容器

C++11 引入了无序容器(如 std::unordered_mapstd::unordered_set),基于哈希表实现。

1
2
3
4
5
#include <unordered_map>

std::unordered_map<int, std::string> umap;
umap[1] = "one";
umap[2] = "two";

(10) 正则表达式

标准库新增了正则表达式支持,提供了灵活的字符串模式匹配功能。

1
2
3
4
5
6
7
8
#include <regex>

std::regex pattern("\\d+");
std::smatch matches;
std::string text = "There are 123 numbers here";
if (std::regex_search(text, matches, pattern)) {
std::cout << "Found a match: " << matches[0] << std::endl;
}

(11) Lambda 表达式

允许在代码中定义匿名函数,简化函数对象的创建。

1
2
auto add = [](int a, int b) { return a + b; };
std::cout << add(2, 3) << std::endl; // 输出 5

三、智能指针

智能指针(Smart Pointer)是一种自动管理动态内存的机制,可以避免内存泄漏悬空指针(Dangling Pointer)等问题。

0. 实现原理

智能指针的实现原理基于RAII(Resource Acquisition Is Initialization)原则,通过在构造函数中获取资源,在析构函数中释放资源,确保对象在生命周期结束时自动释放其占用的资源。不同类型的智能指针实现方式略有不同,但基本原理类似,下面详细介绍std::unique_ptrstd::shared_ptrstd::weak_ptr的实现原理。

1. std::unique_ptr的实现原理

std::unique_ptr是一种独占所有权的智能指针,其实现非常简单,主要依赖于C++的移动语义。

核心概念:
  • 独占所有权:同一时间只能有一个std::unique_ptr对象管理某个资源。
  • 移动语义:通过移动构造函数和移动赋值运算符来转移所有权。
主要成员函数:
  • 构造函数:初始化时获取资源。
  • 析构函数:销毁时释放资源。
  • 移动构造函数和移动赋值运算符:转移资源所有权。
示例实现:
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
template<typename T>
class unique_ptr {
private:
T* ptr; // 原生指针,管理资源

public:
explicit unique_ptr(T* p = nullptr) : ptr(p) {}

~unique_ptr() {
delete ptr; // 析构时释放资源
}

// 禁止复制构造函数和赋值运算符
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;

// 移动构造函数
unique_ptr(unique_ptr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr; // 转移所有权
}

// 移动赋值运算符
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
delete ptr; // 释放当前资源
ptr = other.ptr;
other.ptr = nullptr; // 转移所有权
}
return *this;
}

T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
T* get() const { return ptr; }
T* release() {
T* temp = ptr;
ptr = nullptr;
return temp;
}
void reset(T* p = nullptr) {
delete ptr;
ptr = p;
}
};

2. std::shared_ptr的实现原理

std::shared_ptr是一种共享所有权的智能指针,通过引用计数来管理资源的生命周期

核心概念:
  • 引用计数:记录有多少std::shared_ptr对象共享同一个资源,当引用计数归零时释放资源。
  • 控制块(Control Block):存储资源指针和引用计数。
主要成员函数:
  • 构造函数:初始化时创建控制块并增加引用计数。
  • 析构函数:销毁时减少引用计数,当引用计数为0时释放资源和控制块。
  • 拷贝构造函数和拷贝赋值运算符:增加引用计数。
示例实现:
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
template<typename T>
class shared_ptr {
private:
T* ptr; // 原生指针,管理资源
std::size_t* ref_count; // 引用计数

void release() {
if (ref_count && --(*ref_count) == 0) {
delete ptr;
delete ref_count;
}
}

public:
explicit shared_ptr(T* p = nullptr)
: ptr(p), ref_count(p ? new std::size_t(1) : nullptr) {}

~shared_ptr() {
release(); // 析构时减少引用计数并释放资源
}

// 拷贝构造函数
shared_ptr(const shared_ptr& other)
: ptr(other.ptr), ref_count(other.ref_count) {
if (ref_count) {
++(*ref_count); // 增加引用计数
}
}

// 拷贝赋值运算符
shared_ptr& operator=(const shared_ptr& other) {
if (this != &other) {
release(); // 释放当前对象资源
ptr = other.ptr;
ref_count = other.ref_count;
if (ref_count) {
++(*ref_count); // 增加引用计数
}
}
return *this;
}

T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
T* get() const { return ptr; }
};

3. std::weak_ptr的实现原理

std::weak_ptr是一种弱引用智能指针,不会影响资源的生命周期。它用于解决std::shared_ptr的循环引用问题。循环引用问题(Circular Reference Problem)是指两个或多个对象之间相互引用,导致这些对象的引用计数无法归零,从而造成内存泄漏。

核心概念:
  • 弱引用:不增加引用计数,不管理资源生命周期。
  • 需要与std::shared_ptr配合使用,通过lock函数获取std::shared_ptr
主要成员函数:
  • 构造函数:初始化时从std::shared_ptr获取控制块。
  • 提升函数lock:尝试获取共享所有权,如果资源已释放则返回空的std::shared_ptr
示例实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename T>
class weak_ptr {
private:
T* ptr; // 指向实际对象的原生指针
std::size_t* ref_count; // 指向引用计数的指针
public:
weak_ptr() : ptr(nullptr), ref_count(nullptr) {}

weak_ptr(const shared_ptr<T>& sp) : ptr(sp.ptr), ref_count(sp.ref_count) {}

shared_ptr<T> lock() const {
return (ref_count && *ref_count > 0) ? shared_ptr<T>(*this) : shared_ptr<T>();
}
};

这个weak_ptr类模板展示了如何实现一个简化版的std::weak_ptr,其核心功能是:

  • 不增加对象的引用计数,从而避免循环引用。
  • 通过lock函数安全地访问对象,只有在对象仍然存在时,才返回有效的shared_ptr

1. shared_ptr

std::shared_ptr是C++11中引入的一个共享所有权的智能指针。多个shared_ptr可以指向同一个对象,并且当最后一个拥有该对象的shared_ptr被销毁时,该对象才会被删除。这通过引用计数实现。

std::shared_ptr适用于需要多个拥有者共同使用资源的场景,如容器、动态数组、链表等。它提供了共享所有权的语义,并通过引用计数来管理资源的生命周期。当引用计数变为零时,资源将被自动释放。

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
#include <iostream>  
#include <memory>

void functionA(std::shared_ptr<MyClass> ptr) {
ptr->sayHello(); // 输出 "Hello from MyClass"
// 当functionA返回时,ptr不会销毁MyClass实例,因为还有其他引用
}

void functionB(std::shared_ptr<MyClass> ptr) {
ptr->sayHello(); // 输出 "Hello from MyClass"
// 当functionB返回时,如果这是最后一个引用,MyClass实例将被销毁
}

int main() {
// 使用std::shared_ptr来管理MyClass的实例
std::shared_ptr<MyClass> ptr(new MyClass());

// 将ptr传递给两个函数,它们共享对MyClass实例的所有权
functionA(ptr);
functionB(ptr);

// 当main函数结束时,如果还有其他函数或变量引用了ptr所指向的MyClass实例
// 它将不会被销毁;否则,它将被销毁,并输出 "MyClass Destructor"

return 0;
}
线程安全
  • 读操作是线程安全的。
  • 写操作是线程不安全的,需要额外的同步措施。
  • 共享引用计数的不同 shared_ptr 被多个线程写是安全的。

2. unique_ptr

std::unique_ptr是C++11中引入的一个独占所有权的智能指针。它“拥有”它所指向的对象,并在离开其作用域时自动删除它。在任意给定的时刻,一个对象只能由一个unique_ptr拥有。unique_ptr不允许复制构造或复制赋值,但允许移动构造和移动赋值。

std::unique_ptr适用于需要确保资源有且仅有一个拥有者的场景,如函数返回值、局部变量、类成员等。它提供了独占所有权的语义,并通过移动语义支持所有权的转移。

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
#include <iostream>  
#include <memory>

class MyClass {
public:
MyClass() { std::cout << "MyClass Constructor\n"; }
~MyClass() { std::cout << "MyClass Destructor\n"; }
void sayHello() { std::cout << "Hello from MyClass\n"; }
};

int main() {
// 使用std::unique_ptr来管理MyClass的实例
std::unique_ptr<MyClass> ptr1(new MyClass());
ptr1->sayHello(); // 输出 "Hello from MyClass"

// 尝试复制unique_ptr会失败(编译错误)
// std::unique_ptr<MyClass> ptr2 = ptr1; // Error: can't copy unique_ptr

// 但可以通过移动语义转移所有权
std::unique_ptr<MyClass> ptr2 = std::move(ptr1);
if (!ptr1) {
std::cout << "ptr1 is now empty\n"; // 输出 "ptr1 is now empty"
}
ptr2->sayHello(); // 输出 "Hello from MyClass"

// 当ptr2离开作用域时,它会自动销毁并释放所指向的MyClass实例
// 此时会输出 "MyClass Destructor"

return 0;
}

3. weak_ptr

std::weak_ptr是为了解决std::shared_ptr可能导致的循环引用问题而引入的。它是对对象的弱引用,不会增加对象的引用计数。weak_ptr可以用来“观察”一个对象,但不会影响其生命周期。当需要访问对象时,必须将其转换为shared_ptr(这可能会失败,如果对象已经被删除)。

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
#include <iostream>

class B; // 前向声明

class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
std::weak_ptr<A> a_ptr; // 使用weak_ptr打破循环引用
~B() { std::cout << "B destroyed\n"; }
};

int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;

if (auto sp = b->a_ptr.lock()) {
std::cout << "A is still alive\n";
} else {
std::cout << "A has been destroyed\n";
}

return 0;
}

四、类型推导

1. auto

auto关键字用于自动类型推导,让编译器根据初始化表达式来推断变量的类型。

  • 必须初始化:使用auto定义的变量在声明时必须立即初始化,否则编译器无法推断其类型。
  • 一行定义多个变量:虽然可以在一行中使用auto定义多个变量,但每个变量都必须能够被单独初始化,并且这些初始化不能产生类型推导的二义性。
  • 不能用作函数参数:函数参数的类型在编译前必须明确,因此不能使用auto
  • 非静态成员变量:在类的定义中,auto不能用作非静态成员变量的类型,因为非静态成员变量在类的定义时就需要知道其类型。
  • 定义数组auto不能直接用于定义数组的大小,但你可以通过初始化一个std::initializer_liststd::array来间接定义。
  • 模板参数auto不能用作模板参数的类型,因为模板参数在实例化之前必须已知。
  • 引用和cv限定符:当auto用于非引用类型时,它会忽略等号右边的引用和cv(const和volatile)限定符。但是,如果auto&&&结合使用(即声明为引用类型),则会保留这些属性。
1
2
3
4
auto x = 10;       // x 是 int 类型  
auto& y = x; // y 是 int& 类型,引用 x
const auto z = 20; // z 是 const int 类型,但 auto 忽略了 const
const auto& w = z; // w 是 const int& 类型,保留了 const 和引用属性
  • auto 必须立即初始化,否则无法推导类型。
  • auto 不能用作函数参数。

2. decltype

decltype关键字用于在编译时确定表达式的类型。与auto不同,decltype不会计算表达式的值,只是分析表达式的类型。

  • 保留引用和cv属性decltype会保留表达式的引用和常量性(constness)或易变性(volatility)。
  • 函数调用:如果exp是一个函数调用,decltype(exp)将是函数返回值的类型。
  • 左值:如果exp是一个左值(例如变量、数组元素、结构体的成员等),decltype(exp)将是该左值类型的左值引用。如果exp是一个非常量左值,但你希望得到的类型是常量引用,可以使用decltype((exp))(注意额外的括号)。
1
2
3
4
int x = 10;  
decltype(x) y = 20; // y 是 int 类型
decltype((x)) z = x; // z 是 int& 类型,注意额外的括号
decltype(x + 5) sum = 30; // sum 是 int 类型,因为 x + 5 是一个右值表达式

auto 和 decltype 的配合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}

template<typename T, typename U>
auto add(T t, U u) -> decltype(auto) {
return t + u;
}
// 或者,使用更简洁的语法(C++14及以后)
template<typename T, typename U>
auto add(T t, U u) {
return t + u;
}
// 在这里,编译器会自动推导返回类型为 decltype(t + u)

这种方式允许函数返回类型根据参数类型自动推导。

五、右值引用:

右值引用(Rvalue References)是C++11引入的一种新类型引用,用于支持移动语义和实现完美转发。右值引用通过两个&&符号表示,例如T&&。它主要用于绑定右值(临时对象)和将资源从一个对象“移动”到另一个对象

1. 左值和右值

  1. 左值(Lvalue):

    • 定义: 可以放在等号左边进行赋值操作的表达式。
    • 性质: 有名字,可以取地址,通常表示一个具体的对象或变量。
  2. 右值(Rvalue):

    • 定义: 不能放在等号左边进行赋值操作的表达式。
    • 性质: 通常是临时的、无名的值,不可以取地址。

    示例:

    1
    2
    3
    4
    5
    6
    7
    int a = 10;
    int& ref = a; // ref 是左值引用,引用变量 a

    int b = 20;
    int&& rref = 20; // rref 是右值引用,引用右值 20
    int&& another_rref = std::move(b); // another_rref 是右值引用,通过 std::move 将 b 转换为右值

2. 将亡值(Rvalue Reference):

将亡值是右值的一种特殊类型,指即将被销毁的对象,可以通过移动语义来“盗取”其资源。典型的将亡值包括函数返回的右值引用、std::move 返回值等。

3. 左值引用和右值引用

左值引用是对左值进行引用的类型,用 & 表示。左值引用可以修改被引用的对象,但不能绑定到右值。

1
2
int a = 42;
int& lvalueRef = a; // lvalueRef 是左值引用

右值引用是对右值进行引用的类型,用 && 表示。右值引用允许绑定到右值,并且在移动语义中用于资源的转移。

1
int&& rvalueRef = 42; // rvalueRef 是右值引用

左值引用

左值引用主要用于以下场景:

  1. 别名:为变量创建别名,以便在函数内部或其他作用域中使用相同的对象。
  2. 传递参数:通过传递引用参数,避免函数参数的拷贝,提高效率。
  3. 返回值:函数可以返回左值引用,以便修改返回的对象。

右值引用

右值引用主要用于以下场景:

  1. 移动语义
    移动语义通过右值引用实现资源的转移,而不是复制资源,从而提高程序的性能,特别是在处理大型数据结构时。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <vector>

class MyClass {
public:
MyClass(std::vector<int>&& vec) : data(std::move(vec)) {
std::cout << "Move Constructor" << std::endl;
}

private:
std::vector<int> data;
};

int main() {
std::vector<int> vec = {1, 2, 3};
MyClass obj(std::move(vec)); // 使用移动构造函数
return 0;
}

在这个例子中,通过std::movevec转换为右值引用,从而调用MyClass的移动构造函数,将vec的资源转移给对象obj

  1. 完美转发
    完美转发允许函数模板将其参数完全按照传递给它们的方式传递给另一个函数。使用右值引用和std::forward,可以在函数模板中保留参数的左值或右值性质。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <utility> // for std::forward

void process(int& x) {
std::cout << "Lvalue reference: " << x << std::endl;
}

void process(int&& x) {
std::cout << "Rvalue reference: " << x << std::endl;
}

template <typename T>
void forwardToProcess(T&& x) {
process(std::forward<T>(x));
}

int main() {
int a = 10;
forwardToProcess(a); // 调用 lvalue reference 版本的 process
forwardToProcess(20); // 调用 rvalue reference 版本的 process
forwardToProcess(std::move(a)); // 调用 rvalue reference 版本的 process
return 0;
}
1
2
3
4
template <typename T>
void forwardToProcess(T&& x) {
process(std::forward<T>(x));
}
  • T 是一个模板参数,可以是任何类型。

  • T&& x 是一个通用引用(Universal Reference),也称为转发引用(Forwarding Reference)。

    • 通用引用是一种特殊的右值引用,其类型依赖于传递的参数。如果传递的是左值,它是左值引用;如果传递的是右值,它是右值引用。
    • std::forward 是一个标准库函数模板,用于完美转发
  • std::forward<T>(x) 将参数 x 转发给 process 函数,并保留 x 的左值或右值性质。

    • 如果 T 是左值引用类型(如 int&),std::forward<T>(x)x 转发为左值。
    • 如果 T 是右值引用类型(如 int&&),std::forward<T>(x)x 转发为右值。

在这个例子中,forwardToProcess函数模板通过std::forward将参数x完美转发给process函数,从而保留参数的左值或右值性质。

左值引用和右值引用的转换

通过std::move,可以将左值显式转换为右值,从而绑定到右值引用。这是实现移动语义的关键。

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

void process(int&& x) {
std::cout << "Processing rvalue: " << x << std::endl;
}

int main() {
int a = 10;
process(std::move(a)); // 将左值 a 转换为右值,并绑定到右值引用 x
return 0;
}
特性 左值引用 (Lvalue References) 右值引用 (Rvalue References)
表示方式 T& T&&
绑定对象类型 左值 右值
用途 别名、传递参数、返回值 移动语义、完美转发
典型场景 函数参数、变量别名 移动构造函数、移动赋值运算符

4. 移动语义:

移动语义(Move Semantics)是C++11引入的一项重要特性,旨在提高程序的性能,尤其是在涉及大量对象复制的场景中。移动语义通过“移动”资源而不是复制资源,减少了不必要的性能开销。下面是移动语义的作用和原理的详细解释:

  1. 减少不必要的复制:在传统的复制语义下,当对象被传递、返回或赋值时,往往会进行深拷贝操作,导致性能开销和资源浪费。移动语义通过移动资源,避免了这些不必要的复制操作。

  2. 提高程序性能:移动操作比复制操作更高效,因为它只是简单地转移指针或资源的所有权,而不需要分配新的内存或复制数据。特别是在处理大数据结构(如大型数组、容器)时,性能提升显著。

  3. 优化资源管理:移动语义可以与智能指针(如std::unique_ptr)结合使用,优化资源的管理和生命周期,避免内存泄漏。

移动语义的原理

移动语义(Move Semantics)通过移动而不是复制资源,提高了程序的性能,特别是在涉及大数据结构的情况下。移动语义的核心概念包括右值引用(Rvalue References)、移动构造函数(Move Constructor)和移动赋值运算符(Move Assignment Operator)。它们共同作用,允许对象的资源所有权从一个对象转移到另一个对象,而不需要昂贵的深拷贝操作。

移动语义的主要原理
  1. 右值引用:用于绑定临时对象或将要被销毁的对象。
  2. 移动构造函数:从一个右值引用构造新的对象,通过转移资源来避免深拷贝。
  3. 移动赋值运算符:从一个右值引用赋值给已有对象,通过转移资源来避免深拷贝。

例子

以下是一个详细的例子,展示了如何实现和使用移动语义。

定义一个支持移动语义的类
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
51
#include <iostream>
#include <utility> // for std::move

class MyClass {
public:
MyClass(int size) : data(new int[size]), size(size) {
std::cout << "Constructor" << std::endl;
}

~MyClass() {
delete[] data;
std::cout << "Destructor" << std::endl;
}

// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr; // 防止其他对象析构时释放同一块内存
other.size = 0;
std::cout << "Move Constructor" << std::endl;
}

// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete[] data; // 释放当前对象的资源
data = other.data;
size = other.size;
other.data = nullptr; // 防止其他对象析构时释放同一块内存
other.size = 0;
std::cout << "Move Assignment" << std::endl;
}
return *this;
}

// 禁用拷贝构造函数和拷贝赋值运算符
MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;

private:
int* data;
int size;
};

int main() {
MyClass a(10); // 调用构造函数
MyClass b = std::move(a); // 调用移动构造函数
MyClass c(20); // 调用构造函数
c = std::move(b); // 调用移动赋值运算符

return 0;
}
运行结果解释
1
2
3
4
5
6
7
Constructor
Move Constructor
Constructor
Move Assignment
Destructor
Destructor
Destructor

解释:

  1. MyClass a(10);:调用构造函数,分配内存并初始化a的资源。
  2. MyClass b = std::move(a);:调用移动构造函数,转移a的资源到b,并将a的资源指针置空。这样,b接管了a的资源,避免了深拷贝。
  3. MyClass c(20);:调用构造函数,分配内存并初始化c的资源。
  4. c = std::move(b);:调用移动赋值运算符,释放c的原有资源,然后将b的资源转移到c,并将b的资源指针置空。
  5. 对象abc在作用域结束时析构,调用析构函数。由于ab的资源指针已经置空,所以它们的析构函数不会重复释放资源。

移动语义的优势

  1. 性能提升:通过移动语义,可以避免不必要的深拷贝操作,大大提高程序的性能,尤其是在处理大型数据结构时。
  2. 资源管理:移动语义有助于更高效地管理资源,避免内存泄漏和重复释放资源的问题。

小结

移动语义利用右值引用、移动构造函数和移动赋值运算符,通过转移资源所有权而不是复制资源,实现高效的资源管理和性能优化。在现代C++编程中,充分利用移动语义可以显著提高代码的性能和可靠性。

六、nullptr

在C++中,nullptr 是用于表示空指针的关键字。它是在C++11中引入的,用来代替传统的 NULL。以下是一些关于 nullptr 的关键点:

1. 区别于 NULL:

  • NULL 通常被定义为 0((void*)0)。在某些情况下,这会导致编译器混淆 0 和空指针。例如,char *ch = NULL 会被解释为 char *ch = 0
  • nullptr 是一个类型为 nullptr_t 的字面量,专门用于表示空指针。它不会与整数 0 混淆。

2. 避免混淆:

  • 当函数重载时,使用 NULL 可能会导致错误的函数被调用。例如,假设有两个重载函数 void foo(char*);void foo(int);。调用 foo(NULL) 时,如果 NULL 被定义为 0,则编译器可能会选择调用 foo(int)
  • 使用 nullptr 可以避免这种混淆,因为 nullptr 明确表示为空指针。

3. 例子

1
2
3
4
5
void foo(char*); // Function for char* argument
void foo(int); // Function for int argument

foo(NULL); // May call foo(int) if NULL is defined as 0
foo(nullptr); // Unambiguously calls foo(char*)

七、范围 for 循环

C++11 引入了基于范围的 for 循环,用于简化遍历容器或其他集合的语法。

1. 遍历 string 对象的每个字符:

1
2
3
4
std::string str("some thing");
for (char c : str) {
std::cout << c << std::endl;
}

2. 遍历 vector 中的元素:

1
2
3
4
5
6
7
8
9
10
11
std::vector<int> arr(5, 100);

// 使用传统的迭代器循环
for (std::vector<int>::iterator i = arr.begin(); i != arr.end(); ++i) {
std::cout << *i << std::endl;
}

// 使用范围 for 循环
for (auto &i : arr) {
std::cout << i << std::endl;
}

八、列表初始化

C++11 引入了列表初始化,可以使用花括号 {} 进行初始化。这种方式对于内置类型和用户定义类型都适用。

例子

  1. 初始化 int 变量:
1
2
3
4
int x = 0;    // 传统初始化
int x = {0}; // 列表初始化
int x{0}; // 列表初始化
int x(0); // 传统初始化
  1. 防止信息丢失:
1
2
3
long double d = 3.1415926536;
int a = {d}; // 编译错误:存在信息丢失风险
int a = d; // 编译成功,但确实丢失信息

九、Lambda 表达式

Lambda 表达式是C++11引入的一种匿名函数,用于编写内联的、无命名的可调用代码单元。

语法Lambda 表达式的语法:

1
[capture list] (parameter list) -> return type {function body}
  • 捕获列表 (capture list): 用于捕获外部变量。
  • 参数列表 (parameter list): 与普通函数的参数列表相似。
  • 返回类型 (return type): 指定返回值的类型,可以省略。
  • 函数体 (function body): 匿名函数的实现。

变量捕获: Lambda 的独特之处在于其能够捕获外部变量。

  1. [] 不捕获任何变量。
  2. [&] 以引用方式捕获所有变量。
  3. [=] 以值的方式捕获所有变量。
  4. [=, &foo] 以引用捕获变量 foo,其余变量值捕获。
  5. [&, foo] 以值捕获变量 foo,其余变量引用捕获。
  6. [bar] 以值方式捕获变量 bar
  7. [this] 捕获所在类的 this 指针。
1
2
3
int a = 1, b = 2, c = 3;
auto lam2 = [&, a]() { b = 5; c = 6; cout << a << b << c << endl; };
lam2(); // 输出 1 5 6
  1. 基本 lambda 表达式:
1
2
3
4
5
6
auto lam = []() -> int {
std::cout << "Hello, World!";
return 88;
};
auto ret = lam();
std::cout << ret << std::endl; // 输出 88
  1. 变量捕获:
1
2
3
4
5
6
7
8
9
10
int a = 1, b = 2, c = 3;

// 以引用方式捕获 b 和 c,值捕获 a
auto lam2 = [&, a]() {
b = 5;
c = 6;
std::cout << a << b << c << std::endl; // 输出 156
};

lam2();
  1. 值捕获和引用捕获:
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
void fcn_value_capture() {  
size_t v1 = 42;

// 值捕获
auto f = [v1] { return v1; };
v1 = 0; // 这里的修改不会影响到lambda内部捕获的v1的副本
auto j = f(); // j = 42
}

void fcn_mutable_lambda() {
size_t v1 = 42;

// 可变 lambda,但捕获的是v1的副本
auto f = [v1]() mutable { return ++v1; }; // 这里递增的是lambda内部的v1副本
v1 = 0; // 这里的修改不会影响到lambda内部捕获的v1的副本
auto j = f(); // j 应该是递增后的副本值,但由于初始值是42,递增后为43
}

void fcn_reference_capture() {
size_t v1 = 42;

// 引用捕获
auto f = [&v1]() { return ++v1; };
v1 = 0; // 这里的修改会影响到lambda内部对v1的引用
auto j = f(); // j 应该是递增后的v1值,由于v1在调用f之前被设置为0,所以递增后为1
}
  1. 使用 lambda 表达式进行数组排序:
1
2
3
4
5
6
7
8
9
10
11
12
13
std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6, 5};
std::sort(vec.begin(), vec.end(), [](int a, int b) {
return a < b;
});

int arr[] = {6, 4, 3, 2, 1, 5};
bool compare = [](const int &a, const int &b) { return a > b; };

std::sort(arr, arr + 6, compare); //lambda形式
//谓词函数
std::sort(arr, arr + 6, [](const int& a, const int& b){return a > b;}); //降序
std::for_each(begin(arr), end(arr), [](const int& e){cout << "After:" << e << endl;});
//6, 5, 4, 3, 2, 1

十、深拷贝和浅拷贝:

  1. 浅拷贝: 多个对象共享同一块内存,修改一个对象的数据会影响其他对象。

    1
    2
    int* arr1 = new int[5];
    int* arr2 = arr1; // 浅拷贝
  2. 深拷贝: 每个对象拥有自己的一份数据,修改一个对象的数据不会影响其他对象。

    1
    2
    3
    int* arr1 = new int[5];
    int* arr2 = new int[5];
    std::copy(arr1, arr1 + 5, arr2); // 深拷贝

十一、完美转发:

完美转发是 C++11 中引入的技术,用于在模板中保留参数的值类别。通过 std::forward 实现,在实现泛型函数时确保参数的类型信息不丢失。

1
2
3
4
template <typename T>
void forwarder(T&& arg) {
target(std::forward<T>(arg)); // 保留参数的值类别进行转发
}

std::forward 用于在泛型函数中将参数完美地转发到目标函数,保持参数的值类别。这在实现通用容器、智能指针等类时非常有用。

十二、多线程同步机制

在C++中,多线程同步机制是为了防止数据竞争和确保多个线程能够安全地访问共享资源。以下是几种常见的同步机制及其用法:

1. 互斥锁(Mutex)

互斥锁是最常用的同步机制,用于保护共享资源,确保在任一时刻只有一个线程可以访问被锁定的资源。

  • std::mutex:基础的互斥锁。
  • std::lock_guard:RAII风格的锁管理,自动管理锁的生命周期。
  • std::unique_lock:更灵活的锁管理,可以手动锁定和解锁。

示例代码:

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

std::mutex mtx;

void print_thread_id(int id) {
std::lock_guard<std::mutex> lock(mtx);
std::cout << "Thread #" << id << std::endl;
}

int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i) {
threads[i] = std::thread(print_thread_id, i + 1);
}
for (auto &th : threads) {
th.join();
}
return 0;
}

2. 读写锁(Shared Mutex)

读写锁允许多个线程同时读取,但只允许一个线程写入。C++17引入了std::shared_mutex来实现这种锁。

  • std::shared_mutex:允许多个线程共享读访问,但独占写访问。
  • std::shared_lock:用于读锁定。

示例代码:

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
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>

std::shared_mutex mtx;
std::vector<int> data;

void read_data(int id) {
std::shared_lock<std::shared_mutex> lock(mtx);
std::cout << "Thread #" << id << " reads data size: " << data.size() << std::endl;
}

void write_data(int id) {
std::unique_lock<std::shared_mutex> lock(mtx);
data.push_back(id);
std::cout << "Thread #" << id << " writes data" << std::endl;
}

int main() {
std::thread readers[5];
std::thread writers[5];
for (int i = 0; i < 5; ++i) {
readers[i] = std::thread(read_data, i + 1);
writers[i] = std::thread(write_data, i + 1);
}
for (int i = 0; i < 5; ++i) {
readers[i].join();
writers[i].join();
}
return 0;
}

3. 条件变量(Condition Variable)

条件变量用于线程间的通知机制,使一个线程可以等待另一个线程的特定条件。

  • std::condition_variable:一般与std::mutex结合使用。
  • std::condition_variable_any:可以与任何锁类型结合使用。

示例代码:

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
#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> lock(mtx);
cv.wait(lock, []{ return ready; });
std::cout << "Thread #" << id << std::endl;
}

void set_ready() {
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_all();
}

int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i) {
threads[i] = std::thread(print_id, i + 1);
}
std::thread notifier(set_ready);

for (auto &th : threads) {
th.join();
}
notifier.join();
return 0;
}

4. 信号量(Semaphore)

C++20引入了信号量,用于限制对资源的访问数量。

  • std::counting_semaphore:一个计数信号量,允许多个线程访问。
  • std::binary_semaphore:一个二进制信号量,相当于一个互斥锁。

示例代码:

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

std::counting_semaphore<3> sem(3); // 允许最多3个线程同时访问

void worker(int id) {
sem.acquire();
std::cout << "Thread #" << id << " is working" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Thread #" << id << " is done" << std::endl;
sem.release();
}

int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i) {
threads[i] = std::thread(worker, i + 1);
}
for (auto &th : threads) {
th.join();
}
return 0;
}

5. 原子操作(Atomic Operations)

原子操作用于无锁编程,保证对共享数据的操作是原子的,不会被中断。

  • std::atomic:提供各种原子类型和操作。

示例代码:

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

std::atomic<int> counter(0);

void increment() {
for (int i = 0; i < 1000; ++i) {
counter++;
}
}

int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i) {
threads[i] = std::thread(increment);
}
for (auto &th : threads) {
th.join();
}
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}

总结

C++提供了多种同步机制来管理多线程程序中的共享资源。这些机制包括互斥锁、读写锁、条件变量、信号量和原子操作。选择合适的同步机制取决于具体的应用场景和需求。通过正确使用这些同步机制,可以有效地防止数据竞争,提高程序的可靠性和性能。

并发编程是指同时执行多个任务的能力。C++标准库提供了多种工具来实现并发编程,其中最重要的包括std::threadstd::lock_guardstd::unique_lock

十三、如何在C++中创建和管理线程?

1. 创建线程

使用 std::thread 类来创建和管理线程。std::thread 的构造函数接受一个可调用对象(如函数、函数对象或 lambda 表达式)和可选的参数,这些参数会被传递给线程执行的函数。

示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <thread>

// 一个简单的函数,线程将执行这个函数
void print_message(const std::string& message) {
std::cout << message << std::endl;
}

int main() {
// 创建一个线程来执行 print_message 函数
std::thread t(print_message, "Hello from the thread!");

// 等待线程结束
t.join();

return 0;
}
解释
  • std::thread t(print_message, "Hello from the thread!");:创建了一个新线程 t,它将执行 print_message 函数,并传递 "Hello from the thread!" 作为参数。
  • t.join();:主线程等待 t 线程完成。如果没有调用 join()detach(),主线程在结束时会终止所有子线程,这可能导致未定义的行为。

2. 线程函数

线程函数可以是普通函数、成员函数、lambda 表达式或函数对象。需要注意的是,如果使用成员函数作为线程函数,则需要将对象实例作为参数传递。

普通函数
1
2
3
void print_message(const std::string& message) {
std::cout << message << std::endl;
}
成员函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <thread>

class Printer {
public:
void print_message(const std::string& message) {
std::cout << message << std::endl;
}
};

int main() {
Printer p;
std::thread t(&Printer::print_message, &p, "Hello from the member function!");
t.join();
return 0;
}
Lambda 表达式
1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <thread>

int main() {
std::thread t([]() {
std::cout << "Hello from the lambda!" << std::endl;
});
t.join();
return 0;
}

3. 线程同步

线程同步机制可以用来避免数据竞争和确保线程安全。

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

std::mutex mtx;

void print_message(const std::string& message) {
std::lock_guard<std::mutex> lock(mtx);
std::cout << message << std::endl;
}

int main() {
std::thread t1(print_message, "Hello from thread 1!");
std::thread t2(print_message, "Hello from thread 2!");

t1.join();
t2.join();

return 0;
}
条件变量
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
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

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

void wait_for_ready() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
std::cout << "Thread is proceeding" << std::endl;
}

void set_ready() {
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_all();
}

int main() {
std::thread t1(wait_for_ready);
std::thread t2(set_ready);

t1.join();
t2.join();

return 0;
}

4. 分离线程

线程可以被分离,使其在后台运行,不需要等待它完成(注意:这会使得主线程无法直接获取该线程的执行结果)。

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

void print_message() {
std::cout << "Hello from a detached thread!" << std::endl;
}

int main() {
std::thread t(print_message);
t.detach(); // 分离线程

// 主线程可能在子线程完成之前退出,因此不适合使用 detach() 如果需要确保线程完成,应该使用 join()
std::this_thread::sleep_for(std::chrono::seconds(1));

return 0;
}

5. 线程管理

  • std::thread::join():主线程等待子线程结束。
  • std::thread::detach():将线程分离,让它在后台运行,不需要等待其结束。

总结

在C++中创建和管理线程可以通过标准库提供的 std::thread 类来实现。你可以创建线程、传递参数、同步线程操作(使用互斥锁、条件变量等),并管理线程的生命周期(使用 join()detach())。掌握这些基础操作是进行高效多线程编程的关键。

评论