前言
最近在寫代碼的過程中,發(fā)現(xiàn)一個大家容易忽略的知識點: 深拷貝和淺拷貝 。
可能對于Java程序員來說,很少遇到深淺拷貝問題,但是對于C++程序員來說可謂是又愛又恨。。
淺拷貝:
-
1.將原對象或者原對象的引用直接賦值給新對象,新對象,新數(shù)組只是原對象的一個引用。
-
2.C++默認(rèn)的拷貝構(gòu)造函數(shù)與賦值運算符重載都是淺拷貝,可以節(jié)省一定空間,但是可能會引發(fā)同一塊內(nèi)存重復(fù)釋放問題,
二次釋放內(nèi)存可能導(dǎo)致嚴(yán)重的異常崩潰等情況。
-
淺拷貝模型:

深拷貝:
-
1.創(chuàng)建一個新的對象或者數(shù)組,將對象或者數(shù)組的屬性值拷貝過來,注意此時新對象指向的不是原對象的引用而是原對象的值,新對象在堆中有自己的地址空間。
-
2.浪費空間,但是不會引發(fā)淺拷貝中的資源重復(fù)釋放問題。
-
深拷貝模型

案例分析
下面使用一個案例來看下一個因為淺拷貝帶來的bug。
#include "DeepCopy.h"
#include
#include
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);
}
這個程序從表面看是不會有啥問題的,運行后,出現(xiàn)如下錯誤:

先說下原因 :
這個錯誤就是由于代碼 String s2(s1) 會調(diào)用String的默認(rèn)拷貝構(gòu)造函數(shù),而 默認(rèn)的拷貝構(gòu)造函數(shù)使用的是淺拷貝,即僅僅只是對新的指針對象pHuman指向原指針對象pHuman指向的地址 。
在退出main函數(shù)作用域后,會回調(diào)s1和s2的析構(gòu)函數(shù),當(dāng)回調(diào)s2析構(gòu)函數(shù)后,s2中的pHuman內(nèi)存資源被釋放,此時再回調(diào)s1,也會回調(diào)s1中的pHuman析構(gòu)函數(shù),可是此時的pHuman指向的地址
已經(jīng)在s2中被釋放了,造成了二次釋放內(nèi)存,出現(xiàn)了崩潰的情況 。
所以為了防止出現(xiàn)二次釋放內(nèi)存的情況,需要使用深拷貝 。
深拷貝需要重寫拷貝構(gòu)造函數(shù)以及賦值運算符重載,且在拷貝構(gòu)造內(nèi)部重新去new一個對象資源.
代碼如下:
#include "DeepCopy.h"
#include
#include
using namespace std;
class Human {
public:
Human(int age):_age(age) {
}
int _age;;
};
class String {
public:
String(Human* pHuman){
this->pHuman = pHuman;
}
//重寫拷貝構(gòu)造,實現(xiàn)深拷貝,防止二次釋放內(nèi)存引發(fā)崩潰
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);
}
默認(rèn)情況下使用:
String s2(s1)或者String s2 = s1 這兩種方式去賦值,就會調(diào)用String的拷貝構(gòu)造方法,如果沒有實現(xiàn),則會執(zhí)行默認(rèn)的拷貝構(gòu)造,即淺拷貝。
可以在拷貝構(gòu)造函數(shù)中使用new重新對指針進(jìn)行資源分配,達(dá)到深拷貝的要求、
說了這么多只要記住一點: 如果類中有成員變量是指針的情況下,就需要自己去實現(xiàn)深拷貝 。
雖然深拷貝可以幫助我們防止出現(xiàn)二次內(nèi)存是否的問題,但是其會浪費一定空間,如果對象中資源較大,拿每個對象都包含一個大對象,這不是一個很好的設(shè)計,而淺拷貝就沒這個問題。
那么有什么方法可以兼容他們的優(yōu)點么? 即不浪費空間也不會引起二次內(nèi)存釋放 ?
兼容優(yōu)化方案:
- 1.引用計數(shù)方式
- 2.使用move語義轉(zhuǎn)移
引用計數(shù)
我們對資源增加一個引用計數(shù),在構(gòu)造函數(shù)以及拷貝構(gòu)造函數(shù)中讓計數(shù)+1,在析構(gòu)中讓計數(shù)-1.當(dāng)計數(shù)為0時,才會去釋放資源,這是一個不錯的注意。
如圖所示:

對應(yīng)代碼如下:
#include "DeepCopy.h"
#include
#include
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();
}
//重寫拷貝構(gòu)造,實現(xiàn)深拷貝,防止二次釋放內(nèi)存引發(fā)崩潰
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;
}
此時的拷貝構(gòu)造函數(shù)使用了淺拷貝對成員對象進(jìn)行賦值,且 只有在引用計數(shù)為0的情況下才會進(jìn)行資源釋放 。
但是引用計數(shù)的方式會出現(xiàn)循環(huán)引用的情況,導(dǎo)致內(nèi)存無法釋放,發(fā)生 內(nèi)存泄露 。
循環(huán)引用模型如下:

我們知道在C++的 智能指針shared_ptr中就使用了引用計數(shù) :
類似java中對象垃圾的定位方法,如果有一個指針引用某塊內(nèi)存,則引用計數(shù)+1,釋放計數(shù)-1.如果引用計數(shù)為0,則說明這塊內(nèi)存可以釋放了。
下面我們寫個shared_ptr循環(huán)引用的情況:
class A {
public:
shared_ptr pa;
**
~A() {
cout << "~A" << endl;
}
};
class B {
public:
shared_ptr pb;
~B() {
cout << "~B" << endl;
}
};
void sharedPtr() {
shared_ptr a(new A());
shared_ptr b(new B());
cout << "第一次引用:" << endl;
cout <<"計數(shù)a:" << a.use_count() << endl;
cout << "計數(shù)b:" << b.use_count() << endl;
a->pa = b;
b->pb = a;
cout << "第二次引用:" << endl;
cout << "計數(shù)a:" << a.use_count() << endl;
cout << "計數(shù)b:" << b.use_count() << endl;
}
運行結(jié)果:
第一次引用:
計數(shù)a:1
計數(shù)b:1
第二次引用:
計數(shù)a:2
計數(shù)b:2
[**
可以看到運行結(jié)果并沒有打印出對應(yīng)的析構(gòu)函數(shù),也就是沒被釋放。
指針a和指針b是棧上的,當(dāng)退出他們的作用域后,引用計數(shù)會-1,但是其計數(shù)器數(shù)是2,所以還不為0,也就是不能被釋放。你不釋放我,我也不釋放你,咱兩耗著唄。
這就是標(biāo)志性的由于循環(huán)引用計數(shù)導(dǎo)致的內(nèi)存泄露.。所以 我們在設(shè)計深淺拷貝代碼的時候千萬別寫出循環(huán)引用的情況 。
move語義轉(zhuǎn)移
在C++11之前,如果要將源對象的狀態(tài)轉(zhuǎn)移到目標(biāo)對象只能通過復(fù)制。
而現(xiàn)在在某些情況下,我們沒有必要復(fù)制對象,只需要移動它們。
C++11引入移動語義 :
源對象資源的控制權(quán)全部交給目標(biāo)對象。注意這里說的是控制權(quán),即使用一個新的指針對象去指向這個對象,然后將原對象的指針置為nullptr
模型如下:

要實現(xiàn)move語義,需要實現(xiàn)移動構(gòu)造函數(shù)
代碼如下:
//移動語義move
class Human {
public:
Human(int age) :_age(age) {
}
int _age;;
};
class String {
public:
String(Human* pHuman) {
this->pHuman = pHuman;
}
//重寫拷貝構(gòu)造,實現(xiàn)深拷貝,防止二次釋放內(nèi)存引發(fā)崩潰
String(const String& str) {
////深拷貝
//pHuman = new Human(str.pHuman->_age);
//淺拷貝
pHuman = str.pHuman;
}
//移動構(gòu)造函數(shù)
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的權(quán)限會由s1讓渡給s2,s2再讓渡給s3 .
使用move語義轉(zhuǎn)移在C++中使用還是比較頻繁的,因為其可以大大縮小因為對象對象的創(chuàng)建導(dǎo)致內(nèi)存吃緊的情況。比較推薦應(yīng)用中使用這種方式來優(yōu)化內(nèi)存方面問題.
總結(jié)
本篇文章主要講解了C++面向?qū)ο?a target="_blank">編程中的深拷貝和淺拷貝的問題,以及使用引用計數(shù)和move語義轉(zhuǎn)移的方式來優(yōu)化深淺拷貝的問題。
C++不像Java那樣,JVM都給我們處理好了資源釋放的問題,沒有二次釋放導(dǎo)致的崩潰情況, C++要懂的東西遠(yuǎn)非Java可比,這也是為什么C++程序員那么少的原因之一吧 。
]()
-
JAVA
+關(guān)注
關(guān)注
20文章
2997瀏覽量
116058 -
C++
+關(guān)注
關(guān)注
22文章
2123瀏覽量
76873 -
面向?qū)ο缶幊?/span>
+關(guān)注
關(guān)注
0文章
22瀏覽量
2142
發(fā)布評論請先 登錄
基于C/C++面向對象的方式封裝socket通信類
Python如何防止數(shù)據(jù)被修改Python中的深拷貝與淺拷貝的問題說明
C++:詳談拷貝構(gòu)造函數(shù)
C++面向?qū)ο缶幊讨械纳羁截惡蜏\拷貝
評論