各位大佬好,我是落羽!一个坚持学习进步的学生。
如果您觉得我的文章还不错,欢迎多多互三分享交流,一起学习进步!
也欢迎关注我的blog主页: 落羽的落羽
文章目录
- 一、C++11简介
- 二、左值和右值是什么
- 三、左值引用与右值引用
- 四、移动构造和移动赋值
- 五、引用折叠与完美转发
一、C++11简介
C++的发展历史上,有许多版本,比如C++98、C++11、C++14,不断更新新的语法。其中,继C++98后,C++11是一个相当重要的版本,更新了许多全新的语法,如右值引用、lambda表达式、function、bind等等,需要我们学习。
今天我们首先来学习C++11的右值引用和移动语义。
二、左值和右值是什么
左值和右值是现代C++中十分重要的概念。
在最早的定义中,判断左值右值有一个非常直观的方法:
- 左值 (lvalue):可以出现在赋值语句等号 (=) 左侧的表达式。它代表一个在内存中有持久存储空间、有名字的、可以取地址的对象。
例如:变量、函数返回的引用、解引用的指针等。
int a = 10; // a是左值
const int b = 1; // b是左值
std::string s;
s = "hello"; // s是左值
s[1] // s[1]是左值
- 右值 (rvalue):只能出现在赋值语句等号 (=) 右侧的表达式。它代表一个临时的、短暂的、没有名字的、无法取地址的值。
例如:字面量、函数返回的非引用类型、临时对象、表达式计算结果等。
int b = 20; // 20是右值
a + b; // a + b 这个表达式的结果是一个临时值,是右值
std::string("xyz") // 临时对象(匿名对象),是右值
- 更深入的理解(C++11及以后)
随着C++11引入移动语义,原来的“左右值”划分变得不够精细,于是引入了更复杂的值类别 (Value Categories)。所有表达式都属于以下三种主类别之一:左值 (lvalue)、将亡值 (xvalue)、纯右值 (prvalue)。后两者统称为右值 (rvalue)。这个不用过于关注,我们主要理解左值与右值。
左值与右值的核心区别就在于能否取地址。
三、左值引用与右值引用
之前我们学习的引用,是左值引用。C++11引入了右值引用,用&&
符号声明。
Type& r1 = x; // 左值引用
Type&& r2 = y; // 右值引用
-
左值引用是给左值取别名,右值就是给右值取别名。
-
左值引用不能直接引用右值,但是const左值引用可以引用右值。
-
右值引用也不能直接引用左值,但是右值引用可以引用
move(左值)
。move是库里的一个函数模板,能将一个左值转换为右值,其内部本质是强制类型转换。
-
需要额外注意的是,变量表达式都是左值属性的,也就是说一个右值引用变量表达式虽然引用的是右值,但他也是左值。如
Type&& r2 = y;
,y是右值,而r2是左值。
在函数传参这方面,我们之前习惯写为const左值引用的形式,因为它既能引用左值,也能引用右值。而有了右值引用后,我们就可以分别重载左值引用、const左值引用、右值引用的参数,会各自匹配对应的重载:
四、移动构造和移动赋值
移动构造函数和移动赋值重载是类的新的默认成员函数。
移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的右值引用,如果还有其他参数,其他参数必须有缺省值。
移动赋值重载是一个赋值运算符的重载,和拷贝赋值构成重载,要求第一个参数是该类类型的右值引用。
移动构造和移动拷贝的价值在于深拷贝的类或包含深拷贝的成员的类,如string、vector。因为移动构造和移动拷贝的第一个参数都是右值引用,他的本质是“窃取”引用的右值对象的资源,而不是像拷贝构造和移动赋值那样去拷贝资源,从而提高了效率。
这个点在于,“右值”的生命周期通常只有当前一行,生命周期很短,那么它的资源就可以不是拷贝走,而是直接移动走,因为它马上就要被销毁了,与其需要降低效率拷贝而且资源还要浪费,不如直接把它的资源移动给被拷贝对象,这样大大提升了效率。
举个栗子,我们自己写一个string类:
namespace lydly
{class string{private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;} iterator end(){return _str + _size;} const_iterator begin() const{return _str;} const_iterator end() const{return _str + _size;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}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){cout << "string(const string& s) -- 拷贝构造" << endl;reserve(s._capacity);for (auto ch : s){push_back(ch);}}// 移动构造string(string&& s){cout << "string(string&& s) -- 移动构造" << endl;swap(s);}string& operator=(const string& s){cout << "string& operator=(const string& s) -- 拷贝赋值" <<endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s){push_back(ch);}} return* this;}// 移动赋值string & operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);} _str[_size] = ch;++_size;_str[_size] = '\0';}size_t size() const{return _size;}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];if (_str){strcpy(tmp, _str);delete[] _str;} _str = tmp;_capacity = n;}}};
}
可以看出,拷贝构造中还需逐字符拷贝,但移动构造中直接swap就好,直接移动走右值参数的资源。
移动赋值同理,就不演示了。
移动构造和移动赋值的一个用处是函数返回深拷贝类型时,极大减少开销。举个例子:vector<int> v = createHugeVector();
(假设 createHugeVector 返回一个巨大的临时 vector),函数返回时需要创建一个临时对象,临时对象再构造出v。如果没有移动构造和移动赋值,这一过程需要有一次拷贝构造和拷贝赋值,就会导致巨大的内存开销;而C++11后STL容器有了移动构造和移动赋值时,构造临时对象和临时对象赋值就会直接“转移”资源而非拷贝,极大提高了效率。
C++11以后的容器,增加了移动构造和移动赋值,push和insert系列的接口也都增加了传右值的版本,当实参是右值时,容器内部调用移动构造进行拷贝,讲对象资源移动到容器空间的对象上,以vector为例:
五、引用折叠与完美转发
C++中不能直接定义引用的引用, 如int& && r = i;
会报错,但可以通过模板或tpyedef中的类型操作构成引用的引用。当构成引用的引用时,C++11有一个引用折叠的规则:右值引用的右值引用折叠成右值引用,其他所有组合均折叠成左值引用:
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n; //r1的类型是int&
lref&& r2 = n; //r2的类型是int&
rref& r3 = n; //r3的类型是int&
rref&& r4 = n; //r4的类型是int&&
实例化模板时,如果折叠后参数类型是左值引用,传右值就会报错;折叠后参数是右值引用,传左值就会报错。
值得注意的是,形如以下的模板:
template<class T>
void Function(T&& t)
{
//...
}
传参为左值时,T推导为Type&,折叠后参数为Type&;传参为右值时,T推导为Tpye,不折叠,参数为Type&&。可以发现,模板参数为右值引用的模板,实例化后参数类型传左值就是左值引用,传右值就是右值引用,称之为万能引用。
但是我们之前讲到,变量表达式都是左值属性的,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中不管t是左值引用还是右值引用的类型,t的属性都是左值,那么如果出现以下情形:
void Fun(int& x)
{cout << "左值" << endl;
}void Fun(int&& x)
{cout << "右值" << endl;
}template<class T>
void Function(T&& t)
{Fun(t);
}int main()
{Function(1);int a = 1;Function(a);return 0;
}
t的属性总是左值,因此只会调用到左值引用版本的Fun。
我们想要保持t对象的属性,就需要使用完美转发:
完美转发forward本质是⼀个函数模板,他其实还是通过引用折叠的方式实现的。左值传给它时,返回左值;右值传给它时,还是返回右值。
使用完美转发必须显式传模板参数。所以,上述例子应该写成:
void Fun(int& x)
{cout << "左值" << endl;
}void Fun(int&& x)
{cout << "右值" << endl;
}template<class T>
void Function(T&& t)
{Fun(forward<T>(t));
}
本篇完,感谢阅读。