【C++11】右值引用 && 移动语义 && 完美转发
Ⅰ. 左值引用和右值引用
传统的 C++
语法中就有引用的语法,而 C++11
中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。 无论左值引用还是右值引用,都是给对象取别名。
一、什么是左值❓❓什么是左值引用❓❓
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址或者可以对它赋值,左值可以出现在 =
的左边,右值不能出现在 =
表达式左边。定义时 const
修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
简单的说,能取地址的就是左值!(虽然 C++11
将 const
修饰的变量认为虽然不能修改值,但是它还是能修改地址的,所以 将常量视为左值)
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
二、什么是右值❓❓什么是右值引用❓❓
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
简单点说,不能取地址的就是右值,且右值引用使用的符号是 &&
。
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y; // 因为表达式返回的值是临时的,所以是没办法取地址的
fmin(x, y); // 函数调用也是没办法取地址的
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。
举个例子,不能取字面量 10
的地址,但是通过 rr1
右值引用后,可以对 rr1
取地址,也可以修改 rr1
。如果不想 rr1
被修改,可以用 const int&& rr1
去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; // const属性,修改会报错
return 0;
}
三、左右值总结
因此关于左值与右值的区分不是很好区分,一般认为:
- 普通类型的变量,因为有名字,可以取地址,都认为是左值。
-
const
修饰的常量,不可修改,是只读类型的,理论应该按照右值对待,但因为其可以取地址,C++11
认为其是左值。 - 如果表达式的运行结果是一个临时变量或者临时对象,认为是右值。
- 如果表达式运行结果或单个变量是一个引用则认为是左值。
总结:
- 不能简单地通过能否放在
=
左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断,比如 常量也是左值。 - 能得到引用的表达式一定能够作为引用,否则就用常引用。
- 能取地址的是左值,不能取地址的是右值
此外,C++11
对右值进行了严格的区分:
- 纯右值。比如:
a+b
、100
等等。 - 将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。(后面讲移动构造的时候会讲到)
Ⅱ. 左值引用与右值引用比较
左值引用总结:
- 非
const
左值引用只能引用左值,一般不能引用右值。 -
const
左值引用既可引用左值,也可引用右值。
int main()
{
// 非const左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
右值引用总结:
- 右值引用只能右值,一般不能引用左值。
- 使用
std::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++98
中的 const
类型引用左值和右值都可以引用,那为什么 C++11
还要复杂的提出右值引用呢?下面我们来看看左值引用的短板,以及右值引用是如何补齐这个短板的!
class string
{
public:
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
string tmp(s);
swap(tmp);
return *this;
}
string operator+(char ch)
{
string tmp(*this);
push_back(ch);
return tmp;
}
~string() { if (_str) delete[] _str;}
private:
char* _str;
size_t _size;
size_t _capacity;
};
int main()
{
string s1("hello");
string s2("world");
string s3(s1+s2);
return 0;
}
上述代码看起来没有什么问题,但是有一个不太尽人意的地方:
这是左值引用无法做到的一个短板,如果这里是重载 operator+=()
的话,那么返回的是 *this
,就可以使用左值引用进行返回。但这里重载的是 operator+()
,其返回的是一个临时对象,所以只能传值返回,传值返回会导致至少一次拷贝构造(如果是一些旧的编译器可能是两次拷贝构造),这大大的降低了程序的效率!
所以就有了下面的右值引用!但是我们先了解一下移动语义:
Ⅳ. 移动语义/移动构造
C++11
中提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,而不是拷贝,这可以有效缓解效率问题!其实就是类似我们之前实现 operator=()
中,我们使用 swap
函数进行交换指针等操作的思想,只不过还要结合右值引用罢了!
那么下面我们结合右值引用来解决这个问题:
要注意的是,我们不是在 operator+()
上面进行 swap
操作,也不是直接将其返回值改为 string&&
,因为即使改成了这样子的话,当这个函数的作用域结束的时候,这个返回的右值引用其实也是和左值引用一样,是不存在的,那么这个时候就错误了,所以我们要换一种思路:移动构造函数!
// string的移动构造
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
this->swap(s);
}
// string的拷贝构造
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
this->swap(tmp);
}
还记得上面我们介绍右值的时候,将右值分为两种:纯右值 和 将亡值 (忘记的翻上去看)
这里我们举的例子是关于 将亡值 的:
不仅仅有移动构造,还有 移动赋值:
代码语言:javascript代码运行次数:0运行复制// 移动赋值(在类内实现)
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
this->swap(s);
return *this;
}
int main()
{
liren::string ret1;
ret1 = liren::to_string(1234);
return 0;
}
// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义
这里运行后,我们看到调用了一次移动构造和一次移动赋值,这是因为如果是用一个已经存在的对象接收,编译器就没办法优化了(如果是在定义的时候就初始化,则原本需要两次的拷贝构造,因为编译器优化之后就只拷贝构造一次,这个下面讲编译器优化的时候会讲)。
liren::to_string
函数中会先构造生成一个临时对象,这里假设这个临时对象为 str
,但是我们可以看到,编译器很聪明的在这里把 str
识别成了右值,调用移动构造。然后再把这个临时对象作为 liren::to_string
函数调用的返回值赋值给 ret1
,这里调用的是移动赋值。
发布者:admin,转转请注明出处:http://www.yc00.com/web/1747995428a4716596.html
评论列表(0条)