在學習了深拷貝&淺拷貝&引用計數後,我簡單的對本節課做了一個小總結:
1.什麼是淺拷貝,裏面存在什麼問題?
簡單來說,淺拷貝就是對象的數據成員之間的簡單賦值。
在淺拷貝中,若在string類中沒有給出拷貝構造函數和賦值運算符重載函數,則系統會默認生成如下拷貝構造和賦值運算符重載函數。
//拷貝構造函數
string(const string&s)
:_pstr(s.pstr)
{}
//賦值運算符重載
string &operator = (const string &s)
{
if (this != &s)
{
_pstr = s.pstr;
}
return *this;
}
淺拷貝在賦值時,例如在string類中,進行strings2(s1)時,即用s1對s2進行賦值時,只是簡單的將s1中所有的東西原模原樣,原封不動的拷貝過來。會導致兩個對象共用同一塊空間,導致一塊空間被釋放兩次,而原來的空間忘記釋放,造成內存泄漏。
2.用深拷貝怎麼解決,給出深拷貝的兩種書寫方式:普通版和簡潔版
在深拷貝的實現中,用戶可以自己給出拷貝構造函數:
普通版:
//拷貝構造函數
string(const string&s)
:_pstr(new char[strlen(s.pstr)+1])
{
strcpy(_pstr, s.pstr);
}
//賦值運算符重載
string &operator = (const string &s)
{
if (this != &s)
{
char*pTemp = new char[strlen(s.pstr) + 1];
strcpy(pTemp, s.pTemp);
delect[]_pstr;
_pstr = pTemp;
}
return *this;
}
簡單版:
//拷貝構造函數
string(const string&s)
{
string temp(s._pstr);
swap(_pstr, temp._pstr);
}
//賦值運算符重載
string &operator = (const string &s)
{
if (this != &s)
{
string temp(s._pstr);
swap(_pstr, temp._pstr);
}
return *this;
}
3.什麼是引用計數,用引用計數能解決淺拷貝存在的問題嗎? 怎麼解決
{
public:
String(const char *pStr = "")
:_pStr(new char[strlen(pStr) + 1])
, count(new int[1])
{
strcpy(_pStr, pStr);
*count = 1;
}
String(const String& s)
{
_pStr = s._pStr;
count = s.count;
(*count)++;
}
~String()
{
if (NULL != _pStr && *count == 1)
{
delete[] _pStr;
delete[] count;
_pStr = NULL;
count = NULL;
}
else if (*count > 1)
{
(*count)--;
}
}
private:
char *_pStr;
int *count;
};
void FunTest()
{
String s1("hello bit");
String s2(s1);
}
int main()
{
FunTest();
system("pause");
return 0;
}
對於淺拷貝所出現的問題,不僅僅可以使用深拷貝來解決,我們可以通過標記儲存字符串那段空間的使用次數,用來真正確定,這塊空間是不是真的要釋放!這就是引用計數法
引用計數,它是多個對象一同進行維護的。比如創建s1之後,s1的引用計數就爲1,通過s1創建s2,s1和s2共用了s1的字符串。則s1和s2的引用計數要一致爲2。爲了實現,所以在成員變量定義一個int* 類型的指針,這個指針指向存儲引用計數的空間,多個對象指向同一塊的引用計數空間時,說明他們使用的同一字符串!
4、對引用計數進行改進,完成string的引用計數版本。即寫時拷貝
寫時拷貝是什麼?簡單來說就是在多個對象共用字符串時,有一個對象需要修改字符串的內容,這個時候把字符串拷貝一份賦給這個對象,以免這個對象去修改其他對象所使用的字符串。
那麼在什麼時候纔會出現寫時拷貝呢?很顯然,在共享同一塊內存的類發生內容改變時,纔會發生Copy-On-Write。比如string類的[]、=、+=、+、操作符賦值,還有一些string類中諸如insert、replace、append等成員函數,包括類的析構時。修改數據纔會觸發Copy-On-Write。寫時拷貝是對引用計數法的一種優化,可以消除引用計數法中的錯誤!
在實現寫時拷貝之前,我們先來對引用計數法進行一下優化,我們之前對引用計數和字符串是分了兩塊不同的內存存放,在我們實現的過程中不利於實現,所以我們將他們放在同一塊內存中,引用計數佔內存的前四個字節,後面存放字符串。
先寫一個簡單的寫時拷貝小程序:class String
{
public:
String(char *pStr = "")
:_pStr(new char[strlen(pStr) + 4 + 1])
{
_pStr += 4;
strcpy(_pStr, pStr);
*(int *)(_pStr - 4) = 1;
}
String(const String& s)
{
_pStr = s._pStr;
int len = Size();
(*(int *)(_pStr - 4))++;
}
~String()
{
if (NULL != _pStr)
{
int len = Size();
if (len == 1)
{
delete[](_pStr + 4);
_pStr = NULL;
}
else
{
(*(int *)(_pStr - 4))--;
}
}
}
void WriteCopy()//寫時拷貝
{
int len = Size();
char *tmp = new char[len + 1 + 4];
tmp += 4;
strcpy(tmp, _pStr);
if (len > 1)
{
(*(int *)(_pStr - 4))--;
std::swap(_pStr, tmp);
(*(int *)(_pStr - 4)) = 1;
}
}
size_t Size()const
{
char* tmp = _pStr;
size_t count = 0;
while (*tmp++)
{
count++;
}
return count;
}
String operator =(const String & s)
{
_pStr = s._pStr;
int len = this->Size();
(*(int *)(_pStr - 4))++;
}
char& operator [](size_t idx)
{
static char sNULL = '\0';
if (idx < Size() && idx > 0)
{
WriteCopy();
return _pStr[idx];
}
return sNULL;
}
const char& operator [](size_t idx)const
{
static char sNULL = '\0';
if (idx < Size() && idx > 0)
{
return _pStr[idx];
}
return sNULL;
}
public:
char *_pStr;
};
void FunTest()
{
String s1("hello ");
cout << *(((int *)s1._pStr) - 1) << endl;
String s2(s1);
String s3 = s2;
cout << *((int *)s1._pStr - 1) << endl;
s3[2] = 'W';
cout << *((int *)s1._pStr - 1) << endl;
}
int main()
{
FunTest();
system("pause");
return 0;
}
先來分析分析代碼:在程序中,先創建對象s1,然後通過s1創建s2,再創建s3通過s2賦值給s3來改變s3的內容,這個時候就會發生寫時拷貝,s3會擁有新的內存空間,s1引用計數的變化。