C++ string的內部究竟是什麼樣的?

RioTian發表於2021-01-02

在C語言中,有兩種方式表示字串:

  • 一種是用字元陣列來容納字串,例如char str[10] = "abc",這樣的字串是可讀寫的;
  • 一種是使用字串常量,例如char *str = "abc",這樣的字串只能讀,不能寫。

兩種形式總是以\0作為結束標誌。

C++ string 與它們在C語言中的前身截然不同。首先,也是最重要的不同點,C++ string 隱藏了它所包含的字元序列的物理表示。程式設計人員不必關心陣列的維數或\0方面的問題。

string 在內部封裝了與記憶體和容量有關的資訊。具體地說,C++ string 物件知道自己在記憶體中的開始位置、包含的字元序列以及字元序列長度;當記憶體空間不足時,string 還會自動調整,讓記憶體空間增長到足以容納下所有字元序列的大小。

C++ string 的這種做法,極大地減少了C語言程式設計中三種最常見且最具破壞性的錯誤:

  • 陣列越界;
  • 通過未被初始化或者被賦以錯誤值的指標來訪問陣列元紊;
  • 釋放了陣列所佔記憶體,但是仍然保留了“懸空”指標。

C++ 標準沒有定義 string 類的記憶體佈局,各個編譯器廠商可以提供不同的實現,但必須保證 string 的行為一致。採用這種做法是為了獲得足夠的靈活性。

特別是,C++ 標準沒有定義在哪種確切的情況下應該為 string 物件分配記憶體空間來儲存字元序列。string 記憶體分配規則明確規定:允許但不要求以引用計數(reference counting)的方式實現。但無論是否採用引用計數,其語義都必須一致。

C++ 的這種做法和C語言不同,在C語言中,每個字元型陣列都佔據各自的物理儲存區。在 C++ 中,獨立的幾個 string 物件可以佔據也可以不佔據各自特定的物理儲存區,但是,如果採用引用計數避免了儲存同一資料的拷貝副本,那麼各個獨立的物件(在處理上)必須看起來並表現得就像獨佔地擁有各自的儲存區一樣。例如:

// #include<bits/stdc++.h>
#include <iostream>
#include <string>
using namespace std;

int main() {
    string s1("12345");
    string s2 = s1;
    cout << (s1 == s2) << endl;
    s1[0] = '6';
    cout << "s1 = " << s1 << endl;  // 62345
    cout << "s2 = " << s2 << endl;  // 12345
    cout << (s1 == s2) << endl;

    return 0;
}

在 GCC 下的執行結果:

1
s1 = 62345
s2 = 12345
0

只有當字串被修改的時候才建立各自的拷貝,這種實現方式稱為寫時複製(copy-on-write)策略。當字串只是作為值引數(value parameter)或在其他只讀情形下使用,這種方法能夠節省時間和空間。

不論一個庫的實現是不是採用引用計數,它對 string 類的使用者來說都應該是透明的。遺憾的是,情況並不總是這樣。在多執行緒程式中,幾乎不可能安全地使用引用計數來實現。

相關文章