C++11重大新增特性:左值引用 & 右值引用 & 移动构造 & 移动赋值
一、右值引用和左值引用概念和区别
C++11为了支持移动操作(移动构造和移动赋值),新标准引入了新的引用类型 —— 右值引用。我们将C++11之前的引用都成为左值引用。但无论是左值引用还是右值引用,本质上都是给对象取别名!
1.1 左值 & 左值引用
左值是一个表达式(如变量、解引用后的指针),表示的是一个对象的身份。我们可以对左值进行赋值操作、取地址。左值可以出现在等号的两边。
评判一个表达式是否为左值的最根本标志就是:是否可以取地址。所以对于一个const修饰的变量
,由于该变量可以取地址,所以也是一个典型的左值。左值引用即是左值的别名。
int main()
{
//a、b、p、*p都是左值
int* p = new int(0);
int a = 10;
const int b = 12;
//rp、ra、rb、rval都是左值引用
int*& rp = p;
int& ra = a;
const int& rb = b;
int& rval = *p;
return 0;
}
1.2 右值 & 右值引用
右值也是一个表达式,和左值不同的是:右值只能出现在等号的右边(即不能被赋值),不能取地址(最根本原因),通常是字面常量、表达式返回值,函数返回值。右值引用就是对右值的引用,通过&&
来获取右值引用。
右值不能取地址。但对右值取别名后,会导致右值被存储到特定的区域,并且可以取到该区域的指针。比如:字面常量10是一个右值,不能取地址。如果10被ra引用后,我们可以对ra取地址,并且可以通过修改ra进而修改右值。如果不想该右值被修改,我们可以通过const进行修饰!!
int Add(const int x, const int y)
{
return x + y;
}
int main()
{
//10、10 + 20、Add函数返回值都是右值,无法取地址
//ra、rb、rc都是右值引用
int&& ra = 10;
int&& rb = 10 + 20;
int&& rc = Add(1, 2);
ra = 20;
return 0;
}
- 需要注意的是:上述
ra、rb、rc
虽然是右值引用,但ra、rb、rc
本身还是一个变量,并且可以取地址,是一个左值!!
二、左值引用和右值引用对比
2.1 左值引用
- 普通的左值引用只能引用左值,不能引用右值。
- const修饰的左值引用,不仅可以引用左值,还可以引用右值!!
【示例】:
int main()
{
int a = 10;
int& ra = a;
//int& rb = 10;//error,普通左值引用不能引用右值
const int& rc = a;
const int& rd = 10;
return 0;
}
2.1 右值引用
- 右值引用可以引用右值,但不能引用左值。
- 我们可以通过move函数,让右值引用引用左值!!move函数调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。需要注意的是,move函数的返回值是一个右值,但move函数本身不会修改左值属性!
【示例】:
int main()
{
int a = 10;
const int&& rb = 10;
//int&& ra = 1;//error, 右值引用无法引用左值
//error,move本身不会修改左值属性
//move(a);
//int&& ra = a;
int&& ra = move(a);//move后的返回值是一个右值
return 0;
}
三、右值和右值引用诞生的意义
在C++11标志之前,如果在vector、list
等容器保存的是一块空间的指针。此时如果调用拷贝构造函数和拷贝赋值函数,编译器会进行一个深拷贝构造出新对象,将就对象释放。
在很多情况下拷贝对象时无法避免的。
但如果返回的是一个临时对象会发生什么呢?
【示例】:
- 我们发现如果用一个临时对象拷贝构造一个变量
s
时,编译器会先拷贝构造出一个临时对象,在用该临时对象出拷贝构造变量s
。 - 但该过程中存在一个问题:临时对象深拷贝创建后仅仅使用一次便立即销毁、原对象
ret
是一个临时对象马上就要出作用域销毁,此时依旧对ret
进行拷贝构造。 - 上述情况在实际过程中会在大量场景中频繁出现,并且意义不大。这也意味着大量的无意义的深拷贝产生,将导致性能的下降。我们是否可以不进行拷贝,直接将原始数据转移到新对象中呢?(该操作的前提是原始数据马上就要被销毁)
- 为了解决上述情况,C++11引入了移动构造函数和移动拷贝函数。移动构造函数和移动拷贝函数可以将一个待销毁的变量数据(该变量通常被编译器识别为右值)直接转移到新对象。而右值和右值引则是为实现这些函数运营而生的!!
四、移动构造 & 移动赋值
在C++中,右值分为两种:内置定义类型右值为纯右值;自定义类型右值为将亡值!!对于纯右值,移动构造函数、移动赋值函数没有太大价值,行为和拷贝构造函数、移动构造函数类型。(上述临时对象ret
虽然可以取地址是一个左值,但编译器会特殊处理将其识别为右值,即将亡值)
只有当自定义类型中存在资源的深拷贝时,此时才能移动构造和移动赋值的价值。(直接转移资源,而非深拷贝!!)
4.1 移动构造函数
类似于拷贝构造函数,移动构造函数的第一个参数是该类类型的引用,不同的是引用参数是一个右值。移动构造函数的本质是直接将右值对象的资源窃取过来,占为己有,此时不在进行深拷贝。所以该构造称为移动构造,用别人的资源来构造自己。
【示例:移动构造和拷贝构造函数实现和对比】:
namespace mystring
{
class string
{
public:
string(const char* str = "")//默认构造函数
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(const char* str = "") ----- 构造函数" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//拷贝构造函数
string(const string& s)
{
cout << "string(const string& s) ---- 拷贝构造函数 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
//移动构造函数
string(string&& s)
{
cout << "string(string&& s) ---- 移动构造函数 移动语义" << endl;
swap(s);
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
};
int main()
{
mystring::string s("123456");
cout << endl;
mystring::string s1 = s;
cout << endl;
mystring::string s2(move(s1));
return 0;
}
【运行结果】:
- 在上述过程中,我们发现移动构造过程中没有深拷贝。原因在于移动构造直接将原资源抢占,交换过来!!
4.2 移动赋值函数
移动赋值函数和移动拷贝函数一样,直接将将亡值
的资源交换抢占过来。
【示例: 移动赋值函数和拷贝赋值函数实现和对比】:
namespace mystring
{
class string
{
public:
string(const char* str = "")//默认构造函数
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(const char* str = "") ----- 构造函数" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//拷贝赋值函数
string& operator=(const string& s)
{
string tmp(s);
swap(tmp);
cout << "string& operator=(const string& s) ---- 拷贝赋值函数 深拷贝" << endl;
return *this;
}
//移动赋值函数
string& operator=(string&& s)
{
swap(s);
cout << "string& operator=(string&& s) ---- 移动语义" << endl;
return s;
}
//拷贝构造函数
string(const string& s)
{
string tmp(s._str);
swap(tmp);
cout << "string(const string& s) ---- 拷贝构造函数 深拷贝" << endl;
}
//移动构造函数
string(string&& s)
{
swap(s);
cout << "string(string&& s) ---- 移动构造函数 移动语义" << endl;
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
};
int main()
{
mystring::string s("123456");
cout << endl;
mystring::string s1, s2;
s1 = s;
cout << endl;
s2 = move(s);
cout << endl;
return 0;
}
【运行结果】:
五、完美转发(引用在模板中的用途)
5.1 模板中的万能引用(折叠引用)&&
模板中的&&
不是右值引用,表示的时万能引用,即可以接收左值也可接受右值。但万能引用只是提供了能够同时接受左值引用和右值引用的能力。
引用类型的唯一作用就是限制接收的类型,后续使用中会退化成左值。(这很好理解,以移动构造和移动赋值为例,其最重要的功能就是转移右值的资源。但显然右值是无法被修改的。所以当一个右值传递给移动构造和移动赋值后,后续使用会退化成左值)
【示例】:
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<class T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
int a = 11;
PerfectForward(a);//左值
PerfectForward(10);//右值
const int b = 10;
PerfectForward(b);//const左值
PerfectForward(move(b));//const 右值
return 0;
【运行结果】:
- 函数模板中的
&&
称为万能引用或折叠引用,上述代码成功运行,从侧面说明万能引用既可以接收右值还可以接收左值。同时最终结果均为左值相关,进一步说明万能引用接收右值后会退化成左值!
5.2 完美转发(std::forward)在传参的过程中保留对象原生类型属性
万能引用在接受相关引用后会退化成一个左值。这个机制也是移动构造和移动赋值的基础。
同时在C++中提出了完美转发的概念,完美转发可以保留对象在传参过程中的原生类型属性。
【示例】:
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<class T>
void PerfectForward(T&& t)
{
Fun(forward<T>(t));//完美转发,保留t的原生属性
}
int main()
{
int a = 11;
PerfectForward(a);//左值
PerfectForward(10);//右值
const int b = 10;
PerfectForward(b);//const左值
PerfectForward(move(b));//const 右值
return 0;
}
【运行结果】:
【完美转发实际场景中的作用】:
namespace mystring
{
class string
{
public:
string(const char* str = "")//默认构造函数
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(const char* str = "") ----- 构造函数" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//拷贝构造函数
string(const string& s)
{
string tmp(s._str);
swap(tmp);
cout << "string(const string& s) ---- 拷贝构造函数 深拷贝" << endl;
}
//移动构造函数
string(string&& s)
{
swap(s);
cout << "string(string&& s) ---- 移动构造函数 移动语义" << endl;
}
//拷贝赋值函数
string& operator=(const string& s)
{
string tmp(s);
swap(tmp);
cout << "string& operator=(const string& s) ---- 拷贝赋值函数 深拷贝" << endl;
return *this;
}
//移动赋值函数
string& operator=(string&& s)
{
swap(s);
cout << "string& operator=(string&& s) ---- 移动语义" << endl;
return s;
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
};
template<class T>
struct ListNode
{
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
};
template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
void PushBack(T&& x)
{
//Insert(_head, x);
Insert(_head, std::forward<T>(x));
}
void PushFront(T&& x)
{
//Insert(_head->_next, x);
Insert(_head->_next, 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 newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
void Insert(Node* pos, const T& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = x; // 关键位置
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head = nullptr;
};
int main()
{
List<mystring::string> lt;
cout << endl;
lt.PushBack("11111111");
return 0;
}
【运行结果】:
- 调试时,
lt.PushBack("11111111");
走的是void Insert(Node* pos, T&& x)
版本的插入,而非void Insert(Node* pos, const T& x)
六、新的类功能
6.1 C++11新增默认成员函数
原来C++类中,有6个默认成员函数:(后两个作用、意义不大)
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
在C++11中又新增两个默认成员函数:移动构造函数和移动赋值运算符重载。
但比较特殊的是,对于移动构造函数和移动赋值运算符重载来说,只有当拷贝构造函数、拷贝赋值重载、析构函数都没显示的写时,编译器才会生成一个默认的移动构造函数和移动赋值运算符重载。
对于默认生成的移动构造函数来说,内置类型成员按值拷贝;对于自定义类型成员,则需要看该自定义成员是否实现了移动构造。如果实现了,调用该自定义类型成员的移动构造;否则调用拷贝构造!!
对于默认生成的移动赋值运算符重载,同上。
6.2 类成员变量初始化
在C++11中,允许类成员变量在声明时给默认缺省值。当调用类的构造函数时,如果没有显示的传递初始值,编译器会用成员变量声明时的默认缺省值去初始化成员变量!
class string
{
public:
//调用该函数时,编译器会应声明是的缺省值去初始化成员变量
//即:_str = nullptr、_size = 0、 _capacity = 0
string()//默认构造函数
{ }
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
6.3 强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
【示例】:
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{}
Person(Person&& p) = default;
private:
mystring::string _name;
int _age;
};
int main()
{
Person s1;
cout << endl;
Person s2 = s1;
cout << endl;
Person s3 = std::move(s1);
cout << endl;
return 0;
}
【成员函数_name的相关实现如下】:
namespace mystring
{
class string
{
public:
string(const char* str = "")//默认构造函数
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(const char* str = "") ----- 构造函数" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//拷贝构造函数
string(const string& s)
{
string tmp(s._str);
swap(tmp);
cout << "string(const string& s) ---- 拷贝构造函数 深拷贝" << endl;
}
//移动构造函数
string(string&& s)
{
swap(s);
cout << "string(string&& s) ---- 移动构造函数 移动语义" << endl;
}
//拷贝赋值函数
string& operator=(const string& s)
{
string tmp(s);
swap(tmp);
cout << "string& operator=(const string& s) ---- 拷贝赋值函数 深拷贝" << endl;
return *this;
}
//移动赋值函数
string& operator=(string&& s)
{
swap(s);
cout << "string& operator=(string&& s) ---- 移动语义" << endl;
return s;
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
};
【允许结果】:
- 由于在Person类中,我们已经显示的实现了拷贝构造函数,因此无法编译器无法生成默认的移动构造函数。但我们通过
default
关键字让编译器强制生成默认的移动构造函数。对于默认生成的移动构造函数,内置类型int _age
按值拷贝;对于自定义类型mystring::string _name;
调用自身的移动构造函数。 - 如果成员变量存在如const、引用的类型时,即使
default
强制让编译器实现,也是错误的,无法生成。
6.4 禁止生成默认函数的关键字delete
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。(C++11之前,如果我们希望阻止拷贝的类,我们一般将相关函数声明为私有。但对于有元和成员函数来说,依旧可以访问它。所以为了阻止友元函数和成员函数进行拷贝,通常将拷贝控制成员声明为私有,但不定义)
在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete;
private:
mystring::string _name;
int _age;
};