左值是什么
左值是一个表示数据的表达式,比如变量名和可以解引用的指针
左值可以出现在赋值符号的两边
左值可以被取地址,也可以被修改(const修饰的左值除外)
右值是什么
右值也是一个表达数据的表达式,如常量,表达式的返回值,函数的返回值等等
int x = 1, y = 2;
// 以下几个表达式都是常见的右值
10;
x + y;
min(x, y);
x + y
,min(x, y)
的返回值都是临时变量,这种无法被更改的值我们称之为右值对于左值引用返回的函数来说,返回值是左值。比如unordered_map
的[]
运算符重载其返回的就是kv
中的value的引用,我们可以对其进行赋值
C++11中新增了右值引用的语法特性,但是不论是左值引用还是右值引用,本质都是给对象去别名
左值引用
左值引用就是给左值去别名,通过&来声明
int a = 10; int& ra = a;
int* p = new int(10); int*& rp = p;
const int c = 2; const int& rc = c;
右值引用
右值引用解释给右值取别名,通常使用&&来声明
int x = 1, y = 2;
int&& rr1 = 10;
int&& rr2 = x + y;
需要注意:右值是不可以取地址的,但是给右值取别名后,会导致右值被存储到特定位置,这个时候到右值可以被取到地址,并且可以被修改,如果不想让被引用的右值被修改,可以使用const修饰右值引用
const 左值引用 and const 右值引用
void test5() {
const int&& ra = 10;
int *p = const_cast<int*>(&ra);
*p = 2;
cout << "&ra = " << &ra << endl;
cout << "p = " << p << endl;
cout << "ra = " << ra << endl;
cout << "*p = " << *p << endl;
cout << endl;
const int x = 10;
const int& b = x;
int *pb = const_cast<int*>(&b);
*pb = 5;
cout << "&b = " << &b << endl;
cout << "pb = " << pb << endl;
cout << "b = " << b << endl;
cout << "*pb = " << *pb << endl;
}
&ra = 0x30445b464
p = 0x30445b464
ra = 2
*p = 2
&b = 0x30445b454
pb = 0x30445b454
b = 5
*pb = 5
可以看到左值引用或是右值引用它们都是一样的,他们都在内存上开辟了空间并将数据存到了空间中,当我们访问空间时就会将空间中的值返回给我们。
注意:左值引用的const 变量是不会被写入常量表的,也不会进行宏替换。其会保持内存可见性访问该变量时会到内存中获取该变量的值
Const 左值引用右值
左值引用不可以直接引用右值,因为这涉及到权限的放大,右值不可被修改但是左值引用可以修改。但是const修饰的左值引用可以引用右值,因为const左值引用能搞保证被引用的数据不被修改
void test6() {
const int& c = 10;
int* pc = const_cast<int*>(&c);
*pc = 5;
cout << "c = " << c << endl; // 5
cout << "*pc = " << *pc << endl; // 5
}
可以看到不管是左值引用还是右值引用亦或是const左值引用右值都不会被写入常量表
右值引用move左值
右值引用只能引用右值,不能引用左值。但是右值引用可以引用move以后的左值。move函数是C++标准提供的函数,被move后的左值能够赋值给右值引用
void test7() {
int a = 10;
int&& b = std::move(a);
b = 3;
cout << a << endl; // 输出3
}
虽然const左值引用既能够接收左值,也可以接收右值,但是左值引用终究存在短板,而C++11提出的右值引用就是来解决左值引用的短板的
准备工作
这里我们使用了前面模拟实现STL容器中的string类。类中实现了一些基本函数
//
// Created by 陈李鑫 on 2023/7/16.
//
#ifndef SIMULATION_REALIZATION_STL_CLX_STRING_HPP
#define SIMULATION_REALIZATION_STL_CLX_STRING_HPP
#endif //SIMULATION_REALIZATION_STL_CLX_STRING_HPP
#include
#include
#include
#include
class clx_string{
public:
typedef char* iterator;
iterator begin() { return _str;}
iterator end() { return _str + _size; }
const char* c_str() const { return const_cast<const char*>(_str); };
void swap(clx_string& s);
clx_string(const char* str = "");
clx_string(const clx_string& s);
~clx_string();
clx_string& operator=(const clx_string& s);
char& operator[](size_t i);
void reserve(size_t n);
void push_back(char ch);
clx_string& operator+=(char ch);
private:
char* _str;
size_t _size;
size_t _capacity;
};
void clx_string::swap(clx_string& s) {
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
std::swap(_str, s._str);
}
clx_string::clx_string(const char* str) {
std::cout << "clx_string(const char* str) -- 直接构造" << std::endl;
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷贝构造函数 以前的写法
// clx_string::clx_string(const clx_string& s) {
// _size = strlen(s.c_str());
// _capacity = _size;
// _str = new char[_capacity + 1];
// strcpy(_str, s.c_str());
// }
// 拷贝构造函数 现代写法
clx_string::clx_string(const clx_string& s)
: _str(nullptr), _size(0), _capacity(0)
{
std::cout << "clx_string(const clx_string& s) -- 拷贝构造" << std::endl;
clx_string tmp(s.c_str());
swap(tmp);
std::cout << std::endl;
std::cout << std::endl;
}
clx_string::~clx_string() {
_size = 0;
_capacity = 0;
delete[] _str;
_str = nullptr;
}
clx_string& clx_string:: operator=(const clx_string& s) {
std::cout << "clx_string& clx_string:: operator=(const clx_string& s) -- 赋值函数重载" << std::endl;
clx_string tmp(s.c_str());
clx_string::swap(tmp);
std::cout << std::endl;
std::cout << std::endl;
return *this;
}
char& clx_string::operator[](size_t i) {
assert(0 <= i && i < _size);
return _str[i];
}
void clx_string::reserve(size_t n) {
if (n > _capacity) {
char* tmp = new char[n + 1];
strncpy(tmp, _str, _size + 1);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void clx_string::push_back(char ch) {
while (_size >= _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
_str[_size + 1] = 0;
_size++;
}
clx_string& clx_string::operator+=(char ch) {
push_back(ch);
return *this;
}
这里主要关注两个函数,一是拷贝构造函数,二是赋值运算符重载,它们内部都包含了一个构造函数
clx_string tmp(s.c_str());
// 拷贝构造函数 现代写法
clx_string::clx_string(const clx_string& s)
: _str(nullptr), _size(0), _capacity(0)
{
std::cout << "clx_string(const clx_string& s) -- 拷贝构造" << std::endl;
clx_string tmp(s.c_str());
swap(tmp);
std::cout << std::endl;
std::cout << std::endl;
}
clx_string& clx_string:: operator=(const clx_string& s) {
std::cout << "clx_string& clx_string:: operator=(const clx_string& s) -- 赋值函数重载" << std::endl;
clx_string tmp(s.c_str());
clx_string::swap(tmp);
std::cout << std::endl;
std::cout << std::endl;
return *this;
}
我们可以写一个简单的案例测试一下每个函数的调用打印是否清晰
void clx_string_test1() {
clx_string s1;
cout << endl;
clx_string s2(s1);
clx_string s3;
s3 = s1;
}
clx_string(const char* str) -- 直接构造 // s1 的直接构建
clx_string(const clx_string& s) -- 拷贝构造 // s2 的拷贝构建
clx_string(const char* str) -- 直接构造
clx_string(const char* str) -- 直接构造 // s3 的直接构建后调用赋值函数重载
clx_string& clx_string:: operator=(const clx_string& s) -- 赋值函数重载
clx_string(const char* str) -- 直接构造
这样我们的准备工作就算完成了,接下来就要进入右值引用的价值的正是讲解
左值引用的使用场景
在介绍左值引用的短板之前,我们先强调一下左值引用的价值
clx_string func1(clx_string s) { return s; }
clx_string& func2(clx_string& s) { return s; };
void clx_string_test2() {
clx_string s1;
std::cout << std::endl;
std::cout << "func1 begin" << std::endl;
func1(s1);
std::cout << "func1 end" << std::endl;
std::cout << std::endl;
std::cout << "func1 begin" << std::endl;
func2(s1);
std::cout << "func1 end" << std::endl;
}
clx_string(const char* str) -- 直接构造
func1 begin
clx_string(const clx_string& s) -- 拷贝构造
clx_string(const char* str) -- 直接构造
clx_string(const clx_string& s) -- 拷贝构造
clx_string(const char* str) -- 直接构造
func1 end
func1 begin
func1 end
可以看到使用引用传参和引用返回减少了两次的拷贝构造,我们知道stirng这种类型进行的拷贝都是深拷贝,如果string很大那么深拷贝的代价是非常高的,使用左值传参和做返回值起到的作用还是非常明显的
左值引用的短板
左值引用虽然在某些情况下可以避免不必要的拷贝操作,但是并不能完全避免。
clx_string clx_string::to_string(int value) {
clx_string res;
bool flag = false;
if (value < 0) {
flag = true;
value = -1 * value;
}
while (value > 0) {
char ch = static_cast<char>(value % 10);
res += ch + '0';
value /= 10;
}
if(flag) res += '-';
std::reverse(res.begin(), res.end());
return res;
}
void clx_string_test3() {
clx_string s;
s = clx_string::to_string(1);
}
clx_string(const char* str) -- 直接构造 // 直接构造s
clx_string(const char* str) -- 直接构造 // to_string 内部构建 res
clx_string& clx_string:: operator=(const clx_string& s) -- 赋值函数重载 // s接收返回值
clx_string(const char* str) -- 直接构造
比如to_string
函数就不可能使用左值返回,因为该函数生成的字符串就在函数域中,出了函数域就销毁了,因此该函数只能返回一个局部变量所以必须传值返回
当传值返回后我们需要调用变量来接收to_string
函数返回的局部变量这里又要调一次拷贝函数,那么to_stirng函数在函数内生成字符串(第一次),传值返回(第二次),将其的数据传递给接收变量(第三次)可以看到同一份数据生成了三次,也就是说在这之间白白进行了两次深拷贝
注意:近代编译器对上述情况进行了优化,使得传值返回的变量可以直接给父域的对象拷贝构造,可以减少一次拷贝
C++11提出右值引用就是为了解决左值引用这个短板的,但解决方式并不是简单的将右值引用作为函数的返回值
右值引用和移动语义解决上述问题的方法就是,给当前模拟实现的string类增加移动构造和移动赋值的方法
移动构造
移动构造是一个构造函数,该构造函数的参数就是右值类型的,移动构造的本质就是将传入的右值资源窃取过来,占为己有,这样就避免了深拷贝
在当前string类中新增一个移动构造函数,该函数要做的就是调用swap函数将传入的右值的资源窃取过来
clx_string::clx_string(clx_string&& s)
:_size(0), _capacity(0), _str(nullptr)
{
std::cout << "clx_string::clx_string(clx_string&& s) -- 移动构造" << std::endl;
swap(s);
}
clx_string& clx_string::operator=(clx_string&& s) {
std::cout << "clx_string& clx_string::operator=(clx_string&& s) -- 移动赋值重载" << std::endl;
swap(s);
return *this;
}
移动构造和拷贝构造的区别:
拷贝构造采用的一直是const 左值引用接收参数,因此无论拷贝构造对象传入的是左值还是右值,都会调用拷贝构造函数
增加移动构造之后,如果传入的参数是右值,那么就会匹配到移动构造函数(最匹配原则)
string的拷贝构造函数做的就是深拷贝,而移动构造函数只需要调用swap函数进行资源转移,移动构造的代价比拷贝构造小很多
void clx_string_test3() {
clx_string s1;
s1 = clx_string::to_string(1);
}
clx_string(const char* str) -- 直接构造 // to_string 创建res
clx_string(const char* str) -- 直接构造 // 创建 s1
clx_string& clx_string::operator=(clx_string&& s) -- 移动赋值重载 // 返回值移动赋值给s1
可以看到又了移动构造和移动拷贝,就不会调用原来的拷贝创建以及拷贝赋值了,使用了移动构造替换拷贝构造,移动赋值替换拷贝赋值,提高了效率
注意⚠️:虽然to_string中的局部string 对象是一个左值,但由于该string对象在当前函数调用结束后就会被立即销毁,我们可以把这种被消耗的值叫做将亡值,比如匿名对象也可以叫做将亡值。即然将亡值都要被销毁了,还不如把自己的资源转移给别人,因此编译器会讲这种将亡值识别为右值,这样就可以匹配到参数类别为右值的移动构造函数
STL中的容器
C++11标准出来后,STL容器都增加了移动构造和移动赋值,以我们刚刚说的string为例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gF0dvrSO-1689499058827)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230716141556485.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rByFUe8c-1689499058828)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230716141712003.png)]
右值引用还在各种容器的插入中使用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2NFd0hPw-1689499058828)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230716144526460.png)]
C++11后很多STL容器的push_back类似接口都提供了右值引用版本,如果传入的参数是右值可以直接进行资源转移,避免了深拷贝,提高了效率
右值引用虽然不能直接引用左值,但是可以通过move函数将左值转化成右值。move函数的名称非常具有迷惑性,move函数其实并不能搬移任何东西,该函数的唯一功能就是将一个左值强转成右值引用,然后实现移动语义
// 声明
template <class T>
typename remove_reference<T>::type&& move (T&& arg) noexcept;
//实现
template <class T>
typename remove_reference<T>::type&& move (T&& arg) noexcept {
return ((typename remove_reference<T>::type&&)_Arg);
}
move函数中arg参数类型并非右值引用,而是万能引用。万能引用跟右值引用的形式相同,但是右值引用必须得钥匙确定类型的。一个左值被move后,它的资源有可能已经被转移给别人了,因此要慎用一个被move后的左值
void clx_string_test4() {
clx_string s1("hello world");
clx_string s2;
s2 = std::move(s1);
cout << "s1 : " << s1.c_str() << endl;
cout << "s2 : " << s2.c_str() << endl;
}
int main() {
clx_string_test4();
return 0;
}
clx_string(const char* str) -- 直接构造
clx_string(const char* str) -- 直接构造
clx_string& clx_string::operator=(clx_string&& s) -- 移动赋值重载
s1 :
s2 : hello world
可以看到s1本来是一个左值,我们将其强转成右值赋值给了s2,那么s1内部的资源就已经被转走了,所以一个左值被move后资源可能被转移其是很危险的,慎用
模版中的&&不能代表右值引用,而是万能引用,其既能接受左值也能接受右值。万能引用和右值引用的区别就是,右值引用必须要确定类型,而万能引用是根据传入实参的类型进行推导
void Func(int& x) {
cout << "左值引用" << endl;
}
void Func(const int& x) {
cout << "const 修饰的左值引用" << endl;
}
void Func(int&& x) {
cout << "右值引用" << endl;
}
void Func(const int&& x) {
cout << "const 修饰的右值引用" << endl;
}
template<class T>
void PerfectForward(T&& t) {
Func(t);
};
void clx_string_test5() {
int a = 10;
PerfectForward(a); // 左值
PerfectForward(move(a)); // 右值
const int b = 10;
PerfectForward(b); // const修饰的左值
PerfectForward(std::move(b)); // const修饰的右值
}
左值引用
左值引用
const 修饰的左值引用
const 修饰的左值引用
由于PerfectForward函数的参数类型是万能引用,因此既可以接受左值也可以接受右值,我们在PerfectForward中调用Func函数,希望我们传什么类型的值就能给我们匹配什么类型的函数
如果想要在参数的传递过程中保持其原有的属性,就需要在传参的时候调用forward函数
template<class T>
void PerfectForward(T&& t) {
Func(std::forward<T>(t));
};
经过完美转发后,调用PerfectForward函数传入的右值就会被保持右值属性,就会匹配到右值版本,这就是完美转发的价值