C++模擬實現string的方法詳解

1.string 成員變量

首先需要一個動態開辟的指針指向這個字符串,然後還需要容量和存儲的個數,並且我們不能和標準庫的string進行沖突所以我們需要寫在我們自己的類域中,並且我們庫中還有一個靜態的變量是npos,就是無符號的-1,代表整形的最大值:

namespace cyf
{
    class string
    {
    public:
        //成員函數
    private:
        char *_str;
        size_t size;
        size_t capaticy;
        const static size_t npos = -1;
    };
}

這裡有一個特例:static成員變量一般在類中聲明在類外定義,但是const static int型的變量可以直接在類中定義。

2.構造函數

strlen求出的是\0之前的字符個數,所以_size和_capacity標識的是實際存儲的字符個數,在開辟空間時多開辟一個字符用來存儲'\0'。

string(const char* s = "")  
        {
            _size = strlen(s);  
            _capacity = _size;
            _str = new char[_capacity + 1];   
 
            strcpy(_str, s);  //開辟好空間後將s的內容拷貝至_str
        }

3.拷貝構造、賦值重載和析構函數

1.拷貝構造

_str 維護的是一塊空間,所以不能簡單的將s._str的值賦值給_str (淺拷貝),而是單獨開辟一塊空間,讓_str指向這一塊空間,再將s._str空間中的值拷貝至新開辟的空間,新開辟的空間比_capacity多開一個字節用來存儲'\0',作為字符串的結束標志。

//string s1(s)
string(const string& s)
        {
            _size = s._size;
            _capacity = s._capacity;
            _str = new char[s._capacity + 1];
 
            strcpy(_str, s._str);
        }

2.賦值重載

首先開辟一塊空間,將字符串的內容拷貝至這個空間,將_str原來指向的空間釋放,_str再指向這個新開辟的空間,size和capacity還是原來的大小。

//s2=s1
        string& operator=(const string& s)
        {
            if (this != &s)   //避免自己給自己賦值
            {
                char* tmp = new char[s._capacity + 1];
                strcpy(tmp, s._str);
                delete[] _str;
                _str = tmp;
                _size = s._size;
                _capacity = s._capacity;
            }
            return *this;
        }

3.析構函數

~string()
        {
            delete[] _str;
            _str = nullptr;
            _size = _capacity = 0;
        }

4.訪問成員變量

提供接口可以在類的外邊查看字符串的內容,存儲字符串的元素個數和容量

        const char* c_str()
        {
            return _str;
        }
 
        size_t size()
        {
            return _size;
        }
        size_t capacity()
        {
            return _capacity;
        }

配合之前的構造函數和這裡的接口,我們進行驗證:

運行結果:

5.遍歷

遍歷有三種模式:

1.下標+【】

_str是一個指針,那麼我們可以通過數組的方式來訪問,隻需要重載operator []即可。我們還是要重載兩個版本的,因為普通變量和const變量的訪問權限不一樣。

//普通變量,可讀可寫
char& operator[](size_t pos)
{
    assert(pos < _size);  //檢查不能越界訪問
 
    return _str[pos];
}
 
//const變量,隻讀屬性
char& operator[](size_t pos) const
{
    assert(pos < _size);
 
 
    return _str[pos];
}

2.迭代器(iterator)

在string中,迭代器就是一個指針,隻不過我們進行瞭封裝,typedef一下就可以啦,同樣我們也要實現兩個版本的,const和非const的。

typedef char* iterator;
typedef const char* const_iterator;
 
iterator begin()
{
    return _str;
}
 
const_iterator begin()const
{
    return _str;
}
 
iterator end()
{
    return _str +_size;
}
 
const_iterator end() const
{
    return _str +_size;
}

3.范圍for

我們范圍for的底層就是迭代器,所以我們不用實現,隻要實現瞭迭代器,那麼我們就可以直接使用范圍for,范圍for在執行的時候實際還是通過迭代器實現的,上例子:

運行結果:

6.空間的申請

1.reserve

一般是我們原空間容量滿瞭,需要申請空間擴容,我們的擴容函數還是要先申請空間,然後在進行拷貝,接著我們delete原來的空間,把申請的空間的指針和 容量 賦值過去即可。 

void reserve(size_t n)
        {
            if (n > _capacity)
            {
                char* tmp = new char[n + 1];    //多開一個字節留給'\0';
                strcpy(tmp, _str);
                delete[] _str;
                _str = tmp;
 
 
                _capacity = n;
            }
 
 
        }

2.resize

1. 如果我們是傳入一個正整數大於_size的值,那麼我們可以使用傳入的字符(或者缺省值)把我們申請的空間進行初始化,也就是從_size到n-1置為我們傳入的字符,n置為' \0 ',最後把_size置為n。

2.如果傳入一個小於_size正整數,那麼我們把0~_size-1進行初始化為傳入的字符(或者缺省值),把n位置置為' \0 ',接著我們會把_size置為n,而_capaticy不變。

void resize(size_t n, char ch = '\0')
        {
            if (n > _size)
            {
                reserve(n);
                for (size_t i = _size; i < n; ++i)
                {
                    _str[i] = ch;
                }
                _size = n;
                _str[_size] = '\0';
 
            }
 
            else   //小於
            {
                _str[n] = '\0';
                _size = n;
 
            }
 
    }

7.增刪查改

1.push_back   尾插一個字符

上來就檢查容量,_size==_capacity時就說明沒有容量瞭,得分類討論:1.原來的字符串有元素2.原來的是空字符串,如果字符串為空,就給4個字節大小的容量 。如果原來的字符串不為空但是需要擴容就調用reserve函數進行擴容,容量擴為2倍,2倍比較合適,避免給的小瞭頻繁的擴容,但是也不能給的太大瞭,太大瞭會造成空間的浪費。在_size的位置插入字符ch ,_size++,插入的字符ch 將原來的'\0'給覆蓋瞭,最後再補上'\0',作為字符串的結束標志。

void push_back(char ch)
        {
            if (_size == _capacity)
            {
                size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
                reserve(newcapacity);
            }
            _str[_size] = ch;   
            ++(_size);
            _str[_size] = '\0'; //記得處理 \0
        }

2.append 尾部插入一個字符串

插入字符串的大小不確定,就需要確定是否需要擴容。當插入的字符串的長度加上當前字符串的有效元素個數大於容量_capacity時,就需要擴容,擴後的容量大小為_size+len ,這裡給reverse函數傳的是有效元素的個數,在reverse函數內部為我們多開瞭一個字符的大小用來存儲'\0'。再進行拷貝工作,這裡記得插入元素後_size要進行變換。

void append(const char* str)
        {
            size_t len = strlen(str);
            if (len + _size > _capacity)
            {
                reserve(_size + len);   
 
            }
            strcpy(_str + _size, str);
            _size += len;
        }

3.insert  在指定位置插入一個字符

上來首先檢查要插入的位置是否合理,_size 代表的位置是有效元素的下一個位置即'\0'的位置,pos的范圍【0,_size】string字符串的頭到尾部之間 ,插入元素要檢查容量,記得考慮原string是否為空串的情況。插入數據需要挪動數據,從前往後挪動數據,將end位置確定到'\0'的下一個位置,這樣方便頭插。最終將pos位置騰出來,插入字符ch 插入數據後_size++。

string& insert(size_t pos, char ch)
        {
            assert(pos <= _size);  //等於size 時候相當於尾部插入
            if (_size == _capacity)
            {
                size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
                reserve(newcapacity);
            }
            size_t end = _size + 1;  //這裡是把\0往  \0的下一個位置挪動 方便頭插   
            while (end > pos)
            {
                _str[end] = _str[end - 1];
                --end;
            }
            _str[pos] = ch;
            _size++;
            return *this;
        }

4.insert 在指定位置插入一個字符串

string& insert(size_t pos, const char* str),我們先進行斷言pos不能超過_size,接著我們開辟空間,這次就不考慮空串的問題瞭,因為我們要指定開辟的字節數,和上面一樣的我們也要進行挪動數據,我們隻不過是由每次挪動一個步,變為瞭挪動 len 步瞭,最後使用strncpy插入字符串,把_szie +=len 即可。

畫圖理解:

string& insert(size_t pos, const char* str)
        {
            size_t len = strlen(str);
            if (_size + len > _capacity)
            {
                reserve(_size + len);
            }
            size_t end = _size + len;
            while (end > pos + len - 1)
            {
                _str[end] = _str[end - len];
                --end;
            }
            strncpy(_str + pos, str, len);
            _size += len;
            return *this;
        }

實現瞭上述的接口當然最好用的還是下面的接口,對push_back和append進行封裝實現string+=

一個字符 和 string+=一個字符串

    string& operator+=(char ch)
        {
            push_back(ch);
            return *this;
        }
 
        string& operator+=(const char* str)
        {
            append(str);
            return *this;
 
        }

5.刪除接口:erase

指定字符串從開始位置到指定位置刪除元素。得保證刪除的位置在string的內部,當隻給定瞭刪除的起始位置沒有給結束的位置那麼就觸法我們的缺省值,即從pos位置開始直到將字符串刪完,或者說給定的結束位置大於瞭字符串的本身長度,那就從pos位置開始直到刪除完字符串,實現的方法很簡單,直接在pos位置添字符串的結束標志'\0'。  如果給定的兩個值都在字符串的內部直接進行從len位置往前拷貝覆蓋掉要刪除的元素。

string& erase(size_t pos, size_t len = npos)  
        {
            assert(pos <= len);
            if (len == npos || len >= _size - pos)
            {
                _str[pos] = '\0';
                _size = pos;
            }
            else
            {
                strcpy(_str + pos, _str + pos + len);
                _size -= len;
 
            }
            return *this;
        }

6.find字符 從某個位置開始查找字符,如果沒有給定開始位置,就用缺省值,默認從開頭尋找,找到瞭就返回元素的下標,沒有找到就返回npos。

size_t find(char ch, size_t pos = 0)  ///默認從pos位置開始尋找,有缺省值0,從pos 位置開始往後尋找
        {                                     //對比找到瞭就返回下標,找不到返回npos
            assert(pos < _size);
            while (pos < _size)
            {
                if (_str[pos] == ch)  //遍歷尋找
                {
                    return pos;
                }
                pos++;
            }
            return npos;
        }

7.find字符串  ,從某個位置開始往後尋找字符串,找到瞭就返回下標,找不到就返回npos

這裡套用c語言的庫函數strstr進行實現

size_t find(const char* str, size_t pos = 0)
        {
            assert(pos < _size);
            const char* ptr = strstr(_str + pos, str);   
            if (ptr == nullptr)  //strstr找不到返回空指針
            {
                return npos;    //轉換至cpp找不到就返回npos
            }
            else
            {
                return ptr - _str;  //返回的是下標  指針-指針  ==下標
            }
 
        }

8.clear  清空字符串的內容

直接在第一個位置加入結束標志就將字符串清空瞭,將清空後_size就為0,

void clear()
        {
            _size = 0;
            _str[0] = '\0';
        }

8.重載cin 和 cout

1.cout 

依次的輸出string對象的內容即可

    ostream& operator<<(ostream& out,const string& s)
    {
        for (size_t i = 0; i < s.size(); i++)
        {
            out << s[i];
        }
 
        return out;
    }

2.cin

這裡註意因為要改變字符串的內容,首先調用clear函數清空原來的內容,因為要改變字符串的內容所以不用const 直接引用改變的就是字符串的本身。因為我們的 in 會默認 '空格' 和 ' \n '是分割符,不進行讀取,這樣我們就沒辦法停止。需要使用下 in 的get函數,讓我們的來讀取 ‘  ’  和         ' \n ',我們看下代碼:

void clear()
{
    _str[0] = '\0';
    _size = 0;
}
 
istream& operator>>(istream& in, string& s)
{
    s.clear();//要先進行清理,否則就會出現剩餘的數據也被我們寫入瞭。
    char ch;
    ch = in.get();
 
    char buff[32];
    size_t i = 0;
 
    while (ch != ' ' && ch != '\n')
    {
        buff[i++] = ch;
        if (i == 31)
        {
            buff[i] = '\0';
            s += buff;
            i = 0;
        }
        ch = in.get();
    }
 
    buff[i] = '\0';
    s += buff;
 
    return in;
}

cin和cout的重載不一定是類的友元函數,在類中提供接口,我們也可以直接訪問類的成員變量!

到此這篇關於C++模擬實現string的方法詳解的文章就介紹到這瞭,更多相關C++實現string內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: