C++11新特性超详解(逐步更新中...)

1. C++11新特性

1.1 std::tiestd::ignore

  • std::tie 主要用于将元组或对组中的元素解包到多个变量中。
  • std::ignore 可以与 std::tie 结合使用,用于忽略元组中的某些元素,避免不必要的赋值。

1.1.1 std::tie

std::tie 是 C++11 引入的一种工具,用于将多个变量绑定到一个元组(std::tuple)上,从而可以解包元组中的各个元素到单独的变量中。这在处理多个返回值时特别有用,比如函数返回一个元组或者一个 std::pair

1.1.1.1 std::tie 的典型用法
#include <iostream>
#include <tuple>

std::tuple<int, double, std::string> getValues() {
    return std::make_tuple(42, 3.14, "Hello");
}

int main() {
    int a;
    double b;
    std::string c;

    std::tie(a, b, c) = getValues();  // 解包元组,将值赋给变量

    std::cout << a << ", " << b << ", " << c << std::endl;  // 输出:42, 3.14, Hello
    return 0;
}

在这个例子中,getValues 返回一个包含三个元素的元组,std::tie 将元组中的值依次解包到变量 abc 中。

1.1.1.2 std::tie 用于比较

std::tie 也可以用于将多个变量组合成一个可比较的对象。例如,当你需要比较两个对象的多个字段时,可以用 std::tie 将它们组合起来。

#include <iostream>
#include <tuple>

struct Person {
    std::string name;
    int age;
    double height;
};

bool operator<(const Person& lhs, const Person& rhs) {
    return std::tie(lhs.name, lhs.age, lhs.height) < std::tie(rhs.name, rhs.age, rhs.height);
}

int main() {
    Person p1{"Alice", 30, 165.5};
    Person p2{"Bob", 25, 170.2};

    if (p1 < p2) {
        std::cout << "p1 比 p2 小" << std::endl;
    } else {
        std::cout << "p1 不比 p2 小" << std::endl;
    }
    return 0;
}

在这里,operator< 利用 std::tiePerson 对象的多个成员进行比较。

1.1.2 std::ignore

std::ignore 是一个特殊的常量,用于 std::tie 时忽略不需要的元组元素。这在你不需要所有返回值的情况下特别有用。std::ignore 的典型用法

#include <iostream>
#include <tuple>

std::tuple<int, double, std::string> getValues() {
    return std::make_tuple(42, 3.14, "Hello");
}

int main() {
    int a;
    std::string c;

    std::tie(a, std::ignore, c) = getValues();  // 忽略元组中的第二个元素

    std::cout << a << ", " << c << std::endl;  // 输出:42, Hello
    return 0;
}

在这个例子中,std::ignore 被用来忽略元组中的 double 类型值,只解包 intstd::string

1.1.3 结合 std::tiestd::ignore 的更多用法

你可以灵活运用 std::tiestd::ignore 来解包不同长度的元组,或者只提取你感兴趣的值。例如:

#include <iostream>
#include <tuple>

std::tuple<int, double, std::string, char> getValues() {
    return std::make_tuple(1, 2.71, "world", 'X');
}

int main() {
    int a;
    char d;

    std::tie(a, std::ignore, std::ignore, d) = getValues();  // 只解包第一个和最后一个值

    std::cout << a << ", " << d << std::endl;  // 输出:1, X
    return 0;
}

1.2 std::functionstd::bind

1.2.1 std::function

std::function 是一个通用的、多态的函数包装器,用于存储、传递和调用任意可调用对象(如普通函数、Lambda 表达式、函数对象、成员函数等)。它的作用是在不明确具体类型的情况下,对函数进行封装,从而可以更灵活地处理各种类型的可调用对象。

1.2.1.1 std::function 的基本用法
#include <iostream>
#include <functional>

// 普通函数
int add(int a, int b) {
    return a + b;
}

int main() {
    // 使用 std::function 包装普通函数
    std::function<int(int, int)> func = add;

    std::cout << "10 + 20 = " << func(10, 20) << std::endl;  // 输出:10 + 20 = 30
    return 0;
}

这里的 std::function<int(int, int)> 意思是“这个变量 func 可以存储任何接受两个 int 参数并返回一个 int 的函数”。

1.2.1.2 std::function 用于 Lambda 表达式

假设我们有一个简单的 Lambda 表达式,用来计算两个数的乘积:

auto multiply = [](int a, int b) {
    return a * b;
};

这个 multiply 是一个 Lambda 表达式,它接受两个 int 参数,返回它们的乘积。

我们可以用 std::function 来存储这个 Lambda 表达式:

#include <functional>

std::function<int(int, int)> func = multiply;
int result = func(10, 20);  // 结果是 200

这里,std::function<int(int, int)> 表示这个 func 变量可以存储任何接受两个 int 参数并返回 int 的函数,Lambda 表达式也是一种函数,所以可以存储在 func 中。

也就是下面这个例子:

#include <iostream>
#include <functional>

int main() {
    // 使用 std::function 包装 Lambda 表达式
    std::function<int(int, int)> func = [](int a, int b) {
        return a * b;
    };

    std::cout << "10 * 20 = " << func(10, 20) << std::endl;  // 输出:10 * 20 = 200
    return 0;
}

在这个例子中,std::function 包装了一个 Lambda 表达式,使其可以在需要时调用。

1.2.1.3 std::function 的应用场景
  • 回调函数std::function 可以用来存储回调函数的引用或副本,以便在需要时调用。
  • 通用函数接口:在需要传递不同类型的函数作为参数时,std::function 提供了一种统一的接口。

1.2.2 std::bind

std::bind 是 C++11 引入的一种工具,用于绑定函数的参数,从而生成一个新的可调用对象。可以使用 std::bind 将函数的一些参数提前绑定到特定的值,创建一个新的函数对象,该对象可以在需要时调用。

1.2.2.1 std::bind 的基本用法
#include <iostream>
#include <functional>

// 普通函数
int add(int a, int b) {
    return a + b;
}

int main() {
    // 绑定 add 函数的第一个参数为 10,生成一个新的函数对象
    auto add10 = std::bind(add, 10, std::placeholders::_1);
	// std::placeholders::_1 是一个占位符,表示这个参数将在调用 add10 时传递
    std::cout << "10 + 20 = " << add10(20) << std::endl;  // 输出:10 + 20 = 30
    return 0;
}

在这个例子中,std::bindadd 函数的第一个参数固定为 10,创建了一个新的函数对象 add10,该对象只需要传递第二个参数即可。

1.2.2.2 std::bind 与成员函数

成员函数是类中的函数,它们需要对象来调用。std::bind 可以帮我们将成员函数和对象绑定在一起,创建一个新的可调用对象。

1.2.2.3 例子

假设我们有一个 Math 类,其中有一个 add 成员函数:

class Math {
public:
    int add(int a, int b) {
        return a + b;
    }
};

通常情况下,你需要一个 Math 对象来调用 add 函数:

Math math;
int result = math.add(10, 20);  // 结果是 30

但是如果你想创建一个新的函数对象,这个对象可以在稍后调用 add 函数,并且固定某些参数,可以用 std::bind

include <functional>

Math math;
auto add20 = std::bind(&Math::add, &math, 20, std::placeholders::_1);

这里,std::bindMath 对象 mathadd 成员函数绑定在一起,并将第一个参数固定为 20。std::placeholders::_1 是一个占位符,表示第二个参数将在调用 add20 时传递。

你可以这样使用 add20

int result = add20(30);  // 相当于调用 math.add(20, 30),结果是 50

完整例子:

#include <iostream>
#include <functional>

class Math {
public:
    int add(int a, int b) {
        return a + b;
    }
};

int main() {
    Math math;

    // 绑定成员函数 add,第一个参数为对象 math
    auto add20 = std::bind(&Math::add, &math, 20, std::placeholders::_1);

    std::cout << "20 + 30 = " << add20(30) << std::endl;  // 输出:20 + 30 = 50
    return 0;
}

在这个例子中,std::bind 将成员函数 add 绑定到对象 math 上,并将第一个参数固定为 20,生成了一个新的函数对象 add20

1.2.2.4 std::bindstd::function 配合使用

std::bind 生成的可调用对象可以直接与 std::function 结合使用,使得函数的参数绑定更加灵活。

1.2.2.5 例子

假设你有一个减法函数 subtract

int subtract(int a, int b) {
    return a - b;
}

你可以用 std::bind 来创建一个新函数,固定第二个参数为 10,然后用 std::function 来存储这个函数:

#include <functional>

auto sub10 = std::bind(subtract, std::placeholders::_1, 10);
std::function<int(int)> func = sub10;

现在,func 是一个接受一个参数的函数,调用时会自动减去 10:

int result = func(20);  // 相当于调用 subtract(20, 10),结果是 10

完整例子:

#include <iostream>
#include <functional>

int subtract(int a, int b) {
    return a - b;
}

int main() {
    // 使用 std::bind 绑定 subtract 函数
    auto sub10 = std::bind(subtract, std::placeholders::_1, 10);

    // 使用 std::function 包装 sub10
    std::function<int(int)> func = sub10;

    std::cout << "20 - 10 = " << func(20) << std::endl;  // 输出:20 - 10 = 10
    return 0;
}

在这个例子中,std::bindstd::function 一起使用,创建了一个新的函数对象 func,可以在需要时调用。

1.2.3 总结

  • std::function 是一个通用的函数包装器,可以用于存储和调用各种类型的可调用对象。
  • std::bind 用于绑定函数的部分参数,从而生成新的可调用对象,简化后续的调用过程。

1.3 Lambda 表达式

Lambda 表达式是 C++11 引入的一种语法,用于定义匿名函数,也就是说,这种函数没有名字。Lambda 表达式可以直接在代码中定义并使用,通常用于简化代码,特别是在需要短小的函数或回调函数时。

1.3.1 Lambda 表达式的基本语法

[捕获列表](参数列表) -> 返回类型 {
    函数体
}
  • 捕获列表:这是一个用方括号 [] 括起来的部分,用来指定 Lambda 表达式中可以访问的外部变量。捕获列表可以为空,也可以捕获外部变量(按值捕获或按引用捕获)。
  • 参数列表:和普通函数一样,指定 Lambda 表达式接受的参数。
  • 返回类型:用 -> 指定返回类型,如果不写,编译器会自动推断返回类型。
  • 函数体:定义 Lambda 表达式的具体操作,也就是这个函数的实现部分。

1.3.2 简单例子

我们从一个最简单的 Lambda 表达式开始:

auto add = [](int a, int b) {
    return a + b;
};
  • []:空的捕获列表,表示不捕获任何外部变量。
  • (int a, int b):这是 Lambda 表达式的参数列表,表示它接受两个 int 类型的参数。
  • { return a + b; }:这是函数体,表示这个 Lambda 表达式会返回两个参数的和。

这个 Lambda 表达式定义了一个匿名函数,用来计算两个数的和,并将它赋值给了 add 变量。现在,你可以像调用普通函数一样使用 add

int result = add(10, 20);  // result = 30

1.3.3 捕获外部变量

Lambda 表达式可以访问它定义时周围作用域的变量,通过捕获列表指定如何捕获这些变量。

1.3.3.1 按值捕获

如果你想在 Lambda 表达式中使用一个外部变量的副本,可以按值捕获它:

int x = 10;
auto func = [x](int y) {
    return x + y;
};

这里,x 的值被复制到 Lambda 表达式中,func 内部使用的是 x 的副本。

1.3.3.2 按引用捕获

如果你想在 Lambda 表达式中直接访问外部变量本身,可以按引用捕获它:

int x = 10;
auto func = [&x](int y) {
    x += y;
    return x;
};

在这个例子中,func 内部操作的是 x 的引用,因此它可以修改 x 的值。

1.3.4 示例:Lambda 表达式作为参数

Lambda 表达式可以作为参数传递给函数。例如,标准库中的 std::sort 函数允许你传递一个比较函数来决定排序方式,你可以使用 Lambda 表达式来定义这个比较函数:

#include <vector>
#include <algorithm>
#include <iostream>

int main() {
    std::vector<int> numbers = {5, 2, 9, 1, 5, 6};

    // 使用 Lambda 表达式按降序排序
    std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
        return a > b;
    });

    for(int num : numbers) {
        std::cout << num << " ";  // 输出:9 6 5 5 2 1
    }

    return 0;
}

1.3.5 总结

  • Lambda 表达式 是一种简洁的方式来定义匿名函数,特别适合需要临时使用的小函数。
  • 它可以捕获外部变量,通过按值或按引用捕获。
  • Lambda 表达式可以像普通函数一样使用,并且在处理回调、排序等操作时非常有用。

好的,接下来我会将 std::pairstd::tuple 进行对比,并介绍它们各自的用法。

1.4 std::pairstd::tuple

  • std::pair:专门用于存储两个相关联的元素。它的元素数量固定为两个,并且通过 firstsecond 来访问这两个元素。
  • std::tuple:可以存储任意数量和任意类型的元素。它可以看作是一个更通用的版本,适用于多个元素的组合。

1.4.1 std::pair 的基本用法

1.4.1.1 创建 std::pair

你可以使用 std::pair 来创建一个包含两个元素的对象。std::pair 的两个元素可以是不同的类型。

#include <iostream>
#include <utility>  // std::pair 定义在 <utility> 头文件中

int main() {
    std::pair<int, std::string> p1(1, "Hello");
    std::pair<int, double> p2 = std::make_pair(2, 3.14);

    std::cout << "p1: " << p1.first << ", " << p1.second << std::endl;  // 输出:p1: 1, Hello
    std::cout << "p2: " << p2.first << ", " << p2.second << std::endl;  // 输出:p2: 2, 3.14

    return 0;
}

解释:

  • std::pair<int, std::string> p1(1, "Hello");: 创建了一个 pair,它包含一个整数 1 和一个字符串 "Hello"
  • std::make_pair(2, 3.14);: 这是一个辅助函数,用于创建 pair 对象。p2 包含一个整数 2 和一个双精度浮点数 3.14
1.4.1.2 访问 std::pair 的元素

std::pair 的两个元素分别通过 firstsecond 成员来访问。

std::cout << "First element: " << p1.first << std::endl;
std::cout << "Second element: " << p1.second << std::endl;
1.4.1.3 std::pair 的应用场景
1.4.1.3.1 返回多个值

std::pair 通常用于函数返回多个值,而不必定义一个新的结构体或类。

#include <iostream>
#include <utility>

std::pair<int, int> getMinMax(int a, int b) {
    if (a < b) {
        return std::make_pair(a, b);
    } else {
        return std::make_pair(b, a);
    }
}

int main() {
    std::pair<int, int> result = getMinMax(10, 20);
    std::cout << "Min: " << result.first << ", Max: " << result.second << std::endl;  // 输出:Min: 10, Max: 20

    return 0;
}
1.4.1.3.2 存储键值对

在容器如 std::mapstd::unordered_map 中,std::pair 用于存储键值对。first 是键,second 是值。

#include <iostream>
#include <map>

int main() {
    std::map<int, std::string> myMap;
    myMap.insert(std::make_pair(1, "Apple"));
    myMap.insert(std::make_pair(2, "Banana"));

    for (const auto& item : myMap) {
        std::cout << item.first << ": " << item.second << std::endl;  // 输出:1: Apple, 2: Banana
    }

    return 0;
}

1.4.2 std::tuple 的基本用法

1.4.2.1 创建 std::tuple

std::tuple 可以包含任意数量的元素,每个元素的类型可以不同。

#include <iostream>
#include <tuple>  // std::tuple 定义在 <tuple> 头文件中

int main() {
    std::tuple<int, double, std::string> t1(1, 3.14, "Hello");

    return 0;
}
1.4.2.2 访问 std::tuple 的元素

std::pair 不同,std::tuple 没有 firstsecond 成员,访问 tuple 中的元素需要使用 std::get<N>,其中 N 是从 0 开始的索引。

int a = std::get<0>(t1);  // 获取第一个元素,结果为 1
double b = std::get<1>(t1);  // 获取第二个元素,结果为 3.14
std::string c = std::get<2>(t1);  // 获取第三个元素,结果为 "Hello"
1.4.2.3 结合 std::tie 解构 std::tuple

你可以使用 std::tie 来解构 std::tuple,将它的元素分别赋值给多个变量。

int x;
double y;
std::string z;

std::tie(x, y, z) = t1;
1.4.2.4 std::make_tuple

类似于 std::make_pair,你可以使用 std::make_tuple 来创建一个 tuple

auto t2 = std::make_tuple(2, 6.28, "World");

std::pair 是 C++ 标准库中的一个简单而有用的模板类,它用于存储两个相关的值或对象。std::pair 经常用于函数返回多个值、或者在容器中存储键值对。

1.4.3 std::pairstd::tuple 的对比总结

特性std::pairstd::tuple
元素数量固定为两个任意数量
元素类型两个元素的类型可以不同每个元素的类型可以不同
访问方式通过 firstsecond 成员访问通过 std::get<N> 访问
用途通常用于存储两个相关联的数据,如键值对或返回多个值适用于需要存储多个不同类型的数据,例如返回多个值或传递多个参数

1.5 std::move

std::move 是 C++11 引入的一个标准库函数,用于实现所谓的“移动语义”(move semantics)。移动语义通过减少不必要的数据拷贝,提高了程序的效率,特别是在处理大量数据或资源时非常有用。

1.5.1 什么是移动语义?

在传统的 C++ 中,对象通常通过复制语义(copy semantics)进行传递或赋值。也就是说,当你把一个对象赋值给另一个对象时,会复制这个对象的内容。

但是有时候,我们只需要转移对象的所有权,而不需要复制它的内容。这就是移动语义的作用。它避免了昂贵的复制操作,而是直接“移动”资源。

1.5.2 std::move 的作用

std::move 其实并不真的移动任何东西。它的作用是将一个左值转换为右值引用,从而允许对象的资源被“移动”而不是“复制”。

1.5.2.1 左值和右值

左值(lvalue)

  • 定义:左值是指在内存中有地址的对象,即可以被取地址的值。在表达式中,左值通常位于赋值运算符 = 的左边,因此得名“左值”。

  • 特点:左值是持久的,可以被引用和修改。

  • 例子

    int a = 10;  // 变量 a 是一个左值
    int* p = &a; // 可以取 a 的地址,因此 a 是左值
    a = 20;      // 可以给 a 赋新值
    

右值(rvalue)

  • 定义:右值是指在表达式中不具有持久地址的临时值或字面量。右值通常位于赋值运算符 = 的右边,因此得名“右值”。

  • 特点:右值是临时的,不能被取地址或修改,通常只在当前表达式中存在。

  • 例子

    int b = 10 + 20;  // 表达式 10 + 20 的结果是右值,不能取地址
    int c = 30;       // 30 是一个右值
    

在上面的例子中,变量 a 是一个左值,因为它有持久的存储空间,可以被引用或修改。而表达式 10 + 20 或字面量 30 则是右值,它们是临时的、不可取地址的值

1.5.3 右值引用 &&

这部分内容太多了,拆出一节来讲,见 下一小节[右值引用(T&&)](## 右值引用(T&&))

1.5.3.1 移动构造函数
  • 定义:移动构造函数是用来“移动”对象资源的构造函数,而不是复制资源。它通过右值引用作为参数,从而可以接管右值的资源。

  • 语法

    ClassName(ClassName&& other);
    

    在这里,

    ClassName&&
    

    是一个右值引用,用于接收临时对象(右值)的资源。

  • 为什么要用 &&

    • 第一个 & 是为了创建一个引用,引用不会复制对象,而是直接使用原对象。
    • 第二个 & 是为了表示这是一个右值引用,表明它只能绑定到右值而不是左值。

1.5.4 std::move 的基本用法

例子:移动构造函数

假设你有一个管理大量数据的类,你可以通过移动构造函数来提高效率。

#include <iostream>
#include <vector>
#include <utility>

class DataHolder {
public:
    std::vector<int> data;

    // 默认构造函数
    DataHolder() = default;

    // 移动构造函数
    DataHolder(DataHolder&& other) noexcept
        : data(std::move(other.data)) {
        std::cout << "Move constructor called" << std::endl;
    }

    // 复制构造函数
    DataHolder(const DataHolder& other)
        : data(other.data) {
        std::cout << "Copy constructor called" << std::endl;
    }
};

int main() {
    DataHolder dh1;
    dh1.data = {1, 2, 3, 4, 5};

    DataHolder dh2 = std::move(dh1);  // 调用移动构造函数

    std::cout << "dh1 size: " << dh1.data.size() << std::endl;  // 输出:dh1 size: 0
    std::cout << "dh2 size: " << dh2.data.size() << std::endl;  // 输出:dh2 size: 5

    return 0;
}

解释:

  • DataHolder(DataHolder&& other):这是一个移动构造函数。它接收一个右值引用 DataHolder&&,并使用 std::move 将数据从 other 转移到当前对象,而不进行复制。
  • std::move(other.data):将 other.data 的内容移动到当前对象的 data 中,other.data 之后将为空。

在这个例子中,std::move 避免了对大量数据的复制,提升了程序的效率。

1.5.5 何时使用 std::move

  • 传递大对象:当你传递或返回一个大对象,并且不再需要原对象时,可以使用 std::move 来避免复制。
  • 资源管理:当一个对象拥有资源(如文件句柄、网络连接等),可以通过移动语义转移资源的所有权,而不进行昂贵的资源复制。

1.6 右值引用(T&&

  • 定义:右值引用是 C++11 引入的一种引用类型,用于引用右值。它的语法是 T&&,其中 T 是类型。例如,int&& 表示一个右值引用类型的整数。
  • 特点:右值引用可以绑定到右值(临时对象)上,从而允许我们对右值进行修改或移动资源。

1.6.1 右值和右值引用的本质

  • 右值(rvalue):右值是表达式的结果或字面量,比如 10。它是短暂的,在表达式结束后就会被销毁。右值通常不能被取地址,也不能被赋值。
  • 右值引用(rvalue reference,T&&):右值引用是一种特殊的引用类型,它可以绑定到右值,从而延长这个右值的生命周期,使得你可以在它被销毁之前对它进行操作。

1.6.2 例子

看这个例子:int r = 10; r = 20;int&& r = 10; r = 20;

int r = 10;
r = 20;

解释

  • int r = 10;:这里,r 是一个普通的左值变量,绑定到了右值 10。在内存中为 r 分配了一块存储空间,并将 10 复制到这个空间中。

  • r = 20;:这是一个赋值操作,将 20 复制到 r 所指向的内存地址中。由于 r 是左值,它可以持久存在,并且可以被多次赋值。

int&& r = 10;
r = 20;

解释

  • 10 是右值10 是一个字面量,通常它是一个临时值,没有名字,也没有地址,它的生命周期很短暂。

    r 是右值引用:当你写 int&& r = 10; 时,r 并没有复制或存储 10,而是引用了 10 这个右值。关键是,通过右值引用 r10 的生命周期被延长了。也就是说,10 的地址(尽管它是一个临时对象)可以通过 r 访问,直到 r 本身超出了作用域,一旦超出这个范围,r 就会被销毁,同时引用的右值 10 也会被销毁

  • r = 20;:这个操作通过右值引用修改了 r 所绑定的值。由于 r 是一个右值引用,它可以操作右值(临时对象),所以这个赋值是合法的。

1.6.2.1 两者的区别

普通变量定义(例如 int x = 10;):

  • x 是一个左值变量,有自己的存储位置,10 被复制到 x 的存储位置中。
  • x 可以被多次赋值和修改。

右值引用(例如 int&& r = 10;):

  • r 是右值引用,它并不拥有 10,而是引用了 10 的地址。
  • 右值引用允许你操作临时对象,延长它的生命周期,避免不必要的复制。

1.6.3 右值引用的主要用途

右值引用的设计目的是为了解决 C++ 中的一些效率问题,特别是在处理临时对象和大对象时。它有以下几个主要用途:

  • 移动语义:右值引用允许你“移动”对象的资源,而不是复制它们。这在处理大量数据或需要频繁复制对象的场景中,可以极大地提高性能。例如,当你将一个大型容器(如 std::vector)传递给另一个容器时,移动语义可以避免不必要的深拷贝。
  • 完美转发:在泛型编程中,右值引用配合 std::forward 允许将参数完美地转发到另一个函数中,无论它是左值还是右值,这大大增强了泛型代码的灵活性。

1.6.4 为什么右值引用可以修改右值

通常,我们认为右值(如 10)是不可修改的。然而,右值引用打破了这一限制。右值引用是 C++11 引入的一种新型引用,它允许我们对临时对象进行操作,包括修改它们的值。通过右值引用,编译器允许你将右值视为左值,因而可以修改它所引用的值。

1.7 智能指针

1.7.1 为什么需要智能指针?

在C++中,当你使用new分配内存时,你需要手动调用delete来释放内存。如果忘记释放内存,就会导致内存泄漏。这种手动管理内存的方式容易出错,尤其是在复杂的代码中。智能指针可以帮助你自动管理这些资源。例如,当一个智能指针不再使用时,它会自动释放内存,无需手动调用delete

1.7.2 2. C++中的三种常用智能指针

C++11引入了三种常用的智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr

1.7.2.1 std::unique_ptr
  • 用途unique_ptr 表示独占所有权,意味着某个对象只能有一个unique_ptr拥有。如果你尝试复制一个unique_ptr,编译器会报错。
  • 使用场景:当你确定一个资源只会被一个指针拥有时,使用unique_ptr

例子:

#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> ptr1 = std::make_unique<int>(10); // 创建一个`unique_ptr`,指向一个值为10的整数
    std::cout << *ptr1 << std::endl; // 输出:10
    
    // std::unique_ptr<int> ptr2 = ptr1; // 错误!不能复制unique_ptr
    std::unique_ptr<int> ptr2 = std::move(ptr1); // 将所有权转移给ptr2
    std::cout << *ptr2 << std::endl; // 输出:10
    
    // 此时ptr1不再拥有指针,ptr1为空
    if (!ptr1) {
        std::cout << "ptr1 is empty" << std::endl;
    }
    return 0;
}

解释std::move(ptr1)ptr1的所有权转移给ptr2,之后ptr1不再指向任何东西。

1.7.2.2 std::shared_ptr
  • 用途shared_ptr 表示共享所有权,意味着多个shared_ptr可以同时拥有同一个对象。只有当最后一个shared_ptr被销毁时,内存才会被释放。
  • 使用场景:当你希望多个指针可以共享同一个对象的所有权时,使用shared_ptr

例子:

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> ptr1 = std::make_shared<int>(20); // 创建一个`shared_ptr`,指向一个值为20的整数
    std::shared_ptr<int> ptr2 = ptr1; // 复制ptr1,两个指针共享所有权
    
    std::cout << *ptr1 << std::endl; // 输出:20
    std::cout << *ptr2 << std::endl; // 输出:20
    
    std::cout << "Use count: " << ptr1.use_count() << std::endl; // 输出:2,表示两个`shared_ptr`共享该对象
    return 0;
}

解释ptr1ptr2 共享同一个对象,use_count() 函数返回当前共享对象的指针数量。

1.7.2.3 std::weak_ptr
  • 用途weak_ptr 是一种不控制对象生命周期的指针。它与shared_ptr配合使用,用于解决循环引用问题。
  • 使用场景:当你需要引用一个可能已经被销毁的对象,而不影响其生命周期时,使用weak_ptr

例子:

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sptr = std::make_shared<int>(30); // 创建一个`shared_ptr`
    std::weak_ptr<int> wptr = sptr; // 使用`weak_ptr`指向相同的对象
    
    if (auto spt = wptr.lock()) { // 尝试获取`shared_ptr`
        std::cout << *spt << std::endl; // 输出:30
    } else {
        std::cout << "Object no longer exists." << std::endl;
    }
    
    sptr.reset(); // 重置`shared_ptr`,对象被销毁
    
    if (auto spt = wptr.lock()) { // 再次尝试获取`shared_ptr`
        std::cout << *spt << std::endl;
    } else {
        std::cout << "Object no longer exists." << std::endl; // 输出:Object no longer exists.
    }
    return 0;
}

解释weak_ptr 不会影响对象的生命周期,lock() 函数返回一个shared_ptr,如果对象已经销毁,lock() 返回空指针。

1.8 overridefinal

1.8.1 override 关键字

作用override 关键字用于明确指定一个函数是从基类中继承并且被重写的。它能帮助编译器检查你是否真的在重写一个基类中的虚函数,而不是意外地创建了一个新的函数。

使用场景:当你在派生类中重写基类的虚函数时,建议使用override关键字。这不仅提高了代码的可读性,还能在重写函数时避免一些常见错误。

例子

#include <iostream>

class Base {
public:
    virtual void PrintMessage() const {
        std::cout << "Base class message" << std::endl;
    }
};

class Derived : public Base {
public:
    void PrintMessage() const override { // 使用 override 关键字
        std::cout << "Derived class message" << std::endl;
    }
};

int main() {
    Base* obj = new Derived();
    obj->PrintMessage(); // 输出:Derived class message
    delete obj;
    return 0;
}

解释

  • Derived 类中,PrintMessage() 函数使用了 override 关键字。这表明该函数是对 Base 类中 PrintMessage() 函数的重写。
  • 如果基类中的函数签名发生变化,或者你在派生类中拼写错误(例如参数不同),编译器会提示错误。这可以有效防止意外地定义了一个新的函数,而不是重写基类的函数。

1.8.2 final 关键字

作用final 关键字用于禁止进一步的重写或继承。它可以用于类或虚函数上,来防止其他类继承或重写这个函数。

  • 函数上的final:表示该函数不能在派生类中被重写。
  • 类上的final:表示该类不能被继承。

使用场景:当你希望某个虚函数不再被进一步重写,或者某个类不再被继承时,使用final关键字。

例子1:函数上的final

#include <iostream>

class Base {
public:
    virtual void ShowMessage() const {
        std::cout << "Base class" << std::endl;
    }
};

class Derived : public Base {
public:
    void ShowMessage() const final { // 使用 final 关键字,禁止进一步重写
        std::cout << "Derived class" << std::endl;
    }
};

class AnotherDerived : public Derived {
public:
    // void ShowMessage() const override; // 错误!无法重写已被 final 限制的函数
};

int main() {
    Base* obj = new Derived();
    obj->ShowMessage(); // 输出:Derived class
    delete obj;
    return 0;
}

例子2:类上的final

#include <iostream>

class Base final { // 使用 final 关键字,禁止进一步继承
public:
    void ShowMessage() const {
        std::cout << "Base class" << std::endl;
    }
};

// class Derived : public Base { }; // 错误!无法继承已被 final 限制的类

int main() {
    Base obj;
    obj.ShowMessage(); // 输出:Base class
    return 0;
}

解释

  • 函数上的finalDerived类中的ShowMessage()函数被声明为final,因此在AnotherDerived类中不能再重写这个函数。如果你尝试这么做,编译器会报错。
  • 类上的finalBase类被声明为final,因此不能有任何派生类继承自Base类。如果你尝试继承这个类,编译器也会报错。

1.9 std::future

std::future 是 C++11 引入的一种用于异步操作的机制,允许你在一个线程中启动一个任务,并在将来的某个时间点获取该任务的结果。它与 std::asyncstd::promise 以及 std::thread 结合使用,可以实现简单而强大的并发编程模型。

1.9.1 std::future 的基本概念

std::future 对象可以保存一个异步操作的结果,当这个操作完成后,你可以从 std::future 对象中获取结果。在操作完成之前,std::future 还提供了一种机制,允许你检查操作是否已经完成。

std::future 通常与 std::async 配合使用。std::async 启动一个异步任务并返回一个 std::future 对象,表示任务的结果。你可以在将来的某个时刻通过 std::futureget() 方法获取这个结果。

1.9.2 使用 std::future 的基本示例

例子:

#include <iostream>
#include <future>
#include <chrono>
#include <thread>

int CalculateSum(int a, int b) {
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
    return a + b;
}

int main() {
    // 启动异步任务
    std::future<int> result = std::async(std::launch::async, CalculateSum, 5, 10);
    
    std::cout << "Doing other work..." << std::endl;
    
    // 等待结果并获取
    int sum = result.get();
    std::cout << "Sum is: " << sum << std::endl;

    return 0;
}

解释

  • std::async(std::launch::async, CalculateSum, 5, 10) 启动了一个异步任务,在一个独立的线程中计算 CalculateSum(5, 10) 的结果,并返回一个 std::future<int> 对象。
  • result.get() 将阻塞(即等待)直到异步操作完成,并返回计算结果。在此之前,程序可以继续执行其他任务。
  • 如果你在异步任务完成之前调用 get(),程序会等待任务完成。如果任务已经完成,get() 会立即返回结果。

1.9.3 std::future 的状态检查

你可以使用 std::futurewait()wait_for()wait_until() 方法来检查异步任务的状态。

例子:

#include <iostream>
#include <future>
#include <chrono>
#include <thread>

int CalculateSum(int a, int b) {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return a + b;
}

int main() {
    std::future<int> result = std::async(std::launch::async, CalculateSum, 5, 10);

    while (result.wait_for(std::chrono::milliseconds(500)) != std::future_status::ready) {
        std::cout << "Waiting for the result..." << std::endl;
    }

    int sum = result.get();
    std::cout << "Sum is: " << sum << std::endl;

    return 0;
}

解释

  • wait_for(std::chrono::milliseconds(500)) 用于检查任务是否在指定时间内完成。它返回一个 std::future_status 枚举值,可以是 std::future_status::deferred(任务被延迟执行)、std::future_status::ready(任务已经完成)或 std::future_status::timeout(任务还未完成)。
  • 这个例子中的 while 循环每隔 500 毫秒检查一次任务是否完成。如果任务完成,while 循环终止,并获取结果。

1.9.4 std::promisestd::future`

std::promise 是另一个异步机制,用于设置 std::future 的结果。你可以在一个线程中创建一个 std::promise 对象,并在另一个线程中通过 std::future 获取结果。

例子:

#include <iostream>
#include <future>
#include <thread>

void CalculateSum(std::promise<int>&& promise, int a, int b) {
    int sum = a + b;
    promise.set_value(sum); // 设置结果
}

int main() {
    std::promise<int> promise;
    std::future<int> result = promise.get_future(); // 获取与 promise 关联的 future 对象

    std::thread t(CalculateSum, std::move(promise), 5, 10); // 将 promise 传递给线程

    int sum = result.get(); // 从 future 获取结果
    std::cout << "Sum is: " << sum << std::endl;

    t.join(); // 等待线程完成

    return 0;
}

解释

  • std::promise<int> 创建了一个 promise 对象,你可以通过它设置一个 int 类型的结果。
  • promise.get_future() 返回一个与 promise 关联的 std::future<int> 对象。
  • 在线程 t 中,调用 promise.set_value(sum) 设置了 sum 的值,随后可以在主线程中通过 result.get() 获取该值。

1.10 std::mutex

std::mutex 是 C++ 标准库提供的一种用于线程同步的机制,它用于防止多个线程同时访问共享资源,从而避免数据竞争问题。在多线程编程中,多个线程可能会同时读取和修改共享数据,这会导致不确定性和数据错误。std::mutex 可以帮助你确保在同一时间只有一个线程可以访问某个资源,从而避免这些问题。

1.10.1 什么是 std::mutex

std::mutex 是一种锁(lock),它可以保护一个共享资源,使得在同一时刻只有一个线程可以访问该资源。mutex 是 “mutual exclusion”(互斥)的缩写。

当一个线程想要访问共享资源时,它首先需要锁定 mutex,这时 mutex 被这个线程独占,其他线程无法访问该资源。线程完成操作后,需要解锁 mutex,其他线程才能继续访问该资源。

1.10.2 std::mutex 的基本用法

例子:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx; // 定义一个全局 mutex

void PrintMessage(const std::string& message) {
    mtx.lock(); // 锁定 mutex
    std::cout << message << std::endl; // 访问共享资源
    mtx.unlock(); // 解锁 mutex
}

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

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

    return 0;
}

解释

  • mtx.lock() 锁定 mutex,防止其他线程进入临界区。
  • mtx.lock()mtx.unlock() 之间的代码就是所谓的“临界区”(critical section),只有持有锁的线程可以执行这些代码。
  • mtx.unlock() 解锁 mutex,允许其他线程进入临界区。
  • t1.join()t2.join() 确保主线程等待这两个线程完成。

注意:如果一个线程调用 mtx.lock() 后没有调用 mtx.unlock(),那么其他线程将永远无法访问被保护的资源,这可能会导致死锁(deadlock)。

1.10.3 使用 std::lock_guard 自动管理锁

手动调用 lock()unlock() 虽然可以工作,但容易出现错误,尤其是在异常处理的情况下。为了避免忘记解锁 mutex,C++ 提供了 std::lock_guard,它是一个 RAII(资源获取即初始化)风格的工具,自动管理锁的生命周期。

例子:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void PrintMessage(const std::string& message) {
    std::lock_guard<std::mutex> lock(mtx); // 自动锁定和解锁
    std::cout << message << std::endl;
}

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

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

    return 0;
}

解释

  • std::lock_guard<std::mutex> lock(mtx); 创建一个 lock_guard 对象时,自动锁定 mutex
  • lock_guard 对象超出作用域时(函数结束或异常抛出)会自动解锁 mutex,确保不会忘记解锁。

1.10.4 其他互斥机制

  • std::unique_lock:比 std::lock_guard 更加灵活,允许显式锁定和解锁,还支持延迟锁定和条件变量等高级特性。
  • std::recursive_mutex:允许同一线程多次锁定同一个 mutex,适用于递归函数。
  • std::timed_mutex:支持尝试在一段时间内锁定 mutex,如果超时则失败。
  • std::shared_mutex:支持多读单写,即多个线程可以同时读取,但只有一个线程可以写入。

1.11 using

在 C++ 中,using 关键字有几种不同的用法,可以用于类型别名、命名空间的引入、以及模板别名。以下是 using 关键字的主要用法和详细解释:

1.11.1 类型别名(Type Alias)

using 可以用来为复杂的类型定义别名,使代码更简洁易读。这种用法在 C++11 中引入,替代了早期的 typedef 语法。

1.11.1.1 基本语法
using AliasName = ExistingType;
  • AliasName:这是你为现有类型(ExistingType)定义的新名称。
  • ExistingType:这是你要给它起别名的原始类型。
1.11.1.2 示例
#include <vector>

// 定义一个别名 `IntVector`,表示 `std::vector<int>` 类型
using IntVector = std::vector<int>;

int main() {
    IntVector v = {1, 2, 3, 4};  // 现在可以使用 `IntVector` 代替 `std::vector<int>`
    return 0;
}

在这个例子中,using IntVector = std::vector<int>; 定义了一个别名,使得你可以使用 IntVector 来代替 std::vector<int>,这让代码更简洁。

1.11.2 引入命名空间中的名称(Namespace Alias)

using 还可以用来引入命名空间中的名称,使代码更简洁。这样可以避免在使用命名空间中的元素时,每次都需要写出完整的命名空间路径。

1.11.2.1 使用命名空间别名
namespace LongNamespaceName {
    void func() {
        // Some code
    }
}

// 定义一个命名空间别名
using LNN = LongNamespaceName;

int main() {
    LNN::func();  // 使用别名来调用命名空间中的函数
    return 0;
}

在这个例子中,using LNN = LongNamespaceName; 创建了一个命名空间的别名,使得我们可以用 LNN 来代替 LongNamespaceName,从而减少代码量。

1.11.2.2 引入命名空间中的所有名称
#include <iostream>
using namespace std;

int main() {
    cout << "Hello, world!" << endl;
    return 0;
}
  • using namespace std; 语句将 std 命名空间中的所有名称引入到当前作用域中,使得你在使用 std::cout 时可以直接写 cout

1.11.3 模板别名(Template Alias)

在 C++11 中,using 也可以用于为模板创建别名。这在处理复杂的模板类型时非常有用。

1.11.3.1 基本语法
template<typename T>
using AliasName = ExistingTemplate<T>;
1.11.3.2 示例
#include <vector>
#include <map>

// 定义一个别名 `IntMap`,表示 `std::map<int, std::vector<T>>` 类型
template<typename T>
using IntMap = std::map<int, std::vector<T>>;

int main() {
    IntMap<int> myMap;  // 使用模板别名
    return 0;
}

在这个例子中,using IntMap = std::map<int, std::vector<T>>; 创建了一个模板别名,使得你可以方便地定义包含 int 键和 std::vector 值的 std::map 类型。

1.11.4 使用 using 声明成员函数的覆盖

在类继承中,using 可以用来声明父类中某个重载函数集中的某些函数,允许子类在继承时保留这些重载版本。

1.11.4.1 示例
class Base {
public:
    void func(int) {}
    void func(double) {}
};

class Derived : public Base {
public:
    using Base::func;  // 引入 Base 类中的 func 函数
    void func(const char*) {}
};

int main() {
    Derived d;
    d.func(42);      // 调用 Base::func(int)
    d.func("text");  // 调用 Derived::func(const char*)
    return 0;
}

在这个例子中,using Base::func; 使得 Derived 类继承了 Base 类中重载的 func 函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值