# C++學習筆記------String寫時拷貝
c/c++中耗時最大的幾個操作:
(1)檔案操作
(2)記憶體的申請和釋放
寫時拷貝(copy_on_write)是一種計算機程式設計領域的優化策略。其核心思想是,如果有多個物件同時要求相同資源(如記憶體或磁碟上的資料儲存),他們會共同獲取相同的指標指向相同的資源,直到某個物件試圖修改資源的內容時,系統才會真正複製一份專用副本給物件,而其他呼叫者所見到的最初的資源仍然保持不變。(參考)
引用計數
String類中的寫時拷貝技術是指用淺拷貝的方法拷貝其他物件,多個指標指向同一塊空間,只有當對其中一個物件修改時,才會開闢一個新的空間給這個物件,和它原來指向同一空間的物件不會受到影響,顯然記憶體的申請和釋放操作少了很多,所以寫時拷貝的效率遠高於深拷貝。
可以通過增加一個成員變數,作為計數器來實現寫時拷貝,這個變數叫做引用計數,統計這塊空間被多少個物件的_str同時指向。當用指向這塊空間的物件拷貝一個新的物件出來時引用計數加1,當指向這塊空間的一個物件指向別的空間或析構時引用計數減1。只有當引用計數等於0時才可以真正釋放這塊空間,否則說明還有其他物件指向這塊空間,不能釋放。
以上部分(參考)
引用計數該怎麼設定?
- 設定成普通成員變數(int _num)----->error,每個物件都有獨立的__num
- 設定成靜態成員變數 static int _num----->error,所有物件使用一個__num,但實際上只有指向相同資源的物件才可以共有一個引用計數(不知道怎麼表達,但是確實明顯與寫時拷貝的策略不同)
- static map<char*, int> _map; 這種方式是可以的,但是現在還沒學過這個
- 如下所示,在開闢_str時在前面4個位元組即在這塊空間的頭部儲存引用計數
基於上一節的Mstring類的實現,我們要進行一定修改,下面只列出修改的部分
寫時拷貝新增的操作(私有成員方法,不對外提供介面)
- 獲取字串的實際起始地址
char* get_str_begin()//常物件無法呼叫普通方法 { return _str+STR_MORE_LEN;//找到有效字串的實際位置 } const char* get_str_begin()const { return _str + STR_MORE_LEN; }
-
獲取引用計數
返回值是引用,這樣對返回值進行修改,就可以直接修改引用計數了
int& get_str_num()//獲取引用計數, 注意返回的是引用, 常物件無法呼叫該方法
{//強轉+解引用--->目的:取前四位元組
return *((int*)_str);
}
const int& get_str_num()const
{//強轉+解引用--->目的:取前四位元組
return *((int*)_str);
}
-
引用計數的增加和減少
注意,有物件要共享資源時,引用計數才加1(用已構造的物件*拷貝構造新的物件,顯然可以處理成共享一塊記憶體資源)
我的問題:等號賦值運算子(==)要不要增加引用計數? 答:要加
當有物件要對共享的資源進行修改時,拷貝一份副本給這個物件,原來的引用計數減一,新申請的空間中,引用計數初始為1
int add_str_num()//引用計數增加,利用已構造的物件拷貝構造新的物件時,需要增加引用計數
{
if (_str != NULL)//引用計數放在申請的_str的頭部
{
get_mutex()->lock();
int num = ++get_str_num();
get_mutex()->unlock();
return num;
}
return 0;
}
int down_str_num()//引用計數減少,物件析構或者是要對共享資源進行寫操作,得到了一份共享資源的副本
{
if(_str != NULL)
{
get_mutex()->lock();
int num = --get_str_num();
get_mutex()->unlock();
return num;
}
return 0;
}
-
銷燬_str原來指向的區域
注意引用計數為0時才會實際進行摧毀
void destory_str()//摧毀_str
{
if (_str != NULL && down_str_num()==0)//引用計數為0時才會實際進行摧毀
{
delete get_mutex();
delete[]_str;
_str = NULL;
_len = 0;//老師這裡寫的是_len = 0
_val_len = 0;
}
}
- 寫時拷貝的實現
- 要呼叫寫時拷貝,至少有兩個物件完成了構造,並指向一塊相同的區域,所以引用計數(get_str_num()) >1
- 注意this->str指標的指向將會發生變化, this指標沒變, 我在程式碼旁做了註釋
void copy_while_write(/*this*/)
{
if (get_str_num() > 1)//要呼叫寫時拷貝,至少有兩個物件完成了構造,並指向一塊相同的區域,所以
{ //深拷貝
char* tmp = new char[_len];
strcpy_s(tmp + STR_MORE_LEN, _val_len + 1, get_str_begin());
down_str_num();
_str = tmp;//this->_str現在指向了一塊複製好的空間
get_str_num() = 1;//新申請的記憶體的引用計數部分初始值為1, this->get_str_num()
get_mutex() = new mutex();
}
}
加入寫時拷貝策略後修改的部分
- 拷貝構造的實現
Mstring(const Mstring& src)//利用以構造的物件拷貝構造另一個物件,那麼我們就讓它們訪問相同的資源
{
_len = src._len;
_val_len = src._val_len;
_str = src._str;//淺拷貝(共用一塊記憶體)
//沒問題,指向一塊記憶體區域就增加引用計數,如果沒有指向,就不操作
if (_str != NULL)
{
add_str_num();//增加引用計數
}
}
- 等號賦值運算子過載
- 防止自賦值
- 拷貝一份副本給呼叫該方法的物件,引用計數減一,若為0,要銷燬 _str原來指向的區域,防止記憶體洩漏
- 淺拷貝
Mstring& operator=(/*this,*/const Mstring& src)
{
//防止自賦值
if (&src == this)
{
return *this;
}
destory_str();//引用計數先減,減後若為0則銷燬
//這裡有問題吧
_len = src._len;
_val_len = src._val_len;
_str = src._str;//this->_str與src._str指向相同資源
add_str_num();
return *this;
}
- push_back()的實現
void push_back(/*this, */char c)//物件要對共享資源的區域進行修改時,複製一份副本給呼叫的物件
{
if (_str == NULL)//預設構造後push_back
{
_val_len = 1;
_len = _val_len + 1;
}
if (_str != NULL)
{//寫時拷貝
copy_while_write();
}//this->_str指標已經指向了複製好的副本
if (is_full())//this->is_full()
{
revert();//this->revert()
}
get_str_begin()[_val_len] = c;//把原來'\0'的位置覆蓋掉
get_str_begin()[++_val_len] = 0;//後一位放'\0',_val_len加一
}
- pop_back()的實現
char pop_back(/*this*/)//對共享資源有修改,同樣要進行寫時拷貝
{
if (_str == NULL || is_empty())
{
return 0;//我覺得實際應該要進行異常處理
}
if (_str != NULL)
{
copy_while_write();
}//this->_str指標已經指向了複製好的副本
char c = get_str_begin()[_val_len - 1];
get_str_begin()[_val_len-1] = 0;
_val_len--;
return c;//不能返回區域性變數的引用和指標
}
- +運算子過載
Mstring operator+(/*this, */const Mstring& src)//字串中是拼接操作
{
Mstring tmp = *this;
//注意i=0, src._str[0]是第一個元素的位置,src._str[_val_len-2]是'\0'的位置
for (int i = 0; i < src._val_len - 1; i++) //(_val_len-1)---->'\0'的位置
{
tmp.push_back(src.get_str_begin()[i]);
}
return tmp;
}
- []運算子過載
//1.為啥返回引用?---->能夠直接修改字串內容
//2.為啥返回引用可能造成寫時拷貝--->對返回值修改,會直接對共享資源進行修改
char& operator[](int pos)//普通物件可以進行修改,為了修改才返回引用
{
if (_str != NULL)
{
copy_while_write();
}
return get_str_begin()[pos];
}
//常物件呼叫該方法
//不返回引用,因為常物件不能被修改
char operator[](/*常物件指標*/int pos)const //對常物件無法進行修改
{
return get_str_begin()[pos];
}
測試部分
int main()
{
Mstring str1;
//cout << str1 << endl;
Mstring str2 = "qqqq123456";
cout << str2 << endl;
str1 = str2;
cout << str1 << str2 << endl;
Mstring str3(str1);
cout << str3 << endl;
str1[1] = '0';
cout << str1 << endl;
cout << str3 << endl;
str2.push_back('a');
cout << str2 << endl;
cout << str3 << endl;
str3.pop_back();
cout << str3 << endl;
}
結果(注意地址和引用計數_num的變化):