最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C++ string 類原理、踩坑與對(duì)象語義詳解

 更新時(shí)間:2026年05月06日 09:00:46   作者:薛定諤のbug  
本文給大家介紹C++ string 類原理、踩坑與對(duì)象語義詳解,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧

學(xué) C++ 到一定階段,std::string 用起來順手,但總感覺底下有一片黑盒子。我拷貝一個(gè)字符串,內(nèi)存是怎么分配的?兩個(gè)對(duì)象賦值,舊資源去哪了?函數(shù)結(jié)束時(shí)發(fā)生了什么?

帶著這些問題,我決定自己實(shí)現(xiàn)一遍 string 類。這篇文章記錄整個(gè)過程:包括每個(gè)函數(shù)的設(shè)計(jì)思路、我踩過的坑、以及由此引出的 C++ 對(duì)象語義。如果你也在這個(gè)階段,這篇文章大概對(duì)你有用。

一、為什么 string 是一個(gè)"資源管理類"

在開始寫代碼之前,先理解 string 的本質(zhì)。

普通的 int、double 變量,值存在棧上,函數(shù)結(jié)束自動(dòng)消失,不用操心。但 string 不一樣——字符數(shù)據(jù)存在上,對(duì)象只持有一個(gè)指針。這塊堆內(nèi)存的生命周期需要手動(dòng)管理:什么時(shí)候分配、什么時(shí)候釋放、被復(fù)制的時(shí)候怎么處理。

這種"持有資源、負(fù)責(zé)管理資源生命周期"的類,在 C++ 里叫做 RAII 類(Resource Acquisition Is Initialization)。string 就是最典型的例子之一:

  • 構(gòu)造函數(shù):獲取資源(new 內(nèi)存)
  • 析構(gòu)函數(shù):釋放資源(delete 內(nèi)存)
  • 拷貝構(gòu)造 / 賦值:處理資源的復(fù)制或轉(zhuǎn)移

理解了這一點(diǎn),后面所有的設(shè)計(jì)決策都能說清楚了。

二、整體結(jié)構(gòu)設(shè)計(jì)

我選用三個(gè)成員變量來描述一個(gè)字符串的完整狀態(tài):

private:
    char*  _str;       // 指向堆上的字符數(shù)組
    size_t _size;      // 當(dāng)前字符串長度(不含 '\0')
    size_t _capacity;  // 當(dāng)前已分配容量(不含 '\0')
    static const size_t npos = -1;

_str 是真正存數(shù)據(jù)的地方,_size 記錄當(dāng)前有多少個(gè)字符,_capacity 記錄申請(qǐng)了多大的空間。三者之間的關(guān)系始終滿足:

_size <= _capacity
實(shí)際分配字節(jié)數(shù) = _capacity + 1(多一個(gè)給 '\0')

npos 定義為靜態(tài)常量,值是 (size_t)-1,也就是 size_t 類型的最大值(在 64 位系統(tǒng)上是 0xFFFFFFFFFFFFFFFF)。它的語義是"不存在"或"到末尾",和標(biāo)準(zhǔn)庫一致。

關(guān)于頭文件和源文件的分離:我把函數(shù)聲明放在 String.h,定義放在 String.cpp,原因是避免重復(fù)定義。如果把函數(shù)體寫在 .h 里,每個(gè) include 這個(gè)頭文件的翻譯單元都會(huì)有一份定義,鏈接時(shí)報(bào)重定義錯(cuò)誤。聲明放 .h、定義放 .cpp 是 C++ 的標(biāo)準(zhǔn)做法。

整個(gè)類放在 namespace Jianyi 里。namespace 的作用是防止命名沖突——我們自己實(shí)現(xiàn)的 string 和標(biāo)準(zhǔn)庫的 std::string 同名,用 namespace 隔開就不會(huì)互相干擾。

三、構(gòu)造函數(shù)與析構(gòu)函數(shù)

構(gòu)造函數(shù)

string(const char* str = "")
{
    assert(str);
    _size = strlen(str);
    _capacity = _size;
    _str = new char[_capacity + 1];
    strcpy(_str, str);
}

幾個(gè)細(xì)節(jié)值得說:

首先,參數(shù)默認(rèn)值是 ""(空字符串),而不是 nullptr。這樣 string s;string s(""); 都能工作,而且 assert(str) 能攔住真正傳 null 的情況。

其次,new char[_capacity + 1] 多申請(qǐng)一個(gè)字節(jié)。_capacity 不計(jì)入 '\0',但數(shù)組必須留位置給它,strcpy 依賴這個(gè)終止符。這個(gè)"差一"的約定在整個(gè)實(shí)現(xiàn)中要保持一致,是容易出錯(cuò)的地方。

strcpy 會(huì)把 str 包括終止符在內(nèi)整個(gè)復(fù)制過去,所以不需要單獨(dú)寫 _str[_size] = '\0'。

析構(gòu)函數(shù)

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

delete[] 對(duì)應(yīng) new[],這是硬性規(guī)則。如果用 delete(不帶方括號(hào)),行為是未定義的——對(duì)于內(nèi)置類型數(shù)組通常僥幸沒問題,但不能依賴。

析構(gòu)后把 _str 置為 nullptr,是防御性編程:如果析構(gòu)后有代碼試圖再次訪問這個(gè)指針,至少不會(huì)訪問已釋放的內(nèi)存(會(huì)立即崩潰,比靜默地讀到垃圾數(shù)據(jù)容易發(fā)現(xiàn)問題)。

delete[] 的本質(zhì):它先調(diào)用數(shù)組中每個(gè)元素的析構(gòu)函數(shù)(對(duì) char 無意義,但對(duì)對(duì)象數(shù)組很重要),再釋放整塊內(nèi)存。這是 new[] / delete[] 必須配對(duì)的原因——分配時(shí) new[] 會(huì)在內(nèi)存塊頭部記錄元素?cái)?shù)量,delete[] 靠這個(gè)數(shù)量來逐一調(diào)用析構(gòu)函數(shù)。如果用 delete 釋放,這個(gè)元數(shù)據(jù)就不會(huì)被讀,析構(gòu)函數(shù)就不會(huì)被逐一調(diào)用。

四、深拷貝 vs 淺拷貝,以及 double free

這是整個(gè) string 實(shí)現(xiàn)最核心的概念。

淺拷貝:直接復(fù)制成員變量的值。對(duì)于 _str 來說,就是復(fù)制指針的值,兩個(gè)對(duì)象指向同一塊堆內(nèi)存。

s1._str ──┐
           ▼
       [ h e l l o \0 ]
           ▲
s2._str ──┘

這個(gè)結(jié)構(gòu)有致命問題:當(dāng) s1 和 s2 分別析構(gòu)時(shí),同一塊內(nèi)存會(huì)被 delete[] 兩次,這就是 double free。double free 是未定義行為,輕則程序崩潰,重則內(nèi)存結(jié)構(gòu)被破壞、引發(fā)安全漏洞。

如果不手動(dòng)實(shí)現(xiàn)拷貝構(gòu)造函數(shù)和賦值運(yùn)算符,編譯器默認(rèn)生成的版本就是淺拷貝。所以,凡是持有堆內(nèi)存的類,必須自己實(shí)現(xiàn)"Big Three":析構(gòu)函數(shù)、拷貝構(gòu)造函數(shù)、賦值運(yùn)算符。

深拷貝:為新對(duì)象單獨(dú)申請(qǐng)內(nèi)存,把數(shù)據(jù)完整復(fù)制一份。

s1._str ──? [ h e l l o \0 ]
s2._str ──? [ h e l l o \0 ]  (獨(dú)立的一份)

兩者獨(dú)立,析構(gòu)互不影響。代價(jià)是每次拷貝都需要一次 new,但安全性有保證。

五、拷貝構(gòu)造函數(shù)(我踩的第一個(gè)大坑)

我注釋掉的第一版:直接深拷貝

// 被注釋掉的版本
string(const string& s)
{
    _str = new char[s._capacity + 1];
    strcpy(_str, s._str);
    _size = s._size;
    _capacity = s._capacity;
}

這個(gè)版本邏輯上是完全正確的,就是樸素的深拷貝:給自己申請(qǐng)內(nèi)存,把對(duì)方的數(shù)據(jù)復(fù)制過來。寫法清晰,沒有問題。

我最終用的版本:copy-and-swap

string(const string& s)
{
    string tmp(s._str);   // 用 char* 構(gòu)造函數(shù)創(chuàng)建臨時(shí)對(duì)象
    swap(tmp);            // 把 *this 和 tmp 互換
}                         // tmp 析構(gòu),釋放 *this 原來(未初始化)的資源

這里用的是 copy-and-swap 慣用法。理解這個(gè)版本,關(guān)鍵是搞清楚 swap 交換的是誰。

swap(tmp) 等價(jià)于 this->swap(tmp),交換的是 *thistmp。執(zhí)行完之后,*this 持有 tmp 創(chuàng)建的那塊內(nèi)存(也就是從 s._str 深拷貝來的數(shù)據(jù)),tmp 則拿走了 *this 原來的內(nèi)容。由于 tmp 是局部變量,函數(shù)結(jié)束時(shí)它會(huì)自動(dòng)析構(gòu),順帶釋放掉 *this 原來那塊資源。

我曾經(jīng)寫錯(cuò)的版本

// 錯(cuò)誤寫法?。?
string(const string& s)
{
    string tmp(s._str);
    swap(s);              // swap 的對(duì)象是 s,不是 tmp!
}

問題有兩個(gè):第一,swap(s) 交換的是 *this 和參數(shù) s,tmp 根本沒被用到;第二,sconst string&,不能被修改,而 swap 需要修改雙方,這從設(shè)計(jì)上就矛盾了。

當(dāng)時(shí)我把 swap 的簽名也寫錯(cuò)了:

void swap(const string& s)  // 錯(cuò)誤:const 參數(shù)無法被修改

正確的簽名應(yīng)該是:

void swap(string& s)  // 去掉 const

swap 必須修改雙方,參數(shù)加 const 在邏輯上就說不通。

六、swap 的實(shí)現(xiàn)與意義

void swap(string& s)
{
    std::swap(_str, s._str);
    std::swap(_size, s._size);
    std::swap(_capacity, s._capacity);
}

這里內(nèi)部調(diào)用的是 std::swap,交換的是指針和兩個(gè)整數(shù)。不管字符串有多長,只有三次賦值操作,時(shí)間復(fù)雜度永遠(yuǎn)是 O(1)。

STL 為什么追求 O(1) 的 swap?因?yàn)?swap 在標(biāo)準(zhǔn)庫算法中被大量使用(排序、partition、各種容器操作),如果 swap 是 O(n) 的,這些算法的復(fù)雜度就會(huì)惡化。

這里有一個(gè)關(guān)于模板的細(xì)節(jié):std::swap 的通用實(shí)現(xiàn)是三次賦值(tmp = a, a = b, b = tmp),對(duì) string 來說意味著兩次深拷貝,是 O(n) 的。但標(biāo)準(zhǔn)庫對(duì) std::string 有特化版本,會(huì)調(diào)用成員函數(shù) swap,也就是我們實(shí)現(xiàn)的這個(gè)版本,降回 O(1)。

普通函數(shù)和函數(shù)模板的優(yōu)先級(jí):當(dāng)存在完全匹配的普通函數(shù)時(shí),編譯器優(yōu)先選擇普通函數(shù),而不是實(shí)例化模板。std::string 的 swap 特化本質(zhì)上就是這個(gè)機(jī)制的體現(xiàn)。我們自己的 Jianyi::string 沒有做這個(gè)特化,但如果有人寫 std::swap(a, b) 來交換我們的對(duì)象,就會(huì)走通用版本(O(n));寫 a.swap(b) 才能走我們的成員函數(shù)(O(1))。

七、賦值運(yùn)算符

我注釋掉的第一版

// 被注釋掉的版本
string& operator=(const string& s)
{
    delete[] _str;
    _str = new char[s._capacity + 1];
    strcpy(_str, s._str);
    _size = s._size;
    _capacity = s._capacity;
    return *this;
}

邏輯清晰:先釋放自己的舊資源,再按對(duì)方重新分配。但有一個(gè)致命缺陷——自賦值問題。

如果寫 s = s,進(jìn)入函數(shù)后先執(zhí)行 delete[] _str,此時(shí) s._str 指向的內(nèi)存已經(jīng)被釋放。接下來 strcpy(_str, s._str) 訪問的是懸空指針,行為未定義。

我的錯(cuò)誤判斷

// 錯(cuò)誤寫法
if (_str != s._str)

這個(gè)判斷想攔住自賦值,但條件判斷的是指針值,不是對(duì)象地址。兩個(gè)不同的對(duì)象,在淺拷貝場景下完全可能 _str 相同(指向同一塊內(nèi)存)。正確的自賦值判斷是比較對(duì)象本身的地址:

if (this != &s)

最終版本:copy-and-swap

string& operator=(const string& s)
{
    if (this != &s)
    {
        string tmp(s._str);   // 深拷貝到臨時(shí)對(duì)象
        swap(tmp);            // 交換資源,tmp 拿走舊資源
    }
    return *this;
}

這個(gè)版本的妙處在于:資源的釋放由 tmp 的析構(gòu)函數(shù)完成,不需要手動(dòng) delete。整個(gè)過程異常安全——如果 new 失敗拋異常,tmp 根本沒構(gòu)造成功,*this 的狀態(tài)沒有被改動(dòng)。

八、reserve:擴(kuò)容的核心

void string::reserve(size_t n)
{
    if (n >= _capacity)
    {
        char* temp = new char[n + 1];
        strcpy(temp, _str);
        delete[] _str;
        _str = temp;
        _capacity = n;
    }
}

reserve 只擴(kuò)容,不縮容(if (n >= _capacity) 保證了這一點(diǎn))。這和 std::string::reserve 的語義一致——你可以申請(qǐng)更大的空間,但不能用 reserve 強(qiáng)行縮小。

操作順序很關(guān)鍵:先 new,再 strcpy,再 delete[] 舊指針,最后更新 _str。順序不能倒。如果先 delete[]new,一旦 new 失敗,_str 就成了懸空指針,對(duì)象處于損壞狀態(tài)。

潛在優(yōu)化點(diǎn):strcpy 只適合復(fù)制以 '\0' 結(jié)尾的字符串,換成 memcpy(_str, _capacity + 1) 在某些實(shí)現(xiàn)里更快(省去逐字符檢查終止符的開銷),但這里用 strcpy 足夠清晰。

九、PushBack 和 append

PushBack:追加單個(gè)字符

void string::PushBack(char ch)
{
    if (_size == _capacity)
        reserve(_capacity == 0 ? 4 : 2 * _capacity);
    _str[_size] = ch;
    _size++;
    _str[_size] = '\0';
}

擴(kuò)容策略是翻倍(2 * _capacity),初始容量為 0 時(shí)直接給 4。翻倍策略保證均攤 O(1) 的追加代價(jià)——即使偶爾觸發(fā)擴(kuò)容(O(n)),均攤到每次追加上依然是常數(shù)時(shí)間。

string& string::operator+=(char ch)
{
    PushBack(ch);
    return *this;
}

operator+= 直接復(fù)用 PushBack,邏輯不重復(fù)。

append:追加字符串

void string::append(const char* str)
{
    assert(str);
    size_t len = strlen(str);
    if (_size + len > _capacity)
        reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
    size_t n = 0;
    while (n < len)
    {
        _str[n + _size] = str[n];
        ++n;
    }
    _size += len;
    _str[_size] = '\0';
}

擴(kuò)容判斷:優(yōu)先翻倍,如果翻倍后還不夠就直接擴(kuò)到需要的大?。?code>_size + len)。這個(gè)邏輯在 insert 里也有類似寫法,是一個(gè)常見的"至少滿足需求,盡量翻倍"策略。

追加數(shù)據(jù)后必須手動(dòng)設(shè)置 _str[_size] = '\0',因?yàn)檫@里用的是逐字符賦值,不像 strcpy 會(huì)自動(dòng)附帶終止符。

operator+= 有一個(gè) const char* 版本聲明在頭文件里,但實(shí)現(xiàn)體我沒有寫(被注釋掉了)。實(shí)際會(huì)調(diào)用 append:

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

十、insert:插入時(shí)的無符號(hào)類型陷阱

插入單個(gè)字符

void string::insert(size_t pos, char ch)
{
    assert(pos <= _size);
    if (_size == _capacity)
        reserve(_capacity == 0 ? 4 : 2 * _capacity);
    size_t end = _size + 1;
    while (end > pos)
    {
        _str[end] = _str[end - 1];
        --end;
    }
    _str[pos] = ch;
    _size += 1;
}

后移數(shù)據(jù)從末尾開始,end_size + 1pos 走,每次把 end - 1 位置的字符移到 end

核心踩坑:我一開始寫的是 while (end >= pos)。endpos 都是 size_t(無符號(hào)整數(shù))。當(dāng) pos = 0、end 減到 0 之后再執(zhí)行 --end,結(jié)果不是 -1,而是 size_t 的最大值(約 1.8 × 10^19),條件永遠(yuǎn)為真,死循環(huán)。

改成 while (end > pos),當(dāng) end == pos 時(shí)循環(huán)停止,從根本上避免了無符號(hào)數(shù)下溢。這是 C++ 里用 size_t 做循環(huán)變量的經(jīng)典陷阱,C 語言的隱式類型轉(zhuǎn)換讓這類 bug 非常難發(fā)現(xiàn)。

代碼注釋里也寫了:while(end <= (int)pos) 是另一種解法——強(qiáng)轉(zhuǎn)成有符號(hào)類型,但不如直接改邏輯條件來得干凈。

插入字符串

void string::insert(size_t pos, const char* str)
{
    size_t len = strlen(str);
    if (_size + len > _capacity)
        reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
    size_t end = _size + len;
    while (end > pos + len - 1)
    {
        _str[end] = _str[end - len];
        --end;
    }
    for (size_t i = 0; i < len; ++i)
        _str[pos + i] = str[i];
    _size += len;
}

和單字符版本類似,但每次移動(dòng) len 個(gè)位置。先把 pos 之后的數(shù)據(jù)整體后移 len 格,再把 str 的內(nèi)容填入 [pos, pos + len) 的位置。

注釋里還有一個(gè)被廢棄的版本:

// 廢棄版本
for (size_t i = _size; i >= pos; --i)
{
    _str[i + len] = _str[i];
}

同樣是 size_t 的無符號(hào)下溢問題——i 減到 0 之后再減會(huì)繞回最大值,死循環(huán)。

十一、erase:刪除子串

void string::erase(size_t pos, size_t len)
{
    assert(pos <= _size);
    if (len == npos || len > _size - pos)
    {
        _str[pos] = '\0';
        _size = pos;
    }
    else
    {
        for (size_t i = pos + len; i <= _size; ++i)
            _str[i - len] = _str[i];
        _size -= len;
    }
}

兩種情況:如果 lennpos(表示"到末尾"),或者刪除長度超出剩余部分,就直接截?cái)?mdash;—在 pos 處寫 '\0',更新 _size 即可,不需要移動(dòng)任何數(shù)據(jù)。

否則,把 pos + len 之后的數(shù)據(jù)前移 len 格。循環(huán)條件 i <= _size 包含了 _size 本身,這樣連終止符 '\0' 也會(huì)一起被移過來,不需要單獨(dú)設(shè)置。

邊界注意:len > _size - pos 這個(gè)判斷,如果 _size < pos 會(huì)發(fā)生無符號(hào)數(shù)下溢(值繞回變成超大數(shù)),但 assert(pos <= _size) 保證了進(jìn)函數(shù)時(shí) pos <= _size,所以 _size - pos 不會(huì)下溢,是安全的。

十二、find:字符串查找

查找單個(gè)字符

size_t string::find(char ch, size_t pos)
{
    assert(pos < _size);
    for (size_t i = pos; i < _size; ++i)
        if (_str[i] == ch)
            return i;
    return npos;
}

pos 開始線性掃描,找到返回下標(biāo),找不到返回 npos

查找子串

size_t string::find(char* str, size_t pos)
{
    assert(pos < _size);
    const char* temp = strstr(_str + pos, str);
    if (temp == nullptr)
        return npos;
    else
        return temp - _str;
}

這里借助了標(biāo)準(zhǔn)庫函數(shù) strstr。strstr 的原理是滑動(dòng)匹配:從主串的每個(gè)位置開始,嘗試和子串逐字符比較;如果當(dāng)前位置不匹配,移動(dòng)到下一個(gè)位置重新開始。樸素實(shí)現(xiàn)是 O(n × m),標(biāo)準(zhǔn)庫的實(shí)現(xiàn)通常有優(yōu)化(類似 KMP 或 Boyer-Moore),但接口語義是一樣的。

strstr 返回的是指針,指向主串中子串開始的位置。用這個(gè)指針減去 _str(數(shù)組起始地址),就得到了下標(biāo)。這是指針運(yùn)算的一個(gè)典型用法:兩個(gè)指針相減得到元素個(gè)數(shù)(距離),前提是兩者指向同一塊數(shù)組。

十三、substr:截取子串

string string::substr(size_t pos, size_t len)
{
    if (len > _size - pos)
        len = _size - pos;
    string sub;
    sub.reserve(len);
    sub._str[0] = '\0';
    for (size_t i = 0; i < len; ++i)
        sub += _str[pos + i];
    return sub;
}

先修正 len(不讓它超出剩余長度),然后構(gòu)造一個(gè)空字符串 sub,reserve 好空間,再逐字符追加。

注釋里有一個(gè)優(yōu)化版本:

// 優(yōu)化版本(被注釋掉)
string sub;
sub._str = new char[len + 1];
sub._capacity = len;
sub._size = len;
memcpy(sub._str, _str + pos, len);
sub._str[len] = '\0';
return sub;

這個(gè)版本一次性分配內(nèi)存、用 memcpy 批量復(fù)制,性能更好,不需要逐字符調(diào)用 operator+= 可能觸發(fā)多次擴(kuò)容。代價(jià)是代碼稍微復(fù)雜一點(diǎn),需要直接操作成員變量(所以只能在類內(nèi)部寫,或者聲明為友元)。

十四、比較運(yùn)算符

bool operator<(const string& s1, const string& s2)
{
    return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator==(const string& s1, const string& s2)
{
    return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator>(const string& s1, const string& s2)  { return strcmp(...) > 0; }
bool operator<=(const string& s1, const string& s2) { return (s1 < s2) || s1 == s2; }
bool operator>=(const string& s1, const string& s2) { return (s1 > s2) || s1 == s2; }
bool operator!=(const string& s1, const string& s2) { return !(s1 == s2); }

核心是 <==,其他運(yùn)算符全部復(fù)用這兩個(gè)。strcmp 返回負(fù)數(shù)表示"小于",0 表示"等于",正數(shù)表示"大于"——和返回 bool 的比較運(yùn)算符語義完全對(duì)應(yīng)。

這些運(yùn)算符都是非成員函數(shù),寫在類外面。原因是 ==< 等是對(duì)稱的二元運(yùn)算,左右操作數(shù)應(yīng)該平等對(duì)待,如果寫成成員函數(shù),左操作數(shù)必須是 string 對(duì)象,會(huì)限制使用靈活性。

十五、c_str() 的意義

const char* c_str() const
{
    return _str;
}

這個(gè)函數(shù)看起來簡單,但它是 string 和 C 風(fēng)格字符串交互的橋梁。很多 C 標(biāo)準(zhǔn)庫函數(shù)(printf、fopen、strlen 等)只接受 const char*,不認(rèn)識(shí) C++ 的 string 對(duì)象。c_str() 提供了一個(gè)合法的、以 '\0' 結(jié)尾的字符指針,讓 C++ string 能融入 C 的生態(tài)。

返回 const char* 而不是 char*,是有意為之:禁止外部通過這個(gè)指針修改字符串內(nèi)容,保護(hù)對(duì)象的內(nèi)部狀態(tài)。

十六、operator<< 和 operator>>

為什么要實(shí)現(xiàn)這兩個(gè)

標(biāo)準(zhǔn)庫的 coutcin 不認(rèn)識(shí)我們自己實(shí)現(xiàn)的 string 類。如果不重載 operator<<operator>>,cout << s 就沒法編譯。實(shí)現(xiàn)了之后,我們的 string 就能無縫接入 C++ 的流體系。

operator<<

ostream& operator<<(ostream& out, string& s)
{
    for (auto ch : s)
        out << ch;
    return out;
}

逐字符輸出。這里用了范圍 for 循環(huán),依賴 string 提供了 begin()end() 迭代器:

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

指針就是最簡單的迭代器,對(duì)字符數(shù)組完全適用。

operator>>(我踩的最隱蔽的 bug)

錯(cuò)誤版本(注釋里):

// 錯(cuò)誤寫法
buff[i++] = ch;
s += buff;   // buff 里有 ch
s += ch;     // ch 又被單獨(dú)加了一次 —— 重復(fù)!

每個(gè)字符被寫入了兩次,一次在 buff 里追加到 s,一次單獨(dú) += ch。讀進(jìn)來的字符串會(huì)變成雙倍。

正確版本

istream& operator>>(istream& in, string& s)
{
    s.clear();
    const int N = 256;
    char buff[N];
    int i = 0;
    char ch = in.get();
    while (ch != ' ' && ch != '\n' && ch != EOF)
    {
        buff[i++] = ch;
        if (i == N - 1)
        {
            buff[i] = '\0';
            s += buff;
            i = 0;
        }
        ch = in.get();
    }
    if (i > 0)
    {
        buff[i] = '\0';
        s += buff;
    }
    return in;
}

每個(gè)字符只走一條路徑:先進(jìn) buff,buff 滿了就 flush 到 s,清空 buff 繼續(xù)。讀完循環(huán)后,把剩余不滿一 buff 的數(shù)據(jù)再 flush 一次。

用緩沖區(qū)的好處:每 255 個(gè)字符才觸發(fā)一次 operator+=,大幅減少了可能的擴(kuò)容次數(shù),比逐字符 s += ch 高效很多。

十七、引用計(jì)數(shù):為什么現(xiàn)代 string 不用它

除了深拷貝,還有第三種資源管理策略:引用計(jì)數(shù)。思路是讓多個(gè)對(duì)象共享同一塊內(nèi)存,同時(shí)用一個(gè)計(jì)數(shù)器記錄有多少個(gè)對(duì)象指向它。

s1._str ──┐
           ▼
       [ h e l l o \0 ]  (count = 2)
           ▲
s2._str ──┘

拷貝時(shí)不申請(qǐng)新內(nèi)存,只把 count 加 1。析構(gòu)時(shí)把 count 減 1,只有 count 減到 0 時(shí)才真正 delete。這避免了頻繁 new/delete 的開銷,在大量拷貝場景下性能更好。

早期的 std::string 實(shí)現(xiàn)(GCC 的 libstdc++ 在 C++11 之前)確實(shí)用過引用計(jì)數(shù)。但 C++11 之后基本被廢棄,原因有兩個(gè):

第一,多線程問題。引用計(jì)數(shù)本身需要原子操作來保證線程安全,而原子操作有不小的開銷。在多核環(huán)境下,多個(gè)線程頻繁地增減同一個(gè)引用計(jì)數(shù),會(huì)導(dǎo)致緩存行頻繁失效(false sharing),性能反而不如深拷貝。

第二,寫時(shí)拷貝(COW)的隱患。引用計(jì)數(shù)通常配合寫時(shí)拷貝使用:只有在修改字符串時(shí)才真正復(fù)制一份("lazy copy")。但這個(gè)機(jī)制在多線程下存在微妙的 race condition,實(shí)現(xiàn)正確性極難保證。

現(xiàn)代 string 普遍采用SSO(Small String Optimization):短字符串(通常 15 個(gè)字符以內(nèi))直接存在對(duì)象內(nèi)部的固定緩沖區(qū)里,不 new 堆內(nèi)存,徹底避免了動(dòng)態(tài)分配的開銷,只有超過閾值的長字符串才退化到堆分配。這是目前性能最好且實(shí)現(xiàn)最干凈的方案。

十八、代碼里可以優(yōu)化的地方

結(jié)合整個(gè)實(shí)現(xiàn),有幾個(gè)地方可以做得更好:

substr 的逐字符追加:當(dāng)前實(shí)現(xiàn)用 operator+= 逐字符追加,每次都有可能觸發(fā)擴(kuò)容判斷。改用 memcpy 一次性復(fù)制更高效(注釋里已經(jīng)有這個(gè)優(yōu)化版本)。

operator>> 的參數(shù)operator<< 的第二個(gè)參數(shù)寫的是 string&,應(yīng)該改成 const string&,因?yàn)檩敵霾僮鞑恍薷膶?duì)象。

substr 沒有檢查 pos:如果 pos > _size,_size - pos 會(huì)下溢(無符號(hào)數(shù))。應(yīng)該加一個(gè) assert(pos <= _size) 或直接返回空字符串。

find 的 pos 邊界findassert(pos < _size) 會(huì)拒絕在空字符串或末尾位置查找,實(shí)際上 pos == _size 時(shí)應(yīng)該允許(直接返回 npos),斷言條件過嚴(yán)。

erase 沒有更新 _str[_size]:在截?cái)喾种Вㄖ苯訉?'\0' 的那條),_str[pos] = '\0' 是正確的;在移動(dòng)分支,循環(huán)已經(jīng)把 _str[_size]'\0' 移過來了。這里是對(duì)的,但值得在代碼注釋里說清楚,否則容易讓人擔(dān)心。

總結(jié)

寫完這個(gè) string 類,我對(duì)幾個(gè)核心概念有了真實(shí)的理解:

深拷貝不只是"多 new 一份內(nèi)存",它的本質(zhì)是讓每個(gè)對(duì)象對(duì)自己的資源負(fù)全責(zé),互不干擾。copy-and-swap 慣用法把資源管理的復(fù)雜性封裝進(jìn)構(gòu)造函數(shù)和析構(gòu)函數(shù)里,讓賦值運(yùn)算符變得異常安全且簡潔。swap 的 O(1) 不是魔法,而是因?yàn)榻粨Q的只是指針,不是數(shù)據(jù)。size_t 的無符號(hào)特性在循環(huán)邊界處是一個(gè)持續(xù)的坑,必須特別小心。

C++ 的核心難點(diǎn)不是語法,是對(duì)象的生命周期資源歸屬。這一點(diǎn)寫完 string 之后,我覺得理解得清楚多了。

到此這篇關(guān)于C++ string 類原理、踩坑與對(duì)象語義詳解的文章就介紹到這了,更多相關(guān)C++ string 類原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • C語言中單目操作符++、–的實(shí)例講解

    C語言中單目操作符++、–的實(shí)例講解

    C語言的操作符共分為算術(shù)操作符,移位操作符,位操作符,賦值操作符,單目操作符,關(guān)系操作符,邏輯操作符,條件操作符,逗號(hào)表達(dá)式,下表引用、函數(shù)調(diào)用和結(jié)構(gòu)成員這10大類,這篇文章主要給大家介紹了關(guān)于C語言中單目操作符++、–的相關(guān)資料,需要的朋友可以參考下
    2021-12-12
  • C++新特性詳細(xì)分析基于范圍的for循環(huán)

    C++新特性詳細(xì)分析基于范圍的for循環(huán)

    C++11這次的更新帶來了令很多C++程序員期待已久的for?range循環(huán),每次看到j(luò)avascript,?lua里的for?range,心想要是C++能有多好,心里別提多酸了。這次C++11不負(fù)眾望,再也不用羨慕別家人的for?range了。下面看下C++11的for循環(huán)的新用法
    2022-04-04
  • c語言struct結(jié)構(gòu)體強(qiáng)制類型轉(zhuǎn)換的實(shí)現(xiàn)

    c語言struct結(jié)構(gòu)體強(qiáng)制類型轉(zhuǎn)換的實(shí)現(xiàn)

    本文深入探討了C語言中結(jié)構(gòu)體的定義、初始化及成員訪問,包括無標(biāo)簽和顯示標(biāo)簽聲明,以及如何通過typedef簡化結(jié)構(gòu)體使用,具有一定的參考價(jià)值,感興趣的可以了解一下
    2026-01-01
  • C++實(shí)現(xiàn)經(jīng)典24點(diǎn)紙牌益智游戲

    C++實(shí)現(xiàn)經(jīng)典24點(diǎn)紙牌益智游戲

    這篇文章主要介紹了C++實(shí)現(xiàn)經(jīng)典24點(diǎn)紙牌益智游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-03-03
  • 詳解C語言中的rename()函數(shù)和remove()函數(shù)的使用方法

    詳解C語言中的rename()函數(shù)和remove()函數(shù)的使用方法

    這篇文章主要介紹了詳解C語言中的rename()函數(shù)和remove()函數(shù)的使用方法,是C語言入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下
    2015-09-09
  • C語言二維數(shù)組的處理實(shí)例

    C語言二維數(shù)組的處理實(shí)例

    這篇文章主要介紹了C語言二維數(shù)組的處理實(shí)例,有需要的朋友可以參考一下
    2013-12-12
  • C++實(shí)現(xiàn)對(duì)象化的矩陣相乘小程序

    C++實(shí)現(xiàn)對(duì)象化的矩陣相乘小程序

    這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)對(duì)象化的矩陣相乘小程序,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-09-09
  • 詳解C語言-二級(jí)指針三種內(nèi)存模型

    詳解C語言-二級(jí)指針三種內(nèi)存模型

    這篇文章主要介紹了詳解C語言-二級(jí)指針三種內(nèi)存模型的相關(guān)知識(shí),文中代碼非常詳細(xì),供大家參考和學(xué)習(xí),感興趣的朋友可以了解下
    2020-06-06
  • 仿現(xiàn)代C++智能指針實(shí)現(xiàn)引用計(jì)數(shù)

    仿現(xiàn)代C++智能指針實(shí)現(xiàn)引用計(jì)數(shù)

    這篇文章主要為大家詳細(xì)介紹了如何仿現(xiàn)代C++智能指針實(shí)現(xiàn)引用計(jì)數(shù),文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,有需要的小伙伴可以了解下
    2024-03-03
  • C語言入門篇--關(guān)鍵字static詳解

    C語言入門篇--關(guān)鍵字static詳解

    本篇文章是C語言系列基礎(chǔ)篇,C語言中,static是用來修飾變量和函數(shù):1.修飾局部變量–>靜態(tài)局部變量2.修飾全局變量–>靜態(tài)全局變量3.修飾函數(shù)–>靜態(tài)函數(shù)
    2021-08-08

最新評(píng)論

东阳市| 萨嘎县| 景德镇市| 上饶市| 乐陵市| 舟曲县| 莱西市| 额尔古纳市| 甘孜县| 大新县| 九龙县| 洛宁县| 定陶县| 新野县| 虞城县| 车致| 合阳县| 清河县| 扎兰屯市| 连云港市| 苏尼特左旗| 夏津县| 盐池县| 辽阳市| 涟源市| 英德市| 福州市| 天镇县| 博罗县| 繁昌县| 青铜峡市| 延庆县| 桦南县| 南城县| 娄烦县| 全南县| 霞浦县| 衡阳县| 云林县| 寻甸| 积石山|