这篇文章的内容是拷贝控制。
接下来这一系列将开始着重介绍C++的面向对象特性了,并且将是以更深入的角度去理解其面向对象特性。
这一章有点难,都是一些需要经验积累的知识点,需要多理解领悟。
总结:当我们还没进阶,不需要维护较大的工程时,我们往往不用在意拷贝控制操作,毕竟编译器已经帮我们补充了缺失的操作了,并且通常情况下是不会出现问题的。但是当我们想要进阶,那么有必要深入了解一下了。
一、拷贝、赋值与销毁
1.拷贝构造函数
1)合成拷贝构造函数
以上通过举例介绍合成拷贝构造函数的运行流程。
2)拷贝初始化
3)参数和返回值
4)拷贝初始化的限制
5)编译器可以绕过拷贝构造函数
可参考:c++ 编译器会绕过拷贝构造函数 - uangyy - 博客园
2.拷贝赋值运算符
初始化时候的等号调用的是拷贝构造函数,初始化后的等号调用的是拷贝赋值运算符。
1)重载赋值运算符
使用=号运算符可看成是在调用拷贝赋值函数。
2)合成拷贝赋值运算符
接下来举个例子说明拷贝构造与拷贝赋值:
//testcopy.h
class testcopy
{
public:
testcopy(const testcopy&);
testcopy(int);
testcopy& operator=(const testcopy&);
inline int coutval() { return val; }
private:
int val;
};
//testcopy.cpp
#include "testcopy.h"
#include <iostream>
using namespace std;
testcopy::testcopy(const testcopy& t)
{
val = t.val;
cout << "usecopy" << endl;
}
testcopy::testcopy(int v)
{
val = v;
cout << "straight" << endl;
}
/**/
testcopy& testcopy::operator=(const testcopy& t)
{
val = t.val;
cout << "="<<endl;
return *this;
}
void Learn12CopyControl::main()
{
usecopy();
}
void main()
{
testcopy t(2),t1(1);//直接构造
testcopy* p=new testcopy(t1);//拷贝构造
cout << 1 << endl;
*p = t;//拷贝赋值
cout << 2 << endl;
testcopy t2 = t;//拷贝构造
cout << 3 << endl;
t2 = t1;//拷贝赋值
cout << t2.coutval()<<endl;
cout << p->coutval() << t1.coutval()<<endl;
}
3.析构函数
1)析构函数完成什么工作
2)什么时候会调用析构函数
还有要注意析构函数要清理成员的功能是其自动调用时才执行的,如果手动调用仅仅只是执行函数体中的功能。
3)合成析构函数
4.三/五法则
1)需要析构函数的类也需要拷贝和赋值操作
下面的例子要用到的HasPtr类。
解释:因为合成构造函数会简单拷贝不同对象的ps,这些ps均指向相同的地址,在调用delete ps时将会清楚ps所指向的空间,但因为简单拷贝会不断清除ps所指的空间以造成灾难。
2)需要拷贝构造的类也需要赋值操作,反之亦然
5.使用=default
6.阻止拷贝
当拷贝不重要甚至会产生负面影响了,那么我们要想办法怎么阻止拷贝。
1)定义删除的函数
通过定义删除函数可以抑制相应函数的合成。
2)析构函数是不能删除的成员
3)合成的拷贝构造成员可能是删除的
个人感觉这一块相当重要,都是我们不知不觉中容易遇到的坑。
4)private拷贝控制
private是老版的拷贝控制方法了,咱们还是用=delete更方便。
二、拷贝控制与资源管理
接下来通过介绍例子来讲明这两种类是如何进行拷贝控制与资源管理的。举的例子相当经典,值得细细品味。
关于此涉及到了深拷贝与浅拷贝的知识,可参考:C++深拷贝和浅拷贝(深复制和浅复制)完全攻略
1.行为像值的类
上面举的例子告诉了我们行为像值的类的析构函数和拷贝构造函数是要怎么定义的。
1)类值拷贝赋值运算符
2.定义行为像指针的类
通过引入引用计数以模拟类似shared_ptr的效果。
1)引用计数
行为像指针的类远比行为像值的类更复杂。
2)定义一个使用引用计数的类
介绍行为像指针的类的常规构造函数与拷贝构造函数。难点在于其对use指针的操作
3)类指针的成员篡改拷贝构造函数
介绍行为像指针的类的析构函数以及拷贝赋值函数。难点在于何时对指针空间进行释放。
三、交换操作
1.编写自己的swap函数
写一个自己的swap函数专门处理自定义类,可以有效对自定义类的交换进行优化。
2.swap函数应该调用swap而非std::swap
其实就是在提醒我们要注意,别调用到标准库中的swap函数了,要调用自己重新编写的swap函数。
3.在赋值运算符中使用swap
即在重载赋值运算符时用swap功能完成拷贝赋值功能。
四、拷贝控制示例
主要在介绍例子,先暂且略过
五、动态内存管理类
主要在介绍例子,先暂且略过
六、对象移动
先打个预防针,这节是本章最难的内容。
如果可以直接移动空间,那么显然这比先拷贝再删除空间快得多。
1.右值引用
右值引用即只能绑定到右值上的引用,而左值引用为常规引用,只是为了跟右值区分,所以叫左值引用。
关于左值右值:
1)变量是左值
右值引用的对象是左值,但是在定义的时候是有限制条件的,只能绑定右值
2)标准库move函数
int r = 1;
int& r1 = r;
int&& r2 = std::move(r1);
cout << r1<<endl<<r<<endl<<r2<<endl;
r = 5;
cout << r1 << endl << r << endl << r2 << endl;
int r3 = r1;
使用move之后,r、r1、r2均指向一个空间。
关于move函数,进一步参考:C++11 move()函数:将左值强制转换为右值
2.移动构造函数和移动赋值运算符
个人理解:移动构造的本质是在复制地址。其与拷贝构造的区别在于对指针成员,移动构造直接采用浅拷贝即可。
1)移动操作、标准库容器和异常
testcopy(testcopy&&)noexcept;//声明
testcopy::testcopy(testcopy&& t) noexcept:val(t.val)//初始化器
{
cout << "move" << endl;
}
关于移动构造函数可进一步参考:C++11移动构造函数详解
2)移动构造运算符
testcopy& operator=(testcopy&&)noexcept;
testcopy& testcopy::operator=(testcopy&& t) noexcept
{
val = t.val;
cout << "移动=" << endl;
return *this;
}
3)移后源对象必须可析构
4)合成的移动操作
5)移动右值,拷贝左值
等号右边为右值则为移动构造,为左值则为拷贝构造。
6)没有移动构造函数,右值也要被拷贝
7)拷贝并交换赋值运算符和移动操作
五个即:拷贝构造、拷贝赋值、析构、移动构造、移动赋值。
8)Message类的移动操作
举例说明移动构造的好处。
9)移动迭代器
移动赋值虽然效率更高,但是脑子不清醒的话还是尽量少用吧。
3.右值引用和成员函数
不仅构造函数和赋值函数是拷贝或者移动的,其他的普通成员函数也可以定义为拷贝或是移动的。
1)右值和左值引用成员函数
2)重载和引用函数
主要在讲一些注意事项,值得注意。
最后大致总结一下:本章其实主要在介绍&与&&