前言
最近在写代码的过程中,发现一个大家容易忽略的知识点:深拷贝和浅拷贝。
可能对于Java程序员来说,很少遇到深浅拷贝问题,但是对于C++程序员来说可谓是又爱又恨。。
浅拷贝:
-
1.将原对象或者原对象的引用直接赋值给新对象,新对象,新数组只是原对象的一个引用。
-
2.C++默认的拷贝构造函数与赋值运算符重载都是浅拷贝,可以节省一定空间,但是可能会引发同一块内存重复释放问题,
二次释放内存可能导致严重的异常崩溃等情况。 -
浅拷贝模型:
深拷贝:
-
1.创建一个新的对象或者数组,将对象或者数组的属性值拷贝过来,注意此时新对象指向的不是原对象的引用而是原对象的值,新对象在堆中有自己的地址空间。
-
2.浪费空间,但是不会引发浅拷贝中的资源重复释放问题。
-
深拷贝模型
案例分析
下面使用一个案例来看下一个因为浅拷贝带来的bug。
#include "DeepCopy.h"
#include <iostream>
#include <string>
using namespace std;
class Human {
public:
Human(int age):_age(age) {
}
int _age;;
};
class String {
public:
String(Human* pHuman){
this->pHuman = pHuman;
}
~String() {
delete pHuman;
}
Human* pHuman;
};
void DeepCopy::main()
{
Human* p = new Human(100);
String s1(p);
String s2(s1);
}
这个程序从表面看是不会有啥问题的,运行后,出现如下错误:

先说下原因:
这个错误就是由于代码 String s2(s1) 会调用String的默认拷贝构造函数,而默认的拷贝构造函数使用的是浅拷贝,即仅仅只是对新的指针对象pHuman指向原指针对象pHuman指向的地址。
在退出main函数作用域后,会回调s1和s2的析构函数,当回调s2析构函数后,s2中的pHuman内存资源被释放,此时再回调s1,也会回调s1中的pHuman析构函数,可是此时的pHuman指向的地址
已经在s2中被释放了,造成了二次释放内存,出现了崩溃的情况。
所以为了防止出现二次释放内存的情况,需要使用深拷贝。
深拷贝需要重写拷贝构造函数以及赋值运算符重载,且在拷贝构造内部重新去new一个对象资源.
代码如下:
#include "DeepCopy.h"
#include <iostream>
#include <string>
using namespace std;
class Human {
public:
Human(int age):_age(age) {
```
}
int _age;;
```
};
class String {
public:
String(Human* pHuman){
this->pHuman = pHuman;
}
//重写拷贝构造,实现深拷贝,防止二次释放内存引发崩溃
String(const String& str) {
pHuman = new Human(str.pHuman->_age);
}
~String() {
delete pHuman;
}
Human* pHuman;
};
void DeepCopy::main()
{
Human* p = new Human(100);
String s1(p);
String s2(s1);
}
默认情况下使用:
String s2(s1)或者String s2 = s1 这两种方式去赋值,就会调用String的拷贝构造方法,如果没有实现,则会执行默认的拷贝构造,即浅拷贝。
可以在拷贝构造函数中使用new重新对指针进行资源分配,达到深拷贝的要求、
说了这么多只要记住一点:如果类中有成员变量是指针的情况下,就需要自己去实现深拷贝。
虽然深拷贝可以帮助我们防止出现二次内存是否的问题,但是其会浪费一定空间,如果对象中资源较大,拿每个对象都包含一个大对象,这不是一个很好的设计,而浅拷贝就没这个问题。
那么有什么方法可以兼容他们的优点么?即不浪费空间也不会引起二次内存释放?
兼容优化方案:
-
1.引用计数方式
-
2.使用move语义转移
引用计数
我们对资源增加一个引用计数,在构造函数以及拷贝构造函数中让计数+1,在析构中让计数-1.当计数为0时,才会去释放资源,这是一个不错的注意。
如图所示:

对应代码如下:
#include "DeepCopy.h"
#include <iostream>
#include <string>
using namespace std;
class Human {
public:
Human(int age):_age(age) {
}
int _age;;
};
class String {
public:
String() {
addRefCount();
}
String(Human* pHuman){
this->pHuman = pHuman;
addRefCount();
}
//重写拷贝构造,实现深拷贝,防止二次释放内存引发崩溃
String(const String& str) {
////深拷贝
//pHuman = new Human(str.pHuman->_age);
//浅拷贝
pHuman = str.pHuman;
addRefCount();
}
~String() {
subRefCount();
if (getRefCount() <= 0) {
delete pHuman;
}
}
Human* pHuman;
private:
void addRefCount() {
refCount++;
}
void subRefCount() {
refCount--;
}
int getRefCount() {
return refCount;
}
static int refCount;
};
int String::refCount = 0;
void DeepCopy::main()
{
Human* p = new Human(100);
String s1(p);
String s2 = s1;
}
此时的拷贝构造函数使用了浅拷贝对成员对象进行赋值,且只有在引用计数为0的情况下才会进行资源释放。
但是引用计数的方式会出现循环引用的情况,导致内存无法释放,发生内存泄露。
循环引用模型如下:

我们知道在C++的智能指针shared_ptr中就使用了引用计数:
类似java中对象垃圾的定位方法,如果有一个指针引用某块内存,则引用计数+1,释放计数-1.如果引用计数为0,则说明这块内存可以释放了。
下面我们写个shared_ptr循环引用的情况:
class A {
public:
shared_ptr<B> pa;
```
~A() {
cout << "~A" << endl;
}
```
};
class B {
public:
shared_ptr<A> pb;
```
~B() {
cout << "~B" << endl;
}
```
};
void sharedPtr() {
shared_ptr<A> a(new A());
shared_ptr<B> b(new B());
cout << "第一次引用:" << endl;
cout <<"计数a:" << a.use_count() << endl;
cout << "计数b:" << b.use_count() << endl;
a->pa = b;
b->pb = a;
cout << "第二次引用:" << endl;
cout << "计数a:" << a.use_count() << endl;
cout << "计数b:" << b.use_count() << endl;
}
运行结果:
第一次引用:
计数a:1
计数b:1
第二次引用:
计数a:2
计数b:2
可以看到运行结果并没有打印出对应的析构函数,也就是没被释放。
指针a和指针b是栈上的,当退出他们的作用域后,引用计数会-1,但是其计数器数是2,所以还不为0,也就是不能被释放。你不释放我,我也不释放你,咱两耗着呗。
这就是标志性的由于循环引用计数导致的内存泄露.。所以我们在设计深浅拷贝代码的时候千万别写出循环引用的情况。
move语义转移
在C++11之前,如果要将源对象的状态转移到目标对象只能通过复制。
而现在在某些情况下,我们没有必要复制对象,只需要移动它们。
C++11引入移动语义:
源对象资源的控制权全部交给目标对象。注意这里说的是控制权,即使用一个新的指针对象去指向这个对象,然后将原对象的指针置为nullptr
模型如下:

要实现move语义,需要实现移动构造函数
代码如下:
//移动语义move
class Human {
public:
Human(int age) :_age(age) {
```
}
int _age;;
```
};
class String {
public:
```
String(Human* pHuman) {
this->pHuman = pHuman;
}
//重写拷贝构造,实现深拷贝,防止二次释放内存引发崩溃
String(const String& str) {
////深拷贝
//pHuman = new Human(str.pHuman->_age);
//浅拷贝
pHuman = str.pHuman;
}
//移动构造函数
String(String&& str) {
pHuman = str.pHuman;
str.pHuman = NULL;
}
~String() {
if (pHuman != NULL) {
delete pHuman;
}
}
Human* pHuman;
```
};
void DeepCopy::main()
{
Human* p = new Human(100);
String s1(p);
```
String s2(std::move(s1));
String s3(std::move(s2));
```
}
该案例中,指针p的权限会由s1让渡给s2,s2再让渡给s3.
使用move语义转移在C++中使用还是比较频繁的,因为其可以大大缩小因为对象对象的创建导致内存吃紧的情况。比较推荐应用中使用这种方式来优化内存方面问题.
总结
本篇文章主要讲解了C++面向对象编程中的深拷贝和浅拷贝的问题,以及使用引用计数和move语义转移的方式来优化深浅拷贝的问题。
C++不像Java那样,JVM都给我们处理好了资源释放的问题,没有二次释放导致的崩溃情况,C++要懂的东西远非Java可比,这也是为什么C++程序员那么少的原因之一吧。
这篇文章就讲解到这了,我是小余,我们下期见。
原文始发于微信公众号(小余的自习室):【重学C/C++系列(九)】:深拷贝和浅拷贝的那点事
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/126998.html