目录
左值和右值
前言:C++11引入右值引用的主要原因是为了支持移动拷贝,这是一种允许资源从一个对象转移到另一个对象的机制,而不是进行复制
左值:是一个表达数据的表达式,比如变量和解引用的指针等
int& func1()
{
static int x = 1;
return x;
}
//p、b、c、*p、func1函数的返回值 都是左值
int* p = new int(0);
int b = 1;
const int c = 2;//只能取c的地址但是不能对c赋值
-
左值的判断标准:可以取地址、赋值、可以出现在=的左右两侧
-
注意事项:被const修饰的左值不能被修改,但仍可以取地址
左值引用: 即给左值起的别名(&)
int* & rp = p;//rp是左值p的引用
int& rb = b;//rb是左值b的引用
const int& rc = c;//rc是左值c的引用
int& pvalue = *p;//pvalue是左值*p的引用
右值:也是一个表达数据的表达式,比如字面常量、表达式的返回值、函数的返回值(返回值不能是左值的引用)等,即临时生成的或即将被销毁的对象
int func2()
{
static int x = 3;
return x;
}
//10、x+y、func2函数的返回值 都是右值
10;
x + y;
func2();
-
右值的判断标准:不能取地址、不能赋值、且只能出现在=的右侧
右值引用:即给右值起的别名(&&)
int& rr1 = 10;//rr1是10的右值引用
double&& rr2 = x + y;//rr2是x+y的右值引用
double&& rr3 = func1();//rr3是func1函数的右值引用
注意事项:
1、左值引用不能给右值取别名,但是const左值引用可以(因此在没有右值引用前,向某个有const左值引用为参数的函数传递左值和右值均可);右值引用不能给左值起别名,但是可以给move后的左值起别名
2、C++11将右值的概念又进行了细分,便于理解:
- 纯右值(内置类型的右值)如:10 a+b
- 将亡值(自定义类型的右值)如:匿名对象、传值返回函数
move
基本概念:move 是 C++ 标准库中的一个函数模板,它将一个对象的左值引用转换为右值引用(本质上是一个静态类型转换,它告诉编译器将一个左值当作右值来处理)通常用于准备对象进行移动构造或移动赋值,而不是进行实际的移动操作本身,move 的主要用途是:
- 触发移动构造函数或移动赋值操作符,从而避免对象的复制构造,特别是在涉及到大量资源(如动态内存、文件句柄等)时
- 与标准库中的容器和算法一起使用,以优化临时对象的处理
注意事项:
1、int i = 2; move(i)后,i仍然是左值,move(i)是右值
2、不要轻易对左值move,除非你做好了自己的左值被移走的准备
左值引用的使用场景及其缺陷
1、左值引用做函数参数时解决了传参时的拷贝消耗问题(不论是深拷贝还是浅拷贝,还是传值)
//深拷贝
void func1(string s)//string的拷贝构造函数是深拷贝
{}
//浅拷贝()
void func2(bit::string s)//bit::string中的拷贝构造函数是浅拷贝
{}
//传值
void func3(int x)//x是x1的副本,不涉及深浅拷贝
{}
//不拷贝
void func4(const std::string& s)
{}
int main()
{
string s1("hello world");
bit::string s2("hello world");
int x1 = 10;
// 左值引用做函数参数减少了拷贝,提高效率
func1(s1);
func2(s2);
func3(x1);
func4(s2);
return 0;
}
2、左值引用解决了非局部对象做返回值(出函数作用域不会销毁)的拷贝消耗问题
#include <iostream>
int globalVar = 42;
int& getGlobalVar() {
return globalVar; // 返回全局变量的引用
}
int main() {
int& ref = getGlobalVar();
std::cout << ref << std::endl; // 输出 42
return 0;
}
但当局部对象做返回值时不能使用传引用返回(出了作用域就被销毁的对象)因为会出现的“悬空指针”问题(依然使用引用访问已经被销毁的对象的原空间)
#include <iostream>
int& getLocalVar()
{
int localVar = 42;
return localVar; // 返回局部变量的引用(错误)
}
int main() {
int& ref = getLocalVar();
std::cout << ref << std::endl; // 未定义行为,虽然可能打印出正确结果,但该位置上的空间在localVar销毁后就可以被覆盖了,此时能打印可能是因为该空间还未被覆盖
return 0;
}
两次拷贝构造及优化
因此,对于局部对象做返回值时,无论是内置类型还是自定义类型,我们采用的都是传值返回 + 两次拷贝构造 的方式(内置类型这里就不再叙述了,它们如何返回官方已经规定好了,并且它们的内存大小一般不大即使是两次拷贝构造也占用不了多少内存空间,主要还是自定义类型)(str是左值)
// 拷贝构造 -- 传左值/右值均可
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
_str = new char[s._capacity+1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
补充:不优化时若返回值占用空间小则该返回值是存放在寄存器中的,若返回值占用空间大则是会放在一片新建的位于调用函数与主函数间的栈帧上(压栈),编译器采取优化是因为若要返回的对象过大时,经历两次拷贝构造会消耗大量内存资源,所以在编译器知道要返回的对象一定会经历两次拷贝构造时,就会在调用函数结束前将返回对象直接拷贝给接收者(大多数编译器都会进行这一优化)
- 如果ret是一个已经存在的对象,再让它接收Solution函数的返回值时就不会触发上述编译器的优化,因为当=左侧的对象已经存在时,叫做赋值,当=左侧的对象不存在时,叫拷贝构造
- 但即使是优化后,还是要进行一次拷贝构造,能不能再优化一下?
右值引用的使用场景
传值返回
移动构造
基本概念:有了右值引用,我们就可以对局部对象做返回值要经历的拷贝构造再次优化,此时我们可以重载一个接收将亡值的移动构造函数,当返回值是将亡值时会经过该函数,利用std::swap直接交换资源即可
特点:不会经历拷贝构造,直接进行资源交换
// 移动构造 -- 仅支持右值
string(string&& s)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);//swap(this,s)
}
此时原来的传值返回 + 两次拷贝构造变为了,传值返回 + 拷贝构造 + 移动构造,因为str在销毁前是一个左值,会经过拷贝构造函数形成临时对象,新形成的临时对象是一个将亡值,该将亡值会进入移动构造函数与ret进行资源交换,而不是再进行一次拷贝;同时编译器也会对这一过程进行优化,即先隐式的将返回的左值str强转为右值,然后直接调用移动构造,这样就可以进一步省去中间的临时对象(通过两次优化的前后对比可以发现,有了右值引用后原本即使是优化后str还是要进行一次拷贝构造给ret,但是有了右值引用后这一次的拷贝构造也可以被优化掉)
注意事项:涉及深拷贝(有new的)的类需要移动构造、涉及浅拷贝(没有new的)的类不需要移动构造(深拷贝:拷贝后两者各自独享资源,浅拷贝:拷贝后两者共享一个资源)
// 浅拷贝的拷贝构造函数
string(const string& s)
: _str(s._str), _size(s._size), _capacity(s._capacity)
{
cout << "string(const string& s) -- 浅拷贝" << endl;
}
// 深拷贝的拷贝构造函数
string(const string& s)
: _str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
_str = new char[s._capacity + 1]; // 分配新的内存
strcpy(_str, s._str); // 复制内容
_size = s._size;
_capacity = s._capacity;
}
移动赋值
基本概念:上面我们处理的是创建一个新的对象,但如果对象已经存在,我们对该对象使用的=就是赋值操作了,没有右值引用前,str会先调用拷贝构造生成一个将亡值,然后该将亡值会调用赋值重载函数,在赋值重载函数中又会调用拷贝构造将该将亡值生成一个tmp对象,然后将tmp与ret1的内容交换后返回
上述过程需要经历两次深拷贝,很明显这并不是我们想要的结果,引入移动构造函数可以帮助我们解决第一次深拷贝的问题(str被隐式move后调用移动构造生成一个匿名对象,然后将该匿名对象即将亡值传递给赋值重载函数),但在赋值重载函数中涉及的深拷贝怎么办?换个思路,此时我们需要考虑的问题是ret1 = ?的问题(即图中的方括号->ret1的红线这一过程),如果?是左值(非将亡值)的话,一定会进入上面的赋值重载函数进行深拷贝(保证两个资源的独立性)如果?是右值(将亡值)的话,参考之前的例子,我们可以基于右值引用重写一个新的赋值重载函数,即移动赋值函数
此时str生成的匿名对象(将亡值)就会进入移动赋值函数中,交换ret1和将亡值的内容,此外原本ret1中的内容会变为将亡值,会随着将亡值得销毁而销毁,不需要再手动销毁了
总结:左值引用没有解决的问题(局部对象做返回值时不能使用传引用返回,需要传值返回 + 两次拷贝构造)右值引用解决了(移动构造 + 移动赋值使得传值返回时可以不用经历拷贝,只需要交换资源即可),我们把移动构造和移动赋值叫做右值引用的移动语义
C++11以后,所有容器都增加了移动构造和移动赋值
补充
1、右值被右值引用后,它的属性是左值,因为右值引用的底层还是指针,int&& r = 10的汇编代码的解释是这样的:
- 分配内存:将字面量
10
存储在栈上。 - 获取地址:将存储
10
的内存地址加载到eax
寄存器中。 - 存储地址:将
eax
中的地址赋值给右值引用r
2、这样设计是为了能够顺利的将右值的资源进行转移,否则如果s仍是右值,则它不能被改变
完美转发与forward
基本概念:模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值,但是模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,后续使用中其中的右值会退化成左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要使用forward
#include <iostream>
using namespace std;
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);//右值,预计调用void Fun(int&& x) { cout << "右值引用" << endl; }
int a;
PerfectForward(a); //左值
PerfectForward(move(a)); // 右值,预计调用void Fun(int&& x) { cout << "右值引用" << endl; }
const int b = 8;
PerfectForward(b);//左值
PerfectForward(move(b)); // const 右值,预计调用void Fun(const int&& x) { cout << "const 右值引用" << endl; }
return 0;
}
可以发现无论是左值还是右值在经过函数模板后,右值都变为了左值,即属性退化,为了能向代码注释中那样成功的调用右值引用的函数,我们需要使用下面的形式
Func( forward<T>(t) );//forward<T>(t)在传参的过程中保持了t的原生类型属性。
forward的作用:原来是左值,就仍是左值,原来是右值,就将其变为左值
- 如果向模板中传入的是右值,则t是该右值的右值引用,属性是左值,因此无法调动接收右值引用的函数Fun函数
新的类默认成员函数
基本概念:C++11 新增了两个默认成员函数 移动构造函数 和 移动赋值运算符重载
默认移动构造函数的产生条件:
- 没有自定义的移动构造函数
- 没有自定义的析构函数 或 拷贝构造函数 或赋值重载函数
默认移动构造函数的功能:对内置类型会逐成员按字节拷贝,对于自定义类型,如果该成员实现了移动构造,就调用它的移动构造,如果没有就去调用它的拷贝构造
默认移动赋值函数的产生条件:
- 没有自定义的移动赋值函数
- 没有自定义的析构函数 或 拷贝构造函数 或赋值重载函数
默认移动构造函数的功能:对内置类型会逐成员按字节拷贝,对于自定义类型,如果该成员实现了移动赋值,就调用它的移动赋值,如果没有就去调用它的拷贝赋值
强制生成默认函数的关键字default
基本概念:用于显式地要求编译器生成默认版本的特殊成员函数(如构造函数、析构函数、拷贝构造函数、移动构造函数、拷贝赋值运算符和移动赋值运算符)
禁止生成默认函数的关键字delete
基本概念:用于禁止编译器生成某个特殊成员函数或明确删除某个函数
不希望某一个类被拷贝的解决办法
C++98:只声明不实现,定义到私有
class A
{
private:
A(const A& aa);
}
- 因为已经不希望A类被拷贝了,所以就没有必要再去实现它的定义了 ,同时定义为私有也防止在类中使用它
C++11:delete关键字
class A
{
A(const A& aa) = delete;//不论public还是其他的位置都可以,A类的拷贝共组无法生成
}
~over~