1. 程式人生 > 其它 ># C++學習筆記------String寫時拷貝

# C++學習筆記------String寫時拷貝

​ c/c++中耗時最大的幾個操作:

​ (1)檔案操作

​ (2)記憶體的申請和釋放

寫時拷貝(copy_on_write)是一種計算機程式設計領域的優化策略。其核心思想是,如果有多個物件同時要求相同資源(如記憶體或磁碟上的資料儲存),他們會共同獲取相同的指標指向相同的資源,直到某個物件試圖修改資源的內容時,系統才會真正複製一份專用副本給物件,而其他呼叫者所見到的最初的資源仍然保持不變。(參考)

引用計數

​ String類中的寫時拷貝技術是指用淺拷貝的方法拷貝其他物件,多個指標指向同一塊空間,只有當對其中一個物件修改時,才會開闢一個新的空間給這個物件,和它原來指向同一空間的物件不會受到影響,顯然記憶體的申請和釋放操作少了很多,所以寫時拷貝的效率遠高於深拷貝。

​ 可以通過增加一個成員變數,作為計數器來實現寫時拷貝,這個變數叫做引用計數,統計這塊空間被多少個物件的_str同時指向。當用指向這塊空間的物件拷貝一個新的物件出來時引用計數加1,當指向這塊空間的一個物件指向別的空間或析構時引用計數減1。只有當引用計數等於0時才可以真正釋放這塊空間,否則說明還有其他物件指向這塊空間,不能釋放。

以上部分(參考)

引用計數該怎麼設定?

  1. 設定成普通成員變數(int _num)----->error,每個物件都有獨立的__num
  2. 設定成靜態成員變數 static int _num----->error,所有物件使用一個__num,但實際上只有指向相同資源的物件才可以共有一個引用計數(不知道怎麼表達,但是確實明顯與寫時拷貝的策略不同)
  3. static map<char*, int> _map; 這種方式是可以的,但是現在還沒學過這個
  4. 如下所示,在開闢_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;
    }
}
  • 寫時拷貝的實現
    1. 要呼叫寫時拷貝,至少有兩個物件完成了構造,並指向一塊相同的區域,所以引用計數(get_str_num()) >1
    2. 注意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();//增加引用計數
    }

}
  • 等號賦值運算子過載
    1. 防止自賦值
    2. 拷貝一份副本給呼叫該方法的物件,引用計數減一,若為0,要銷燬 _str原來指向的區域,防止記憶體洩漏
    3. 淺拷貝
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的變化):