文章目录
- 一.左值引用和右值引用
- 二.C++11区分左值和右值的语法设计意义--对象的移动构造和移动赋值
- 场景分析1:
- C++11之前
- C++11之后
- 场景分析2:
- 函数std::move
- 右值引用的广泛使用
- 三.引用折叠
一.左值引用和右值引用
- 左值:可以取到地址的对象(可以出现在赋值符号的左边),对左值的引用称为左值引用
&
. - 右值(将亡值):无法取到地址的对象(不能出现在赋值符号的左边),对右值的引用称为右值引用
&&
.- 常见的右值有:字面常量,表达式返回值,函数返回值(非const限定的引用)(通常是常量或临时变量)
void Testreference1()
{
//以下的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;
}
void Testreference2()
{
double x = 1.1, y = 2.2;
//以下几个都是对右值的右值引用
//常量
int&& rr1 = 10;
//表达式返回值
double&& rr2 = (x + y);
//非const修饰的函数返回值(fmin返回值为double)
double&& rr3 = fmin(x, y);
}
const type&
(被const限定的左值引用)既可以引用左值,也可以引用右值
二.C++11区分左值和右值的语法设计意义–对象的移动构造和移动赋值
- C++11区分出左值和右值是为了能够让编译器识别出一些即将析构的类对象(将亡),当这些类对象作为引用形参拷贝给其他对象时,通过右值的类型识别,就可以调用相应重载版本的拷贝构造或赋值运算符重载来实现对象堆区资源的交换转移(通过交换指向堆区的指针实现),从而避免了没必要的深拷贝
场景分析1:
C++11之前
- C++11之前的string对象模拟(只含对象的构造,拷贝构造,赋值重载,析构函数):
namespace mystring
{
class string
{
public:
//构造函数(深拷贝)
string(const char* str = "")
:_size(strlen(str))
,_capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//交换两个string对象(堆区资源交换)
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//拷贝构造(复用构造函数实现)(深拷贝)
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
string tmp(s._str);
swap(tmp);
}
//赋值重载(复用拷贝构造实现)(深拷贝)
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 赋值重载(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
- 执行以下代码:
string to_string()
{
string tem("对象测试");
return tem;
}
//tem在函数调用完后就是一个即将析构的对象
int main()
{
string s(to_string());
return 0;
}
- 类似的场景下,在C++11之前,要接收tem的数据就会进行对象的深拷贝:
- tem完成拷贝后就会调用析构函数释放其内存资源,这种情况下tem的堆区资源其实是被浪费掉的,如果不进行深拷贝,直接将tem对象与s对象中的指针进行交换,就可以实现堆区资源的转移,但是这种指针交换操作会让被拷贝的对象无法再使用,因此就需要确定被拷贝的对象是否为即将析构的对象,于是C++区分出了左值和右值来识别一些即将析构的对象
C++11之后
- C++11之后的string对象模拟(只含对象的构造,拷贝构造,赋值重载,析构函数):
namespace mystring
{
class string
{
public:
//构造函数
string(const char* str = "")
:_size(strlen(str))
,_capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//交换两个string对象(堆区资源交换)
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//拷贝构造(复用构造函数实现)
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
string tmp(s._str);
swap(tmp);
}
//赋值重载(复用拷贝构造实现)
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 赋值重载(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
//移动构造
string(string && s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
_str = new char[_capacity + 1];
cout << "string(string&& s) -- 移动构造(资源转移)" << endl;
swap(s);
}
//移动赋值
string& operator=(string && s)
{
cout << "string& operator=(string&& s) -- 移动赋值(资源转移)" << endl;
swap(s);
return *this;
}
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
- C++11之后类对象新增了两个默认成员函数:移动赋值和移动构造函数(本质上就是构造函数和赋值运算符重载函数形参为右值引用类型的重载版本)
- 当类对象直接申请了堆区内存,移动赋值和移动构造函数就需要开发者自行将其实现用于对象拷贝的场景,实现方式一般就是交换两个对象指向堆区内存的指针.
- 执行同样的代码:
string to_string()
{
string tem("对象测试");
return tem;
}
//tem在函数调用完后就是一个即将析构的对象
int main()
{
//函数返回值被识别成右值,调用移动构造交换堆区资源,避免了深拷贝
string s(to_string());
return 0;
}
- tem被识别成了右值(将亡值),s的创建调用了形参为右值引用的构造函数重载版本(也就是移动构造),进行了s对象和tem对象的堆区资源交换,避免了深拷贝
场景分析2:
- 使用前述的C++11之前的string对象和C++11之后的string对象分别执行以下代码:
string to_string()
{
string tem("对象测试");
return tem;
}
//tem在函数调用完后就是一个即将析构的对象
int main()
{
string s;
s = to_string();
return 0;
}
- 上面类似的场景中,移动构造和移动赋值避免了两次深拷贝
函数std::move
- 当一个左值对象需要被拷贝并且拷贝完后不再使用,它作为对象拷贝函数的引用形参时,可以使用move函数将其强制识别为右值,比如:
int main()
{
//测试构造函数
string s1("对象测试");
//s1是左值对象,调用拷贝构造
string s2(s1);
//move强制让编译器将s1识别成右值,调用移动构造交换堆区资源
string s3(std::move(s1));
cout << endl;
//测试赋值重载函数
//函数返回值被识别成右值,调用移动赋值交换堆区资源,避免了深拷贝
string s4("对象测试");
//s4是左值对象,调用赋值重载
s2 = s4;
//move强制让编译器将s1识别成右值,调用移动赋值,交换堆区资源
s3 = std::move(s1);
}
右值引用的广泛使用
- C++11之后,STL标准模板库中的所有数据结构对象中,所有可能涉及对象深拷贝的成员接口(包括对象的构造函数,赋值重载函数以及增删查改接口)都增加了形参为对象右值引用的重载版本,从而有效地减少了STL在使用的过程中对象深拷贝的次数.类对象移动构造和移动赋值的设计模式,一定程度上提高了C++代码的时间效率
三.引用折叠
- 引用折叠的概念:作为类型模板参数的引用可以起到引用折叠的作用,既可以接收左值引用也可以接收右值引用
template<typename T>
void PerfectForward(T&& t)
{
}
- 代码段中的函数形参t既可以接收左值引用也可以接收右值引用,但是当t接收到右值引用时,会将其转化为左值引用(底层机制是在内存中开一段空间存储对象),我们希望能够在引用传递的过程中保持它的左值或者右值的属性,这时就需要
std::forward
函数来辅助引用参数传递(称为完美转发):
int main()
{
string s1("对象测试");
list<string> List;
List.push_back(std::move(s1));
}
- 上面的STL的使用场景中就发生了引用的完美转发,list的
push_back
接口的内部代码结构:
template<class T>
struct ListNode
{
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
};
template<class T>
class list
{
typedef ListNode<T> Node;
public:
//复用Insert实现
void push_back(T&& x)//引用折叠
{
//引用完美转发
Insert(_head, std::forward<T>(x));
}
//链表结点插入接口
void Insert(Node* pos, T&& x)//引用折叠
{
Node* prev = pos->_prev;
Node* newnode = new Node;
//引用完美转发
newnode->_data = std::forward<T>(x);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
- 最终s1的右值引用传递到了string对象的移动赋值函数中,避免了s1对象的深拷贝