自實現string類

2023-11-27 18:00:31

一. 環境

Linux x86_64,g++ 8.5.0

二. 實現

自實現 string 之前一直想寫來著,一直拖著,現在把它完稿。這個版本是比較簡單的版本,有一些可能有不同的或者更好的實現方式,後面有機會會加到裡面。

打算實現的介面如下

class MyString
{
    friend std::ostream & operator<<(std::ostream & co, const MyString &ms);
    friend std::istream & operator>>(std::istream & ci, MyString &ms);
public:
    MyString(const char * s = nullptr);
    ~MyString();
    MyString(const MyString & another);
    MyString & operator=(const MyString & another);

    MyString operator+(const MyString & another);
    MyString & operator+=(const MyString & another);
    bool operator>(const MyString & another);
    bool operator<(const MyString & another);
    bool operator==(const MyString & another);
    char & operator[](int n);
    char & at(int n);
private:
    char * m_str;
};
  1. 建構函式,引數使用預設引數,預設引數作標記位。不論是否傳遞實參,申請資源時均以陣列形式申請。不傳遞實參時,申請一個 char 的陣列,有傳遞實參時,以實際的為準。這樣,在釋放資源時,均可以 delete []m_str 形式釋放。
MyString::MyString(const char *str)
{
    if (nullptr == str)
    {
        m_str = new char[1];
        *m_str = '\0';
    }
    else
    {
        m_str = new char[strlen(str)+1];
        strcpy(m_str, str);
    }
}
  1. 拷貝賦值,使用了兩種方式。

第一種是基礎的寫法,先 delete 堆上的空間,再申請新的空間,然後複製內容。需要注意的是,需判斷是否是自賦值的情況。

第二種採用了 copy && swap 技術,相對完善一點,前一種方式,在 delete []m_str 後如果程式出現異常,此時其它地方有使用到 m_str 的話就尷尬了,而後一種方式就沒有這個問題。

// version1
/*
MyString & MyString::operator=(const MyString &another)
{
    if (this == &another)
    {
        return *this;
    }

    delete []m_str;
    int len = strlen(another.m_str);
    m_str = new char[len+1];
    strcpy(m_str, another.m_str);

    return *this;
}
*/

// version2,採用 copy and swap 技術
MyString & MyString::operator=(const MyString &another)
{
    if (this == &another)
    {
        return *this;
    }

    MyString ms(another);
    std::swap(this->m_str, ms.m_str);

    return *this;
}
  1. 過載 + 運運算元,成員函數返回一個臨時物件。在申請新的空間後,在使用 strcat() 之前需要初始化,否則可能會出現問題。strcat() 是從末尾為 '\0' 的地方開始拼接的。
MyString MyString::operator+(const MyString &another)
{
    MyString ms;

    int len = strlen(this->m_str) + strlen(another.m_str);
    delete []ms.m_str;
    ms.m_str = new char[len +1]{0};  // 注意初始化
    strcat(strcat(ms.m_str, this->m_str), another.m_str);

    return ms;
}
  1. 過載 += 運運算元,返回值型別是參照型別,這樣可以連續使用 +=

使用 realloc() 後,在使用 strcat() 連線兩個字串之前,需要將 m_str 後面一部分新擴充的空間進行初始化。

MyString & MyString::operator+=(const MyString &another)
{
    int lenOfSource = strlen(this->m_str);
    int lenOfAnother = strlen(another.m_str);
    this->m_str = (char *)realloc(this->m_str, lenOfSource+lenOfAnother+1);
    memset(this->m_str+lenOfSource, 0, lenOfAnother+1);
    strcat(this->m_str, another.m_str);

    return *this;
}
  1. 過載 > 運運算元
bool MyString::operator>(const MyString &another)
{
    return strcmp(this->m_str, another.m_str) > 0;
}

過載 <== 與上面類似,就不重複列舉了。

  1. 過載 [] 運運算元,這個沒啥好說的了。
char & MyString::operator[](int n)
{
    return m_str[n];
}
  1. 成員函數 at()
char & MyString::at(int n)
{
    return m_str[n];
}
  1. 過載輸出 << 和 輸入 >> 運運算元。

在測試成員函數前,可以早點寫這兩個函數,測試時就方便列印了,不然還需要單獨新增一個成員函數返回 m_str 了。

過載運運算元,目標形式是:

Mystring ms;
cout << ms; 
cin >> ms;

對於過載,一般會考慮到成員函數過載和全域性過載,但是 ostream 類和 istream 類都是系統提供的類,我們不可能在 ostream 類和 istream 類中進行修改,因此只能放棄成員函數過載。此時,只能是全域性過載,即全域性函數過載了。

考慮到會連續輸出(cout << a << b;),因此返回型別是 ostream & 型別,它是經入參而來,入參型別也是 ostream &

std::ostream & operator<<(std::ostream & co, const MyString &ms)
{
    co << ms.m_str;
    return co;
}

輸入運運算元與輸出運運算元類似,第二個入參不能是 const 型別,因為需要修改入參 ms。這裡處理的相對簡單了,棧上申請了1024位元組的字元陣列用以儲存輸入的資料,實際上會有不夠用的情況。

std::istream & operator>>(std::istream & ci, MyString &ms)
{
    // 簡單處理,申請一塊固定大小的記憶體
    char ch[1024];
    ci >> ch;
    delete []ms.m_str;
    ms.m_str = new char[strlen(ch)+1];
    strcpy(ms.m_str, ch);
    return ci;
}

三. 完整程式碼,可點選連結 mystring ,如有有問題或不到之處,請指出並交流,看到後我會修改。

四. 參考

C++基礎與提高 王桂林