前言
最近在寫代碼的過程中,發現一個大家容易忽略的知識點: 深拷貝和淺拷貝 。
可能對于Java程序員來說,很少遇到深淺拷貝問題,但是對于C++程序員來說可謂是又愛又恨。。
淺拷貝:
-
1.將原對象或者原對象的引用直接賦值給新對象,新對象,新數組只是原對象的一個引用。
-
2.C++默認的拷貝構造函數與賦值運算符重載都是淺拷貝,可以節省一定空間,但是可能會引發同一塊內存重復釋放問題,
二次釋放內存可能導致嚴重的異常崩潰等情況。
-
淺拷貝模型:
深拷貝:
-
1.創建一個新的對象或者數組,將對象或者數組的屬性值拷貝過來,注意此時新對象指向的不是原對象的引用而是原對象的值,新對象在堆中有自己的地址空間。
-
2.浪費空間,但是不會引發淺拷貝中的資源重復釋放問題。
-
深拷貝模型
案例分析
下面使用一個案例來看下一個因為淺拷貝帶來的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);
}
這個程序從表面看是不會有啥問題的,運行后,出現如下錯誤:
先說下原因 :
這個錯誤就是由于代碼 String s2(s1) 會調用String的默認拷貝構造函數,而 默認的拷貝構造函數使用的是淺拷貝,即僅僅只是對新的指針對象pHuman指向原指針對象pHuman指向的地址 。
在退出main函數作用域后,會回調s1和s2的析構函數,當回調s2析構函數后,s2中的pHuman內存資源被釋放,此時再回調s1,也會回調s1中的pHuman析構函數,可是此時的pHuman指向的地址
已經在s2中被釋放了,造成了二次釋放內存,出現了崩潰的情況 。
所以為了防止出現二次釋放內存的情況,需要使用深拷貝 。
深拷貝需要重寫拷貝構造函數以及賦值運算符重載,且在拷貝構造內部重新去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;
}
//重寫拷貝構造,實現深拷貝,防止二次釋放內存引發崩潰
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
#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();
}
//重寫拷貝構造,實現深拷貝,防止二次釋放內存引發崩潰
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 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 <<"計數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++程序員那么少的原因之一吧 。
]()
-
JAVA
+關注
關注
19文章
2960瀏覽量
104563 -
C++
+關注
關注
22文章
2104瀏覽量
73503 -
面向對象編程
+關注
關注
0文章
22瀏覽量
1807
發布評論請先 登錄
相關推薦
評論