1. C++11新特性
1.1 std::tie
和 std::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
将元组中的值依次解包到变量 a
、b
和 c
中。
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::tie
对 Person
对象的多个成员进行比较。
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
类型值,只解包 int
和 std::string
。
1.1.3 结合 std::tie
与 std::ignore
的更多用法
你可以灵活运用 std::tie
与 std::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::function
和 std::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::bind
将 add
函数的第一个参数固定为 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::bind
将 Math
对象 math
和 add
成员函数绑定在一起,并将第一个参数固定为 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::bind
与 std::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::bind
和 std::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::pair
和 std::tuple
进行对比,并介绍它们各自的用法。
1.4 std::pair
和 std::tuple
std::pair
:专门用于存储两个相关联的元素。它的元素数量固定为两个,并且通过first
和second
来访问这两个元素。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
的两个元素分别通过 first
和 second
成员来访问。
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::map
和 std::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
没有 first
和 second
成员,访问 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::pair
和 std::tuple
的对比总结
特性 | std::pair | std::tuple |
---|---|---|
元素数量 | 固定为两个 | 任意数量 |
元素类型 | 两个元素的类型可以不同 | 每个元素的类型可以不同 |
访问方式 | 通过 first 和 second 成员访问 | 通过 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
这个右值。关键是,通过右值引用r
,10
的生命周期被延长了。也就是说,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_ptr
、std::shared_ptr
和std::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;
}
解释:ptr1
和 ptr2
共享同一个对象,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 override
和final
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;
}
解释:
- 函数上的
final
:Derived
类中的ShowMessage()
函数被声明为final
,因此在AnotherDerived
类中不能再重写这个函数。如果你尝试这么做,编译器会报错。 - 类上的
final
:Base
类被声明为final
,因此不能有任何派生类继承自Base
类。如果你尝试继承这个类,编译器也会报错。
1.9 std::future
std::future
是 C++11 引入的一种用于异步操作的机制,允许你在一个线程中启动一个任务,并在将来的某个时间点获取该任务的结果。它与 std::async
、std::promise
以及 std::thread
结合使用,可以实现简单而强大的并发编程模型。
1.9.1 std::future
的基本概念
std::future
对象可以保存一个异步操作的结果,当这个操作完成后,你可以从 std::future
对象中获取结果。在操作完成之前,std::future
还提供了一种机制,允许你检查操作是否已经完成。
std::future
通常与 std::async
配合使用。std::async
启动一个异步任务并返回一个 std::future
对象,表示任务的结果。你可以在将来的某个时刻通过 std::future
的 get()
方法获取这个结果。
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::future
的 wait()
和 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::promise与
std::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
函数。