• 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; }
    };
    
  • 相关阅读:
    Selection Sort
    Alwayson环境下为备库创建查询用户
    Sencha Touch 数据层篇 Proxy(下)
    mapbox 栅格图层处理
    mapbox 删除 新增图层
    ant design vue 处理返回信息
    关于输入框中输入 特殊字符 get请求报错的办法
    mapbox url中xyz处理
    python中bisect模块使用
    ClickHouse字符串匹配探究
  • 原文地址:https://www.cnblogs.com/tuapu/p/15948261.html
Copyright © 2020-2023  润新知