C++拷贝控制
实践才是检验真理的唯一标准。多看、多敲、多写、多练。
本文章是对C++ primer中第13章做出的一个简短的总结和一些扩展
一个类定义五种特殊的成员函数来控制对象的拷贝、移动、赋值和销毁。拷贝构造函数、拷贝赋值运算符、移动构造、移动赋值运算符和析构函数我们称这些操作为拷贝控制操作
什么是构造函数?
构造函数就是一个方法名与类名相同,没用返回值,且可以重载的函数,在C++中,如果你没有写构造函数,编译器会自动生成一个无参构造函数,不过需要注意的是,如果类中存在一个有参构造,那么是不会生成一个无参构造的。
class FoolishStudent {
private:
int *m_num;
public:
//无参构造函数
FoolishStudent()
:m_num(new int()){}
//有参构造函数
FoolishStudent(int num)
:m_num(new int())
{
*m_num = num;
}
};
什么是拷贝构造函数?
拷贝构造函数是一个参数是自身类类型的引用,且额外参数都有默认值,如果类中没有写拷贝构造函数,编译器也会自动给你生成一个拷贝构造函数不过自动生成的是浅拷贝
//拷贝构造函数
FoolishStudent(const FoolishStudent& foolish)
:m_num(new int(*foolish.m_num))
{
}
为什么参数要加const关键字?
这只是一个编码习惯,在该函数中,我们不会对形参的值进行改变,所以会加一个const关键字
为什么要使用引用传递参数?
如果这里不是引用传递是造成递归调用,并且没有退出条件,就会出现爆栈导致程序异常退出。我们假设这里不是引用传递,那么程序是怎么运行的?程序会调用拷贝构造函数,因为拷贝构造函数里面是一个非引用参数,所以还会调用拷贝构造函数…久而久之栈空间满了,就异常退出了。
什么是浅拷贝?
浅拷贝也就是对值的拷贝,问题的关键就是出在指针上,因为创建在堆空间的变量都要考虑内存释放的问题
#include <iostream>
class FoolishStudent {
public:
int *m_num;
//无参构造函数
FoolishStudent()
:m_num(new int()){}
//析构函数
~FoolishStudent()
{
delete m_num;
m_num = nullptr;
}
//拷贝构造函数
FoolishStudent(const FoolishStudent& foolish)
:m_num(foolish.m_num)
{
}
};
int main()
{
FoolishStudent *a = new FoolishStudent();
FoolishStudent b(*a);
*b.m_num = 100;
delete a;
std::cout << *b.m_num << std::endl;
}
在输出执行输出语句的时候程序就崩溃了。这是为什么呢?因为a和b的指针成员变量指向了同一块内存空间,a已经释放掉内存了,但是b还在访问,所以就出现了问题,这就是浅拷贝存在最大的问题,内存释放问题。
什么是深拷贝?
最开始我们写的拷贝构造函数就是深拷贝,区别在于深拷贝遇到指针类似的问题,会去申请内存空间,两个指针指向不同的区域,从而避免了访问一块释放过的内存
什么是拷贝赋值运算符?
赋值运算符通常组合了析构函数和构造函数的操作。类似析构函数,赋值操作会销毁左侧运算对象的资源。类似拷贝构造函数,赋值操作会从右侧运算对象拷贝数据。笔者在未看C++ Primer之前,一直觉得在拷贝赋值运算符中不需要分配空间,因为在构造函数中已经分配空了。但是错了。在构造函数中并不一定会分配空间,会给默认值nullptr,这个时候就会出现bug,具体是什么bug,留下悬念自己思考。
//拷贝赋值运算符
FoolishStudent& operator=(const FoolishStudent & foolish)
{
auto temp = new int(*foolish.m_num);
delete this->m_num;
this->m_num = temp;
return *this;
}
什么时候调用过拷贝构造,什么时候调用拷贝赋值运算符?
这里是笔者自己总结的一些知识点,并不一定正确,需要读者自己思考。
FoolishStudent *a = new FoolishStudent(); //调用无参构造
FoolishStudent b = *a; //调用拷贝构造
FoolishStudent c; //调用无参构造
c = *a; //调用移动赋值运算符
调用过无参构造或者有参构造的变量再使用=运算符就会调用拷贝赋值运算符;你也可以理解成有类型名的调用的是拷贝构造
什么是移动构造函数?
在C++11新标准中,新增了右值引用&&,导致构造函数有了更多的可玩性。笔者对移动构造函数的概述是共用一块内存地址
//移动构造函数
FoolishStudent(FoolishStudent&& foolish) noexcept
:m_num(foolish.m_num)
{
foolish.m_num = nullptr;
}
代码就显得言简意赅了,就是把一块即将要释放掉的内存拿过来直接用,这样较少了拷贝的次数从而提高效率,切记一定要给形参指针类型赋值nullptr,不然调用析构函数内存就被释放了
为什么要使用noexcept关键字
在移动构造函数中是不可以有异常的,因为销毁的对象会执行析构函数,如果抛出异常,赋值为nullptr就有可能中止,执行析构函数内存就被释放掉了
移动复制运算符
移动赋值运算符执行与析构函数和移动构造函数相同的工作。与移动构造函数一样,如果我们的移动赋值运算符不抛出任何异常,我们就应该将它标记为noexcept。类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值
//移动赋值运算符
FoolishStudent &operator=(FoolishStudent&& foolish) noexcept
{
if (this != &foolish)
{
delete this->m_num;
this->m_num = foolish.m_num;
foolish.m_num = nullptr;
}
return *this;
}
与移动构造函数是非相似,不做过多的描述