1.左值和右值
左值:非临时对象,可以在多条语句里面使用的对象。
右值:临时对象,只能在本条语句里面使用。
如:int i = 0;//i是持久对象,能在多条语句里面使用,0是临时对象,只能在本条语句里面使用
2.左值引用和右值引用
在C++11以前,右值不能被引用,最大限度就是用常量引用绑定一个右值:const int &a = 1
左值引用:&
右值引用:&&
如下:
void process_value(int& i) {
std::cout << "LValue processed: " << i << std::endl;
}
void process_value(int&& i) {
std::cout << "RValue processed: " << i << std::endl;
}
int main() {
int a = 0;
process_value(a);//左值
process_value(1);//右值
}
结果为:
LValue processed: 0
RValue processed: 1
3.移动构造函数和移动赋值运算符
注意:移动操作通常不应该抛出异常,不抛出异常的移动构造函数和移动赋值运算符函数后面应该标记为noexcept,在声明和定义都必须指定。
如下代码实现了拷贝构造和赋值运算符:
class MyString {
private:
char* _data;
size_t _len;
void _init_data(const char *s) {
_data = new char[_len+1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
public:
MyString() {
_data = NULL;
_len = 0;
}
MyString(const char* p) {
_len = strlen (p);
_init_data(p);
}
MyString(const MyString& str) {
_len = str._len;
_init_data(str._data);
std::cout << "Copy Constructor is called! source: " << str._data << std::endl;
}
MyString& operator=(const MyString& str) {
if (this != &str) {
_len = str._len;
_init_data(str._data);
}
std::cout << "Copy Assignment is called! source: " << str._data << std::endl;
return *this;
}
virtual ~MyString() {
if (_data) free(_data);
}
};
int main() {
MyString a;
a = MyString("Hello”);//先调用构造函数再调用赋值运算符
vector<MyString> vec;
vec.push_back(MyString("World"));//先调用构造函数再调用拷贝构造函数
}
运行结果为:
Copy Constructor is called! source: Hello
Copy Assignment is called! source: World
* 这段代码中,由于MyString(“Hello”)和MyString(“World”)都是临时对象,但依然调用了拷贝构造函数和赋值运算符,造成了不必要的空间和性能上不损失,所以,移动构造函数和移动赋值就是直接使用临时对象所申请的资源。
改变后的代码如下:
//移动构造函数
MyString(MyString&& str) {
std::cout << "Move Constructor is called! source: " << str._data << std::endl;
_len = str._len;
_data = str._data;
str._len = 0;
str._data = NULL;
}
//移动赋值运算符
MyString& operator=(MyString&& str) {
std::cout << "Move Assignment is called! source: " << str._data << std::endl;
if (this != &str) {
_len = str._len;
_data = str._data;
str._len = 0;
str._data = NULL;
}
return *this;
}
注意:
* 参数(右值)的符号必须是右值引用符号,&&
* 参数(右值)不可以是常量,因为需要修改右值
* 参数(右值)的资源链接和标记必须修改,否则,右值的析构函数会释放资源,转移到新对象的资源就会无效。
运行结果:
Move Assignment is called! source: Hello
Move Constructor is called! source: World
因此,可以清楚知道移动构造函数不分配任何内存,只是接管对象的内存,接管后将源对象指针置为nullptr,最终移后源对象会被销毁,也就是会调用析构函数。如果不改变源对象的指向,就会释放掉移动的内存。移动赋值函数也应该不抛出任何异常,并且应该处理自赋值。
移动一定会改变源对象的值
4.std::move()函数
move()函数在标准库utility中,可以将左值转化为右值,如:int &&rval = std::move(lval);
move调用告诉编译器,有一个左值,但是我们想像右值一样处理它。调用move就意味着承诺:除了对lval赋值和销毁外,将不再使用它。
注意:使用这个函数后,lval不能再被使用,只能使用rval,特别的move()函数对于swap操作性能提升很大,由于一个移后源对象具有不确定状态,调用move()是危险的。当调用move()时,必须确认移动源对象没有其他用户。
int main()
{
string str = "Hello";
vector<std::string> v;
v.push_back(str);
cout << "After copy, str is \"" << str << "\"\n";
v.push_back(std::move(str));
cout << "After move, str is \"" << str << "\"\n";
cout << "The contents of the vector are \"" << v[0] << "\", \"" << v[1] << "\"\n";
return 0;
}
输出结果为:
After copy, str is “Hello”
After move, str is “”
The contents of the vector are “Hello”, “Hello”
5.合成的移动操作
当一个类定义了自己的拷贝构造函数,拷贝赋值运算符或者析构函数,编译器就不会合成移动构造函数和移动赋值运算符。
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。
移动操作不会隐式定义为删除的函数,但是如果显式要求编译器生成=default移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。
* 有类成员定义了自己的拷贝构造函数且为定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符类似,移动操作会被定义为删除。
* 如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则移动操作被定义为删除。
* 如果类的析构函数被定义为删除的或不可访问的,则移动构造函数被定义为删除。
* 如果有类成员是const或引用,则移动赋值运算符被定义为删除。
6.调用方式
如果定义了移动操作和拷贝控制,编译器会自动匹配。
但是如果没有定义移动操作,那么就算是使用了move()函数,也只会调用拷贝构造函数。
如下:
class Foo{
public:
Foo() = default;
Foo(const Foo&);//拷贝构造函数
};
int main()
{
Foo x;
Foo y(x); //调用拷贝构造函数
Foo z(move(x));//依然调用拷贝构造函数
return 0;
}
7.三五法则
新标准下,类的五种操作:拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符,析构函数。只要其中一个定义了,那么其他的几个都应该定义。