一:对象的移动概念
有时对于一个类的对象进行大量的对象拷贝和赋值操作都是非常消耗性能的!因此C++11中提出了“对象移动”的操作。那么什么叫做“对象移动”呢?
所谓的对象移动:其实就是把该对象所占据的内存空间的访问权限转移(移动)给另一个对象
比如:原来这块内存空间是属于张三的,你现在进行了对象转移,则该内存空间就属于李四了!
二:移动构造函数和移动赋值运算符概念
目的:为了提高效率
C++11引入右值引用、std::move()函数以及对象移动的概念就是为了提高程序运行的效率!为什么这么说呢?因为我们平时在类中定义的拷贝构造函数以及拷贝赋值运算符重载函数会do大量的拷贝和赋值的操作。这些操作都是非常地耗时的。因此这样你写的代码的效率就会非常低下。C++11引入了移动构造函数 和 移动赋值运算符重载函数。这两个函数可以帮助我们避免进行大量的拷贝和赋值操作,从而大大地提高我们写的代码的执行效率!(也即提高程序的效率了!)
由于移动构造函数以及移动赋值运算符重载函数 与 拷贝构造函数以及拷贝赋值运算符重载函数 非常地类似因此,下面给出说明:
- 如果把 对象A 移动给 对象B后,那么 对象A 就 不能 再使用了
- 这里所谓的“移动”,并不是说把内存中的数据所占据的内存 从一个地址 倒腾 到另一个地址,而只是变更一下所属权而已!
拷贝构造函数:Time::Time(const Time &t,int index = 0) const的左值引用& 还有其他参数的话必须有默认值。
移动构造函数:Time::Time(Time &&t,int index = 0) 右值引用 && 还有其他参数的话必须有默认值。
注意这里的右值引用不能是const的,因为你用右值引用do函数参数就算为了让其绑定到一个右值上去的!就是说这个右值引用是一定要变的,但是你一旦加了const就没法改变该右值引用了
三:移动构造函数演示:
#include <iostream>
using namespace std;
class B
{
public:
int m_bm;
B():m_bm(100)
{
// cout << "类B的构造函数执行了" << endl;
};
B(const B &temp):m_bm(temp.m_bm)
{
// cout << "类B的拷贝构造函数执行了" << endl;
};
virtual ~B()
{
// cout << "类B的析构函数执行了" << endl;
}
};
class A
{
public:
A():m_pb(new B()) //调用类B的构造函数
{
cout << "类A的构造函数执行了" << endl;
}
A(const A &tmp):m_pb(new B(*(tmp.m_pb)))//调用了B的拷贝构造函数
{
cout << "类A的拷贝构造函数执行了" << endl;
}
//noexcept:不抛出异常 提高效率
A(A&& tmp) noexcept :m_pb(tmp.m_pb)//临时对象指向原来对象a指向的内存m_pb
{
tmp.m_pb = nullptr;//打断
cout << "类A的移动构造函数执行了" << endl;
}
A& operator=(const A& src)
{
if (&src == this)
return *this;
delete m_pb;
m_pb = new B(*(src.m_pb));
cout << "类A的赋值运算符函数执行了" << endl;
return *this;
}
A& operator=(A && src) noexcept
{
if (&src == this)
return *this;
delete m_pb;//先把自己的内存干掉
m_pb = src.m_pb;//把对方内存拿过来直接使用
src.m_pb = nullptr;//斩断旧的内存
cout << "类A的移动赋值运算符函数执行了" << endl;
return *this;
}
virtual ~A()
{
if (this->m_pb) {
delete m_pb;
m_pb = nullptr;
}
cout << "类A的析构函数执行了" << endl;
}
private:
B *m_pb;
};
//这里来一个static静态函数(别的.cpp源文件不可访问!)
static A getA()
{
A a;
return a;//临时对象
//如果A有移动构造函数的话,会调用移动构造函数,把a对象的数据移动给了临时对象
}
struct Tc
{
int i; // 内置类型可以移动
std::string s; // string类型定义了自己的移动操作
};
演示1:
//main中
B *pb = new B();//构造函数
pb->m_bm = 101;
B *pb2 = new B(*pb);//拷贝构造函数
delete pb; // 析构
delete pb2;// 析构
演示2:
//main中
A a = getA();//一个构造一个移动构造一个析构 //创建新对象a,且调用移动构造函数将getA函数返回值临时对象移动给a
A a1(a);//拷贝构造函数
A a4(std::move(a)); //类A的移动构造函数执行了 有新对象 不在使用a了 a不全了
A &&a2(std::move(a)); //没有新对象 对象a有了新的别名a2
A &&a3 = getA();//从getA的临时变量的别名是a3 被a3接管了
四:移动赋值运算符的功能
//main中
A a = getA();
A a2;
a2 = std::move(a); // 移动赋值运算符
五:合成移动构造函数和移动赋值运算符
某些条件下:编译器会合成移动构造函数和移动赋值运算符
1)有自己的拷贝构造,拷贝赋值运算符或者析构就不会合成
2)如果没有自己移动构造函数和移动赋值运算符,系统会调用我们自己写的拷贝构造函数,拷贝赋值运算符
3)只有一个类没有定义任何自己版本的拷贝构造成员(拷贝构造,拷贝赋值运算符),且类的每个非静态成员都可以移动,系统才能为我们合成
什么叫成员可以移动?
1)内置类型是可以移动的
2)类类型成员,这个类要有对应的移动操作相关的函数
//main中
Tc a;
a.i = 100;
a.s = "I Love China";
const char *p = a.s.c_str();
Tc b = std::move(a); //系统合成 因为string类的移动构造函数的作用
const char *q = b.s.c_str();
六:总结
- 在写自定义的类时,尽量给你的类写上对应的移动构造函数以及移动赋值运算符重载函数-以减少大量的关于该类的拷贝和赋值的操作。当然,这只是针对复杂的类,或说一些会大量调用其拷贝构造函数or拷贝赋值运算符函数的类;若是比较简单的类or不会大量调用上述两种函数的类就可以不写上移动函数。
- 写移动函数时,一定要在对应的位置上加上noexcept关键字,来通知编译器你写的这个函数并不会抛出异常!
- 当把原对象所代表的内存空间的使用权限过继给新对象后,一定要记得把原对象所占据的内存空间指向空(值 = NULL | 指针 = nullptr)!
- 若没有移动函数,编译器会为你自动调用对应的拷贝函数完成相应的创建对象和给对象赋值的操作(相比用移动函数,这样do你的代码效率就是低的!)。