这一章讲的更多是关于拷贝初始化, 拷贝赋值, 移动初始化, 移动赋值和析构的问题
拷贝初始化
相比于直接初始化的方式: myClass c(args..), 拷贝初始化是有 "=" 参与的. 例外的几种情况是, 实参的传递, return的返回值和push_back()等操作也是拷贝初始化.
相对应的类内方法需要提供这样的参数: myClass::myClass(const myClass&,args..)
, args是可选其他参数, 但是这些参数必须提供默认值.
析构函数
在这里, 只说一下类内成员变量的生命周期顺序.
首先, 成员变量按照在类的声明顺序被先后初始化, 然后执行构造函数的{ }函数体. 在析构的时候, 先执行析构函数{ }的函数体, 然后按照初始化顺序的逆序被析构.
赋值函数myClass &operator={}
这里需要注意一点, 我们通常能记得将原成员变量析构掉, 但是我们同时需要保证构造的赋值函数必须保证异常安全, 即, 出现了将自身赋值给自身的情况也不会出错, 也就是说, 我们不能在赋值发生前先析构了原来的变量. 小技巧是: 在拷贝右侧前, 先找一个临时变量保存, 再析构左侧.
另外一个方法是使用swap函数. 将*this和传入的类swap一下, 那么在函数结束的时候, *this才会被析构, 而且swap函数的效率比较高. 我们也可以定义自己的swap函数, 如果定义, 那么使用优先权将高于std::swap(). 如果将swap声明称inline函数更好, 那么性能将进一步提升.
三五法则
注意到, 只要需要自定义析构函数(而不是系统合成的析构函数)的地方, 一定证明了我们有用到类似指针的地方, 从而内存不会直接释放. 因此推测, 一定会需要自定义的拷贝构造函数, 否则系统合成的拷贝构造函数会直接把对方的指针拿来使用. Moreover, 肯定就会有一个被重构的拷贝赋值函数.
(放在后面的话), 正因为有类似指针的变量, 因此我们考虑在移动变量的时候, 默认合成的移动函数一定会重新开辟内存空间, 把原来指针的东西逐个写复制. 如果我们能自定义移动构造函数和移动赋值函数, 那么性能也会提升.
以上五个函数, 相互关联.
=delete
如果我们不希望类拥有某个特性, 比如接受=
,或者不允许拷贝初始化, 那么我们就需要在类内声明这个成员函数, 然后在后面=delete
. 有些时候, 系统会为嘞自动生成delete, 比如成员变量有const类型, 有引用类型, 那么就自动为赋值运算符加上=delete
.(因为const类型不能被重新赋值/ 引用并不属于本类)
实现自己的智能指针
class myClass{
public:
myClass() :useCount(new int(0)), s(new string("")) {};
myClass &operator=(const myClass &r) { useCount = r.useCount; ++(*useCount);*s = *r.s; return *this; };
~myClass(){ --(*useCount); if (*useCount == 0) { delete s; delete useCount; } }
private:
int* useCount;
string *s;
};
很绝妙的地方在于, 赋值函数中, 两个对象将共享一个useCount. 如果一个对象被析构, 则会检查useCount是否是0, 如果是, 则删除s.
移动函数
头文件<utility>
std::move()
可以防止在移动时发生拷贝, 取而代之的是指针的转移. 在move()之后, 原来的变量需要保证可以安全析构.(手动将各个指针归nullptr
)
移动函数的出现不只是为了提升效率. 默认的移动方法是拷贝移动, 而对于像unique_ptr
或者IO变量, 这些是不允许被拷贝的, 那么移动的时候必须使用move().
在这里需要引入新的概念: 右值引用. 左值是一个"持久"的变量, 而右值是一个"短暂"的变量, 右值可以是常量, 返回右值的表达式等.简而言之, 右值是一个"即将被销毁的量".
右值没有其他用户, 安全更改.
左值有可能有其他变量在使用, 谨慎更改.
int &p = i * 32;//不可以, 运算结果是右值, 不可以交给左值
const int &p = i * 32; //可以, const int是一个右值
int &&p = i * 32; //可以, &&p是一个右值
std::move()会返回一个右值, 因此我们保证传入的参数在之后不会再有除了重新赋值或析构之外的任何操作, 即必须处于一个安全态.
那么现在写移动赋值函数:
myClass::myClass(myClass &&old) noexcept: //如果不需要开辟内存, 通知编译器无异常将会省去一些开销
useCount(old.useCount), s(old.s)
{
old.s = nullptr;
old.useCount = nullptr;
//令old进入析构安全
}//此时old会自动析构, 因为old是右值
有时, 系统也会自动合成移动赋值函数, 条件是: 系统没有任何拷贝或者析构函数, 且每一个成员都是可以移动的时候. 如果仅自定义了移动赋值函数, 那么合成拷贝函数是delete的
一般地, 对于构造函数, 赋值函数我们会创建一个形参是const myClass&
的和形参是myClass&&
的两个重构版本, 使用效率会极大提升.
移动迭代器
make_move_iterator()将普通的迭代器转换为移动迭代器, 会生成右值引用, 所有使用了移动迭代器的语句都将使用移动构造以代替拷贝构造, 但是同时, (可能会)销毁移动迭代器内的内容.
引用限定符&和&&
这个符号类似于const, 放在const后. 限定本函数只能用于左值操作或者右值操作.
网友评论