ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++ 模拟实现 string 庖丁解牛|(分层拆解 + 可行性分析 + 缺陷踩坑)

C++ 模拟实现 string 庖丁解牛|(分层拆解 + 可行性分析 + 缺陷踩坑) 整体说明这份代码是手写简易版 STL string只实现核心接口用于理解底层原理不能直接用于生产环境下面按【头文件 hpp】【源文件 cpp】两大模块再拆成最小子模块逐条解析每一段附带代码作用 原理 可行性 存在 BUG / 隐患。 整体结构划分模块 1string.hpp 头文件类定义模块 2string.cpp 成员函数实现模块 3全局重载运算符 getline模块 4整体代码综合评估模块 1string.hpp 头文件声明部分作用类的蓝图只做声明不写函数实现头文件保护、成员变量、成员函数、迭代器 typedef、静态常量。1.1 头文件保护宏#ifndef STRING_HPP #define STRING_HPP #define _CRT_SECURE_NO_WARNINGS #includeiostream #includestring.h #includeassert.h #includestring using namespace std;✅ 代码作用 头文件保护宏防止多次#include string.hpp造成类重复定义。1.2 命名空间 namespace strnamespace str { class string { //成员 }; }✅作用自定义str::string和标准std::string隔离防止名字冲突。 ✅可行性推荐学习模拟实现必加。1.3 类内迭代器 typedefpublic: typedef char* iterator; typedef const char* const_iterator;✅作用 简易版 string 迭代器直接用原生指针封装。iteratorchar*可读可写begin/end 返回它const_iteratorconst char*const 对象专用只可读不可修改1.4 成员函数声明分组1.4.1 构造、析构、拷贝、赋值、swapstring(const char* s ); ~string(); string(const string str); string operator (const string str); string operator (string str); void swap(string str);构造函数给默认参数一个构造函数同时充当默认无参构造 C 字符串构造替代注释掉的无参构造析构释放堆内存拷贝构造现代写法swap 版两个赋值重载常量引用版本 / 值传参版本现代 swap 写法swap交换三个成员变量现代拷贝 / 赋值的核心工具函数✅可行性设计思路没问题一个构造函数兼任无参构造简化代码。1.4.2 基础访问接口const char* c_str()const; size_t size()const; iterator begin(); iterator end(); const_iterator begin()const; const_iterator end()const; char operator[](size_t i); const char operator[](size_t)const;c_str ()返回底层 char*兼容 C 语言字符串size ()获取有效字符个数不含\0begin/end迭代器接口支持范围 for[] 重载下标访问普通对象返回引用可写const 对象返回 const 引用只读1.4.3 修改接口增删改string operator(char ch); string operator(const char* s); void push_back(char ch); void append(const char* s); void reserve(size_t n); string insert(size_t pos, char ch); string insert(size_t pos, const char* s); string erase(size_t pos 0, size_t len npos); void pop_back(); string substr(size_t pos 0, size_t len npos) const; size_t find(char ch, size_t pos 0)const; size_t find(const char* s, size_t pos 0)const; void clear();接口对齐 std::string支持尾插、扩容、任意位置插入、删除、查找、截取子串、清空。1.4.4 比较运算符重载bool operator(const string s)const; bool operator(const string s)const; bool operator(const string s)const; bool operator(const string s)const; bool operator(const string s)const; bool operator!(const string s)const;字典序比较全部为 const 成员保证 const 对象可以调用。1.5 私有成员变量private: char* _str; size_t _size; size_t _capacity; public: static size_t npos;char* _str堆上字符数组末尾带\0兼容 C 字符串_size有效字符数量不包含末尾 \0_capacity容量最多能存多少有效字符_str数组实际开辟大小是capacity1预留\0static size_t npos静态常量size_t 无符号npos-1本质是极大值代表查找失败 / 删除到末尾✅可行性这三个成员是 string 经典底层模型。 ⚠️注意npos 是静态成员必须类外初始化也就是 cpp 里的size_t string::npos -1;1.6 全局运算符声明友元不需要这里全局重载ostream operator (ostream out,const string s); istream operator(istream in, string s); istream getline(istream in, string s, char delim \n);流提取、流插入、getline不属于类成员全局函数。1.7 头文件末尾 #endif#endif配合保护宏但是前面宏写反导致失效。名空间namespace str成员变量固定private: char* _str; // 堆字符数组末尾带\0兼容C字符串 size_t _size; // 有效字符数量不含\0 size_t _capacity; // 可存放有效字符上限不含\0 public: static size_t npos;// size_t无符号npos (size_t)-1查找失败标记模块 2string.cpp 成员函数实现逐段拆解一、六大默认成员函数类的核心1. 构造函数string(const char* s );string::string(const char* s) :_size(strlen(s)) { assert(s ! nullptr); _str new char[_size 1]; _capacity _size; strcpy(_str, s); }功能构造 string 对象支持无参默认参数、C 风格字符串构造初始化列表_size(strlen(s))先算出传入字符串有效字符个数内存开辟new char[_size1]多开 1 字节专门存放末尾 \0这是 c_str () 的基础strcpy把 C 字符串拷贝到堆空间自动带上\0可行性✅ 学习场景完全可行工程要注意异常捕获new 失败会抛异常坑点不加assert(s ! nullptr)传入nullptrstrlen(nullptr)程序直接崩溃不能new char[_size]否则没有空间存\0c_str 读取乱码2. 析构函数~string();string::~string() { delete[] _str; _str nullptr; _size _capacity 0; }功能对象生命周期结束释放堆上_str开辟的字符数组防止内存泄漏delete[] _str释放new[]申请的连续字符数组不能用 delete置空_strnullptr防止野指针size、capacity 清零可行性✅ 稳定基础内存释放逻辑坑点忘记[]delete _str只会释放第一个 char内存泄漏不置空_str对象销毁后如果有人访问这个指针就是野指针3. 拷贝构造string(const string str);现代写法string::string(const string str) { string tmp(str._str); this-swap(tmp); }功能用已有 string 对象新建一个独立对象深拷贝原理string tmp(str._str)调用构造函数用原对象的 C 字符串在堆上开辟一份新内存this-swap(tmp)交换this和 tmp 底层三个成员_str/_size/_capacity函数结束局部 tmp 析构释放 tmp 原来旧的内存也就是 this 原来的空内存可行性✅ 面试推荐写法异常安全代码简洁坑点传统写法容易忘记深拷贝写成浅拷贝两个对象共用同一块堆内存析构时重复释放程序崩溃现代写法依赖 swap 函数必须正确实现4. swap 成员函数void swap(string str);void string::swap(string str) { std::swap(_str, str._str); std::swap(_size, str._size); std::swap(_capacity, str._capacity); }功能交换两个 string 底层资源只交换指针和两个数字不拷贝字符O (1) 时间复杂度原理交换三个成员变量堆内存所有权互换没有内存拷贝可行性✅ 性能极高现代拷贝构造、现代赋值重载的基石坑点不要手动循环交换每个字符O (n) 效率极低完全没必要5. 赋值重载 1string operator (const string str);传统版本string string::operator (const string str) { if (this ! str) { char* tmp new char[str._capacity 1]; memcpy(tmp, str._str, str._size 1); delete[] _str; _str tmp; _size str._size; _capacity str._capacity; } return *this; }功能s1 s2;把 s2 内容赋值给 s1深拷贝if(this ! str)自赋值判断防止s1s1场景下先 delete 把自己内存释放后面无数据拷贝先开新空间 tmp拷贝数据成功之后再 delete 旧内存异常安全new 失败不会破坏原有 s1 数据返回*this支持连续赋值s1s2s3可行性✅ 逻辑可行但是代码偏长面试优先现代版本坑点自赋值判断如果写成if(*this ! str)每次要逐个字符比较效率差必须比较地址this ! str6. 赋值重载 2string operator (string str);现代版本值传递string string::operator (string str) { swap(str); return *this; }功能现代写法赋值重载同样实现s1 s2原理参数值传递调用拷贝构造生成临时对象 strswap 交换资源函数结束临时对象析构释放旧内存可行性✅ 代码极简面试首选坑点参数不能写成引用string str否则不会自动拷贝生成临时对象整个逻辑失效二、基础访问接口7. c_strconst char* c_str()const;const char* string::c_str()const { return _str; }功能返回底层 C 风格字符串首地址兼容 C 语言函数保证末尾带\0const 修饰不允许外部通过返回指针修改内部字符串可行性✅ 标准 string 核心接口坑点扩容、修改 string 之后原来 c_str 返回的指针会失效底层内存换了8. sizesize_t size()const;size_t string::size()const { return _size; }功能返回当前有效字符个数不含 \0const 成员const 对象也可以调用可行性✅ 简单读取成员变量O (1)坑点不要和 strlen 混淆strlen 是遍历到 \0size 直接读变量9. begin /end 迭代器string::iterator string::begin() { return _str; } string::iterator string::end() { return _str _size; } string::const_iterator string::begin()const { return _str; } string::const_iterator string::end()const { return _str _size; }自定义iterator char*const_iterator const char*begin返回第一个字符地址end返回有效字符的下一位不指向有效字符左闭右开[begin,end)两套版本普通对象返回可读写迭代器const 对象返回只读迭代器可行性✅ 简化实现原生指针充当迭代器支持范围 for、算法库坑点扩容之后迭代器失效原指针指向已经释放的旧堆内存10. [] 重载char operator[](size_t i);char string::operator[](size_t i) { assert(i _size); return _str[i]; } const char string::operator[](size_t i)const { assert(i _size); return _str[i]; }功能支持s[0]下标访问字符返回引用支持修改assert(i _size)越界断言调试阶段捕获越界release 下 assert 失效两个重载普通对象可读可写const 对象只读可行性✅ 模拟标准 string 行为坑点标准库 std::string 的 []不强制检查越界at () 函数会抛异常我们这里没有实现 at三、容量相关reserve11. reservevoid reserve(size_t n);void string::reserve(size_t n) { if (n _capacity) { return; } char* tmp new char[n 1]; memcpy(tmp, _str, _size); tmp[_size] \0; delete[] _str; _str tmp; _capacity n; }功能预扩容只修改 capacity不改变 size不新增字符n 小于当前容量则什么都不做。逻辑开辟新的更大空间 tmp大小 n1预留\0memcpy 拷贝原来有效字符_size 个手动加上tmp[_size] \0这是修复原始代码的关键删除旧内存把_str 指向新空间更新_capacity可行性✅ 减少频繁扩容带来的内存开销坑点忘记手动补\0c_str 拿到的字符串后面是脏数据reserve不会修改_size不会初始化新增空间字符四、尾插接口 push_back /append/ 12. push_backvoid push_back(char ch);void string::push_back(char ch) { if (_size _capacity) { size_t newcapacity _capacity 0 ? 4 : _capacity * 2; reserve(newcapacity); } _str[_size] ch; _size; _str[_size] \0; }功能尾部插入单个字符扩容判断有效字符等于容量触发扩容空 string 第一次扩容到 4后续二倍扩容写入字符size末尾补上\0可行性✅ 尾插 O (1) 均摊复杂度坑点新增字符后必须重新放\0c_str 才正常13. appendvoid append(const char* s);void string::append(const char* s) { assert(s ! nullptr); size_t len strlen(s); if (_size len _capacity) { size_t newcapacity _size len _capacity * 2 ?_capacity * 2 : _size len; reserve(newcapacity); } memcpy(_str _size, s, len1); _size len; }功能尾部追加 C 风格字符串计算追加字符串长度判断是否需要扩容扩容策略二倍不够就按总长度扩容memcpy 从当前_str 尾部位置拷贝字符串连同末尾\0更新_size可行性✅ 批量追加字符串比循环 push_back 高效坑点传入空指针会触发 assert 崩溃14. operator 两个重载string string::operator(char ch) { push_back(ch); return *this; } string string::operator(const char* s) { append(s); return *this; }功能s a、s abc底层直接复用 push_back 和 append返回引用支持连续s123a可行性✅ 封装简化调用代码复用五、插入 insert15. insert (pos, char ch) 单个字符插入string string::insert(size_t pos, char ch) { assert(pos _size); if (_size _capacity) { size_t newcapacity _capacity 0 ? 4 : _capacity * 2; reserve(newcapacity); } size_t end _size 1; while (end pos) { _str[end] _str[end - 1]; --end; } _str[pos] ch; _size; return *this; }功能在 pos 下标位置插入一个字符pos 可以等于 size尾部插入原理从后向前挪动字符end 初始值_size1因为插入 1 个字符整体多占 1 位\0也要后移循环把 pos 后面所有字符向后挪一位腾出 pos 位置在 pos 写入字符size可行性✅ 插入逻辑正确中间插入最坏 O (n)需要挪动大量字符坑点绝对不能从前向后挪前面字符会覆盖还没有移动的数据16. insert (pos, const char* s) 插入字符串string string::insert(size_t pos, const char* s) { assert(s ! nullptr); size_t len strlen(s); if (_size len _capacity) { size_t newcapacity _size len _capacity * 2 ? _capacity * 2 : _size len; reserve(newcapacity); } 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[posi] s[i]; } _size len; return *this; }功能pos 位置插入一整个 C 字符串先计算待插入字符串长度 len判断扩容end _size len整体字符后移 len 位循环从后往前每次向前取 len 位置字符向后移动填充 pos 位置的新字符串更新 size可行性✅ 逻辑可行长字符串中间插入开销很大 O (n)坑点移动终点边界容易写错边界错误直接覆盖有效数据六、删除 erase /pop_back/clear17. erasestring erase(size_t pos 0, size_t len npos);string string::erase(size_t pos, size_t len) { assert(pos _size); if (pos _size) { return *this; } if (len npos || len (_size - pos)) { _size pos; _str[_size] \0; } else { size_t end pos len; memmove(_str pos, _str end, _size 1 - end); _size - len; } return *this; }功能从 pos 位置删除 len 个字符len 默认 npos 代表删除 pos 到末尾全部字符分支pos 等于 size没有字符可删直接返回len 足够大直接截断字符串修改 size补上\0普通删除使用memmove把后面字符向前覆盖覆盖掉被删除区域✅重点为什么 memmove 而不是 memcpymemcpy 不处理内存重叠erase 场景源地址和目标地址重叠memmove 自动处理重叠内存安全。可行性✅ 符合标准 string erase 行为坑点如果换成 memcpy重叠区域拷贝会出现乱码18. pop_backvoid pop_back();void string::pop_back() { assert(_size 0); _str[--_size] \0; }功能删除最后一个字符逻辑size 先减 1然后在新 size 位置写\0不释放容量内存保留assert 防止空字符串尾删可行性✅ O (1)高效坑点只是逻辑删除底层堆内存容量不会缩小19. clearvoid clear();void string::clear() { _str[0] \0; _size 0; }功能清空所有有效字符不释放堆内存capacity 保持不变size 置 0首位置\0可行性✅ O (1)保留内存方便后续复用减少扩容开销坑点clear 不会释放内存容量不变七、查找与截取 find /substr20. find 查找单个字符size_t string::find(char ch, size_t pos)const { assert(pos _size); size_t i1 pos; while (i1 _size) { if (_str[i1] ch) { return i1; } i1; } return npos; }功能从 pos 位置向后查找字符 ch返回下标找不到返回 npos循环只遍历有效字符i1 _size不会匹配末尾\0const 成员只读不修改可行性✅ O (n) 顺序查找坑点size_t 是无符号判断返回值不能写if(find 0)要用if(find ! npos)21. find 查找子串C 字符串size_t string::find(const char* s, size_t pos)const { assert(s ! nullptr); const char* str1 strstr(_str pos, s); if (str1 ! nullptr) { return str1 - _str; } else { return npos; } }功能从 pos 位置查找子串找到返回起始下标找不到 npos调用C 库strstr完成子串匹配指针相减得到下标可行性✅ 简化代码如果面试要求不能调用库函数就要手写朴素字符串匹配坑点依赖 strstr 库函数底层是朴素匹配长文本效率一般22. substr 截取子串string string::substr(size_t pos, size_t len) const { assert(pos _size); if(pos _size) { return string(); } if (len _size - pos || len npos) { len _size - pos; } string ret; ret.reserve(len); for (size_t i 0; i len; i) { ret _str[i pos]; } return ret; }功能截取从 pos 开始长度 len 的子串返回全新 string 对象独立内存原字符串不变逻辑创建空 ret预 reserve 开辟空间循环追加字符len 为 npos直接截取 pos 到末尾全部字符可行性✅ 符合标准行为返回值是值拷贝坑点substr 是新建对象不是返回原字符串的指针修改 ret 不会影响原 string八、比较运算符重载 !bool string::operator(const string s)const { size_t i1 0, i2 0; while (i1 _size i2 s._size) { if (_str[i1] s[i2]) { return false; } else if (_str[i1] s[i2]) { return true; } else { i1; i2; } } return i2 s._size; } bool string::operator(const string s)const { return (*this s) || (*this s); } bool string::operator(const string s)const { return !(*this s); } bool string::operator(const string s)const { return !(*this s); } bool string::operator(const string s)const { size_t i1 0, i2 0; while (i1 _size i2 s._size) { if (_str[i1] ! s[i2]) { return false; } else { i1; i2; } } return i2 s._size i1 _size; } bool string::operator!(const string s)const { return !(*this s); }功能字符串字典序比较s1 s2s1 s2operator逐字符按 ASCII 对比前面字符相同短字符串更小其余运算符复用已经写好的 和 不要全部手写减少重复代码可行性✅ 逻辑和 std::string 字典序规则一致坑点比较结束条件两个字符串前面全部相同需要判断长度九、全局流重载 getline不属于类成员函数23. operator 输出std::ostream operator (std::ostream out, const string s) { for (size_t i 0; i s.size(); i) { out s[i]; } return out; }全局函数不能写在类里面除非友元这里不需要遍历每一个字符输出返回 ostream 引用支持链式cout s1 s2可行性✅24. operator 输入空白分隔std::istream operator(std::istream in, string s) { s.clear(); char buff[128]; char ch; ch in.get(); size_t i 0; while ((ch ! ) (ch ! \n)) { buff[i] ch; if (i 127) { buff[i] \0; s buff; i 0; } ch in.get(); } if (i ! 0) { buff[i] \0; s buff; i 0; } return in; }读到空格 / 换行就停止先 clear 清空原有字符串缓冲区 buff 数组批量读取减少 调用次数优化性能坑点遇到空格终止和 cin std::string 行为一致不能读取带空格句子25. getline 全局函数std::istream getline(std::istream in, string s, char delim) { s.clear(); char buff[128]; char ch; ch in.get(); size_t i 0; while (ch ! delim) { buff[i] ch; if (i 127) { buff[i] \0; s buff; i 0; } ch in.get(); } if (i ! 0) { buff[i] \0; s buff; i 0; } return in; }读取一行直到分隔符 delim默认 \n 换行可以读取空格同样使用本地缓冲区批量读取可行性✅ 模拟标准 getline补充静态成员 npossize_t string::npos -1;静态成员属于整个类不属于单个对象类外初始化size_t 是无符号整数(size_t)-1等价于无符号最大值用作 find 查找失败、erase 默认长度标记坑点无符号不能直接和负数比较结尾本篇我们从零实现简易 string 容器并对每一个成员函数进行拆解。从构造析构、深浅拷贝到扩容、插入删除、查找截取每个接口都配套了原理说明与踩坑提醒。学习 C STL动手模拟实现是最高效的方式。在手写代码的过程中你会直面内存泄漏、野指针、数组越界、内存重叠拷贝这些经典问题这是单纯看书很难获得的体验。一起加油深耕 C 底层
返回列表