C++中的std::string


字符串字面量

字符串字面量位於字面量池中,字面量池位於程序的常量區

void show_address(const char* str) {
    std::cout << reinterpret_cast<const void*>(str) << std::endl;
}

int main()
{
    // 三者位於同一個地址上
    show_address("Hello");
    show_address("Hello");
    show_address("Hello");
    // C++中允許對字符串字面量取地址,即&"Hello" 得到的地址與上文相同
}

對於指針和數組,它們代表的含義不同

// pStr指針位於全局區中 指向位於常量區中的字符串字面量
const char* pStr = "Hello";

int main() 
{
    // strArr位於棧中 將數據從常量區拷貝到函數棧中
	char strArr[] = "Hello";
}

std::string的內存分配

C++對std::string的內部實現有如下約定

  • 如果傳入的字符串字面量小於某閾值,那么該std::string內部在棧上分配內存(即短字符串優化——SSO);如果大於指定的閾值,那么將會根據傳入的字符串的尺寸,在堆上開辟相應的空間。不管是短字符串還是長字符串,在使用字符串字面量構建std::string的時候,都會產生拷貝的操作
  • 如果后續對std::string采用了“增”操作,那么將會采用double的形式進行擴容(雙倍擴容)

在通常情況下,若數據的長度小於等於15(還有一位是'\0'結束符),那么會采用短字符串優化(這主要取決於不同庫的實現)

// MSVC中的實現
// length of internal buffer, [1, 16]:
static constexpr size_type _BUF_SIZE = 16 / sizeof(value_type) < 1 ? 1 : 16 / sizeof(value_type);

std::string的結構

在MSVC-Release-x64的環境下,std::string的大小是32B

using string  = basic_string<char, char_traits<char>, allocator<char>>;
using _Alty        = _Rebind_alloc_t<_Alloc, _Elem>;
using _Alty_traits = allocator_traits<_Alty>;

using _Scary_val = _String_val<conditional_t<_Is_simple_alloc_v<_Alty>, _Simple_types<_Elem>,
    _String_iter_types<_Elem, typename _Alty_traits::size_type, typename _Alty_traits::difference_type,
        typename _Alty_traits::pointer, typename _Alty_traits::const_pointer, _Elem&, const _Elem&>>>;

_Compressed_pair<_Alty, _Scary_val> _Mypair;

std::string采用std::allocator<char>作為分配器,由_Compressed_pair的EBO得,分配器並不會占用內存空間。該分配作用於std::_Is_simple_alloc_v<std::_Rebind_alloc_t<std::allocator<char>, char>>true,因此std::string的內存布局可以拆解如下

// std::string同一時間只可能是短字符串或長字符串
union _Bxty { // storage for small buffer or pointer to larger one
    char _Buf[16];
    char* _Ptr;
    char _Alias[16]; // TRANSITION, ABI: _Alias is preserved for binary compatibility (especially /clr)
} _Bx;

std::size_t _Mysize = 0; // current length of string
std::size_t _Myres = 0; // current storage reserved for string
  • std::string中記錄的是短字符串時,_Buf代表棧上的字符串,如"Hello World"是存儲在_Buf數組中

  • std::string中記錄的是長字符串時,_Ptr代表指向堆上數據的指針,可通過該指針訪問數據

當我們調用c_str()時,本質上是在調用如下方法

constexpr const value_type* _Myptr() const noexcept {
    const value_type* _Result = _Bx._Buf;
    // 判斷是否是長字符串
    if (_Large_string_engaged()) {
        _Result = _Unfancy(_Bx._Ptr);
    }

    return _Result;
}

constexpr bool _Large_string_engaged() const noexcept {
#if _HAS_CXX20
    // 判斷當前函數調用是否發生在常量求值場合
    if (std::is_constant_evaluated()) {
        return true;
    }
#endif // _HAS_CXX20
    return _BUF_SIZE <= _Myres;
}

SSO與移動

若無特殊說明,本小節建立在MSVC-Release-x64的環境下進行分析,且源碼在便於理解的基礎上略有刪減。std::string在Debug和Release模式下內存分配的機理不同(Debug模式下無短字符串優化等)

// 如果是MSVC-Debug-x64環境 那么會在堆上分配2次16B的內存
std::string name = "Hello World";
std::string newName = std::move(name);

下面進行源碼剖析

constexpr basic_string(basic_string&& _Right) noexcept
    : _Mypair(_One_then_variadic_args_t{}, _STD move(_Right._Getal())) // 標簽分發
{
    // 根據優化等級選擇不同的分配器 在Release模式下取得_Fake_allocator 它是空類 不負責任何功能
    _Mypair._Myval2._Alloc_proxy(_GET_PROXY_ALLOCATOR(_Alty, _Getal()));
    // 拿走被移動對象中的數據
    _Take_contents(_Right);
}
constexpr void _Take_contents(basic_string& _Right) noexcept {
    // assign by stealing _Right's buffer
    auto& _My_data    = _Mypair._Myval2;
    auto& _Right_data = _Right._Mypair._Myval2;

    // We need to ask if pointer is safe to memcpy.
    // size_type must be an unsigned integral type so memcpy is safe.
    // _Elem must be trivial standard-layout, so memcpy is safe.
    // We also need to disable memcpy if the user has supplied _Traits, since they can observe traits::assign and similar.
    if constexpr (_Can_memcpy_val) {
#if _HAS_CXX20
        if (!_STD is_constant_evaluated())
#endif
        {
            // 該宏判斷優化等級 Release模式下_ITERATOR_DEBUG_LEVEL為0
#if _ITERATOR_DEBUG_LEVEL != 0
            if (_Right_data._Large_string_engaged()) {
                // take ownership of _Right's iterators along with its buffer
                _Swap_proxy_and_iterators(_Right);
            } else {
                _Right_data._Orphan_all();
            }
#endif

            // memcpy右值字符串中的數據
            _Memcpy_val_from(_Right);
            // 將右值字符串置回默認狀態
            _Right._Tidy_init();
            return;
        }
    }

    // 下方代碼處理 when is unsafe to memcpy 的情況
    // Codes...
}
void _Memcpy_val_from(const basic_string& _Right) noexcept {
    // 添加偏移量 使memspy正常工作
    const auto _My_data_mem =
        reinterpret_cast<unsigned char*>(std::addressof(_Mypair._Myval2)) + _Memcpy_val_offset;
    const auto _Right_data_mem =
        reinterpret_cast<const unsigned char*>(std::addressof(_Right._Mypair._Myval2)) + _Memcpy_val_offset;
    // 對數據進行拷貝 Debug和Release模式的不同會導致偏移量不同 但最終拷貝的是同一份數據
    ::memcpy(_My_data_mem, _Right_data_mem, _Memcpy_val_size);
}

由於MSVC中對將存儲數據的結構設計為union,因此在::memcpy的時候並不需要考慮是長字符串還是短字符串,直接對數據進行拷貝,然后再讀取的時候進行判定即可即可(即上文中提到的_Myptr()以及_Large_string_engaged()

過時的COW

[標准C++類std::string的內存共享和Copy-On-Write(寫時拷貝)

Legality of COW std::string implementation in C++11

std::string_view與const std::string&

對於std::string而言,當它從一個原生的c-style-string上構造時,都伴隨着內存分配(可能是堆也可能是棧);但對於std::string_view而言,它內部只維護了一個原生指針和一個長度

const char* _Mydata;
std::size_t _Mysize;

這代表着std::string_view在構造的時候,只是進行一次淺拷貝,同時進行一次O(n)復雜度的長度求值

constexpr basic_string_view(const char* _Ntcts) noexcept
    : _Mydata(_Ntcts), _Mysize(_Traits::length(_Ntcts)) {}
const char* word = "Hello";

std::string_view sv1 = word;
std::string_view sv2 = word;

std::string s = word;

因此在對c-style-string進行操作時,為此構建一個std::string是一個不值當的操作,我們需要的是一個“視圖”,即std::string_view

Example1

std::string extract_part(const std::string& bar) {
    return bar.substr(2, 3);
}
if (extract_part("ABCDEFG").front() == 'C') {
    // do something...
}

盡管編譯器已經開啟了RVO,但上述代碼仍然包含了兩次std::string對象的構造,若檢測的字符串是長字符串,那么這代表着高額的性能開銷

std::string_view extract_part(std::string_view bar) {
    return bar.substr(2, 3);
}
if (extract_part("ABCDEFG").front() == 'C') {
    // do something...
}

Problem1

但由於std::string_view執行的是淺拷貝,所以也伴隨着dangling的問題

std::vector<std::string_view> elements;

// 若elem的生命周期短於elements 那么可能會訪問到已經被釋放的內存
void Save(const std::string& elem) {
    elements.push_back(elem);
}

Problem2

std::map<std::string, int> frequencies;

int GetFreqForKeyword(std::string_view keyword) {
    // 無法通過編譯 不存在std::string_view到std::string的隱式轉換
    return frequencies.at(keyword);
}

Problem3

class Sink
{
public:
    Sink(std::string_view sv) : str(std::move(sv)) {}
private:
    std::string str;
};
  • 對一個std::string_view,而言,std::move 它是無害但無用的
  • std::string_view去構造std::string,存在sv在構建時內部指針懸空的風險

總結

  • 考慮使用std::string_view代替const std::string&
  • 函數傳參按值傳遞std::string_view即可,不需要pass-by-const-reference,也沒有移動操作

手撕簡易my_string

class my_string
{
protected:
    std::size_t size;
    char* pStr;

    void init_null_impl() {
        size = 0;
        pStr = new char[1]{'\0'};
    }

    void init_impl(const char* newData) {
        size = std::strlen(newData);
        pStr = new char[size + 1];
        strcpy_s(pStr, size + 1, newData);
    }

    void str_swap(my_string& _another) {
        std::swap(pStr, _another.pStr);
        std::swap(size, _another.size);
    }

public:
    my_string() {
        init_null_impl();
    }

    my_string(const char* newData) {
        if (newData == nullptr)
            init_null_impl();
        else
            init_impl(newData);
    }

    my_string(const my_string& _copy) {
        if (_copy.pStr == nullptr)
            init_null_impl();
        else
            init_impl(_copy.pStr);
    }

    my_string(my_string&& _another) : size(_another.size), pStr(_another.pStr) {
        _another.init_null_impl();
    }

    my_string& operator=(my_string _another) {
        str_swap(_another);
        return *this;
    }

    ~my_string() {
        delete[] pStr;
    }

    char operator[](std::size_t index) const { return pStr[index]; }

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM