目录

一、概述与设计理念

二、核心数据结构

2.1 类的成员变量

2.2 迭代器实现

三、构造函数与析构函数

3.1 默认构造函数

3.2 C 字符串构造函数

3.3 注释中的合并构造函数

3.4 拷贝构造函数(现代写法)

3.5 析构函数

四、赋值运算符与资源管理

4.1 现代写法的赋值运算符

4.2 swap 成员函数

4.3 注释中的传统赋值实现

五、容量管理与扩容策略

5.1 reserve 函数

5.2 push_back 的扩容策略

5.3 append 的智能扩容

六、插入操作与边界处理

6.1 插入单个字符

6.2 插入字符串的复杂情况

6.3 注释中的问题代码

七、删除操作实现

7.1 erase 函数

7.2 注释中的替代实现

八、查找与子串操作

8.1 查找字符

8.2 查找子串

8.3 获取子串

九、比较运算符实现

9.1 基本比较运算符

9.2 运算符复用策略

9.3 相等性判断

十、流操作符实现

10.1 输出运算符

10.2 输入运算符(优化前)

10.3 输入运算符(优化后)

10.4 getline 函数

十一、测试代码分析

11.1 test_string1:基础功能测试

11.2 test_string2:插入功能测试

11.3 test_string3:删除功能测试

11.4 test_string4:拷贝和子串测试

11.5 test_string5:比较和流操作测试

11.6 test_string6:getline 测试

11.7 test_string7 和 test_string8:深拷贝和交换测试

十二、静态成员的特殊性

12.1 npos 的定义

12.2 静态常量初始化规则

十三、设计总结与优化建议

13.1 设计亮点

13.2 潜在改进


一、概述与设计理念

这是一个完全从头实现的 C++ string 类,位于 yyq 命名空间下,避免了与标准库 std::string 的命名冲突。该实现涵盖了字符串处理的核心功能,包括动态内存管理、深拷贝、迭代器支持、运算符重载和流操作等。

二、核心数据结构

2.1 类的成员变量

cpp

private:
    char* _str = nullptr;      // 指向动态分配的字符数组
    size_t _size = 0;          // 当前字符串长度(不包含结尾的\0)
    size_t _capacity = 0;      // 当前分配的容量
    static const size_t npos;  // 特殊值,表示未找到

设计分析:

  • 使用动态分配的字符数组存储字符串内容

  • _size 表示实际字符数,_capacity 表示分配的容量

  • npos 是静态常量,模仿标准库设计,表示查找失败的特殊值

2.2 迭代器实现

cpp

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

关键点:

  • 由于字符串在内存中连续存储,原生指针可以直接作为迭代器

  • 提供 const 和非 const 两个版本,支持常量和非常量迭代

  • 实现了 begin() 和 end() 方法,支持范围 for 循环

三、构造函数与析构函数

3.1 默认构造函数

cpp

string()
    :_str(new char[1] {'\0'})
    ,_size(0)
    ,_capacity(0)
{}

实现细节:

  • 分配 1 字节内存,初始化为 \0

  • _size 和 _capacity 都设为 0

  • 注意:虽然 _capacity 为 0,但实际分配了 1 字节内存,这可能导致概念上的混淆

3.2 C 字符串构造函数

cpp

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

设计选择:

  • 这是单参数构造函数,支持隐式类型转换

  • 精确分配所需内存,没有预留额外空间

  • 使用 strcpy 复制字符串,包括结尾的 \0

3.3 注释中的合并构造函数

cpp

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

为什么没有采用:

  • 将默认构造函数和 C 字符串构造函数合并为一个

  • 使用空字符串 "" 作为默认参数

  • 虽然代码更简洁,但可能降低可读性,作者选择分开实现

3.4 拷贝构造函数(现代写法)

cpp

string(const string& s)
{
    string temp(s._str);
    swap(temp);
}

实现原理:

  1. 先通过 C 字符串构造函数创建临时对象 temp

  2. 然后与当前对象交换资源

  3. 临时对象在函数结束时析构,释放原资源

对比传统写法:

cpp

// 传统写法
string(const string& s)
{
    _str = new char[s._capacity + 1];
    strcpy(_str, s._str);
    _size = s._size;
    _capacity = s._capacity;
}

现代写法的优势:

  • 代码更简洁

  • 利用已有构造函数,减少重复代码

  • 通过 swap 实现,异常安全

3.5 析构函数

cpp

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

安全考虑:

  • 检查 _str 是否为 nullptr,避免对空指针调用 delete[]

  • 释放后将指针置空,防止悬空指针

  • 重置 _size 和 _capacity,确保对象状态一致

四、赋值运算符与资源管理

4.1 现代写法的赋值运算符

cpp

string& operator=(string temp)
{
    swap(temp);
    return *this;
}

工作原理:

  1. 参数 temp 通过值传递,会调用拷贝构造函数创建副本

  2. 交换当前对象与副本的资源

  3. 副本在函数结束时析构,释放原对象的资源

优势分析:

  • 自动处理自赋值情况(s = s

  • 代码简洁,利用拷贝构造函数和 swap

  • 异常安全:如果拷贝构造失败,原对象状态不变

4.2 swap 成员函数

cpp

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

实现细节:

  • 使用 std::swap 交换三个成员变量

  • std::swap 对于内置类型是高效的

  • 这个函数是拷贝和赋值操作的核心

4.3 注释中的传统赋值实现

cpp

//string& operator=(const string& s)
//{
//    if (this != &s)
//    {
//        delete[] _str;
//        _str = new char[s._capacity + 1];
//        strcpy(_str, s._str);
//        _size = s._size;
//        _capacity = s._capacity;        
//    }
//    return *this;
//}

传统写法的问题:

  • 需要显式检查自赋值

  • 如果 new 失败抛出异常,原对象的内存已被释放,对象处于无效状态

  • 代码较冗长

五、容量管理与扩容策略

5.1 reserve 函数

cpp

void string::reserve(size_t n)
{
    if (n > _capacity)
    {
        char* temp = new char[n + 1];  // +1 给 \0 预留空间
        strcpy(temp, _str);
        delete[] _str;
        _str = temp;
        _capacity = n;
    }
}

特点:

  • 只扩大容量,不缩小

  • 使用 strcpy 复制原字符串(包括 \0

  • 新容量为 n,实际分配 n+1 字节

5.2 push_back 的扩容策略

cpp

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

扩容规则:

  • 初始容量为 0 时,扩容到 4

  • 后续每次容量不足时,容量翻倍

  • 这种指数级扩容策略均摊时间复杂度为 O(1)

5.3 append 的智能扩容

cpp

void string::append(const char* str)
{
    size_t len = strlen(str);
    if (_size + len > _capacity)
    {
        // 需要的大小超过2倍容量时,按需扩容;否则按2倍扩
        reserve(len + _size > 2 * _capacity ? len + _size : 2 * _capacity);
    }
    strcpy(_str + _size, str);
    _size += len;
}

智能策略:

  • 如果要添加的内容很大(超过当前容量的 2 倍),按实际需要扩容

  • 否则,按 2 倍扩容

  • 平衡了内存使用和扩容次数

六、插入操作与边界处理

6.1 插入单个字符

cpp

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

边界处理:

  • 使用 assert(pos <= _size) 确保位置合法

  • 如果 pos == _size,相当于在末尾插入

  • 从后向前移动字符,避免覆盖未移动的数据

6.2 插入字符串的复杂情况

cpp

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

关键技巧:

  • 循环条件 while (end > len + pos - 1) 避免了 pos-1 在 pos=0 时下溢

  • 先移动原字符串内容,再插入新字符串

  • 处理了空字符串的情况(虽然注释中检查 len==0 的代码被注释掉了)

6.3 注释中的问题代码

cpp

// 方法一:有问题的实现
//int end = _size;
//while (end >= (int)pos)
//{
//    _str[end + 1] = _str[end];
//    --end;
//}

问题分析:

  • end 是 int 类型,pos 是 size_t 类型

  • 比较时 int 会被提升为无符号类型

  • 当 pos=0end=-1 时,-1 被提升为很大的无符号数,循环条件不满足

七、删除操作实现

7.1 erase 函数

cpp

void string::erase(size_t pos, size_t len)
{
    assert(pos < _size);
    if (len >= _size - pos)
    {
        _str[pos] = '\0';
        _size = pos;
        return;
    }
    
    size_t end = pos + len;
    while (end <= _size)
    {
        _str[end - len] = _str[end];
        end++;
    }
    _size -= len;
}

两种情况的处理:

  1. 删除到末尾:直接截断字符串

  2. 删除中间部分:向前移动后续字符

注意:

  • 循环条件 end <= _size 确保拷贝结尾的 \0

  • 使用 assert(pos < _size),不允许在 pos == _size 处删除

7.2 注释中的替代实现

cpp

//if (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;
//}

对比分析:

  • 逻辑基本相同,只是结构略有差异

  • 当前实现提前返回,可能更清晰

  • for 循环版本更紧凑

八、查找与子串操作

8.1 查找字符

cpp

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;
}

简单实现:

  • 线性查找,时间复杂度 O(n)

  • 使用 assert(pos < _size) 确保起始位置有效

  • 找不到时返回 npos

8.2 查找子串

cpp

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

利用标准库:

  • 使用 C 标准库的 strstr 函数

  • ptr - _str 计算偏移量(指针算术)

  • 注意:需要确保 _str 以 \0 结尾

8.3 获取子串

cpp

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

实现细节:

  • 调整 len 确保不超过字符串边界

  • 先 reserve 预留空间,减少扩容次数

  • 逐个字符添加,确保正确性

  • 传值返回,可能触发拷贝构造(编译器可能优化)

九、比较运算符实现

9.1 基本比较运算符

cpp

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

为什么是全局函数?

  • 全局函数支持隐式类型转换:"hello" < s 和 s < "hello" 都有效

  • 不能直接访问私有成员,必须使用 c_str() 公共接口

9.2 运算符复用策略

cpp

bool operator <= (const string& s1, const string& s2)
{
    return s1 < s2 || s1 == s2;
}

bool operator > (const string& s1, const string& s2)
{
    return !(s1 <= s2);
}

设计理念:

  • 只实现 operator< 和 operator==

  • 其他运算符通过这两个复用

  • 修改比较逻辑时只需改两个地方

9.3 相等性判断

cpp

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 !(s1 == s2);
}

注意:

  • "hello world" == "hello world" 比较的是指针地址,不是字符串内容

  • 只有当 const char* 隐式转换为 string 时,才会调用重载的 operator==

十、流操作符实现

10.1 输出运算符

cpp

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

简洁实现:

  • 使用范围 for 循环遍历字符串

  • 逐个字符输出到流

  • 返回流引用以支持链式调用

10.2 输入运算符(优化前)

cpp

// 原始版本(频繁扩容)
//istream& operator>>(istream& in, string& s)
//{
//    s.clear();
//    char ch;
//    ch = in.get();
//    while (ch != ' ' && ch != '\n')
//    {
//        s += ch;
//        ch = in.get();
//    }
//    return in;
//}

问题:

  • 每次读取一个字符就追加,可能频繁扩容

  • 性能较差,特别是读取长字符串时

10.3 输入运算符(优化后)

cpp

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')
    {
        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;
}

优化策略:

  • 使用 256 字节缓冲区暂存输入

  • 缓冲区快满时批量追加到字符串

  • 大大减少扩容次数,提高性能

10.4 getline 函数

cpp

istream& getline(istream& in, string& s)
{
    s.clear();
    const int N = 256;
    char buff[N];
    int i = 0;
    
    char ch = in.get();
    while (ch != '\n')
    {
        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;
}

与 operator>> 的区别:

  • 只以换行符 \n 为分隔符

  • 可以读取包含空格的整行输入

  • 实现逻辑与 operator>> 类似

十一、测试代码分析

11.1 test_string1:基础功能测试

测试构造函数、下标访问、迭代器和范围 for 循环。

11.2 test_string2:插入功能测试

测试 += 运算符和 insert 函数的各种情况。

11.3 test_string3:删除功能测试

测试 erase 函数的边界情况,包括删除到末尾和删除部分字符。

11.4 test_string4:拷贝和子串测试

测试拷贝构造、赋值运算符、子串提取和自赋值。

11.5 test_string5:比较和流操作测试

测试比较运算符、输入输出运算符和隐式类型转换。

11.6 test_string6:getline 测试

测试整行输入功能。

11.7 test_string7 和 test_string8:深拷贝和交换测试

测试深拷贝的正确性和 swap 功能。

十二、静态成员的特殊性

12.1 npos 的定义

cpp

const size_t string::npos = -1;

类型转换:

  • -1 赋给无符号 size_t 会变成最大可能值

  • 这是模拟标准库 std::string::npos 的行为

12.2 静态常量初始化规则

cpp

// 在类内:static const size_t npos;
// 在类外:const size_t string::npos = -1;

C++ 规则:

  • 静态成员需要在类外单独定义和初始化

  • 只有静态整型常量可以在类内初始化(特殊情况)

  • 例如:static const int N = 10; 可以在类内初始化

十三、设计总结与优化建议

13.1 设计亮点

  1. RAII 原则:构造函数获取资源,析构函数释放资源

  2. 深拷贝实现:正确处理拷贝和赋值

  3. 现代 C++ 技巧:使用 swap 实现拷贝控制和异常安全

  4. 智能扩容策略:平衡内存使用和性能

  5. 完整迭代器支持:兼容 STL 算法和范围 for 循环

13.2 潜在改进

  1. 小字符串优化:对于短字符串,使用栈存储避免堆分配

  2. 移动语义:添加移动构造函数和移动赋值运算符

  3. 异常安全增强:某些操作可能不是强异常安全的

  4. 性能优化substr 可以一次性拷贝而不是逐个字符添加

  5. Unicode 支持:当前只支持单字节字符

这个实现作为教学项目,完整展示了 string 类的核心原理,对于理解 C++ 的类设计、内存管理和运算符重载有重要价值。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐