C++11引入了右值引用(&&),移动构造函数,移动复制运算符以及std::move, 对于它们之间的关系和具体应用场景很多人还是云里雾里,这里结合具体的例子谈一下自己的看法。
1 拷贝构造函数、赋值构造函数和深浅拷贝
要理解右值引用(&&),移动构造函数,移动复制运算符以及std::move, 首先需要理解拷贝构造、赋值构造和深浅拷贝。
1.1 例子
看下面的例子:
#include <iostream>
#include <utility>
#include <string>
#include <vector>
#include <stdlib.h>
#include <string.h>
using namespace std;
class Str{
private:
char* m_data; //有内存分配,需要考虑深度拷贝的问题
size_t m_len;
void copy_data(const char *s) {
m_data = new char[m_len+1];
memcpy(m_data, s, m_len);
m_data[m_len] = '\0';
}
public:
Str() {
m_data = NULL;
m_len = 0;
}
Str(const char* p) {
m_len = strlen (p);
copy_data(p);
std::cout << "Constructor: " << p << std::endl;
}
/*拷贝构造函数
* 实现深度拷贝
*/
Str(const Str& str) {
m_len = str.m_len;
copy_data(str.m_data);
std::cout << "Copy Constructor: " << str.m_data << std::endl;
}
/*赋值构造函数
* 实现深度拷贝
*/
Str& operator=(const Str& str) {
if (this != &str) {
m_len = str.m_len;
copy_data(str.m_data);
}
std::cout << "Assignment Constructor: " << str.m_data << std::endl;
return *this;
}
void print()
{
printf("data adreess:%p\n", m_data);
}
virtual ~Str()
std::cout << "Desctuctor: " << m_data << std::endl;
if (m_data) free(m_data);
}
};
void test() {
Str a;
a = Str("First"); //执行赋值构造函数
std::vector<Str> vec;
vec.push_back(Str("Second")); //执行拷贝构造函数
}
int main()
{
test();
}
编译命令: g++ -std=c++11 -o move move.cpp
按照代码逻辑,第61行的Str("First") 首先会执行构造函数生成一个临时Str对象, 然后赋值给对象a,这时会执行赋值构造函数进行对象的深度拷贝,最后临时对象Str("First")析构掉。
第64行的Str("Second") 首先会执行构造函数生成一个临时Str对象, 然后存入vector,这时会执行拷贝构造函数进行对象的深度拷贝,最后临时对象Str("Second")析构掉。
最后程序退出时,依次析构Vector中的对象和a。 为什么是这个顺序呢?因为a 和vec是局部变量,存放在栈中,栈按照后进先出的顺序处理元素。
输出结果如下,印证了我们的分析:
Constructor: First
Assignment Constructor: First
Desctuctor: First
Constructor: Second
Copy Constructor: Second
Desctuctor: Second
Desctuctor: Second
Desctuctor: First
1.2 分析
上面的例子中Str("First")和Str("Second")只是一个临时对象,我们最终是需要构造一个对象a, 但是临时对象也分配和释放了一次内存空间,造成了资源的浪费。如果对象很大时,这个对性能的影响还是很大的。
为了解决这个问题,c++11开始引入了移动构造函数,移动复制函数。
2 移动构造函数,移动赋值函数,std::move
移动构造函数和移动赋值函数 与 拷贝构造函数和赋值构造函数的区别就是形参使用右值引用。
2.1 例子
对上面的例子做一个改动:
#include <iostream>
#include <utility>
#include <string>
#include <vector>
#include <stdlib.h>
#include <string.h>
using namespace std;
class Str{
private:
char* m_data;
size_t m_len;
void copy_data(const char *s) {
m_data = new char[m_len+1];
memcpy(m_data, s, m_len);
m_data[m_len] = '\0';
}
public:
Str() {
m_data = NULL;
m_len = 0;
}
Str(const char* p) {
m_len = strlen (p);
copy_data(p);
std::cout << "Constructor: " << p << std::endl;
}
Str(const Str& str) {
m_len = str.m_len;
copy_data(str.m_data);
std::cout << "Copy Constructor: " << str.m_data << std::endl;
}
Str& operator=(const Str& str) {
if (this != &str) {
m_len = str.m_len;
copy_data(str.m_data);
}
std::cout << "Assignment Constructor: " << str.m_data << std::endl;
return *this;
}
/*
*移动构造函数
*/
Str(Str&& str) {
std::cout << "Move Constructor: " << str.m_data << std::endl;
m_len = str.m_len;
m_data = str.m_data; //避免了不必要的拷贝
str.m_len = 0;
str.m_data = NULL;
}
/*
* 移动赋值函数
*/
Str& operator=(Str&& str) {
std::cout << "Move Assignment: " << str.m_data << std::endl;
if (this != &str) {
m_len = str.m_len;
m_data = str.m_data; //避免了不必要的拷贝
str.m_len = 0;
str.m_data = NULL;
}
return *this;
}
void print()
{
printf("data adreess:%p\n", m_data);
}
virtual ~Str() {
// std::cout << "Desctuctor: " << m_data << std::endl;
if (m_data)
{
std::cout << "Desctuctor: " << m_data << std::endl;
free(m_data);
}else
{
std::cout << "Desctuctor... "<< std::endl;
}
}
};
void test() {
Str a;
a = Str("First");
std::vector<Str> vec;
vec.push_back(Str("Second"));
}
int main()
{
test();
}
编译:g++ -std=c++11 -o move move.cpp
输出结果如下:
Constructor: First
Move Assignment: First
Desctuctor...
Constructor: Second
Move Constructor: Second
Desctuctor...
Desctuctor: Second
Desctuctor: First
测试函数test()没有任何变化,只是定义了移动构造函数和移动赋值函数, 编译之后就调用了移动构造函数和移动赋值函数,省去了内存开销。
这是因为编译器会自动优化,对临时变量的构造和赋值会自动查找使用定义了移动构造函数和移动赋值函数, 如果找到,会自动调用响应的函数。
网上有人会说-fno-elide-constructors参数会改变行为禁止优化,但是根据我的测试,对GCC而言,这个参数并没有起作用(gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04) )
2.2 std::move
上面的结论是对临时变量而言的。因为临时变量后面就不会再用了,可以大胆的使用移动函数。对于非临时变量呢?
依然是2.1中类定义的代码,只是改一下test()函数,如下:
void test2() {
Str a("First");
Str b = a;
std::vector<Str> vec;
vec.push_back(b);
}
现在的对象a和b都不是临时对象了,虽然仍然定义了移动构造函数和移动赋值函数,但是编译器不会自动优化的,它不确定后面是否继续使用a 和b。所以还是使用拷贝构造函数进行深拷贝。
运行结果如下:
Constructor: First
Copy Constructor: First
Copy Constructor: First
Desctuctor: First
Desctuctor: First
Desctuctor: First
这时,轮到std::move()函数出场了,再稍微改造一下test2,如下代码:
void testMove2() {
Str a("First");
Str b = std::move(a);
std::vector<Str> vec;
vec.push_back(std::move(b));
}
使用了std::move(), 显示的到时编译器调用移动构造函数或者移动赋值函数,当然,这么做之后,a 和b也不能继续用了,它们已经没有自己的内存空间了。
执行结果如下:
Constructor: First
Move Constructor: First
Move Constructor: First
Desctuctor: First
Desctuctor...
Desctuctor...
所以说:通过std::move,可以避免不必要的拷贝操作。std::move是为性能而生。std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。
3 右值引用
那什么是右值引用呢? 第二节的例子我们已经看到了右值引用的使用:在移动构造函数或者移动赋值函数的定义形参中,就使用了右值引用。
c++11增加了一个新的类型,称作右值引用(R-value reference),标记为T &&
具体的理解可以参考:
https://www.cnblogs.com/qicosmos/p/3369940.html
首先要了解左值、右值和引用。
1)左值和右值的概念
左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体;
右值当在赋值号右边取出值赋给其他变量的值;右值可以在内存也可以在CPU寄存器。
一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。
2)引用
引用是C++语法做的优化,引用的本质还是靠指针来实现的。引用相当于变量的别名。
引用可以改变指针的指向,还可以改变指针所指向的值。
引用的基本规则:
- 声明引用的时候必须初始化,且一旦绑定,不可把引用绑定到其他对象;即引用必须初始化,不能对引用重定义;
- 对引用的一切操作,就相当于对原对象的操作。
3)左值引用和右值引用
3.1 左值引用
左值引用的基本语法:type &引用名 = 左值表达式;
3.2 右值引用
右值引用的基本语法type &&引用名 = 右值表达式;
右值引用在代码优化方面会经常用到。
右值引用的“&&”中间不可以有空格。