C++注解|类中的浅拷贝与深拷贝
类与深拷贝与浅拷贝测试
测试构建类中的成员变量该类的指针,所出现的问题,与解决过程。
编译器 GCC lastest x64
系统 Linux Ubuntu22LTS
错误示例一
1 | class A |
在执行简单的拷贝时,我们使用编译器默认提供的拷贝构造函数。
编译的时候,确实没问题啊,但是一运行就开始报错了。返回一个段错误
众所周知段错误通常是因为访问了非法的内存而导致的。
不过这个问题的根本原因并不是调用了原本的构造函数。而是:在类的初始化构造函数中初始化了该类的指针。
由于new会调用构造函数,在构造函数中,又会在调用一次构造函数。而构造函数中又初始化了…….如此这般递归套娃,无限调用导致内存溢出?最终访问了非法的内存。
我们将初始化为空指针,心想这些构造函数总不能出问题了吧。
接着我们来到了错误示例二
错误示例二
1 |
|
出错问题这下定位到了析构函数上。可以发现堆栈调用了两次析构函数,但是第二次析构出现的段错误。
我们都知道编译器在类中默认提供一个拷贝构造。A(const A& ) ,当你没有定义时,就会提供,该默认拷贝构造实际就是一个简单的浅拷贝
所有值复制一遍给新的实例,指针成员变量也是同样的地址, 在析构时候呢,当然就会出现多次析构同一个内存对象问题。
析构顺序,当然是先进后出,第一次a先执行依次析构函数,由于test指针中的地址与a是相等的,都指向同一块内存,析构的时候,先把指向的内存给先析构了。到了b再来执行析构函数的时候由于test 的值还是原来的地址值,就会在调用依次析构delete, 二次delete同一个内存肯定是要出事的。
所以解决方法就是:我们主动提供一个拷贝构造函数
实现一个深拷贝。
1 | A(const A& a) { |
这下不报错了,可以正常运行。
额外测试
我们都知道,编译器除了提供一个默认的拷贝构造以外,C+11之后还会默认提供一个operator=(const A&)函数(其实也是一个简单的浅拷贝!)。所以我们来测试一个简单的函数然回值为类
1 | A func() { |
我们在main函数调用
1 | A a; |
乍一眼, 可能会觉得奇怪,函数的返回值不是一个空么,不是一个妥妥的左值,为啥还能编译通过呢
可以观察到编译器给出了一个提示:
1 | warning: implicitly-declared ‘constexpr A& A::operator=(const A&)’ is deprecated [-Wdeprecated-copy] |
可以发现确实编译器提供了一个返回引用的重载函数,所以左值又变右值了。
但是这下我们运行该程序,析构函数又报错了,而且观察堆栈,发现居然调用了三次析构函数,这是怎么回事捏,让我们来解析一下
1 | func() = a; |
到底发生了啥。
首先先调用 了一个有参构造,说明,先执行的是函数。(栈上)
然后又调用了一个无参构造,是有参构造里的成员的变量的无参初始化(堆上)。然后 将a 复制给这个匿名对象。
注意:
当这条语句执行完毕,跳到下一条语句,执行析构,先析构栈上的,然后再析构 匿名对象指针指向的内存地址,此时该地址是a 对象的地址。
所以,当最后要析构(第三次析构)a对象了里的地址时,就又出现了二次delete!
解决方案:
所以我们需要在主动自己提供一个operator=函数,将
1 | A& operator=(const A& s) { |
重新编译运行没有问题!这下就解决了重载= 的浅拷贝的问题。
tip:在类中成员变量如果有指针,必须保证用户提供正确的重载= 操作符函数 和 拷贝构造函数!
最后问题
额外测试中似乎已经给出完美答案了,但是实际上,我在继续测试的时候发现,利用内存泄露检测工具valgrind 发现了内存泄露,这是怎么一回事呢?
1 | valgrind --tool=memcheck --log-file=mem.log --leak-check=full ./outDebug |
定位发现,居然是func函数中构造的临时对象中申请的内存没有释放。
1 | func() = a; |
func()中明确了先调用其中的a的构造有参构造函数,而有参构造函数中又new了一次调用的无参构造。
然后又调用了一次重载运算符,由于我们提供了修改的重载运算符,所以这次会再次分配了一次内存,指向了新的地址,之前的地址就再也没有关联所以,自动析构的时候就会无视该地址,最终导致内存溢出!。
问题根源如此,解决方式已然清晰明了。
再修改重载=运算符函数
1 | A& operator=(const A& s) { |
最后我们再进行一次编译运行内存检测:
1 | ==66394== Memcheck, a memory error detector |
再无错误,问题结束。