文章目录
- 左值和右值的概念
- 左值
- 右值
- 左值与右值引用
- 移动语义的概念
- std::move 的作用
- 使用std::move的注意事项
- 右值引用的使用场景
- 右值引用的其他概念
- 万能引用
- 完美转发
- std::forward
- 万能引用和右值引用的区别
- 新的类功能
- 默认成员函数
左值和右值的概念
在C++中, 左值 和 右值 是两种不同的值类型,它们的主要区别在于是否能够引用内存地址。了解这些概念有助于理解变量的作用域、对象的生命周期,以及编译器在表达式求值中的行为。
左值
左值 指的是有持久地址的对象或表达式。换句话说,左值是可以放在赋值运算符左侧的变量或对象,它们可以存储到内存中,并且有明确的内存地址,可以通过引用来操作。
- 左值通常是变量名或返回变量引用的表达式。
- 可以对左值取地址(&),因此可以存储和访问它的内存地址。
// 常见的左值
int& func1()
{static int x = 0;return x;
}
普通变量(int x;)
数组元素(arr[0])
对象的成员(obj.member)
出了函数作用域不会销毁的返回值(func1())
右值
指的是在表达式中临时出现的值,它们没有持久的内存地址,也不能通过引用来操作。右值通常是常量、临时对象或表达式的返回值,并且无法对其取地址。
- 右值通常是常量、临时值、或表达式的计算结果。
- 右值不允许取地址,因为它们是短暂存在的(例如:临时变量或表达式结果)。
// 常见右值
int func2()
{static int x = 0;return x;
}
字面量(10, 'a', 3.14)
表达式的结果(x + y)
临时对象(std::string("hello"))
出了函数作用域不会销毁的返回值(func2())
左值与右值引用
在C++11中,引入了右值引用的概念,以实现更加高效的内存管理(如移动语义和完美转发)。右值引用使用 &&
来表示。
int x = 10;
int& lref = x; // 左值引用,绑定到左值 x
int&& rref = 10; // 右值引用,绑定到右值 10
右值引用常用于移动语义,避免不必要的对象拷贝操作,提高程序性能。
左值引用总结:
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
int main()
{// 左值引用只能引用左值,不能引用右值。int a = 10;int& ra1 = a; // ra为a的别名//int& ra2 = 10; // 编译失败,因为10是右值// const左值引用既可引用左值,也可引用右值。const int& ra3 = 10;const int& ra4 = a;return 0;
}
右值引用总结:
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以
move
以后的左值。
int main()
{// 右值引用只能右值,不能引用左值。int&& r1 = 10;// error C2440: “初始化”: 无法从“int”转换为“int &&”// message : 无法将左值绑定到右值引用int a = 10;int&& r2 = a;// 右值引用可以引用move以后的左值int&& r3 = std::move(a);return 0;
}
移动语义的概念
在C++中,构造和赋值操作通常有两种形式:
- 拷贝语义:通过拷贝构造函数或拷贝赋值运算符将对象的资源逐一复制。拷贝操作通常比较昂贵。
- 移动语义:通过移动构造函数或移动赋值运算符将对象的资源“转移”到另一个对象,而不是逐一复制资源,通常只涉及指针或资源句柄的转移。移动操作通常是轻量级的,因为它不需要真正的资源复制。
移动语义使用右值引用(T&&
)来实现,这使得可以安全地“窃取”另一个对象的资源。右值引用表示对临时对象(右值)的引用,可以安全地对其进行“移动”操作。
std::move 的作用
std::move
的作用是将一个对象显式地转换为右值引用,以便可以调用移动构造函数或移动赋值运算符。std::move
本质上并不会移动对象本身,而是告诉编译器可以安全地将其资源“移动”到目标对象中。
#include <iostream>
#include <vector>class MyClass {
public:MyClass(int size) : data(new int[size]), size(size) {std::cout << "Constructed\n";}// 移动构造函数MyClass(MyClass&& other) noexcept : data(other.data), size(other.size) {other.data = nullptr; // 确保原对象不会再管理这段资源other.size = 0;std::cout << "Move Constructed\n";}// 析构函数~MyClass() {delete[] data;std::cout << "Destroyed\n";}private:int* data;int size;
};int main() {MyClass a(10);MyClass b = std::move(a); // 使用std::move调用移动构造函数return 0;
}
在这个例子中,std::move(a)将a显式地转换为右值引用,从而调用了移动构造函数,而不是拷贝构造函数。
使用std::move的注意事项
-
确保不再使用被移动对象的资源:移动之后,被移动对象处于一种有效但未指定的状态。通常,它的资源已经被转移,因此不要再使用其资源,除非对其进行重新赋值。
-
移动和异常安全:实现移动构造函数和移动赋值运算符时,通常要加上noexcept,以确保在容器(如std::vector)发生重新分配时能够使用移动操作而不是拷贝操作。容器在移动时,如果移动操作不保证不会抛出异常,它们可能会退回到使用更昂贵的拷贝操作。
-
不能对左值使用
std::move
:如果你错误地对一个将来还会使用的左值对象调用std::move
,会导致程序逻辑错误或运行时错误。被移动对象的资源被转移后,它就不再具有原来的功能了。 -
与右值引用配合使用:
std::move
通常用于函数返回值或传递到函数参数时,特别是在需要避免拷贝而是移动资源的场合。通过右值引用参数来避免不必要的拷贝,从而提高效率。
右值引用的使用场景
在说右值引用之前,我先给大家看一下关于编译器的一些优化:需要用到之前模拟实现的string类
这里用了一个常量赋值给一个正在初始化的对象。编译器就直接优化成了一次普通构造。
那如果写成这样呢:
这里我画一个图帮大家理解一下:
所以平时如果我们需要定义一个对象,最好的方式就是在定义时候初始化了,不然后面再进行初始化代价是比较大的。
这里的优化是编译器替我们做了的。那么我再通过右值引用来进行一下优化。
此时,我们就算先定义对象,再初始化对象,花费的代价也比没有加右值引用版的拷贝赋值代价低了很多。
移动赋值的代价是极低的,因为这里是直接将一个将亡值(右值)的资源与我们需要赋值的对象的资源交换一下,并让将亡值带着赋值对象原本的资源删除。
右值引用的其他概念
右值引用还有一个概念:一个右值被右值引用了以后,该右值引用是具有左值属性。
设计这个特性的原因是:移动语义是将右值的资源窃取过来,如果右值引用没有左值属性的话,是无法交换的,因为是左值,所以它的资源才能被改变(交换)。
万能引用
在C++中,“万能引用”(是一个用来同时匹配左值引用和右值引用的引用类型。它的定义依赖于类型推导,是C++11引入的特性之一。
定义万能引用的条件:
- 万能引用通常使用
T&&
的形式,但它必须在类型推导的上下文中。 - 如果类型是通过模板参数推导出来的,并且形式为
T&&
,那么它就是万能引用。
template <typename T>
void func(T&& param) {// param 是万能引用
}
在这个例子中,T&&
是万能引用,因为T的类型是由传入的实参推导出来的。如果传入的是左值,T&&
会被推导为左值引用(T&
),如果传入的是右值,T&&
会被推导为右值引用(T&&
)。
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
template<typename T>
void PerfectForward(T&& t)
{Fun(t);
}int main()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}
完美转发
在C++中,完美转发是一种用于将函数参数保持其原有特性(左值或右值)传递给其他函数的技术。这在编写泛型代码时非常有用,因为它允许你转发参数而不会丢失其特性,提高了代码的灵活性和效率。
完美转发通常用于模板函数中,尤其是在实现泛型包装器或中间层函数时,可以将接收到的参数“完美”地转发给目标函数。
std::forward
std::forward
是 C++ 标准库中的一个模板函数,用于在特定条件下将参数转发,以保留其左值或右值特性。它通常与万能引用配合使用,以实现完美转发。
std::forward
的主要应用场景是在实现泛型函数时,尤其是当一个函数希望将其收到的参数转发给另一个函数,而不改变参数的左值或右值特性时。
以上面那个例子为例:
万能引用和右值引用的区别
右值引用,用于绑定右值的引用类型,允许对右值进行修改和资源的移动操作。右值引用的典型用途是实现移动语义,以提高程序效率。
右值引用的特点:
- 右值引用的声明是
T&&
,但只用于绑定右值。 - 右值引用通常用于捕获即将被销毁的对象,从而进行资源的“移动”操作而不是复制。
- 右值引用可以用来实现移动构造函数和移动赋值运算符,提高对象的移动效率。
class MyClass {
public:MyClass(int&& x) {// 右值引用构造函数}MyClass(MyClass&& other) {// 移动构造函数}
};
万能引用可以绑定左值和右值。这主要出现在模板类型推导的场景中。万能引用能够让函数对参数进行完美转发,因此广泛应用于泛型编程。
万能引用的特点:
- 万能引用同样使用
T&&
的形式,但它依赖于类型推导。 - 如果
T&&
出现在一个模板函数中,且T
是由调用时推导出来的(而不是显式指定的),那么这个T&&
就是万能引用。 - 万能引用可以同时绑定到左值和右值,这使得它在编写泛型函数时特别有用,可以根据传入参数的特性来决定如何处理引用。
如何判断是万能引用?
- 万能引用只出现在模板函数或者具有自动类型推导的函数中。
- 如果
T&&
的类型T
是由调用时推导而来的,那么T&&
就是万能引用。
template <typename T>
void func(T&& param) {// 这里的 T&& 是万能引用
}
新的类功能
默认成员函数
原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
-
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
-
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
-
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}Person(const Person& p):_name(p._name), _age(p._age){}Person(Person && p) = default;
private:hyt::string _name;int _age;
};
int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;
}
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private
,并且只声明补丁而已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上 = delete
即可,该语法指示编译器不生成对应函数的默认版本,称 = delete
修饰的函数为删除函数。
class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}Person(const Person& p) = delete;
private:hyt::string _name;int _age;
};
int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;
}