【c++學習筆記】深入解析淺拷貝與深拷貝
阿新 • • 發佈:2019-02-04
測試環境:vs2013
什麼是淺拷貝
- 也稱位拷貝,編譯器只是將物件中的值拷貝過來,如果物件中管理資源,最後就會導致多個物件共享同一份資源,當一個物件銷燬時就會將該資源釋放掉,而此時另一些物件不知道該資源已經被釋放,以為還有效,所以當繼續對資源進行操作時,就會發生髮生了訪問違規。
先看下面的程式碼有問題嗎
class String
{
public:
String(const char* ptr = "")//建構函式,預設放'\0'
:_ptr(new char[strlen(ptr) + 1])//strlen計算出的長度不加\0
{
strcpy (_ptr, ptr);
}
~String()
{
if (_ptr)
{
cout << "~String()" << this << endl;
delete[] _ptr;
}
}
private:
char* _ptr;
};
int main()
{
String s1("hello");
String s2(s1);
return 0;
}
執行結果如下:
- 可以發現這裡程式會崩,為什麼會造成這樣的原因呢,那就必須搞清楚上面的程式碼都做了什麼
這就是淺拷貝,多個物件共享同一份資源,造成的問題也顯而易見,一份資源被釋放了多次。那麼,怎麼解決呢?
引用計數
- 當多個物件共享一塊資源時,要保證該資源只釋放一次,只需記錄有多少個物件在使用該資源即可,每減少(增加)一個物件使用,給該計數減一(加一),當最後一個物件不使用時,該物件負責將資源釋放掉即可;觀察下面程式碼:
class String
{
public:
String()
{}
String(const char* str )
{
if (str == NULL)
{
_str = new char[4+1];//多申請一個int用來儲存計數器
_str += 4;
_str = '\0';
}
else
{
_str = new char[strlen(str) + 1 + 4];
_str += 4;
strcpy(_str, str);
}
GetCount(_str) = 1;//將計數器初始值設定為1
}
String(const String& s)//拷貝構造
:_str(s._str)
{
GetCount(_str)++;
}
//s1=s2
String& operator=(const String& s)//賦值操作符過載
{
if (_str != s._str)
{
//1.當s1的引用計數為1時
//①釋放空間
//②改變s1指標的指向
//③s2的引用計數加1
Release();
//當s1的引用計數大於1時
//①s1的引用計數減1
//同上②③
_str = s._str;
++GetCount(_str);//引用計數+1
}
return *this;
}
~String()//解構函式
{
Release();
}
private:
int& GetCount(char* str)
{
return *(int*)(str - 4);//因為這塊記憶體型別為char
}
void Release()
{
if (_str != NULL && (--GetCount(_str)) == 0)
{
delete [](_str - 4);//一定要釋放儲存計數器的空間
}
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
String s3("world");
String s4(s3);
String s5(s3);
s1 = s3;
}
上面的程式碼是在構造的時候多申請4位元組的空間用來儲存計數器,從而實現引用計數。那麼具體是怎麼做的呢?
但是這樣還是有問題,看下面的程式碼
class String
{
public:
String()
{}
String(const char* str)
{
if (str == NULL)
{
_str = new char[4 + 1];//多申請一個int用來儲存計數器
_str += 4;
_str = '\0';
}
else
{
_str = new char[strlen(str) + 1 + 4];
_str += 4;
strcpy(_str, str);
}
GetCount(_str) = 1;//將計數器初始值設定為1
}
String(const String& s)//拷貝構造
:_str(s._str)
{
GetCount(_str)++;
}
//s1=s2
String& operator=(const String& s)//賦值操作符過載
{
if (_str != s._str)
{
Release();
_str = s._str;
++GetCount(_str);//引用計數+1
}
return *this;
}
char& operator[](size_t index)//可以採用下標的方式訪問String類
{
return _str[index];
}
~String()//解構函式
{
Release();
}
private:
int& GetCount(char* str)
{
return *(int*)(str - 4);//因為這塊記憶體型別為char
}
void Release()
{
if (_str != NULL && (--GetCount(_str)) == 0)
{
delete[](_str - 4);//一定要釋放儲存計數器的空間
}
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
String s3(s1);
s3[1] = 'a';
return 0;
}
- 當幾個共用同一塊空間的物件中的任一物件修改字串中的值,則會導致所有共用這塊空間的物件中的內容被破壞掉。像上面的程式碼中,我們只想改變s3中的值,但是和他公用空間的另外兩個物件s2,s1中的值也改變了,這就引出了寫時拷貝
寫時拷貝
- 有多個物件共享同一個空間時,當對其中一個物件只讀時,不會有什麼影響,但是如果想要改變(寫入)某一個物件中的值時,這時就要為這個物件重新分配空間。程式碼實現如下:
class String
{
public:
String()
{}
String(const char* str)
{
if (str == NULL)
{
_str = new char[4 + 1];//多申請一個int用來儲存計數器
_str += 4;
_str = '\0';
}
else
{
_str = new char[strlen(str) + 1 + 4];
_str += 4;
strcpy(_str, str);
}
GetCount() = 1;//將計數器初始值設定為1
}
String(const String& s)//拷貝構造
:_str(s._str)
{
GetCount()++;
}
//s1=s2
String& operator=(const String& s)//賦值操作符過載
{
if (_str != s._str)
{
Release();
_str = s._str;
++GetCount();//引用計數+1
}
return *this;
}
char& operator[](size_t index)//可以採用下標的方式訪問String類
{
if (GetCount() > 1)
{
--GetCount();
char* pTmp = new char[strlen(_str) + 1 + 4];
pTmp += 4;
strcpy(pTmp, _str);
_str = pTmp;
GetCount() = 1;//將新空間值賦1
}
return _str[index];
}
const char& operator[](size_t index)const//[]操作符必須成對過載
{
return _str[index];
}
~String()//解構函式
{
Release();
}
private:
int& GetCount()
{
return *(int*)(_str - 4);//因為這塊記憶體型別為char
}
void Release()
{
if (_str != NULL && (--GetCount()) == 0)
{
delete[](_str - 4);//一定要釋放儲存計數器的空間
}
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
String s3(s1);
s3[1] = 'a';
return 0;
}
深拷貝
- 給要拷貝構造的物件重新分配空間
class String
{
public:
String(const char* str = "")
:_str(new char[strlen(str)+1])
{
strcpy(_str, str);
}
String(const String& s)//深拷貝
: _str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
//賦值操作符過載
//方法一
//String& operator=(const String& s)
//{
// if (this != &s)
// {
// delete[] _str;
// _str = new char[strlen(s._str) + 1];
// strcpy(_str, s._str);
// }
// return *this;//為了支援鏈式訪問
//}
//方法二(優)
String& operator=(const String& s)
{
if (this != &s)//自己不能拷貝自己
{
char* tmp = new char[strlen(s._str) + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
}
return *this;//為了支援鏈式訪問
}
~String()
{
if (_str != NULL)
{
delete[]_str;
}
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
String s3("world");
s1 = s3;
}
- 一般情況下,上面對賦值操作符過載的兩種寫法都可以,但是相對而言,第二種更優一點。
對於第一種,先釋放了舊空間,但是如果下面用new開闢新空間時有可能失敗——>拋異常,而這時你是將s2賦值給s3,不僅沒有賦值成功(空間開闢失敗),而且也破壞了原有的s3物件。對於第二種,先開闢新空間,將新空間的地址賦給一個臨時變數,就算這時空間開闢失敗,也不會影響原本s3物件。