關於string類中的一些小問題的總結

在學習了深拷貝&淺拷貝&引用計數後,我簡單的對本節課做了一個小總結:

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.什麼是引用計數,用引用計數能解決淺拷貝存在的問題嗎? 怎麼解決

  首先看一段代碼:
class String
{
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* 類型的指針,這個指針指向存儲引用計數的空間,多個對象指向同一塊的引用計數空間時,說明他們使用的同一字符串!

   創建對象s1,使用s1拷貝構造s2,可以看到他們使用的是同一字符串,同一引用計數。在函數返回之前,s2先銷燬,發現s1也是用的是這塊空間,所以就讓引用計數減去1,不進行銷燬。不過對於引用計數存在一個很重要的缺點,就是當多個對象使用同一個字符串時,任何一個對象修改字符串裏面的值都會造成其他對象所指向的字符串改變。

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引用計數的變化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章