【C++初阶】手撕C++ string类

🎬 博主名称键盘敲碎了雾霭
🔥 个人专栏 : 《C语言》《数据结构》 《C++》

⛺️指尖敲代码,雾霭皆可破


文章目录

  • 一、前言:为什么要手写string类?
  • 二、整体架构设计
    • [2.1 成员变量设计](#2.1 成员变量设计)
    • [2.2 类结构总览](#2.2 类结构总览)
  • 三、核心构造与析构
    • [3.1 构造函数](#3.1 构造函数)
    • [3.2 析构函数](#3.2 析构函数)
  • 四、深浅拷贝与拷贝控制
    • [4.1 拷贝构造函数(现代写法)](#4.1 拷贝构造函数(现代写法))
    • [4.2 赋值运算符重载(现代写法)](#4.2 赋值运算符重载(现代写法))
    • [4.3 swap成员函数](#4.3 swap成员函数)
  • 五、迭代器实现
    • [5.1 普通迭代器](#5.1 普通迭代器)
    • [5.2 const迭代器](#5.2 const迭代器)
  • 六、容量管理
    • [6.1 reserve扩容函数](#6.1 reserve扩容函数)
    • [6.2 扩容策略分析](#6.2 扩容策略分析)
  • 七、修改操作
    • [7.1 push_back尾插字符](#7.1 push_back尾插字符)
    • [7.2 append追加字符串](#7.2 append追加字符串)
    • [7.3 operator+=重载](#7.3 operator+=重载)
    • [7.4 insert插入操作](#7.4 insert插入操作)
  • 八、查找与截取
    • [8.1 find查找字符/子串](#8.1 find查找字符/子串)
    • [8.2 substr截取子串](#8.2 substr截取子串)
  • 九、运算符重载
    • [9.1 比较运算符](#9.1 比较运算符)
    • [9.2 流插入运算符<<](#9.2 流插入运算符<<)
    • [9.3 流提取运算符>>](#9.3 流提取运算符>>)
  • 文章结语

一、前言:为什么要手写string类?

string 相关接口应用 放在了上几篇文章中,不熟悉的朋友可以先点击下方链接快速回顾一下:
【C++初阶】string类(一):从基础到实战
【C++初阶】string类(二):常用接口全解析

手写一个简化版string类,是深入理解C++核心机制的最佳实践。通过这个项目,你将掌握:

核心知识点 应用场景
深拷贝与浅拷贝 自定义类的拷贝控制
RAII原则 资源管理
迭代器模式 容器设计
运算符重载 类的接口设计

二、整体架构设计

2.1 成员变量设计

cpp 复制代码
private:
    char* _arr = nullptr;      // 指向动态分配的字符数组
    size_t _size = 0;          // 字符串当前长度(不含'\0')
    size_t _capacity = 0;      // 实际分配的容量
    static const size_t npos;  // 静态常量,表示无效位置

2.2 类结构总览

cpp 复制代码
namespace A {
    class string {
    public:
        // 类型定义
        typedef char* iterator;
        typedef const char* const_iterator;
        
        // 构造/析构
        string(const char* p = "");
        string(const string& s);
        ~string();
        
        // 赋值
        string& operator=(string tmp);
        
        // 迭代器
        iterator begin();
        iterator end();
        const_iterator begin() const;
        const_iterator end() const;
        
        // 容量
        size_t size() const;
        size_t capacity() const;
        void reverse(size_t capacity);  // 注意:应为reserve
        
        // 修改
        void push_back(char ch);
        string& append(const char* str);
        string& operator+=(char ch);
        string& operator+=(const char* str);
        string& insert(size_t pos, char ch);
        string& insert(size_t pos, const char* str);
        
        // 查找/截取
        size_t find(char ch, size_t pos = 0);
        size_t find(const char* str, size_t pos = 0);
        string substr(size_t pos = 0, size_t len = npos);
        
        // 元素访问
        char& operator[](size_t pos);
        const char& operator[](size_t pos) const;
        const char* c_str() const;
        
        // 交换
        void swap(string& s);
        
    private:
        char* _arr;
        size_t _size;
        size_t _capacity;
        static const size_t npos;
    };
}

三、核心构造与析构

3.1 构造函数

cpp 复制代码
string(const char* p = "") {
    _size = strlen(p);
    _capacity = _size;
    _arr = new char[_size + 1];  // +1 给'\0'
    strcpy(_arr, p);
}

关键点:

  • 默认参数""确保空字符串也能正确处理
  • strlen不计算结尾的\0,所以分配_size + 1
  • 使用strcpy拷贝包括\0在内的完整字符串

3.2 析构函数

cpp 复制代码
~string() {
    delete[] _arr;
    _arr = nullptr;
    _size = _capacity;  // ⚠️ 这里赋值有误,应为 _capacity = 0
}

注意: 析构函数中_size = _capacity是笔误,应该释放资源后将成员清零:

cpp 复制代码
~string() {
    delete[] _arr;
    _arr = nullptr;
    _size = 0;
    _capacity = 0;
}

四、深浅拷贝与拷贝控制

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

cpp 复制代码
// 传统写法
string(const string& s) {
    _arr = new char[s.size() + 1];
    strcpy(_arr, s._arr);
    _size = s.size();
    _capacity = s.capacity();
}

// 现代写法(推荐)
string(const string& s) {
    string tmp(s._arr);  // 复用构造函数
    swap(tmp);           // 交换资源
}

现代写法的优势:

  1. 避免代码重复
  2. 异常安全:如果new失败,tmp构造失败,原对象不受影响
  3. 利用swap实现高效的资源转移

4.2 赋值运算符重载(现代写法)

cpp 复制代码
// 传值参数实现拷贝 + swap
string& operator=(string tmp) {
    swap(tmp);
    return *this;
}

这是C++中经典的"copy and swap"惯用法:

  • 参数传值自动完成拷贝(深拷贝)
  • tmp交换资源
  • 函数结束时tmp销毁,带走原资源

4.3 swap成员函数

cpp 复制代码
void swap(string& s) {
    std::swap(_arr, s._arr);
    std::swap(_size, s._size);
    std::swap(_capacity, s._capacity);
}

五、迭代器实现

5.1 普通迭代器

cpp 复制代码
typedef char* iterator;

iterator begin() {
    return _arr;           // 指向第一个字符
}

iterator end() {
    return _arr + _size;   // 指向'\0'位置
}

为什么end()指向\0

  • 符合STL的"左闭右开"区间 [begin, end)
  • 遍历时it != end(),不会访问\0

5.2 const迭代器

cpp 复制代码
typedef const char* const_iterator;

const_iterator begin() const {
    return _arr;
}

const_iterator end() const {
    return _arr + _size;
}

const迭代器的作用:

  • 允许const对象遍历
  • 防止通过迭代器修改字符

六、容量管理

6.1 reserve扩容函数

cpp 复制代码
void reverse(size_t capacity) {  // 注意:应为 reserve
    if (capacity > _capacity) {
        char* tmp = new char[capacity + 1];
        strcpy(tmp, _arr);
        delete[] _arr;           // ⚠️ 这里有问题
        _arr = tmp;
        _capacity = capacity;
    }
}

函数名问题: 应该是reserve(预留),不是reverse(反转)。

隐藏的bug:

cpp 复制代码
delete _arr;      // 错误!应该用 delete[] _arr
delete[] _arr;    // 正确

因为_arr是通过new char[]分配的,必须用delete[]释放。

6.2 扩容策略分析

当前容量 扩容后容量
0 4
4 8
8 16
... 2倍增长

为什么选择2倍扩容?

  • 均摊时间复杂度为O(1)
  • 避免频繁申请内存
  • 空间利用率与时间效率的平衡

七、修改操作

7.1 push_back尾插字符

cpp 复制代码
void push_back(char ch) {
    if (_size == _capacity) {
        reverse(_capacity == 0 ? 4 : 2 * _capacity);
    }
    _arr[_size++] = ch;
    _arr[_size] = '\0';  // 维护结尾的'\0'
}

注意: 每次修改后都要确保_arr[_size] = '\0'

7.2 append追加字符串

cpp 复制代码
string& append(const char* str) {
    size_t len = strlen(str);
    if (len + _size > _capacity) {
        reverse(len + _size > 2 * _capacity ? 
                len + _size : 2 * _capacity);
    }
    strcpy(_arr + _size, str);
    _size += len;
    return *this;
}

7.3 operator+=重载

cpp 复制代码
string& operator+=(char ch) {
    push_back(ch);
    return *this;
}

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

7.4 insert插入操作

插入单个字符:

cpp 复制代码
string& insert(size_t pos, char ch) {
    assert(pos <= _size);
    if (_size == _capacity) {
        reverse(_capacity == 0 ? 4 : 2 * _capacity);
    }
    size_t end = _size + 1;
    while (end > pos) {
        _arr[end] = _arr[end - 1];
        end--;
    }
    _arr[pos] = ch;
    _size++;
    return *this;
}

插入字符串:

cpp 复制代码
string& insert(size_t pos, const char* str) {
    assert(pos <= _size);
    size_t len = strlen(str);
    if (len + _size > _capacity) {
        reverse(len + _size > 2 * _capacity ? 
                len + _size : 2 * _capacity);
    }
    size_t end = _size + len;
    while (end > pos + len - 1) {
        _arr[end] = _arr[end - len];
        end--;
    }
    for (size_t i = 0; i < len; i++) {
        _arr[pos + i] = str[i];
    }
    _size += len;
    return *this;
}

八、查找与截取

8.1 find查找字符/子串

查找字符:

cpp 复制代码
size_t find(char ch, size_t pos = 0) {
    assert(pos < _size);
    for (size_t i = pos; i < _size; i++) {
        if (_arr[i] == ch) return i;
    }
    return npos;
}

查找子串(利用C库函数):

cpp 复制代码
size_t find(const char* str, size_t pos = 0) {
    assert(pos < _size);
    char* tmp = strstr(_arr + pos, str);
    if (tmp == nullptr) return npos;
    return tmp - _arr;  // 指针相减得到下标
}

8.2 substr截取子串

cpp 复制代码
string substr(size_t pos = 0, size_t len = npos) {
    assert(pos < _size);
    string sub;
    if (len > _size - pos) {
        len = _size - pos;
    }
    for (size_t i = 0; i < len; i++) {
        sub += _arr[pos + i];
    }
    return sub;
}

九、运算符重载

9.1 比较运算符

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 strcmp(s1.c_str(), s2.c_str()) == 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);
}

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

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

设计技巧: 只实现>==,其他运算符通过它们推导,减少重复代码。

9.2 流插入运算符<<

cpp 复制代码
ostream& operator<<(ostream& out, const string& s) {
    for (auto ch : s) {  // 范围for需要迭代器支持
        cout << ch;
    }
    return out;
}

9.3 流提取运算符>>

cpp 复制代码
istream& operator>>(istream& in, string& s) {
    char ch = in.get();
    size_t i = 0;
    const size_t N = 256;
    char buff[N];
    
    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字节的栈缓冲区,减少频繁的append调用。


📌 本文代码已上传至gittee,欢迎Star和PR!

如有错误或补充,欢迎在评论区留言讨论!

文章结语

感谢你读到这里~我是「键盘敲碎了雾霭」,愿这篇文字帮你敲开了技术里的小迷雾 💻

如果内容对你有一点点帮助,不妨给个暖心三连吧👇

👍 点赞 | ❤️ 收藏 | ⭐ 关注

(听说三连的小伙伴,代码一次编译过,bug绕着走~)

你的支持,就是我继续敲碎技术雾霭的最大动力 🚀

🐶 小彩蛋:

复制代码
      /^ ^\
     / 0 0 \
     V\ Y /V
      / - \
    /    |
   V__) ||

摸一摸毛茸茸的小狗,赶走所有疲惫和bug~我们下篇见 ✨

相关推荐
林姜泽樾2 小时前
python入门第六课,其他字符串格式化和input
开发语言·python·pycharm
君鼎2 小时前
C++14 新特性全面总结
c++
追雨潮2 小时前
内存向量检索引擎设计与实现:C# 轻量级 Milvus 替代方案
开发语言·c#·milvus
东离与糖宝2 小时前
Java AI工程化:PyTorch On Java+SpringBoot微服务部署(2025-2026最新实战)
java·人工智能
隐形喷火龙2 小时前
CentOS7 基于 FRP 实现 Java Web 服务内网穿透实操记录
java·开发语言
萝卜白菜。2 小时前
TongWeb8.0支持JBoss Weld‌
java·java-ee
万邦科技Lafite2 小时前
淘宝关键词API接口获取分类商品信息指南
java·前端·数据库·开放api·淘宝开放平台
xxjj998a2 小时前
spring security 超详细使用教程(接入springboot、前后端分离)
java·spring boot·spring
小碗羊肉2 小时前
【从零开始学Java | 第二十五篇】TreeSet
java·开发语言