展开菜单
首页 精品内容 本月促销 装机必备 Windows PDF教程 macOS软件 IOS软件 Android AI 专题
全部分类

当前位置:

首页 > 编程开发 > C++string类原理、踩坑与对象语义详解

C++string类原理、踩坑与对象语义详解

C++string作为资源管理类,通过RAII管理堆内存,实现深拷贝、赋值与析构。采用copy-and-swap技术保证异常安全和自赋值安全,swap操作O(1)。注意无符号类型下溢陷阱,使用翻倍扩容策略。重载比较运算符与流操作实现完整功能。

本文目录

    一、为什么 string 是一个“资源管理类”

    动笔之前,得先搞明白 `string` 到底是个什么角色。 普通的 `int`、`double`,值直接躺在栈上,函数一结束自动烟消云散,不用你操心。但 `string` 不一样——字符数据是住在**堆**上的,对象手里只攥着一个指针。这块堆内存的生死大权,得靠人来手动操控:啥时候分配、啥时候释放、被复制时该怎么处理。 这种“手里攥着资源、还要负责管理资源一辈子”的类,在 C++ 里有个专门的名号——**RAII 类**(Resource Acquisition Is Initialization)。`string` 就是其中最典型的样板之一: - 构造函数:去拿资源(`new` 内存)。 - 析构函数:把资源还回去(`delete` 内存)。 - 拷贝构造 / 赋值:处理资源是复制一份,还是直接过户。 一旦想通了这个本质,后面所有函数的设计逻辑,就都顺理成章了。

    二、整体结构设计

    我选了三个成员变量来完整描述一个字符串的实时状态:
    private:
        char*  _str;       // 指向堆上的字符数组
        size_t _size;      // 当前字符串长度(不含 '\0')
        size_t _capacity;  // 当前已分配容量(不含 '\0')
        static const size_t npos = -1;
    
    `_str` 是真正存数据的老巢,`_size` 记录当前有多少个兵,`_capacity` 则标明了已经申请了多大的地盘。这三者之间始终要维持一个铁律:
    _size <= _capacity
    实际分配字节数 = _capacity + 1(多一个给 '\0')
    
    `npos` 定义成一个静态常量,值就是 `(size_t)-1`,说白了就是 `size_t` 类型的最大值(在 64 位系统上是 `0xFFFFFFFFFFFFFFFF`)。它代表的意思就是“找不到”或“到底了”,跟标准库的用法一致。 再聊聊头文件和源文件的分离问题。我把函数声明摆在 `String.h`,定义放在 `String.cpp`,这么做是为了躲开重复定义。要是把函数体直接写在 `.h` 里,每个 `#include` 这个头文件的翻译单元都会生成一份定义,链接时就会报重定义错误。声明放 `.h`、定义放 `.cpp`,这是 C++ 工程的“基本法”。 整个类我放进了 `namespace Jianyi` 里头。命名空间的作用很明确——防冲突。我们自己写的 `string` 和标准库的 `std::string` 重名了,靠命名空间隔开,大家井水不犯河水。

    三、构造函数与析构函数

    构造函数

    string(const char* str = "")
    {
        assert(str);
        _size = strlen(str);
        _capacity = _size;
        _str = new char[_capacity + 1];
        strcpy(_str, str);
    }
    
    有几个细节值得拎出来讲一讲。 第一,参数的默认值设为 `""`(空字符串),不是 `nullptr`。这么一来,`string s;` 和 `string s("");` 都能正常工作,同时 `assert(str)` 还能防住真有人传个 `null` 进来。 第二,`new char[_capacity + 1]` 多申请了一个字节。`_capacity` 本身不包含 `'\0'`,但数组必须给它留个位置,`strcpy` 依赖这个终止符来“知道完事了”。这个“差一”的约定要在整个实现中保持一致,这是个很容易出纰漏的地方。 `strcpy` 会把 `str` 连着终止符一口气全复制过去,所以不需要再单独写一句 `_str[_size] = '\0'`。

    析构函数

    ~string()
    {
        delete[] _str;
        _str = nullptr;
        _size = _capacity = 0;
    }
    
    `delete[]` 必须配对 `new[]`,这是没得商量的死规矩。要是贪方便用 `delete`(不带方括号),行为就是未定义的——对于内置类型的数组,很多时候能侥幸跑过去,但绝不能依赖这种运气。 析构完后把 `_str` 设成 `nullptr`,这是一种防御性的习惯。如果析构后还有乱七八糟的代码试图再碰这个指针,至少它会立刻崩溃,而不是静悄悄地读到一堆垃圾数据,让 bug 更难定位。 `delete[]` 到底在干嘛?它先挨个调用数组里每个元素的析构函数(对 `char` 这种基础类型来说没什么影响,但对对象数组就至关重要了),然后再释放整块内存。这就是为什么 `new[]` 和 `delete[]` 必须成对出现——分配时 `new[]` 会在内存块头部记下元素个数,`delete[]` 就靠这个数量来逐个调用析构函数。如果用 `delete` 去释放,这个元数据就没人读了,析构函数自然也不会一个一个被调用。

    四、深拷贝 vs 浅拷贝,以及 double free

    这是整个 string 实现里最核心、最绕不开的一个概念。 **浅拷贝**:就是把成员变量的值直接复制过去。对于指针 `_str` 来说,就是复制指针的值,导致两个对象指向同一块堆内存。
    s1._str ──┐
               ▼
           [ h e l l o \0 ]
               ▲
    s2._str ──┘
    
    这种结构藏着一个大杀器:当 s1 和 s2 各自析构时,同一块内存会被 `delete[]` 两次。这就是 **double free**。double free 是未定义行为,轻则让程序崩掉,重则搅乱内存结构,搞出安全漏洞。 如果不手动实现拷贝构造函数和赋值运算符,编译器默认生成的版本就是浅拷贝。所以,凡是持有了堆内存的类,**必须**自己动手实现“Big Three”:析构函数、拷贝构造函数、赋值运算符。 **深拷贝**:给新对象单独申请一块内存,把数据完整地复制一份。
    s1._str ──► [ h e l l o \0 ]
    s2._str ──► [ h e l l o \0 ]  (独立的一份)
    
    两者彼此独立,各自析构,互不干扰。代价就是每次拷贝都得来一次 `new`,但是换来了安全,值了。

    五、拷贝构造函数(最容易踩的坑之一)

    初版方案:直接深拷贝

    // 被注释掉的版本
    string(const string& s)
    {
        _str = new char[s._capacity + 1];
        strcpy(_str, s._str);
        _size = s._size;
        _capacity = s._capacity;
    }
    
    这个版本逻辑上一点毛病没有,就是最朴素的深拷贝:给自己申请内存,把对方的数据搬过来。写法简单直接,功能也正确。

    最终方案:copy-and-swap

    string(const string& s)
    {
        string tmp(s._str);   // 用 char* 构造函数创建临时对象
        swap(tmp);            // 把 *this 和 tmp 互换
    }                         // tmp 析构,顺手释放 *this 原本(未初始化)的资源
    
    这里用的是 **copy-and-swap 惯用法**。要想理解这个版本,关键是搞清楚 `swap` 到底交换了谁。 `swap(tmp)` 等价于 `this->swap(tmp)`,它交换的是 `*this` 和 `tmp`。执行完之后,`*this` 拿到了 `tmp` 创建的那块内存(也就是从 `s._str` 深拷贝来的数据),而 `tmp` 则拿走了 `*this` 原来的内容。因为 `tmp` 是个局部变量,函数一结束它就会自动析构,顺带就把 `*this` 原来的那块资源给释放了。

    曾经写错的版本

    // 错误写法!!
    string(const string& s)
    {
        string tmp(s._str);
        swap(s);              // swap 的对象是 s,不是 tmp!
    }
    
    这里出了两个问题:第一,`swap(s)` 试图交换 `*this` 和参数 `s`,`tmp` 完全成了旁观者;第二,`s` 是 `const string&`,不能被修改,而 swap 需要双方都能改,这个设计本身就自相矛盾了。 当时还把 swap 的签名也写错了:
    void swap(const string& s)  // 错误:const 参数无法被修改
    
    正确的签名应该是:
    void swap(string& s)  // 去掉 const
    
    swap 必须能修改双方,参数加 `const` 在逻辑上根本说不通。

    六、swap 的实现与意义

    void swap(string& s)
    {
        std::swap(_str, s._str);
        std::swap(_size, s._size);
        std::swap(_capacity, s._capacity);
    }
    
    这里内部调用的是 `std::swap`,交换的是指针和两个整数。不管字符串有多长,都只涉及三次赋值操作,时间复杂度永远是 **O(1)**。 STL 为什么如此执着于 O(1) 的 swap?因为 swap 在标准库算法里被大量调用(排序、partition、各种容器操作),如果 swap 是 O(n) 的,那些算法的复杂度就会直线恶化。 这里有一个关于模板的细节:`std::swap` 的通用实现是三次赋值(tmp = a, a = b, b = tmp),对于 `string` 来说就意味着两次深拷贝,是 O(n) 的。但是标准库对 `std::string` 有一个特化版本,会去调用成员函数 `swap`,也就是我们刚实现的那个版本,把复杂度降回 O(1)。 关于普通函数和函数模板的优先级:当存在完全匹配的普通函数时,编译器会优先选择它,而不是去实例化模板。`std::string` 的 swap 特化本质上就是利用了这一点。我们自己的 `Jianyi::string` 没有做这个特化,如果有人写 `std::swap(a, b)` 来交换我们自己的对象,它就会走通用版本(O(n));只有写 `a.swap(b)` 才能调用到我们的成员函数(O(1))。

    七、赋值运算符

    初版方案

    // 被注释掉的版本
    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;
    }
    
    逻辑清晰:先释放自己的旧资源,再根据对方的需求重新分配。但是,这里面藏着一个致命缺陷——**自赋值问题**。 如果你写了 `s = s`,进入函数后第一件事就是 `delete[] _str`,那 `s._str` 指向的内存就立刻被释放了。接下来再执行 `strcpy(_str, s._str)`,访问的就是一个悬空指针,行为未定义。

    曾经写错的自赋值判断

    // 错误写法
    if (_str != s._str)
    
    这段代码想拦住自赋值,但条件是判断指针值,而不是对象地址。两个不同的对象,在浅拷贝场景下完全可能 `_str` 相同(指向同一块内存)。正确的自赋值判断,应该比较对象本身的地址:
    if (this != &s)
    

    最终版本:copy-and-swap

    string& operator=(const string& s)
    {
        if (this != &s)
        {
            string tmp(s._str);   // 深拷贝到临时对象
            swap(tmp);            // 交换资源,tmp 拿走旧资源
        }
        return *this;
    }
    
    这个版本的精妙之处在于:资源的释放完全交给了 `tmp` 的析构函数来干,不需要手动 `delete`。整个过程异常安全——如果 `new` 失败了抛出异常,`tmp` 根本就没构造出来,`*this` 的状态也就没有被破坏掉。

    八、reserve:扩容的核心

    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` 只肯扩容,绝不缩容(`if (n >= _capacity)` 保证了这一点)。这跟 `std::string::reserve` 的语义一致——你可以申请更大的空间,但不能指望用 `reserve` 来强行瘦身。 操作的先后顺序非常重要:先 `new`,再 `strcpy`,再 `delete[]` 旧指针,最后更新 `_str`。这个顺序不能乱。如果先 `delete[]` 再 `new`,一旦 `new` 失败了,`_str` 就成了悬空指针,整个对象就处于一个损坏状态。 这里其实还有一个潜在优化点:`strcpy` 只适合复制以 `'\0'` 结尾的字符串,换成 `memcpy` 在某些实现里会更高效(省去了逐字符检查终止符的开销),但在这个实现里,用 `strcpy` 已经足够清晰了。

    九、PushBack 和 append

    PushBack:追加单个字符

    void string::PushBack(char ch)
    {
        if (_size == _capacity)
            reserve(_capacity == 0 ? 4 : 2 * _capacity);
        _str[_size] = ch;
        _size++;
        _str[_size] = '\0';
    }
    
    扩容策略是翻倍(`2 * _capacity`),容量为 0 时就给个 4。翻倍策略能保证均摊 O(1) 的追加代价——虽然偶尔会触发一次 O(n) 的扩容,但把成本平摊到每次追加操作上,依然是常数时间。
    string& string::operator+=(char ch)
    {
        PushBack(ch);
        return *this;
    }
    
    `operator+=` 直接复用 `PushBack`,代码不重复。

    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';
    }
    
    扩容判断这里有个小策略:优先尝试翻倍,如果翻倍后还不够用,就直接扩到需求的尺寸(`_size + len`)。这个逻辑在 `insert` 里也会出现,是个经典的“至少满足需求,尽量翻倍”的套路。 追加数据之后,必须手动把 `_str[_size]` 设为 `'\0'`,因为这里用的是逐字符赋值,不像 `strcpy` 会帮你自动带上终止符。 `operator+=` 还有一个接受 `const char*` 的版本在头文件里声明了,但实现体我没有写(被注释掉了)。实际它会去调用 `append`:
    string& string::operator+=(const char* str)
    {
        append(str);
        return *this;
    }
    

    十、insert:插入时的无符号类型陷阱

    插入单个字符

    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;
    }
    
    后移数据从末尾开始,`end` 从 `_size + 1` 向 `pos` 移动,每次把 `end - 1` 位置的字符搬到 `end` 的位置。 **核心踩坑**:我一开始写的是 `while (end >= pos)`。`end` 和 `pos` 都是 `size_t`(无符号整数)。当 `pos = 0`、`end` 减到 0 之后再执行 `--end`,结果不是 -1,而是 `size_t` 的最大值(约 1.8 × 10^19),条件永远为真,直接就死循环了。 改成 `while (end > pos)`,当 `end == pos` 时循环自然停止,从根本上避免了无符号数下溢。这是 C++ 里用 `size_t` 做循环变量时最容易掉进去的陷阱,C 语言的隐式类型转换会让这类 bug 变得极其隐蔽。 代码注释里也提到了:`while(end <= (int)pos)` 是另一种解法——强转成有符号类型,但不如直接改循环条件来得干净利落。

    插入字符串

    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;
    }
    
    思路和单字符版本类似,但每次移动 `len` 个位置。先把 `pos` 之后的数据整体后移 `len` 格,再把 `str` 的内容填入 `[pos, pos + len)` 这个区间。 注释里还留着一个被废弃的版本:
    // 废弃版本
    for (size_t i = _size; i >= pos; --i)
    {
        _str[i + len] = _str[i];
    }
    
    又是同样的 `size_t` 无符号下溢问题——`i` 减到 0 之后再减,就会绕回最大值,陷入死循环。

    十一、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;
        }
    }
    
    这里分两种情况:如果 `len` 是 `npos`(表示“删到末尾”),或者删除的长度已经超出了剩余部分,那就直接截断——在 `pos` 处写个 `'\0'`,然后更新 `_size`,完事,不需要挪动任何数据。 否则,就把 `pos + len` 之后的所有数据往前移动 `len` 格。循环条件 `i <= _size` 包含了 `_size` 自己,这样连终止符 `'\0'` 也会一起被移过来,不需要再单独补一个。 边界条件需要注意:`len > _size - pos` 这个判断,如果 `_size < pos` 会发生无符号数下溢(值绕回变成巨大的数),但 `assert(pos <= _size)` 保证了进函数时 `pos` 一定小于等于 `_size`,所以 `_size - pos` 不会下溢,这里是安全的。

    十二、find:字符串查找

    查找单个字符

    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` 位置开始,一个字符一个字符地找,找到就返回下标,找不到就返回 `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;
    }
    
    这里直接借用了标准库函数 `strstr`。`strstr` 的原理是**滑动匹配**:从主串的每个位置开始,尝试和子串逐字符比较;如果当前位置不匹配,就移动到下一个位置,从头再来。朴素的实现是 O(n × m),标准库的实现一般会有优化(类似 KMP 或 Boyer-Moore 的思路),但接口的语义是一样的。 `strstr` 返回的是一个指针,指向主串中子串开始的位置。用这个指针减去 `_str`(数组的起始地址),就得到了下标。这是指针运算的一个经典用法:两个指向同一数组的指针相减,得到的是它们之间相隔的元素个数(距离)。

    十三、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`(确保它不超过剩余长度),然后构造一个空字符串 `sub`,预先 `reserve` 好空间,再逐个字符追加进去。 注释里还有一个优化版本:
    // 优化版本(被注释掉)
    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;
    
    这个版本一次性把内存分配好,用 `memcpy` 批量复制,性能更好,不需要逐字符调用 `operator+=` 而可能触发多次扩容。代价就是代码稍微复杂一些,需要直接操作成员变量(所以只能在类内部写,或者声明为友元)。

    十四、比较运算符

    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); }
    
    整个比较体系的核心就是 ` < ` 和 `==`,其他的运算符全部复用这两个。`strcmp` 返回负数表示“小于”,0 表示“等于”,正数表示“大于”——和返回 bool 的比较运算符语义是完全对应的。 这些运算符都设计为**非成员函数**,写在类的外面。原因是 `==`、`<` 这类运算是对称的二元运算,左右操作数应该被平等对待。如果写成成员函数,左操作数就必须是一个 `string` 对象,这会限制使用的灵活性。

    十五、c_str() 的意义

    const char* c_str() const
    {
        return _str;
    }
    
    这个函数看起来很简单,但它其实是 string 和 C 风格字符串之间交流的桥梁。很多 C 标准库函数(`printf`、`fopen`、`strlen` 等)只认 `const char*`,不认识 C++ 的 `string` 对象。`c_str()` 提供了一个合法的、以 `'\0'` 结尾的字符指针,让 C++ 的 string 能够融入 C 的大家庭。 返回的是 `const char*` 而不是 `char*`,这是有意为之:禁止外部通过这个指针去修改字符串的内容,保护好对象的内部状态。

    十六、operator<< 和 operator>>

    为什么要实现这两个

    标准库的 `cout` 和 `cin` 不认识我们自己实现的 `string` 类。如果不重载 `operator<<` 和 `operator>>`,`cout << s` 就没法编译。实现了这两个函数之后,我们的 string 就能无缝接入 C++ 的流体系。

    operator<<

    ostream& operator<<(ostream& out, string& s)
    {
        for (auto ch : s)
            out << ch;
        return out;
    }
    
    逐字符输出。这里用了范围 for 循环,它依赖 `string` 提供了 `begin()` 和 `end()` 迭代器:
    typedef char*       iterator;
    typedef const char* const_iterator;
    iterator begin() { return _str; }
    iterator end()   { return _str + _size; }
    
    指针本身就是最简单的迭代器,对于字符数组来说完全适用。

    operator>>(隐蔽的 bug)

    **错误版本**(注释里):
    // 错误写法
    buff[i++] = ch;
    s += buff;   // buff 里有 ch
    s += ch;     // ch 又被单独加了一次 —— 重复!
    
    每个字符被写入了两次,一次在 `buff` 里,然后 `buff` 被追加到 `s`,一次又单独 `+= ch`。读进来的字符串会变成双倍长度。 **正确版本**:
    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;
    }
    
    每个字符只走一条路:先进 `buff`,`buff` 满了就倒进 `s`,清空 `buff` 继续。循环结束后,再把剩余不满一 `buff` 的数据也倒进去。 使用缓冲区的好处是:每 255 个字符才触发一次 `operator+=`,大幅减少了潜在的扩容次数,比逐字符 `s += ch` 的效率高得多。

    十七、引用计数:为什么现代 string 不用它

    除了深拷贝,还有第三种资源管理策略:**引用计数**。思路是让多个对象共享同一块内存,同时用一个计数器来记录当前有多少个对象指向它。
    s1._str ──┐
               ▼
           [ h e l l o \0 ]  (count = 2)
               ▲
    s2._str ──┘
    
    拷贝的时候不申请新内存,只是把 count 加 1。析构的时候把 count 减 1,只有 count 减到 0 的时候才真正去 `delete`。这能避免频繁的 new/delete 开销,在大量拷贝的场景下性能会很好看。 早期的 `std::string` 实现(像 GCC 的 libstdc++ 在 C++11 之前)确实用过引用计数。但在 C++11 之后,基本就被抛弃了,原因有两个。 第一,**多线程问题**。引用计数本身需要原子操作来保证线程安全,而原子操作本身有不小的开销。在多核环境下,多个线程频繁地增减同一个引用计数,会导致缓存行频繁失效(false sharing),性能反而不如深拷贝。 第二,**写时拷贝(COW)的隐患**。引用计数通常配合写时拷贝使用:只有在真要修改字符串的时候,才去真正复制一份(所谓的“lazy copy”)。但这个机制在多线程下存在微妙的 race condition,想要保证实现正确性,难上加难。 现代 string 普遍采用的是 **SSO(Small String Optimization)**:短字符串(通常是 15 个字符以内)直接存在对象内部的固定缓冲区里,不 new 堆内存,彻底绕开了动态分配的开销。只有超过阈值的较长字符串,才会退化到堆分配。这是目前性能最好且实现最干净的方案。

    十八、代码里可以优化的地方

    回顾整个实现,有几个地方其实还能做得更好: - **`substr` 的逐字符追加**:当前的实现用 `operator+=` 逐个字符追加,每次都有可能触发扩容判断。改用 `memcpy` 一次性复制会更高效(注释里已经有这个优化版本了)。 - **`operator<<` 的参数**:`operator<<` 的第二个参数写的是 `string&`,应该改成 `const string&`,因为输出操作不应该修改对象。 - **`substr` 没有检查 `pos`**:如果 `pos > _size`,`_size - pos` 会下溢(无符号数)。应该加一个 `assert(pos <= _size)` 或者直接返回一个空字符串。 - **`find` 的 `pos` 边界**:`find` 函数里的 `assert(pos < _size)` 会拒绝在空字符串或末尾位置进行查找。实际上,`pos == _size` 时应该允许(直接返回 `npos` 就行),断言条件过于严格了。 - **`erase` 没有更新 `_str[_size]`**:在截断分支(直接写 `'\0'` 的那条),`_str[pos] = '\0'` 是正确的;在移动分支,循环已经把 `_str[_size]` 处的 `'\0'` 一并移过来了。这里逻辑是对的,但值得在代码注释里说清楚,不然容易让人心里不踏实。

    总结

    写完这个 `string` 类,有几个核心概念算是真正理解到位了。 深拷贝并不只是“多 new 一份内存”,它的本质,是让每个对象对自己的资源全权负责,彼此相互独立。copy-and-swap 这个惯用法,把资源管理的复杂性封装进了构造函数和析构函数里,让赋值运算符变得既安全又简洁。swap 能做到 O(1),并不是什么魔法,是因为它交换的只是指针本身,而不是指针背后的数据。至于 `size_t` 的无符号特性,在循环边界处确实是个连环坑,必须时刻保持警惕。 说到底,C++ 的核心难点从来不是语法,而是对象的**生命周期**和**资源归属**。这一趟走下来,我觉得对这两个问题的理解,比之前要清晰得多了。
    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 资源管理
    相关文章 更多
    精品专题 更多
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    IOS软件

    正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

    AI

    正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

    Mac软件 更多
    IntelliJ IDEA macOS版
    IntelliJ IDEA macOS版

    IntelliJ IDEA 是 JetBrains 推出的专业集成开发环境,重点服务于 Java、Kotlin 与 JVM 项目开发。软件集成代码补全、静态检查、重构、调试、测试、版本控制及构建工具,并可借助插件扩展语言和框架支持。

    PyCharm macOS版
    PyCharm macOS版

    JetBrains PyCharm 是面向 Python 开发者的集成开发环境,提供智能补全、代码检查、重构、调试、测试、版本控制及虚拟环境管理功能。Mac 版兼容 Intel 与 Apple 芯片,核心功能可免费使用。

    WebStorm macOS版
    WebStorm macOS版

    JetBrains WebStorm 是面向 JavaScript、TypeScript 和现代 Web 技术的专业集成开发环境,可智能理解项目结构,提供代码补全、重构、错误检查、运行调试、测试及 Git 工具。

    After Effects macOS版
    After Effects macOS版

    Adobe After Effects 是面向动态图形、影视特效与视频合成的专业创作软件。它提供关键帧动画、遮罩抠像、运动跟踪、三维空间、文字动画和表达式等工具,并可与 Premiere Pro、Photoshop、Illustrator

    Audition macOS版
    Audition macOS版

    Adobe Audition 是面向播客、影视后期、音乐制作及内容创作者的专业音频工作站,提供波形精修、多轨录制与混音、频谱编辑、降噪修复、响度匹配和效果处理工具,并可与 Adobe Premiere 协同完成视频声音制作。

    Bridge macOS版
    Bridge macOS版

    Adobe Bridge 是面向摄影师、设计师及内容团队的免费数字资产管理工具,可集中预览照片、视频和 Adobe 项目文件,并利用评级、标签、关键词、元数据、筛选与收藏集完成分类检索。它还支持批量重命名、格式导出、相机导入及 Camera

    Illustrator macOS版
    Illustrator macOS版

    Adobe Illustrator 是 Adobe 推出的专业矢量图形设计软件,适合在 Mac 上制作标志、图标、插画、包装、信息图及印刷版面。它以路径和锚点构建可无损缩放的图形,并提供钢笔、形状、文字、图像描摹、渐变、画板及生成式功能。

    InDesign macOS版
    InDesign macOS版

    Adobe InDesign 是面向印刷品与数字出版物的专业版面设计软件,可在 Mac 上制作书籍、杂志、宣传册、海报、报告及交互式文档。它提供精细的文字样式、网格、母版、长文档管理和印前输出能力。

    Lightroom Classic macOS版
    Lightroom Classic macOS版

    Adobe Lightroom Classic 是面向桌面摄影工作流的照片管理与后期处理软件,可将原始照片保存在本地硬盘,通过目录、关键词、评分和收藏夹高效整理图库,并提供 RAW 冲印、蒙版、镜头校正、批量同步及多格式导出等功能。

    Media Encoder macOS版
    Media Encoder macOS版

    Adobe Media Encoder 是 Adobe 推出的专业音视频编码与转码工具,可通过队列、预设和监视文件夹批量完成格式转换、代理文件创建及多平台成片输出,并与 Premiere Pro、After Effects 等软件紧密协作。

    Photoshop macOS版
    Photoshop macOS版

    Adobe Photoshop 是面向摄影师、设计师和内容创作者的专业图像处理软件,提供图层、蒙版、选区、修复、调色、文字排版、智能对象及生成式编辑等能力。Mac 版支持 Intel 与 Apple Silicon 处理器。

    Premiere Pro macOS版
    Premiere Pro macOS版

    Adobe Premiere Pro 是面向专业视频制作的非线性剪辑软件,提供多轨时间线、调色、音频处理、字幕、代理工作流和多格式输出能力,并可与 After Effects、Audition、Photoshop 及 Frame.io 协同

    WINDOWS 更多
    IntelliJ IDEA Windows版
    IntelliJ IDEA Windows版

    JetBrains IntelliJ IDEA 是以 Java、Kotlin 和 JVM 项目为核心的专业集成开发环境,提供智能补全、代码检查、安全重构、调试、测试、版本控制及构建工具集成。统一发行版的核心功能可免费用于商业和非商业项目,

    PhpStorm Windows版
    PhpStorm Windows版

    PhpStorm 是 JetBrains 面向 PHP 与现代 Web 开发打造的专业集成开发环境,提供代码补全、静态检查、重构、调试、测试、数据库、版本控制及前端语言支持。Windows 用户可直接下载安装程序,

    PyCharm Windows版
    PyCharm Windows版

    PyCharm 是 JetBrains 推出的 Python 集成开发环境,提供智能代码补全、错误检查、重构、调试、测试、Git、终端及 Jupyter Notebook 支持。统一版安装后可体验 30 天 Pro 功能,

    WebStorm Windows版
    WebStorm Windows版

    WebStorm 是 JetBrains 面向 JavaScript、TypeScript 和现代 Web 技术打造的集成开发环境。它提供项目级代码分析、智能补全、安全重构、导航搜索,并集成调试、测试、终端、Git 与常用前端工具,

    Photoshop Windows版
    Photoshop Windows版

    Adobe Photoshop 是面向摄影师、设计师和内容创作者的专业图像处理工具,可在 Windows 上完成照片修饰、选区抠图、图层合成、文字排版、绘画及生成式编辑。软件采用订阅授权,由 Creative Cloud 管理安装、更新与许

    InDesign Windows版
    InDesign Windows版

    Adobe InDesign 是面向印刷与数字出版的专业页面排版工具,可制作书籍、杂志、宣传册、海报、交互式 PDF 和电子书。它提供精细的文字样式、多页面管理、印前检查及多格式输出能力,并能与 Photoshop、Illustrator

    Illustrator Windows版
    Illustrator Windows版

    Adobe Illustrator 是用于绘制矢量图形、插画、图标、徽标和版式的专业设计工具。它以路径和锚点为核心,作品放大后仍能保持清晰,并提供画板、文字、渐变、图像描摹及精确变换等功能,适合品牌视觉、印刷物料、数字界面和商业插画创作。

    Bridge Windows版
    Bridge Windows版

    Adobe Bridge 是 Adobe 推出的创意资产管理工具,可集中预览、分类和查找照片、视频、PDF及多种设计文件,并支持元数据、关键词、评级、批量重命名和格式导出。它可与 Photoshop、Illustrator 等应用衔接,

    Lightroom Classic Windows版
    Lightroom Classic Windows版

    Adobe Lightroom Classic 是面向 Windows 桌面摄影工作流的照片管理与无损编辑工具,可通过目录集中整理大型图库,并完成 RAW 显影、局部蒙版、批量调整、打印和多格式导出。它强调本地文件管理与精细控制,

    Acrobat Pro Windows版
    Acrobat Pro Windows版

    Adobe Acrobat Pro 是面向专业 PDF 工作的综合解决方案,可在 Windows 中创建、编辑、转换、比较、整理和保护文档,并支持表单、电子签名、扫描件文字识别及敏感信息永久遮盖。订阅还包含网页、移动服务和云端协作能力,

    After Effects Windows版
    After Effects Windows版

    Adobe After Effects 是面向动态图形、动画与影视视觉特效制作的专业工具,可完成关键帧动画、图层合成、遮罩跟踪、抠像、文字动画和二维三维元素整合。Windows 版通过 Adobe Creative Cloud 安装及更新,

    Media Encoder Windows版
    Media Encoder Windows版

    Adobe Media Encoder 是 Adobe 面向视频与音频制作流程提供的专业编码和转码工具,可批量处理媒体文件、创建代理、套用输出预设并监控文件夹自动执行任务。它与 Premiere、After Effects、Audition