歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

C++ string類的隱式共享寫時拷貝的實現及設計要點

字符串一種在程序中經常要使用到的數據結構,然而在C中卻沒有字符串這種類型。在C++中,為了方便字符串的使用,在STL中提供了一個string類。該類維護一個char指針,並封裝和提供各種的字符串操作。

一、為什麼要實現隱式公享寫時拷貝

試想一下,如果我們要自己實現一個string類,最簡單的方式是什麼?就是讓每一個string類的實例維護一個在內存中獨立的字符數組,每個string對象各不相干。這樣一個對象的任何變化都不會影響到其他的對象。這樣做的好處就是處理簡單,不易出錯,但是這樣做的缺點卻是內存的使用量、程序的效率也低。

例如,對於如下的例子:

int swap(string &x, string &y)
{
    string tmp(x);
    x = y;
    y = tmp;
}

在上面的代碼中,我們需要做的事情非常簡單,只是想交換一下兩個string對象的值而已。然而,如果我們采用上面所說的實現方式,一共在內存上進行了三次的用new來創建一個新的數組並復制數據,同時還會調用兩次的delete[]。我們花了這麼大的力氣才完成了這樣一個簡單的動作。而隱式共享寫時復制的內存管理策略卻可以解決這樣的問題。雖然C++11用&&運算符解決了上述問題,但是在程序中使用大量的string的副本,而不改變其值的情況還是不少的。例如string數組,刪除一個元素後的移動操作。

二、隱式共享寫時復制的實現思想

什麼是隱式共享寫時拷貝呢?就是當用一個string對象初始化另一個string對象或把一個string對象賦值給另一個string對象時,它們內部維護的指針其實指向了內存中的同一個字符數組,這就是隱式共享。

使用這種方法,上述的代碼就不需要調用new來創建數組也不需要復制,也不會調用delete[](如果不是很明白也不要緊,看完實現代碼和後自然就明白了)。然後兩個指針指向同一個對象很容易引發錯誤,當其中一個對象執行析構函數釋放掉其內部指針指向的內存時,另一個對象卻對此完全不知情,可能會引用一個不存在的內存,從而讓程序崩潰。所以為了方便資源的管理,我們引用智能指針的思想,為每個內存中的字符數組(用new創建,存在於堆中)添加一個引用計數used,表示有多少個對象引用這個塊內存(即字符數組)。當一個對象析構時,它會把引用計數used減1,當used為0時,表示沒有對象引用這塊內存,從而把這塊內存釋放掉。當然由於used也要在對象中共享,所以它也是一個堆中的數據,每個對象有一個指向它的指針。

而當一個string對象需要改變它的值時,例如

string s1("abc");
string s2(s1);
string s3("edf");
s2 += s3;

此時,s1和s2指向了堆內存中的同一個字符數組,而當s2的值要改變時,因為如果直接在其指向的內存中修改,則會影響到對象s1,所以為了讓s2的操作不影響到s1,s2會在重新new出一塊內存,然後先把之前所引用的字符數組的數據復制到新的字符數組中,然後再把s3中的字符數據復制到新的字符數組中。這就是寫時拷貝。注意,同時還要把之前指向的內存的引用計數減1(因為它指向了新的堆中的字符數組),並在堆中重新new一個塊內存,用於保存新的引用計數,同時把新的字符數組的引用計數置為1。因為此時只有一個對象(就是改變值的對象)在使用這個內存。

三、代碼實現及設計要點詳解

說了這麼多,還是來看看代碼的實現吧,為了與標准C++的string類區別開來,這樣采用第一個字母大寫來表示自定義的字符串類String。

C++ string類的隱式共享寫時拷貝實現代碼下載

免費下載地址在 http://linux.linuxidc.com/

用戶名與密碼都是www.linuxidc.com

具體下載目錄在 /2014年資料/4月/4日/C++ string類的隱式共享寫時拷貝的實現及設計要點

下載方法見 http://www.linuxidc.com/Linux/2013-07/87684.htm

其頭文件_stringv2.h如下:

#ifndef _STRINGV2_H_INCLUDED
#define _STRINGV2_H_INCLUDED
/***
String類的部分實現,采用的內存管理策略是:隱式共享,寫時復制
實現方法:與智能指針的實現類似
***/
class String
{
    public:
        String();
        String(const String& s);
        String(const char *pc, size_t len);
        String(const char *pc);
        ~String();
        String& operator=(const String &s);
        String& operator=(const char *s);
        String& operator+=(const String &rhs);
        String& operator+=(const char *rhs);
        void clear();
        size_t getLength()const {return _length;}
        const char* cstr()const {return _cstr;}
    private://function
        void _initString(const char *cstr, size_t len);
        void _decUsed();
        char* _renewAndCat(const char *cstr, size_t len);
        void _addString(const char *cstr, size_t len);
        void _addAssignOpt(const char *cstr, size_t len);
    private://data
        char *_cstr;
        size_t *_used;
        size_t _length;
        size_t _capacity;
};
String operator+(const String &lhs, const String &rhs);
std::ostream& operator <<(std::ostream &os, const String &s);
std::istream& operator >>(std::istream &in, String &s);
#endif // _STRINGV2_H_INCLUDED

從上面的String的數據成員,我們可以看到String在其內部維護一個指向堆內存的字符數組的char指針_cstr和一個指向堆內存中字符數組的引用計數的size_t指針_used。本類並沒有實現String的所有操作,只是實現了大部分的初始化和String跟寫操作有關的函數。

注意:為了說明的方便,我會使用s._cstr等方式來指明一個成員變量所屬的對象,或使用*s._cstr等方式來引用一個對象的指針成員所指的內存。但這並不是說在類的外面訪問成員變量,只是為了說明的方便和清晰而已。為了方便代碼的閱讀,類的成員變量或私有函數都以下劃線“_”開頭。

Copyright © Linux教程網 All Rights Reserved