前言
最近在寫代碼的過程中,發(fā)現(xiàn)一個(gè)大家容易忽略的知識(shí)點(diǎn): 深拷貝和淺拷貝 。
可能對(duì)于Java程序員來說,很少遇到深淺拷貝問題,但是對(duì)于C++程序員來說可謂是又愛又恨。。
淺拷貝:
-
1.將原對(duì)象或者原對(duì)象的引用直接賦值給新對(duì)象,新對(duì)象,新數(shù)組只是原對(duì)象的一個(gè)引用。
-
2.C++默認(rèn)的拷貝構(gòu)造函數(shù)與賦值運(yùn)算符重載都是淺拷貝,可以節(jié)省一定空間,但是可能會(huì)引發(fā)同一塊內(nèi)存重復(fù)釋放問題,
二次釋放內(nèi)存可能導(dǎo)致嚴(yán)重的異常崩潰等情況。
-
淺拷貝模型:
深拷貝:
-
1.創(chuàng)建一個(gè)新的對(duì)象或者數(shù)組,將對(duì)象或者數(shù)組的屬性值拷貝過來,注意此時(shí)新對(duì)象指向的不是原對(duì)象的引用而是原對(duì)象的值,新對(duì)象在堆中有自己的地址空間。
-
2.浪費(fèi)空間,但是不會(huì)引發(fā)淺拷貝中的資源重復(fù)釋放問題。
-
深拷貝模型
案例分析
下面使用一個(gè)案例來看下一個(gè)因?yàn)闇\拷貝帶來的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);
}
這個(gè)程序從表面看是不會(huì)有啥問題的,運(yùn)行后,出現(xiàn)如下錯(cuò)誤:
先說下原因 :
這個(gè)錯(cuò)誤就是由于代碼 String s2(s1) 會(huì)調(diào)用String的默認(rèn)拷貝構(gòu)造函數(shù),而 默認(rèn)的拷貝構(gòu)造函數(shù)使用的是淺拷貝,即僅僅只是對(duì)新的指針對(duì)象pHuman指向原指針對(duì)象pHuman指向的地址 。
在退出main函數(shù)作用域后,會(huì)回調(diào)s1和s2的析構(gòu)函數(shù),當(dāng)回調(diào)s2析構(gòu)函數(shù)后,s2中的pHuman內(nèi)存資源被釋放,此時(shí)再回調(diào)s1,也會(huì)回調(diào)s1中的pHuman析構(gòu)函數(shù),可是此時(shí)的pHuman指向的地址
已經(jīng)在s2中被釋放了,造成了二次釋放內(nèi)存,出現(xiàn)了崩潰的情況 。
所以為了防止出現(xiàn)二次釋放內(nèi)存的情況,需要使用深拷貝 。
深拷貝需要重寫拷貝構(gòu)造函數(shù)以及賦值運(yùn)算符重載,且在拷貝構(gòu)造內(nèi)部重新去new一個(gè)對(duì)象資源.
代碼如下:
#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)造,實(shí)現(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 這兩種方式去賦值,就會(huì)調(diào)用String的拷貝構(gòu)造方法,如果沒有實(shí)現(xiàn),則會(huì)執(zhí)行默認(rèn)的拷貝構(gòu)造,即淺拷貝。
可以在拷貝構(gòu)造函數(shù)中使用new重新對(duì)指針進(jìn)行資源分配,達(dá)到深拷貝的要求、
說了這么多只要記住一點(diǎn): 如果類中有成員變量是指針的情況下,就需要自己去實(shí)現(xiàn)深拷貝 。
雖然深拷貝可以幫助我們防止出現(xiàn)二次內(nèi)存是否的問題,但是其會(huì)浪費(fèi)一定空間,如果對(duì)象中資源較大,拿每個(gè)對(duì)象都包含一個(gè)大對(duì)象,這不是一個(gè)很好的設(shè)計(jì),而淺拷貝就沒這個(gè)問題。
那么有什么方法可以兼容他們的優(yōu)點(diǎn)么? 即不浪費(fèi)空間也不會(huì)引起二次內(nèi)存釋放 ?
兼容優(yōu)化方案:
- 1.引用計(jì)數(shù)方式
- 2.使用move語義轉(zhuǎn)移
引用計(jì)數(shù)
我們對(duì)資源增加一個(gè)引用計(jì)數(shù),在構(gòu)造函數(shù)以及拷貝構(gòu)造函數(shù)中讓計(jì)數(shù)+1,在析構(gòu)中讓計(jì)數(shù)-1.當(dāng)計(jì)數(shù)為0時(shí),才會(huì)去釋放資源,這是一個(gè)不錯(cuò)的注意。
如圖所示:
對(duì)應(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)造,實(shí)現(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;
}
此時(shí)的拷貝構(gòu)造函數(shù)使用了淺拷貝對(duì)成員對(duì)象進(jìn)行賦值,且 只有在引用計(jì)數(shù)為0的情況下才會(huì)進(jìn)行資源釋放 。
但是引用計(jì)數(shù)的方式會(huì)出現(xiàn)循環(huán)引用的情況,導(dǎo)致內(nèi)存無法釋放,發(fā)生 內(nèi)存泄露 。
循環(huán)引用模型如下:
我們知道在C++的 智能指針shared_ptr中就使用了引用計(jì)數(shù) :
類似java中對(duì)象垃圾的定位方法,如果有一個(gè)指針引用某塊內(nèi)存,則引用計(jì)數(shù)+1,釋放計(jì)數(shù)-1.如果引用計(jì)數(shù)為0,則說明這塊內(nèi)存可以釋放了。
下面我們寫個(gè)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 <<"計(jì)數(shù)a:" << a.use_count() << endl;
cout << "計(jì)數(shù)b:" << b.use_count() << endl;
a->pa = b;
b->pb = a;
cout << "第二次引用:" << endl;
cout << "計(jì)數(shù)a:" << a.use_count() << endl;
cout << "計(jì)數(shù)b:" << b.use_count() << endl;
}
運(yùn)行結(jié)果:
第一次引用:
計(jì)數(shù)a:1
計(jì)數(shù)b:1
第二次引用:
計(jì)數(shù)a:2
計(jì)數(shù)b:2
[**
可以看到運(yùn)行結(jié)果并沒有打印出對(duì)應(yīng)的析構(gòu)函數(shù),也就是沒被釋放。
指針a和指針b是棧上的,當(dāng)退出他們的作用域后,引用計(jì)數(shù)會(huì)-1,但是其計(jì)數(shù)器數(shù)是2,所以還不為0,也就是不能被釋放。你不釋放我,我也不釋放你,咱兩耗著唄。
這就是標(biāo)志性的由于循環(huán)引用計(jì)數(shù)導(dǎo)致的內(nèi)存泄露.。所以 我們?cè)谠O(shè)計(jì)深淺拷貝代碼的時(shí)候千萬別寫出循環(huán)引用的情況 。
move語義轉(zhuǎn)移
在C++11之前,如果要將源對(duì)象的狀態(tài)轉(zhuǎn)移到目標(biāo)對(duì)象只能通過復(fù)制。
而現(xiàn)在在某些情況下,我們沒有必要復(fù)制對(duì)象,只需要移動(dòng)它們。
C++11引入移動(dòng)語義 :
源對(duì)象資源的控制權(quán)全部交給目標(biāo)對(duì)象。注意這里說的是控制權(quán),即使用一個(gè)新的指針對(duì)象去指向這個(gè)對(duì)象,然后將原對(duì)象的指針置為nullptr
模型如下:
要實(shí)現(xiàn)move語義,需要實(shí)現(xiàn)移動(dòng)構(gòu)造函數(shù)
代碼如下:
//移動(dòng)語義move
class Human {
public:
Human(int age) :_age(age) {
}
int _age;;
};
class String {
public:
String(Human* pHuman) {
this->pHuman = pHuman;
}
//重寫拷貝構(gòu)造,實(shí)現(xiàn)深拷貝,防止二次釋放內(nèi)存引發(fā)崩潰
String(const String& str) {
////深拷貝
//pHuman = new Human(str.pHuman->_age);
//淺拷貝
pHuman = str.pHuman;
}
//移動(dòng)構(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)限會(huì)由s1讓渡給s2,s2再讓渡給s3 .
使用move語義轉(zhuǎn)移在C++中使用還是比較頻繁的,因?yàn)槠淇梢源蟠罂s小因?yàn)閷?duì)象對(duì)象的創(chuàng)建導(dǎo)致內(nèi)存吃緊的情況。比較推薦應(yīng)用中使用這種方式來優(yōu)化內(nèi)存方面問題.
總結(jié)
本篇文章主要講解了C++面向?qū)ο?a target="_blank">編程中的深拷貝和淺拷貝的問題,以及使用引用計(jì)數(shù)和move語義轉(zhuǎn)移的方式來優(yōu)化深淺拷貝的問題。
C++不像Java那樣,JVM都給我們處理好了資源釋放的問題,沒有二次釋放導(dǎo)致的崩潰情況, C++要懂的東西遠(yuǎn)非Java可比,這也是為什么C++程序員那么少的原因之一吧 。
]()
-
JAVA
+關(guān)注
關(guān)注
19文章
2943瀏覽量
104097 -
C++
+關(guān)注
關(guān)注
21文章
2085瀏覽量
73302 -
面向?qū)ο缶幊?/span>
+關(guān)注
關(guān)注
0文章
22瀏覽量
1797
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論