右值引用
右值引用和左值引用的区别
为了理解它们之间的区别,首先需要明白什么是左值(lvalue)和右值(rvalue)。
左值(Lvalue)和右值(Rvalue):
- 左值(Lvalue):通常指的是一个持久性的对象,它拥有一个明确的地址可以被取得。左值可以出现在赋值操作符的左边。例如,变量、数组的元素、对对象成员的引用等都是左值。
- 右值(Rvalue):通常指的是临时的、无法取得地址的对象,它们不以变量形式存储。右值可以出现在赋值操作符的右边。例如,字面常数(如
5
或'a'
)、表达式计算的结果、返回临时对象的函数调用等都是右值。
左值引用(Lvalue Reference):
- 使用单个
&
符号定义,例如int&
。 - 左值引用可以引用左值但不能直接引用右值(不包括通过
const
引用进行的例外情况,因为const
引用可以绑定到右值)。
int x = 10;
int& lRef = x; // 正确:lRef 引用了一个左值 x
int& invalidRef = 10; // 错误:不能将左值引用绑定到右值
右值引用(Rvalue Reference):
- 使用双
&&
符号定义,例如int&&
。 - 右值引用专门用来引用右值,它允许对临时对象进行更有效的操作。通过右值引用,可以实现移动语义(Move Semantics)和完美转发(Perfect Forwarding)。
int&& rRef = 10; // 正确:rRef 成功绑定到右值 10
左值引用与右值引用的主要区别:
- 绑定的对象类型不同:左值引用主要绑定左值,而右值引用专门绑定右值。
- 使用目的不同:左值引用主要用于引用长期存在的对象,例如变量或对象的传递等;右值引用则用于引用将要销毁的临时对象,允许通过移动语义优化资源的重新分配,例如在容器类如
std::vector
中移动元素,而不是复制它们。 - 允许对被引用对象进行的操作不同:通过右值引用,可以安全地改变临时对象(即右值)的状态,因为它们即将被销毁,而左值引用通常不改变其引用对象的状态(除非它是一个非
const
的左值引用)。
移动构造和移动赋值
移动构造函数(Move Constructor)
移动构造函数是一个构造函数,它接受该类类型的右值引用作为参数,用于构造新对象,并“窃取”传入对象的资源。这样,数据不再是被复制,而是被移动到新对象中。被移动的对象会被留在一个有效但未定义的状态。
一个简单的移动构造函数的例子如下:
class MyClass {
public:
MyClass(MyClass&& other) {
// “窃取”other的资源
this->data = other.data;
// 将other置于一个未定义状态,但确保对象仍然处于析构安全的状态
other.data = nullptr;
}
// 其他成员...
private:
SomeType* data;
};
移动赋值操作符(Move Assignment Operator)
移动赋值操作符用于重写对象内容,并“窃取”另一个同类型对象的资源。它通常与移动构造函数类似,在移动赋值操作中,左侧对象(赋值操作符的调用者)将释放自己持有的资源,并接管右侧对象的资源。
移动赋值操作符的例子如下:
MyClass& operator=(MyClass&& other) {
if (this != &other) { // 防止自赋值
// 首先释放当前对象的资源
delete this->data;
// “窃取”other的资源
this->data = other.data;
// 将other置于一个未定义的状态
other.data = nullptr;
}
return *this;
}
右值引用的使用场景
移动语义(Move Semantics)
移动语义是 C++11 新引入的一个特性,它允许资源(如动态内存)从一个对象转移到另一个对象,而不是进行传统的复制。这种语义特别适用于处理大量数据或资源密集型对象的情况,因为它可以显著减少不必要的资源复制,从而提高程序的性能和效率。
在 C++中,移动语义是通过右值引用和两个标准库中的函数――std::move
和移动构造函数/移动赋值操作符来实现的。
- 右值引用(Rvalue Reference):使用双
&&
符号声明,它可以绑定到临时对象(右值)。通过右值引用,我们可以安全地从一个对象“窃取”资源,转移到另一个对象,因为我们知道没有其他变量会再访问临时对象。 - 移动构造函数/移动赋值操作符:类型特定的函数,它们接受右值引用作为参数,用于将资源从源对象移动到当前对象。在这个过程中,源对象的状态被适当地修改,以表明它不再拥有这些资源了(通常是将指针置为 nullptr)。
完美转发(Perfect Forwarding)
完美转发是另一个 C++11 引入的特性,用于解决模板函数中参数转发时可能出现的额外拷贝或不正确保持参数值属性(如左值、右值性质)的问题。它允许函数模板以一种方式接受任意类型的参数,并将其准确无误地转发给另一个函数,保持原始参数的所有值类别(左值、右值等)不变。
要实现完美转发,C++ 提供了 std::forward
函数模板。与 std::move
相比,std::forward
可以在编译时根据传入参数的类型来决定是否应该进行移动。
template<typename T>
void wrapper(T&& arg)
{
// 使用 std::forward 保持 arg 的原始类型(左值或右值)不变
someFunction(std::forward<T>(arg));
}
在这个例子中,T&&
类型的参数 arg
是一个“万能引用”,可以绑定到左值和右值上。通过 std::forward<T>(arg)
,arg
被完美转发到 someFunction
函数,保持了其作为左值或右值的原始状态。
lambda
仿函数
仿函数(Functor)是C++中一个重要的概念,它通过让类的实例表现得像函数一样来提供函数调用的功能。这是通过重载类的 operator()
实现的。因此,仿函数也被称为函数对象。仿函数可以存储状态,并且可以有多个重载版本的 operator()
,这为仿函数提供了比普通函数或函数指针更大的灵活性。
语法规则
Lambda表达式是C++11中引入的一种功能,允许你编写内联的匿名函数。它特别适用于简洁地编写函数对象(即仿函数)的场景,尤其是在标准模板库(STL)算法中作为参数时。Lambda表达式的基本语法如下:
[ capture ] ( parameters ) mutable exception_specification -> return_type { body }
让我们一步一步分解这个结构:
1.[ capture ]:捕获列表是Lambda表达式的一部分,它决定了外部作用域中哪些变量被Lambda所捕获,以及如何捕获(通过值、引用或不捕获)。
[=]
:以值的形式捕获外围作用域内的所有变量。[&]
:以引用的形式捕获外围作用域内的所有变量。[a, &b]
:以值的形式捕获变量a
,以引用的形式捕获变量b
。[this]
:捕获类中的this
指针,允许访问类的成员变量和函数。
2.( parameters ):和普通函数一样,参数列表允许你传递参数给Lambda表达式。如果不需要参数,可以留空。
3.mutable(可选):如果你需要在Lambda表达式中修改以值捕获的变量,或者要调用修改其自身状态的成员函数(例如,调用一个非 const
成员函数),可以加上 mutable
关键字。
4.exception_specification(可选):允许你指定Lambda表达式可能抛出的异常类型。
5.-> return_type(可选):显式指定返回类型。在许多情况下,返回类型可以被自动推断,因此这个部分可以省略。
6.{ body }:Lambda表达式的主体,包含了函数的实现。
底层实现原理
Lambda表达式的底层实现原理实际上是通过编译器转换成了一个仿函数的类。当编译器遇到一个Lambda表达式时,它创建一个新的类(有时被称为闭包类型),该类重载了 operator()
,从而使该类的实例可以像函数一样被调用。
这种底层实现允许Lambda表达式在不需要显示定义完全类的情况下被快速定义,并具有和自定义类似的性能和灵活性。同时,这也使得Lambda表达式能够无缝地与C++的函数指针和其他需要可调用对象的场合协同工作。
使用场景及优势
使用场景:
-
STL 算法:Lambda 表达式常用于作为参数传递给 STL 算法,如
std::sort
,std::for_each
,std::transform
等,允许用户提供自定义的操作。 -
事件处理和回调:在设计事件驱动程序或设置回调函数时,Lambda 表达式可以快速地定义在事件发生时需要执行的行为。
-
线程和异步操作:结合 C++11 中的线程库,Lambda 表达式可以简化线程的创建和管理,尤其是当向
std::thread
的构造函数传递一个 Lambda 时。 -
闭包:Lambda 表达式可以捕获其创建上下文中的变量,从而访问和操作这些变量,即便是在其原始作用域之外。
-
替代函数指针或函数对象:在需要传递简单的函数逻辑时,Lambda 是比函数指针更灵活的选择,特别是当涉及局部变量捕获时。
优势:
- 更简洁的代码:Lambda 表达式允许你直接在使用它们的地方定义匿名函数,从而省去了单独定义函数或函数对象的需要。
- 清晰的逻辑流:由于 Lambda 在它们所用的地方定义,因此可以很好地将相关的逻辑组织在一起,提高代码可读性。
- 易于局部变量捕获:Lambda 表达式使得捕获作用域中的变量变得非常容易,这对于访问和使用局部变量而不需要额外参数传递非常有用。
- 避免命名冲突:因为 Lambda 表达式是匿名的,所以你不需要担心在全局或名称空间中为它们找到唯一的名称。
- 支持即时编程(Just-In-Time):可以根据需要立即定义 Lambda 表达式,并且在算法调用期间执行,这样有助于编写高效和响应式的代码。
其他
线程库
C++11 标准引入了线程库,这是C++标准的一部分,提供了用于多线程编程的类和函数。这允许开发者直接在 C++ 代码中创建和控制线程,使得多线程编程变得更加简单而直接,无需依赖于平台特定的线程机制(如 POSIX 线程或 Windows 线程)。
1. std::thread
std::thread
类是 C++ 线程库的中心部分,它代表一个单独的执行线程。创建 std::thread
对象时,可以将一个函数(包括函数指针、Lambda表达式、函数对象或成员函数指针)和它的参数传递给它,这个函数将在新线程中执行。
#include <iostream>
#include <thread>
void foo() {
// 线程执行的代码
std::cout << "Thread function\n";
}
int main() {
std::thread threadObj(foo);
threadObj.join(); // 等待线程完成
return 0;
}
2. 数据共享和互斥
为了避免并发访问中的数据竞争,C++线程库提供了多种互斥量(mutex)类,例如:std::mutex
, std::recursive_mutex
, std::timed_mutex
和 std::recursive_timed_mutex
。使用互斥量来保护对共享数据的访问。
#include <iostream>
#include <thread>
#include <mutex>
int g_number = 0; // 共享数据
std::mutex g_mutex; // 互斥量
void increment_number() {
g_mutex.lock();
++g_number; // 受保护的操作
g_mutex.unlock();
}
3. 条件变量
条件变量(std::condition_variable
)用于线程间的同步,允许一个或多个线程等待某些条件成立,而条件变量与互斥量联合使用可以实现高效等待。
4. 原子操作
C++11 引入了标准的原子类型(std::atomic
),使得基本数据类型的操作可以在多线程环境下安全地执行,无需使用互斥量。
5. 其他工具
- 任务未来(futures)和承诺(promises):
std::future
和std::promise
类提供了一种在未来某个时间点获取异步操作结果的机制。 - 打包任务(packaged_task):
std::packaged_task
封装任何可调用的目标(如函数、Lambda表达式、绑定表达式),使其返回值可以在未来通过std::future
对象获取。
列表初始化
列表初始化是一种在 C++11 和更新的标准中引入的初始化语法,其允许使用花括号 {}
来初始化对象。列表初始化可以用于几乎所有类型的变量,包括基本数据类型、聚合类型、类实例以及 STL 容器。这种初始化方式提供了几个优点:
-
统一的语法:列表初始化为不同类型的初始化提供了统一的语法,简化了初始化操作的编写。
-
防止窄化转换:使用列表初始化时,编译器会禁止窄化转换(narrowing conversions),例如,从浮点数到整数或从大整数类型向小整数类型的转换,这有助于防止数据丢失。
-
方便的容器初始化:STL 容器,例如
vector
,list
,map
等可以直接利用列表初始化进行内容的填充。 -
聚合初始化:用于初始化聚合类型的对象,如数组、结构体。
-
自定义类型:对于类类型,如果定义了接受
std::initializer_list
的构造器,可以使用列表初始化语法进行对象的初始化。
举些例子:
int a = {1}; // 基本数据类型的列表初始化
std::vector<int> v = {1, 2, 3}; // STL 容器的列表初始化
// 结构体聚合初始化
struct Point {
int x;
int y;
};
Point p = {10, 20}; // 聚合初始化
// 带有 initializer_list 构造函数的类
class MyClass {
public:
MyClass(std::initializer_list<int> list) {
for (auto &elem : list) {
//处理元素
}
}
};
MyClass obj = {1, 2, 3, 4}; // 列表初始化自定义类型
int array[] = {1, 2, 3, 4}; // 数组的聚合初始化
STL容器变化
新容器
1. std::array
std::array
是一个固定大小的容器,它封装了一个能够存储固定数量元素的原生数组。与原生数组相比,std::array
提供了更现代的界面,支持迭代器、容器操作如 size()
以及支持 STL 算法。由于其大小在编译时就已确定,所以它不提供动态大小调整的能力。
#include <array>
std::array<int, 4> myArray = {1, 2, 3, 4};
2. std::forward_list
std::forward_list
实现了一个单向链表。与 std::list
(双向链表)相比,std::forward_list
更高效,因为它只需要存储到下一个元素的链接。由于这个特性,它不支持快速随机访问和向后遍历。std::forward_list
是对传统单向链表的现代C++封装,适用于空间和性能要求较高的场景。
#include <forward_list>
std::forward_list<int> myList = {1, 2, 3, 4};
3. std::unordered_set
std::unordered_set
是一个不允许重复的元素集合,它基于哈希表实现。相比于 std::set
(基于红黑树),它在平均情况下提供更快的插入、查找和删除操作,但不维持元素的顺序。std::unordered_set
的性能高效,特别是在元素数量较大时。
#include <unordered_set>
std::unordered_set<int> mySet = {1, 2, 3, 4};
4. std::unordered_map
std::unordered_map
是一个键-值对集合,它基于哈希表实现。与 std::map
(基于红黑树)相比,std::unordered_map
在平均情况下提供更快的访问速度,但不维持键的顺序。当需要快速的查找并不关心元素顺序时,std::unordered_map
是一个很好的选择。
#include <unordered_map>
std::unordered_map<int, std::string> myMap = {{1, "one"}, {2, "two"}, {3, "three"}};
5. std::unordered_multiset
和 std::unordered_multimap
这两个容器分别是 std::unordered_set
和 std::unordered_map
的多重版本,允许存储键相同的元素。和它们的单一键版本一样,基于哈希表实现,不维持元素顺序。
以及在容器中都加入了移动构造和移动赋值等移动语义的支持
可变参数模板
可变参数模板是C++11中引入的一种模板编程技术,它允许你定义接受任意数量类型参数的函数模板或类模板,这些参数数量未知且类型可以不同。使用可变参数模板,程序员可以编写更加灵活且可重用的函数和类,而无需为每种可能的参数数量和类型编写重载或特化。
语法
基本语法使用省略号...
,来表示模板的参数包(parameter pack),这个参数包可以是类型参数包(用于类模板或函数模板),也可以是函数参数包(用于函数模板)。
下面是可变参数模板的一个简单示例:
template<typename... Args>
void print(Args... args) {
// 展开参数包的方法之一
(std::cout << ... << args) << '\n';
}
print(1); // 输出 1
print(1, 2.5, "test"); // 输出 12.5test
类模板
可变参数模板也可以用于类模板的定义。如下所示:
template<typename... Elements>
class Tuple {};
Tuple<int, double, std::string> myTuple;
auto
auto
关键字在 C++11 及其后续版本中,是用于类型推导的。它使得编译器能够自动确定变量的类型。使用 auto
可以让代码更加简洁,尤其是在处理复杂类型或模板类型时,它还可以减少因类型错误引起的编程错误。auto
的使用不仅限于局部变量,还包括循环控制变量、函数返回类型、以及更多场景。
局部变量
在变量初始化时使用 auto
,编译器会自动推导出变量的类型。
auto x = 5; // x 被推导为 int 类型
auto d = 1.2; // d 被推导为 double 类型
范围基 for 循环
在 C++11 的范围基 for 循环中,auto
可以使得循环变量的类型自动匹配集合元素的类型。
std::vector<int> v = {1, 2, 3, 4, 5};
for(auto i : v) {
std::cout << i << std::endl; // i 的类型自动被推导为 int
}
函数返回类型
在 C++14 中,auto
可以用作函数的返回类型,让编译器推导返回类型。
auto add(int x, int y) {
return x + y; // 返回类型自动被推导为 int
}
泛型编程
在模板编程中,auto
可以大大简化代码,尤其是当操作对象的类型复杂或难以显式指定时。
std::map<std::string, std::vector<int>> map;
for(auto& pair : map) {
// pair 的类型自动被推导为 std::pair<const std::string, std::vector<int>>&
}
注意事项
auto
必须在有初始值的情况下使用,因为编译器需要通过初始值来推导类型。- 使用
auto
时,编译器仅通过变量的初始值来推导类型,不会考虑后续的类型转换或赋值。 - 对于复杂的表达式或模板编程,
auto
可以极大简化代码,但在一些情况下过度使用可能会降低代码的可读性。
范围for
范围for循环(也称为基于范围的for循环)是C++11中引入的一个功能,它提供了一种更简洁明了的方式来遍历容器或数组中的所有元素。范围for循环使得代码更加简洁易读,特别是在处理容器和数组时。
语法
范围for循环的基本语法如下:
for (declaration : expression)
{
// 循环体
}
这里的declaration
是用于遍历集合中每个元素的变量声明,expression
是要遍历的容器或数组。
使用示例
假设有一个整数数组,需要遍历数组中的每个元素并打印它:
int arr[] = {1, 2, 3, 4, 5};
for(int elem : arr) {
std::cout << elem << " ";
}
对于容器类(如std::vector
, std::list
, std::set
等),范围for循环也是同样适用的:
std::vector<int> v = {1, 2, 3, 4, 5};
for(auto elem : v) {
std::cout << elem << " ";
}
范围for循环的变种
范围for循环还支持通过引用来遍历元素,这在需要修改容器或数组中元素的值时特别有用:
for(auto& elem : v) {
elem *= 2; // 将每个元素的值翻倍
}
为了避免不必要的复制操作,对于大型对象或容器中的对象,即使不需要修改元素,也推荐使用引用来遍历:
for(const auto& elem : large_vector) {
// 进行一些不会修改elem的操作
}
使用范围for循环时,要注意它只能用于支持开始(begin()
)和结束(end()
)迭代器的类型,这包括了所有的STL容器和原生数组。对于自定义类型,如果想要支持范围for循环遍历,需要实现begin()
和end()
方法或相应的非成员函数版本。