1. 程式人生 > >C++string類的實現

C++string類的實現

C++中提供了一種新的資料型別——字串型別(string)。實際上string並不是C++的基本型別,它是在C++標準庫中宣告的一個字串類,用這種資料型別可以定義物件,每一個字串變數都是string類的一個物件。標準庫型別string表示可變長的字元序列,使用string型別必須首先包含它的標頭檔案。
作為標準庫的一部分,string定義在名稱空間std中。
【例】

#include<string>//注意這裡沒有.h 
using namespace std;

string類的意義有兩個:第一個是為了處理char型別的陣列,並封裝了標準C中的一些字串處理的函式。而當string類進入了C++標準後,它的第二個意義就是一個容器。

string類有106個成員介面函式。C++ 的一個常見面試題是讓你實現一個 String 類,限於時間,不可能要求具備 std::string 的功能,但至少要求能正確管理資源。具體來說:
1)能像 int 型別那樣定義變數,並且支援賦值、複製。
2)能用作函式的引數型別及返回型別。
3)能用作標準庫容器的元素型別,即 vector/list/deque 的 value_type。(用作 std::map 的 key_type 是更進一步的要求,本文從略)。

下面是模擬實現string類的幾個重要函式:
1、建構函式
【例】

#define _CRT_SECURE_NO_WARNINGS
#include<string> using namespace std; class String { public: String(char *str = "") { if (str == NULL) { _str = new char[1];//為了與delete[]配合使用 *_str = '\0'; size = 0; } else { int length = strlen(str); _str = new
char[length + 1]; strcpy(_str, str); size = length; } } ~String() { if (NULL != _str) { delete[] _str;//delete[]釋放的空間必須是動態分配的 } } private: char *_str;//指向字串的指標 size_t size;//儲存當前字串長度 }; void Test2() { String s1("hello"); String s2(new char[3]); } int main() { Test2(); system("pause"); return 0; }

釋:在建構函式中,_str被初始化為空字串(只有‘\0’)而不是NULL。因為C++中的任何字串的長度至少為1(即至少包含一個結束符‘\0’)。孔字串也是有效的字串,它的長度為1,因此他代表一塊合法的記憶體單元而不是NULL。

2、拷貝建構函式
1)淺拷貝
【例】

String::String(const String& s)//淺拷貝
    :_str(s._str)
{}

void Test2()
{
    String s1("hello");
    String s2(s1);
}

以上程式碼會使系統崩潰。因為s1只是將_str的地址傳給s2中的_str,即淺拷貝(位拷貝),如下圖所示:
這裡寫圖片描述
由圖可知:s1和s2指向同一塊記憶體,析構時先對s2釋放“hello”這塊空間,當要再析構s1時,系統將崩潰。

釋:淺拷貝又稱為位拷貝,編譯器只是將指標的內容拷貝過來,導致多個物件共用一塊記憶體空間,當其中任意物件將這塊空間釋放之後,當再次訪問時將出錯。

解決方法:(深拷貝)給要拷貝構造的物件重新分配空間。
【例】

String::String(const String& s)//深拷貝
    :_str(new char[strlen(s._str) + 1])
{
    strcpy(_str, s._str);
    size = strlen(s._str);
}

void Test2()
{
    String s1("hello");
    String s2(s1);
}

其執行的狀態如圖所示:
這裡寫圖片描述
由圖可知:拷貝的物件s2中_str的值(字串的地址)和s1物件中的_str的值不同,“hello”儲存在地址不同的兩個空間裡,說明了系統為物件重新開闢了空間——深拷貝。

其工作原理如圖所示:
這裡寫圖片描述

3、拷貝賦值函式
【例】

    //方法一:
    String& String::operator=(const String& s)
    {
        if (this != &s)//檢查自賦值
        {
            delete[]_str;
            _str = new char[strlen(s._str) + 1];
            strcpy(_str, s._str);
          size = strlen(s._str);

        }
        return *this;//為了支援鏈式訪問
    }

    //方法二:
    String& String:: operator=(const String& s)
    {
        if (this != &s)//1)檢查自賦值
        {//2)建立臨時變數獲得分配的記憶體空間,並複製原來的內容
            char *tmp = new char[strlen(s._str) + 1];
            strcpy(tmp, s._str);
            delete[]_str;//3)釋放原有的記憶體
            _str = s._str;
            size = strlen(s._str);
        }
        return *this;//4)返回本物件引用
    }

其執行狀態如圖所示:
這裡寫圖片描述

分析:一般情況下,上面的兩種寫法都可以,但是相對而言,第二種更優一點。
方法一,先釋放原有的空間,但是如果下面用new開闢新空間時失敗了,而這時將s2賦值給s3,不僅不能成功賦值(空間開闢失敗),還破壞了原有的s3物件。
方法二,先開闢新空間,將新空間的地址賦給一個臨時變數,就算這時空間開闢失敗,也不會影響原本s3物件。
綜上所述:第二種方法更優一點。

注意:最後的返回值是為了支援鏈式訪問。
例如:s3 = s2 = s1;

4、拷貝建構函式的現代寫法:
【例】

String::String(const String& s)
    :_str(NULL)//一定要對_str初始化
{
    String tmp(s._str);
    std::swap(tmp._str, _str);
}

釋:如果沒有初始化,_str的值很可能是一個隨機值,其指向的記憶體空間是不合法的。當tmp._str和_str交換後析構tmp就會出錯。

5、賦值運算子過載函式的兩種現代寫法
【例】

//第一種:
String& String::operator=(String s)
{
    std::swap(_str, s._str);
    return *this;
}

//第二種:
String& String::operator=(const String& s)
{
    if (this->_str != s._str)
    {
        String tmp(s);
        std::swap(tmp._str, _str);
    }
    return *this;
}

在面試時,一般寫出上面四個string類的成員函式即可,除非面試官特別要求。