左值是一个表示数据的表达式,程序可以获取其地址。左值可以出现在赋值语句的左边,也可以出现在赋值语句的右边。左值引用就是对左值的引用,例如:
int a = 20; // a 左值
const int b = 10; // b 左值
int c = b; // c 左值
int &r = a; // r 左值引用
左值一般有下面这些,如下:
右值即可出现在赋值表达式右边,但不能获取其地址。右值包括字面常量(C风格字符串除外,它表示的是地址)、x + y表达式、以及返回值的函数(条件是该函数返回的不是引用)。C++11新增了右值引用,这是使用&&表示的,右值引用就是对右值的引用,例如:
int getValue(){
return 50;
}
int main(){
int x = 10;
int y = 20;
int &&r1 = 30; //字面常量是右值
int &&r2 = x + y; //表达式是右值
int &&r3 = getValue(); //函数返回的是int类型的值
return 0;
}
右值一般有下面这些,如下:
简单来说,左值是指可以取地址的、具有持久性的对象,而右值是指不能取地址的、临时生成的对象。
注意:右值引用本身是左值,右值引用本身有名字且可以取地址
将亡值是指C++11新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&函数的返回值、std::move函数的返回值、转换为T&&类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。例如:
class A {
xxx;
};
A a;
auto c = std::move(a); // c是将亡值
auto d = static_cast<A&&>(a); // d是将亡值
讲移动语义之前一定要先搞清楚浅拷贝与深拷贝。移动语义,可以理解为转移所有权,深拷贝是对于别人的资源,自己重新分配一块内存存储复制过来的资源,而对于移动语义,就是转移资源的所有权,通过C++11新增的移动语义可以省去很多拷贝负担。下面实现一个自定义字符串类MyString,MyString内部管理一个C语言的char *数组,这个时候一般都需要实现拷贝构造函数和拷贝赋值函数,实现深拷贝,例如:
#include
#include
#include
using namespace std;
class MyString
{
public:
static size_t CCtor; //统计调用拷贝构造函数的次数
public:
// 构造函数
MyString(const char* cstr=0){
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
}
// 拷贝构造函数
MyString(const MyString& str) {
CCtor ++;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
// 赋值运算符
MyString& operator=(const MyString& str){
if (this == &str)
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
~MyString() {
delete[] m_data;
}
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
size_t MyString::CCtor = 0;
int main()
{
vector<MyString> vecStr;
vecStr.reserve(1000);
for(int i = 0; i < 1000; i++){
vecStr.push_back(MyString("hello"));
}
cout << MyString::CCtor << endl;
}
输出结果:
1000
Process returned 0 (0x0) execution time : 0.052 s
Press any key to continue.
代码看起来挺不错,却发现执行了1000次拷贝构造函数,如果MyString(“hello”)构造出来的字符串本来就很长,构造一遍就很耗时了,最后却还要拷贝一遍,而MyString(“hello”)只是临时对象,拷贝完就没什么用了,这就造成了没有意义的资源申请和释放操作,如果能够直接使用临时对象已经申请的资源,既能节省资源,又能节省资源申请和释放的时间,而C++11新增加的移动语义就能够做到这一点。要实现移动语义就必须增加两个函数:移动构造函数和移动赋值运算符。
#include
#include
#include
using namespace std;
class MyString
{
public:
static size_t CCtor; //统计调用拷贝构造函数的次数
static size_t MCtor; //统计调用移动构造函数的次数
static size_t CAsgn; //统计调用拷贝赋值函数的次数
static size_t MAsgn; //统计调用移动赋值函数的次数
public:
// 构造函数
MyString(const char* cstr=0){
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
}
// 拷贝构造函数
MyString(const MyString& str) {
CCtor ++;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
// 移动构造函数
MyString(MyString&& str) noexcept
:m_data(str.m_data) {
MCtor ++;
str.m_data = nullptr;
}
// 重载拷贝赋值运算符
MyString& operator=(const MyString& str){
CAsgn ++;
if (this == &str)
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
// 重载移动拷贝赋值运算符
MyString& operator=(MyString&& str) noexcept{
MAsgn ++;
if (this == &str)
return *this;
delete[] m_data;
m_data = str.m_data;
str.m_data = nullptr;
return *this;
}
~MyString() {
delete[] m_data;
}
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
size_t MyString::CCtor = 0;
size_t MyString::MCtor = 0;
size_t MyString::CAsgn = 0;
size_t MyString::MAsgn = 0;
int main()
{
vector<MyString> vecStr;
vecStr.reserve(1000);
for(int i = 0; i < 1000; i++){
vecStr.push_back(MyString("hello"));
}
cout << "CCtor = " << MyString::CCtor << endl;
cout << "MCtor = " << MyString::MCtor << endl;
cout << "CAsgn = " << MyString::CAsgn << endl;
cout << "MAsgn = " << MyString::MAsgn << endl;
}
输出结果:
CCtor = 0
MCtor = 1000
CAsgn = 0
MAsgn = 0
Process returned 0 (0x0) execution time : 0.268 s
Press any key to continue.
可以看到,移动构造函数与拷贝构造函数的区别是,拷贝构造的参数是const MyString& str,是常量左值引用,而移动构造的参数是MyString&& str,是右值引用,而MyString(“hello”)是个临时对象,是个右值,优先进入移动构造函数而不是拷贝构造函数。而移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间,将要拷贝的对象复制过来,而是"偷"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,通过移动构造函数实现了资源转移。
使用过程要注意的问题:
前面已经看到使用std::move是为了实现移动语义,下面看下std::move的实现原理,源码如下:
/**
* @brief Convert a value to an rvalue.
* @param __t A thing of arbitrary type.
* @return The parameter cast to an rvalue-reference to allow moving it.
*/
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
某种意义上来说,
std::move(lvalue)就约等于static_cast,即将左值强制转换为右值。而(lvalue) std::move中封装了一个类型提取器std::remove_reference来方便使用。
std::remove_reference是一个类型提取器,实现的功能比较简单,通过模版提取出最底层类型,提取出来的底层类型保存在std::remove_reference::type,源码如下:
template<typename _Tp>
struct remove_reference
{ typedef _Tp type; };
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp type; };
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp type; };
示例:通过下面的代码可以看出,通过类型提取器,可以从
int &、int &&中分离出最底层的int
#include
#include
using namespace std;
int main() {
int i = 10;
int &ref = i;
std::remove_reference<decltype((i))>::type a = 10; // 左值
std::remove_reference<decltype(200)>::type b = 10; // 右值
std::remove_reference<decltype((ref))>::type c = 10; // 左值引用
std::remove_reference<decltype(std::move(i))>::type d = 10; // 右值引用
std::cout << std::is_same<int, std::remove_reference<decltype((i))>::type>::value << std::endl;
std::cout << std::is_same<int, std::remove_reference<decltype(200)>::type>::value << std::endl;
std::cout << std::is_same<int, std::remove_reference<decltype((ref))>::type>::value << std::endl;
std::cout << std::is_same<int, std::remove_reference<decltype(std::move(i))>::type>::value << std::endl;
return 0;
}
输出结果
1
1
1
1