
| 🍕阿i索 | 个人主页 |
|---|---|
| 《C语言专栏》 | 《C++专栏》 |
| 《数据结构专栏》 | 《LaTeX专栏》 |
| 《软件配置问题》 | 《Linux 专栏》 |
| 待更新... |
前言.
本篇简单整理string类的模拟实现。
一、string底层模拟实现细节注意
模块一:类与对象 & 六大默认成员函数
1. 类成员变量初始化顺序规则
对应代码与原注释
private:
// 成员变量声明顺序
char* _str;
size_t _size;
size_t _capacity;
// 如果将初始化列表后两个成员变量用_size初始化(×,因为初始化列表初始化顺序是按照定义的顺序)
// 如果调整定义的顺序,后期增加变量可能改变顺序,使代码可维护性不高
初始化铁则 :C++ 类成员变量的初始化顺序只由类内的声明顺序决定 ,和构造函数初始化列表的书写顺序无关。
若错误编写如下初始化列表:
// 错误写法:_size 先初始化,再用 _size 初始化 _str
string(const char* str)
: _size(strlen(str)), _str(new char[_size + 1])
{}
按照声明顺序,_str 会优先初始化,但此时 _size 还未赋值,属于使用未初始化变量,触发未定义行为。
解决: 仅在初始化列表初始化 _size,_str 内存开辟和_capacity 赋值放到函数体内部,规避顺序带来的隐患,提升代码可维护性。
2. 缺省参数的语法规则与合并构造函数
// 头文件:函数声明,书写缺省参数
string(const char* str = "");
// 源文件:函数定义,禁止重复写缺省参数
string::string(const char* str)
: _size(strlen(str))
{
_str = new char[_size + 1];
_capacity = _size;
memcpy(_str, str, _size + 1);
}
缺省参数只能在函数声明处定义 ,函数实现(定义)中重复书写缺省参数会编译报错。
利用缺省参数 str = "":将默认构造函数 和带参构造函数合并为一个,减少代码冗余:
string s1;:使用缺省值"",创建空字符串对象;string s2("hello");:传入自定义字符串,正常初始化。
空字符串 "" 调用 strlen 结果为 0,保证空对象内存开辟逻辑正常执行。
3. const 修饰成员函数(常量成员函数)
// 常量成员函数:函数尾部加 const
size_t size() const
{
return _size;
}
const char* c_str() const
{
return _str;
}
// []运算符 const 重载
const char& operator[](size_t pos)const
{
assert(pos < _size);
return _str[pos];
}
成员函数括号后方的 const 表示:该函数承诺不会修改类的任何成员变量 。
调用权限规则:
- 普通对象:可以调用普通成员函数、常量成员函数;
const修饰的对象:仅能调用常量成员函数,调用普通成员函数会编译报错。
重载设计思想 对 operator[]、begin()、end() 这类接口,同时提供普通版本 + const 版本,是 C++ 容器的标准写法:
- 普通版本:供普通对象使用,支持读写;
- const 版本:供 const 对象使用,保证只读,符合封装性要求。
4. 下标运算符 operator[] 返回引用的设计
// 普通版本:返回 char& 引用,支持修改
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
// const版本:返回 const char& 常量引用,禁止修改
const char& operator[](size_t pos)const
{
assert(pos < _size);
return _str[pos];
}
返回引用的原因
- 若返回普通值(值传递):只是返回字符副本,无法执行
s[0] = 'a'这类修改操作; - 若返回引用:直接返回数组元素本身,既可以读取,也可以作为左值修改原数据。
常量引用的作用:
const char&限制返回值不可修改,配合常量成员函数,保证const string对象完全只读。
5. 析构函数细节与动态内存释放
~string()
{
delete[] _str; // 释放动态数组
_str = nullptr; // 指针置空
_size = 0;
_capacity = 0;
}
动态数组释放规则:
- 使用 new char[] 开辟的动态数组 ,必须搭配 delete[] 释放;如果只用 delete,会导致数组后半部分内存泄漏、程序崩溃。
野指针规避 内存释放后,将 _str 赋值为 nullptr:
- 释放后的指针会变成野指针,置空后可避免后续误操作野指针,同时便于调试。
成员变量清零 主动将 _size、_capacity 置 0 是良好编码习惯,防止对象析构后被意外调用,减少 bug。
模块二:静态成员变量
静态常量 static const size_t npos
// 类内:静态常量 声明
const static size_t npos;
// 类外全局域:静态常量 定义+初始化
const size_t string::npos = -1;
//const static整型可以声明定义一体
静态成员通用特性
- 静态成员变量不属于某个对象 ,属于整个类,所有对象共享同一份数据;
常规静态成员:
- 类内仅做声明 ,必须在类外全局作用域 完成定义和初始化,不能在构造函数中初始化。
特殊语法:const static 整型变量 C++ 语法特例:
const static修饰的整型 / 枚举类型变量,可以直接在类内完成初始化(声明定义合一)。
size_t 无符号类型特性:
size_t是无符号整型 ,取值范围[0, 数据类型最大值],永远无法表示负数。 当-1赋值给无符号变量时,会自动转换为该类型的最大值,以此标记「查找、截取操作失败」。
模块三:迭代器体系(原生指针模拟迭代器)
1. 迭代器本质与 typedef 类型别名
// 类型别名:用原生指针模拟迭代器
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;
}
迭代器本质 string 底层是连续堆字符数组 ,因此直接使用原生指针模拟迭代器,是最简单、最高效的迭代器实现方式。
typedef 作用:
- 为指针类型起别名
iterator/const_iterator,对外屏蔽底层实现:使用者只需要关心迭代器接口,无需知道底层是指针还是类对象,符合封装思想。
容器迭代器通用规则(左闭右开区间) C++ 所有标准容器迭代器都遵循 [begin, end) 规则:
begin():指向第一个有效元素;end():指向最后一个有效元素的下一个位置,不代表有效数据。
2. 范围 for 循环底层原理
// 范围for的底层是迭代器
for (auto ch : s4)
{
cout << ch << " ";
}
语法糖本质: 范围 for 是编译器提供的语法糖,编译阶段会自动转换为迭代器遍历,等价代码:
auto it = s4.begin();
while (it != s4.end())
{
auto ch = *it;
cout << ch << " ";
++it;
}
使用前提:
- 自定义类想要支持范围 for 循环,必须实现
begin()和end()两个迭代器接口,否则编译报错。
const 对象适配:
- 当遍历
const string对象时,编译器会自动调用const版本的begin()/end(),保证只读特性。
模块四:字符串基础接口与 C 库函数结合
1. clear() 接口设计思想
void clear()
{
_str[0] = '\0';
_size = 0;
}
仅清空字符串有效内容 ,不释放堆内存、不修改 _capacity 容量。
目的: 复用已开辟的堆空间,避免频繁执行 new/delete 内存操作,提升对象重复使用时的性能。
原理: C 风格字符串以 \0 作为结束标志,将数组首元素置为 \0 后,所有 C 语言字符串函数都会判定该字符串为空。
2. c_str() 接口:C/C++ 语言兼容
const char* c_str() const
{
return _str;
}
作用: 返回底层 C 风格字符数组指针,用于兼容 C 语言接口、老旧代码、系统 API ,实现 C/C++ 代码互通。返回值为 const char*,禁止外部代码修改底层字符数组,防止破坏类内部数据。
注意:
- 类内部遍历(迭代器 / 范围 for):按照
_size遍历所有有效字符,可以识别内嵌\0; c_str()返回的指针:被 C 库函数使用时,遇到第一个\0就终止读取,数据会被截断
3. strstr 库函数与指针运算
size_t string::find(const char* str, size_t pos )
{
assert(pos < _size);
const char* ptr=strstr(_str + pos, str);
if (ptr)
{
return ptr - _str; // 指针相减计算下标
}
else
{
return npos;
}
}
strstr 函数功能:
C 标准库函数:在目标字符串中查找子串。查找成功返回子串首地址 ,查找失败返回 NULL。
同数组指针减法规则:
指向同一块连续内存 的两个 char* 指针相减,结果为两个指针之间的元素个数(数组下标差值),这是数组专属的指针运算特性。
偏移查找: 通过 _str + pos 让查找起点偏移到指定位置,简化手动循环匹配子串的代码。
模块五:运算符重载体系
1. 关系运算符:代码复用设计
bool operator<=(const string& s) const
{
return *this < s || *this == s;
}
bool operator>(const string& s) const
{
return !(*this <= s);
}
bool operator>=(const string& s) const
{
return !(*this > s);
}
复用思想:
- 实现两个基础运算符 :
<(小于)、==(等于),其余四个关系运算符全部基于基础运算符组合实现。
优势:
- 减少重复代码,降低维护成本;
- 全局比较逻辑统一,修改规则时仅需改动基础函数,不会出现逻辑不一致。
2. strcmp 字符串比较规则
bool operator<(const string& s) const
{
return strcmp(_str, s._str) < 0;
}
bool operator==(const string& s) const
{
return strcmp(_str, s._str) == 0;
}
strcmp(a, b) 按照ASCII 码字典序比较两个 C 风格字符串,返回值规则:
- 字符串
a字典序 <b:返回负数; - 字符串
a字典序 ==b:返回 0; - 字符串
a字典序 >b:返回正数。 你的所有关系比较逻辑都基于该规则封装。
3. 复合赋值运算符 += 重载
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
函数复用: += 运算符直接调用已实现的 push_back(尾插字符)、append(追加字符串),不重复编写逻辑。
返回引用的意义: 返回 string& 引用,支持连续运算 语法,例如:s += 'a' += "bcd",符合 C++ 运算符重载规范。
模块六:命名空间、模板与函数重载
1. 自定义命名空间 asuo
namespace asuo
{
class string
{
// 类实现
};
}
作用:命名隔离 自定义 string 放入 asuo 命名空间,避免和 C++ 标准库 std::string 发生命名冲突 。
使用方式:
外部使用时必须指定空间:asuo::string s;,和标准库 std::string 明确区分。
2. 模板函数 + 函数重载(swap 两套实现)
// 1. 通用模板swap:利用函数模板实现泛型交换,适配所有数据类型
template<class T>
void swap(T& a, T& b)
{
// 先拷贝构造临时对象,再两次赋值,共触发三次深拷贝
// 针对string类型,会完整复制堆上字符数据,数据量大时性能很低
T c(a);
a = b;
b = c;
}
// 2. 针对自定义string类的重载swap版本
// inline:内联函数,编译器直接展开代码,减少函数调用栈开销
inline void swap(string& a, string& b)
{
// 调用string成员swap,仅交换内部指针、大小、容量等成员变量
// 不拷贝实际字符内容,彻底避免深拷贝,交换效率极高
a.swap(b);
}
1. 通用模板 swap
依靠template<class T>实现泛型编程 ,一份代码可以支持任意类型交换,通用性强。 但交换string对象时,代码逻辑会执行拷贝构造 + 两次赋值 ,产生三次深拷贝,需要复制堆区所有字符、频繁操作内存,字符串越长、交换越频繁,性能损耗越严重。
2. string 专属重载 swap
- 重载优先级 :当参数为string类型时,普通重载函数优先级高于函数模板,编译器会优先执行该优化版本。
- 性能优势 :内部调用
string成员swap,只交换对象内部指针、长度、容量,不操作堆上字符数据,全程无深拷贝,开销极小。 - inline 作用 :该函数代码简短,
inline建议编译器将代码原地展开,省去函数调用的栈开销,进一步提升效率。
3. 整体设计目的
之所以同时实现模板 swap 和 string 专属重载 swap,就是为了解决多次深拷贝带来的性能问题:既保留函数模板的通用能力,又通过重载为字符串做专项优化,兼顾通用性与运行效率。
模块七:输入输出流相关知识点
1. 流运算符 << / >> 设计为全局函数
// 全局函数,非类成员
std::ostream& operator<<(std::ostream& out, const string& s);
std::istream& operator>>(std::istream& in,string& s);
不能作为成员函数的原因:
成员运算符重载要求:运算符左操作数必须是当前类对象 。 而 cout << s 的左操作数是 std::ostream(cout),不是 string,因此必须实现为全局函数。
返回流引用:
返回 ostream& / istream& 流引用,支持连续流操作 ,例如:cout << s1 << s2 << endl;。
2. operator>> 与 getline 读取规则差异 & 经典坑点
// >> 运算符:以空格、换行作为分隔符
// getline:以指定字符为结束符,默认读取整行(包含空格)
operator>>读取规则- 自动跳过开头的空白字符(空格、换行、制表符);
- 读取过程中遇到空格 / 换行立即停止;
- 适用场景:读取单个单词、无空格字符串。
getline读取规则- 不会跳过空白字符,完整读取所有内容(包含空格);
- 直到遇到指定结束符(默认换行
\n)才停止读取; - 适用场景:读取整行文本。
- 经典坑点
- 使用
cin >> s读取数据后,输入缓冲区会残留换行符 ,后续直接调用getline会立刻读取到残留换行符,导致读取空行。这是 C++ 流使用中高频踩坑点。
模块八:容器设计思想与内存管理
1. reserve 与 resize 职责划分
reserve(size_t n):
- 仅预分配内存(扩容) ,只修改
_capacity,不改变有效字符数_size,不修改字符串内容;且不支持缩容。
resize(size_t n, char ch):
修改有效字符数 _size,分两种场景:
n < _size:缩容,截断多余字符,末尾补\0;n > _size:先扩容,再用指定字符填充新增位置。
解读:
- 职责分离思想 这是 STL 容器的标准设计范式:
reserve:面向性能优化,提前规划内存,避免多次扩容带来的开销;resize:面向内容修改,改变字符串对外展示的实际长度。
- 缩容规则 代码中
reserve仅在n > _capacity时执行扩容,主动放弃缩容。
2. 动态数组内存布局规则
内存结构
// _str 指向堆内存布局
[字符1][字符2]...[有效字符N][\0]
- 容量计算规则
_size:有效字符个数,不包含末尾\0;_capacity:最大可存储的有效字符数,不包含末尾\0;- 实际堆内存总大小:
_capacity + 1,多出的 1 字节专门存放字符串结束符\0,保证兼容 C 语言字符串。
模块九:断言 assert 调试机制
assert(pos < _size);
assert(pos <= _size);
- assert 本质
assert(条件表达式)是 C 语言标准库提供的调试宏,用于校验参数、边界的合法性。 - 运行模式差异
- Debug(调试)模式:表达式为
false时,程序直接终止,并打印报错位置、错误信息,快速定位 bug; - Release(发布)模式:
assert宏会被编译器完全移除,不产生任何代码,不影响程序运行效率。
- Debug(调试)模式:表达式为
- 使用场景 多用于函数入口校验参数合法性(如下标、插入位置、查找位置),属于调试辅助手段,不用于正式的业务逻辑判断。
二、模拟实现代码
string.h文件
#pragma once
#include<assert.h> // 断言校验
#include<string.h> // C语言字符串库函数
#include<algorithm> // 算法库 max/swap
#include<iostream> // 标准输入输出
// 自定义命名空间asuo,避免与C++标准库std::string命名冲突
namespace asuo
{
// 模拟实现C++标准库string类
class string
{
private:
// 三大核心成员变量
char* _str; // 指向堆区字符数组的指针,存储字符串内容
size_t _size; // 有效字符个数(不包含末尾'\0')
size_t _capacity; // 堆空间容量(最大可存储字符数,不包含'\0')
public:
// 静态常量:表示查找失败的位置,无符号最大值
const static size_t npos;
// ===================== 构造 & 析构 & 拷贝 & 赋值(六大默认函数) =====================
// 构造函数:带缺省参数,兼容默认构造 + 带参构造
// 缺省参数"" 代表空字符串,自动补'\0'
string(const char* str = "");
// 析构函数:释放堆区内存,防止内存泄漏
~string();
// 拷贝构造函数:手动实现深拷贝,解决默认浅拷贝内存重复释放问题
string(const string& s);
// 赋值运算符重载:深拷贝赋值,支持对象之间赋值
string& operator=(const string& s);
// ===================== 容量管理接口 =====================
// 调整有效字符个数:扩容/缩容,多出位置用指定字符填充
void resize(size_t n, char ch = '\0');
// 扩容函数:只开辟空间,不修改有效字符数,缩容不处理
void reserve(size_t n);
// 清空有效字符:仅将首位置'\0',不释放堆空间、不修改容量
void clear()
{
_str[0] = '\0';
_size = 0;
}
// ===================== 字符串追加接口 =====================
// 尾插单个字符
void push_back(char ch);
// 追加C风格字符串
void append(const char* str);
// 运算符重载:+= 追加C风格字符串
string& operator+=(const char* str)
{
append(str);
return *this;
}
// 运算符重载:+= 追加单个字符
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
// ===================== 基础信息获取接口 =====================
// 获取当前有效字符个数
size_t size() const
{
return _size;
}
// 获取C风格字符串指针(兼容C语言接口)
const char* c_str() const
{
return _str;
}
// 成员swap:直接交换三个成员变量,避免深拷贝,提升效率
void swap(string& s);
// ===================== 插入 & 删除接口 =====================
// 在pos位置插入单个字符
void insert(size_t pos, char ch);
// 在pos位置插入C风格字符串
void insert(size_t pos, const char* str);
// 从pos位置开始删除len个字符,默认删除到末尾
void erase(size_t pos = 0, size_t len = npos);
// ===================== 查找 & 截取接口 =====================
// 从pos位置开始查找字符ch,返回下标;失败返回npos
size_t find(char ch, size_t pos = 0);
// 从pos位置开始查找子串str,返回起始下标;失败返回npos
size_t find(const char* str, size_t pos = 0);
// 从pos位置截取len长度子串,返回新string对象
string substr(size_t pos = 0, size_t len = npos);
// ===================== 关系运算符重载(大小比较) =====================
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;
// ===================== 迭代器实现(原生指针模拟) =====================
typedef char* iterator; // 普通迭代器:可读可写
typedef const char* const_iterator; // 常量迭代器:仅可读
// 返回起始迭代器(指向第一个有效字符)
iterator begin()
{
return _str;
}
// 返回末尾迭代器(指向最后一个有效字符的下一位,即'\0')
iterator end()
{
return _str + _size;
}
// 常量对象专属起始迭代器
const_iterator begin() const
{
return _str;
}
// 常量对象专属末尾迭代器
const_iterator end() const
{
return _str + _size;
}
// ===================== []下标运算符重载 =====================
// 普通对象调用:可读可写,越界断言拦截
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
// 常量对象调用:只读不可写,越界断言拦截
const char& operator[](size_t pos)const
{
assert(pos < _size);
return _str[pos];
}
};
// ===================== 全局运算符重载(流输入输出) =====================
// 重载<< 输出string对象
std::ostream& operator<<(std::ostream& out, const string& s);
// 重载>> 读取字符串(自动忽略空格/换行,不读取空格)
std::istream& operator>>(std::istream& in, string& s);
// 自定义getline:读取整行,以指定字符为结束符(默认换行符)
std::istream& getline(std::istream& in, string& s, char delim = '\n');
// 通用模板swap:所有类型都可使用,默认会触发拷贝
template<class T>
void swap(T& a, T& b)
{
T c(a); a = b; b = c;
}
// 针对asuo::string的特化swap:调用成员swap,效率更高
inline void swap(string& a, string& b)
{
a.swap(b);
}
}
string.cpp文件
#define _CRT_SECURE_NO_WARNINGS
#include"string.h"
namespace asuo
{
// 静态常量类外初始化:size_t npos = -1 转为无符号即最大值,表示查找失败
const size_t string::npos = -1;
// ===================== 成员函数实现 =====================
// 构造函数:缺省参数仅在头文件声明处书写
string::string(const char* str)
: _size(strlen(str)) // 初始化列表仅调用一次strlen,优化效率
{
// 开辟堆空间:有效字符 + 末尾'\0'
_str = new char[_size + 1];
_capacity = _size;
// 使用strcpy无法拷贝内嵌\0,改用memcpy按字节完整拷贝
memcpy(_str, str, _size + 1);
}
// 析构函数:释放堆内存,置空指针、清零成员变量
string::~string()
{
delete[] _str; // 释放字符数组堆内存
_str = nullptr;
_size = 0;
_capacity = 0;
}
// 拷贝构造函数:深拷贝(核心解决浅拷贝重复析构问题)。string s2(s1)
string::string(const string& s)
{
// 为新对象单独开辟独立堆空间
_str = new char[s._capacity + 1];
// memcpy完整拷贝所有字节(包含内嵌\0)
memcpy(_str, s._str, s._size + 1);
_size = s._size;
_capacity = s._capacity;
}
// 赋值运算符重载:深拷贝赋值,防止自赋值、浅拷贝。s1=s3
string& string::operator=(const string& s)
{
// 自赋值判断:自己赋值给自己直接返回,无需操作
if (this != &s)
{
// 1. 先开辟新空间(防止原空间释放后找不到源数据)
char* tmp = new char[s._capacity + 1];
memcpy(tmp, s._str, s._size + 1);
// 2. 释放当前对象旧堆空间
delete[] _str;
// 3. 指针指向新空间,更新大小与容量
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
// 成员swap:直接交换三个成员变量,零拷贝、高效率
void string::swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// resize:调整有效字符长度
void string::resize(size_t n, char ch)
{
if (n <= _size)
{
// 场景1:缩容,删除多余的,保留前n个,末尾补'\0'
_size = n;
_str[_size] = '\0';
}
else
{
// 场景2:扩容,先保证容量足够,再填充字符
reserve(n);
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
// reserve:扩容接口,仅扩大容量,不缩容、不修改有效字符
void string::reserve(size_t n)
{
// 仅当新容量 > 当前容量时才扩容
if (n > _capacity)
{
char* tmp = new char[n + 1];
memcpy(tmp, _str, _size + 1);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
// push_back:尾插单个字符
void string::push_back(char ch)
{
// 容量已满,执行扩容:空对象扩为4,非空二倍扩容
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
// 尾插字符,更新有效长度,手动补末尾'\0'
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
// append:追加C风格字符串
void string::append(const char* str)
{
size_t len = strlen(str);
// 总长度超过容量,执行扩容(取二倍容量/总长度最大值)
if (_size + len > _capacity)
{
reserve(std::max(_size + len, _capacity * 2));
}
// 追加字符串,memcpy保证内嵌\0正常拷贝
memcpy(_str + _size, str, len);
_str[_size + len] = '\0';
_size += len;
}
// insert:在pos位置插入单个字符
void string::insert(size_t pos, char ch)
{
assert(pos <= _size); // 位置合法性断言
// 容量满则扩容
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
// 无符号数遍历优化:end从后向前挪,避免>=导致无符号越界死循环
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
_size++;
}
// insert:在pos位置插入C风格字符串
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (len == 0) return; // 空字符串直接返回
// 容量不足则扩容
if (_size + len > _capacity)
{
reserve(std::max(_size + len, _capacity * 2));
}
// 数据后移,规避无符号size_t越界问题
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
// 拷贝待插入字符串
memcpy(_str + pos, str, len);
_size += len;
_str[_size] = '\0';
}
// erase:删除指定区间字符
void string::erase(size_t pos, size_t len)
{
assert(pos < _size); // 起始位置合法校验
// 场景1:删除到末尾 / 长度超出剩余字符,直接截断
if (len == npos || len >= _size - pos)
{
_size = pos;
_str[_size] = '\0';
}
// 场景2:删除部分字符,后续字符向前覆盖
else
{
memcpy(_str + pos, _str + pos + len, _size - (pos + len) + 1);
_size -= len;
}
}
// 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; // 查找失败
}
// find:查找子串
size_t string::find(const char* str, size_t pos)
{
assert(pos < _size);
// 调用C库strstr查找子串
const char* ptr = strstr(_str + pos, str);
if (ptr)
{
return ptr - _str; // 指针差值得到下标
}
else
{
return npos;
}
}
// substr:截取子串,返回新string对象
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
// 长度修正:超出范围则截取到末尾
if (len == npos || len > _size - pos)
{
len = _size - pos;
}
string sub;
sub.reserve(len); // 提前扩容,避免多次扩容
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i];
}
return sub;
}
// 关系运算符实现
bool string::operator<(const string& s) const
{
return strcmp(_str, s._str) < 0;
}
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
{
return strcmp(_str, s._str) == 0;
}
bool string::operator!=(const string& s) const
{
return !(*this == s);
}
// ===================== 全局流函数实现 =====================
// 重载<< 输出:借助范围for+迭代器遍历
std::ostream& operator<<(std::ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
// 重载>> 输入:使用缓冲区buff[256],解决长字符串频繁扩容、无法读空格问题
std::istream& operator>>(std::istream& in, string& s)
{
s.clear();
char buff[256];
int i = 0;
char ch;
ch = in.get();
// 遇到空格/换行停止读取
while (ch != '\n' && ch != ' ')
{
buff[i++] = ch;
// 缓冲区存满,批量追加
if (i == 255)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
// 处理缓冲区剩余字符
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
// 自定义getline:读取整行,以delim为结束符
std::istream& getline(std::istream& in, string& s, char delim)
{
s.clear();
char buff[256];
int i = 0;
char ch;
ch = in.get();
while (ch != delim)
{
buff[i++] = ch;
if (i == 255)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
}
test.cpp测试文件
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include"string.h"
using namespace std;
namespace asuo
{
// 测试1:构造函数、[]重载、迭代器、范围for遍历
void test_string1()
{
// 测试默认构造(空字符串)
string s1;
cout << "空字符串:" << s1.c_str() << endl;
// 测试带参构造
string s2("hello world");
cout << "带参构造:" << s2.c_str() << endl;
// 测试尾插字符
s2.push_back('x');
cout << "push_back后:" << s2.c_str() << endl;
// 测试[]运算符(普通对象读写)
s2[0] = 'x';
for (size_t i = 0; i < s2.size(); i++)
{
s2[i]++;
}
cout << "[]修改后:" << s2.c_str() << endl;
// 测试const对象 + const []重载(只读)
const string s5("hello world");
cout << "const对象[]遍历:";
for (size_t i = 0; i < s5.size(); i++)
{
cout << s5[i] << '-';
}
cout << endl;
// 隐式构造、直接构造
string s3 = "hello world";
string s4 = ("hello world");
// 范围for遍历(底层依赖迭代器)
cout << "范围for遍历:";
for (auto ch : s4)
{
cout << ch << " ";
}
cout << endl;
// 普通迭代器遍历 + 修改
cout << "普通迭代器遍历修改:";
string::iterator it4 = s4.begin();
while (it4 != s4.end())
{
*it4 += 1;
cout << *it4 << " ";
++it4;
}
cout << endl;
// const迭代器遍历(只读)
cout << "const迭代器遍历:";
for (auto ch : s5)
{
cout << ch << " ";
}
cout << endl;
string::const_iterator it5 = s5.begin();
while (it5 != s5.end())
{
cout << *it5 << " ";
++it5;
}
cout << endl;
}
// 测试2:push_back、append、+= 追加接口
void test_string2()
{
string s1;
cout << "空串:" << s1.c_str() << endl;
string s2("hello world");
// 连续尾插字符
s2.push_back('x');
s2.push_back('x');
s2.push_back('x');
s2.push_back('x');
cout << "连续push_back:" << s2.c_str() << endl;
// 测试append追加字符串
string s3("hello");
s3.append("xxxxxxxxxxxxxx");
cout << "append长串:" << s3.c_str() << endl;
// 测试+= 字符 + 字符串
string s4("hello");
s4.append("xx");
s4.append("xx");
s4 += '*';
s4 += "hello world";
cout << "+= 混合追加:" << s4.c_str() << endl;
}
// 测试3:insert插入、erase删除接口
void test_string3()
{
string s1("hello world");
cout << "原串:" << s1.c_str() << endl;
// 中间插入单个字符
s1.insert(5, 'x');
cout << "中间插字符:" << s1.c_str() << endl;
// 头部插入单个字符
s1.insert(0, 'x');
cout << "头部插字符:" << s1.c_str() << endl;
string s2("hello world");
s2.insert(5, "xxx");
cout << "中间插字符串:" << s2.c_str() << endl;
s2.insert(0, "yyy");
cout << "头部插字符串:" << s2.c_str() << endl;
// 部分删除、截断删除
s2.erase(3, 3);
s2.erase(2);
cout << "erase删除后:" << s2.c_str() << endl;
}
// 测试5:拷贝构造、赋值重载(深拷贝核心测试)
void test_string5()
{
string s1("hello world");
// 拷贝构造
string s2(s1);
cout << "原串s1:" << s1.c_str() << endl;
cout << "拷贝s2:" << s2.c_str() << endl;
// 修改s1,验证深拷贝(s2不受影响)
s1[0] = 'x';
cout << "修改后s1:" << s1.c_str() << endl;
// 赋值重载
string s3("hello worldxxxxxxxx");
s1 = s3;
cout << "赋值后s1:" << s1.c_str() << endl;
cout << "源串s3:" << s3.c_str() << endl;
}
// 浅拷贝/深拷贝专项测试
void test1()
{
string s1("hello world");
string s2(s1);
cout << "深拷贝测试 s1:" << s1.c_str() << endl;
cout << "深拷贝测试 s2:" << s2.c_str() << endl;
}
// 测试:字符串内嵌\0(验证memcpy替代strcpy的效果)
void test2()
{
string s1("hello world");
s1 += 'x';
s1 += '\0'; // 主动添加内嵌'\0'
s1 += "uuu";
// << 遍历有效字符(识别内嵌\0)
cout << "重载<<输出(识别内嵌\\0):" << s1 << endl;
// c_str C风格输出(遇到第一个\0终止)
cout << "c_str输出(C风格截断):" << s1.c_str() << endl;
// 拷贝构造验证内嵌\0正常拷贝
string s2(s1);
cout << "拷贝后输出:" << s1 << endl;
}
// 测试:resize 扩容/缩容功能
void test3()
{
string s1;
// 扩容+填充字符
s1.resize(11, '*');
cout << "resize扩容填充:" << s1 << endl;
// 缩容截断
s1.resize(10);
cout << "resize缩容:" << s1 << endl;
// 再次扩容
s1.resize(20, '#');
cout << "再次resize扩容:" << s1 << endl;
}
// 测试:find查找、substr截取(模拟URL解析场景)
void test4()
{
string url = "https://legacy.cplusplus.com/reference/string/string/rfind/";
// 查找冒号
size_t i1 = url.find(':');
if (i1 != string::npos)
{
string protocol = url.substr(0, i1);
cout << "协议名:" << protocol << endl;
// 查找斜杠
size_t i2 = url.find('/', i1 + 3);
if (i2 != string::npos)
{
string domain = url.substr(i1 + 3, i2 - (i1 + 3));
cout << "域名:" << domain << endl;
string uri = url.substr(i2 + 1);
cout << "路径:" << uri << endl;
}
}
}
// 测试:>> 流输入、getline整行读取
void test5()
{
string s1, s2("xxxxxx");
// >> 读取(自动分割空格)
cin >> s1 >> s2;
cout << ">>读取s1:" << s1 << endl;
cout << ">>读取s2:" << s2 << endl;
// getline读取整行(包含空格)
getline(cin, s1);
cout << "getline读取整行:" << s1 << endl;
}
// 测试:swap交换接口
void test6()
{
string s3("hello world"), s4("xxxxxxx");
// 成员swap
s3.swap(s4);
// 全局特化swap
swap(s3, s4);
cout << "swap交换完成" << endl;
}
}
int main()
{
// 异常捕获,拦截内存/断言错误
try
{
// 根据需要开启对应测试函数
//asuo::test_string1();
//asuo::test_string2();
//asuo::test_string3();
//asuo::test_string5();
//asuo::test1();
//asuo::test2();
//asuo::test3();
//asuo::test4();
//asuo::test5();
asuo::test6();
}
catch (const exception& e)
{
cout << "异常:" << e.what() << endl;
}
return 0;
}
三、模拟实现可能遇见的问题
问题 1:构造函数多次调用 strlen,效率低下
错误版本代码
// 问题代码:原始带参构造
string(const char* str)
:_str(new char[strlen(str)+1]) // 第1次 strlen
, _size(strlen(str)) // 第2次 strlen
, _capacity(strlen(str)) // 第3次 strlen
{
strcpy(_str, str);
}
问题分析
- 同一字符串连续调用 3 次
strlen,重复遍历字符数组,效率低; - 依赖成员变量定义顺序:如果后续调整
_size/_capacity定义顺序,代码会出错,可维护性差。
解决思路
只在初始化列表执行一次 strlen 给 _size 赋值,堆内存开辟、_capacity 放到函数体内部。
// 修复后:合一构造函数(默认构造 + 带参构造)
string(const char* str = "")
: _size(strlen(str)) // 仅调用 1 次 strlen
{
_str = new char[_size + 1];
_capacity = _size;
strcpy(_str, str); // 后续又替换为 memcpy,见问题3
}
问题 2:默认浅拷贝 → 堆内存重复释放崩溃(拷贝构造)
问题根源:编译器默认拷贝构造
编译器默认生成的拷贝构造是值拷贝(浅拷贝),只复制指针地址,不新开堆内存:
// 编译器默认生成的浅拷贝构造(伪代码,隐患代码)
string::string(const string& s)
{
// 只拷贝指针地址,两个对象 _str 指向**同一块堆内存**
_str = s._str;
_size = s._size;
_capacity = s._capacity;
}
导致的问题
- 多个
string对象共享同一块堆空间; - 析构时同一堆内存被多次
delete[],程序直接崩溃。
解决思路:手动实现深拷贝
为新对象单独开辟独立堆内存,再拷贝数据,两个对象内存完全隔离。
// 深拷贝构造函数(最终版)
string::string(const string& s)
{
// 1. 新对象单独开辟堆空间,和原对象内存分离
_str = new char[s._capacity + 1];
// 2. 拷贝数据(后续替换 memcpy,见问题3)
memcpy(_str, s._str, s._size+1);
_size = s._size;
_capacity = s._capacity;
}
问题 3:strcpy 无法拷贝字符串内嵌 \0
问题代码(所有使用 strcpy )
// 问题代码:strcpy 版本,遇到 \0 立刻停止拷贝
strcpy(_str, str);
strcpy(tmp, s._str);
strcpy(_str + pos, _str + pos + len);
问题分析
strcpy拷贝规则:读到\0就终止;- 如果字符串中间主动存入
\0(如s += '\0'),strcpy只会拷贝\0之前的内容,数据丢失。
解决思路
使用 memcpy:按指定字节数完整拷贝 ,不识别 \0,可以完整拷贝含内嵌空字符的字符串。
// 旧:strcpy(_str, str);
// 新:指定拷贝 _size+1 个字节(包含末尾 \0)
memcpy(_str, str, _size + 1);
拷贝构造、赋值、erase、append 等所有字符串拷贝位置,全部替换为 memcpy。
问题 4:赋值运算符重载 - 自赋值问题
问题代码(无自赋值判断)
// 问题代码:没有判断 this == &s,自赋值会崩溃
string& string::operator=(const string& s)
{
// 先释放旧空间
delete[] _str;
// 此时 _str 已经野指针,再取 s._str 逻辑错乱
char* tmp = new char[s._capacity + 1];
memcpy(tmp, s._str, s._size + 1);
_str = tmp;
_size = s._size;
_capacity = s._capacity;
return *this;
}
问题分析
当执行 s1 = s1;(对象自赋值):
- 先
delete[] _str释放自身内存; - 后续读取
s._str时,内存已被释放,变成野指针,内存访问错误。
解决思路
增加判断:if (this != &s),自己赋值给自己直接返回,不执行内存操作。
string & string::operator=(const string & s)
{
// 核心修复:判断是否自赋值
if(this!=&s)
{
char* tmp = new char[s._capacity + 1];
memcpy(tmp, s._str, s._size + 1);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
问题 5:size_t 无符号类型倒序遍历,越界死循环(insert 函数)
问题代码
// 问题代码:使用 size_t + >= 倒序遍历
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
// size_t 是【无符号整型】
size_t end = _size;
// 致命问题:end 是无符号,end == 0 时 --end 不会变成 -1,而是变成极大正数
while (end >= pos)
{
_str[end + 1] = _str[end];
--end; // 无符号下溢 → 死循环
}
_str[pos] = ch;
_size++;
}
问题分析
size_t= 无符号整数 ,取值范围[0, 极大值],永远不会小于 0;- 当
end = 0执行--end,会发生无符号下溢,变成一个超级大的正数; end >= pos条件永久成立,程序进入死循环。
两种解决方案
方案 1(临时转 int)
把 end 定义为 int,规避无符号下溢:
int end = _size;
while (end >= (int)pos)
{
_str[end + 1] = _str[end];
--end;
}
方案 2(修改遍历边界,纯 size_t 安全写法)
不从 _size 开始倒推,修改起始位置和循环条件 end > pos,全程无下溢:
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
// 修复:起始位置改为 _size+1,循环条件 end > pos,无下溢风险
size_t end = _size+1;
while (end > pos)
{
_str[end] = _str[end-1];
--end;
}
_str[pos] = ch;
_size++;
}
插入字符串版本
insert(pos, const char*)同理,也是用这套边界逻辑修复。
问题 6:空对象(capacity=0)二倍扩容失效
问题代码(单纯二倍扩容)
// 问题代码:空对象 _capacity = 0,0 * 2 依旧 = 0,永远无法扩容
if (_size == _capacity)
{
reserve(_capacity * 2);
}
问题分析
当 string 是空对象(默认构造): _capacity = 0 → 0 * 2 = 0 → 调用 reserve(0) 不会扩容,push_back 永远无法添加字符。
解决思路
三目运算判断:
-
如果
_capacity == 0(空对象),直接默认扩容到 4; -
非空对象,正常二倍扩容。
if (_size == _capacity)
{
// 修复:空对象扩为4,否则二倍扩容
reserve(_capacity==0?4:_capacity * 2);
}
问题 7:全局通用 swap 导致多次深拷贝,效率低
问题代码:直接使用库模板 swap
// 通用模板 swap 源码
template<class T>
void swap(T& a, T& b)
{
T c(a); // 调用拷贝构造(深拷贝)
a = b; // 调用赋值重载(深拷贝)
b = c; // 调用赋值重载(深拷贝)
}
问题分析
对 asuo::string 使用通用 swap,会触发 3 次深拷贝,堆内存频繁申请 / 释放,效率极差。
解决思路
- 实现成员函数 swap:直接交换三个基础成员变量(指针、两个数值),无堆拷贝;
- 重载全局
swap,优先调用成员swap。
第一步:实现高效成员 swap
// 成员swap:只交换三个成员变量,无深拷贝
void string::swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
第二步:重载全局 swap(特化)
// 针对 string 特化,优先调用高效成员swap
inline void swap(string& a, string& b)
{
a.swap(b);
}
问题 8:流运算符 >> 逐字符读取,频繁扩容 + 长串性能差
问题代码(原始逐字符 push_back)
// 问题代码:每读一个字符就 push_back,频繁触发扩容
std::istream& operator>>(std::istream& in, string& s)
{
s.clear();
char ch;
while (in.get(ch) && ch != ' ' && ch != '\n')
{
s.push_back(ch); // 逐字符追加,扩容次数极多
}
return in;
}
问题分析
- 长字符串会触发几十次扩容,性能低;
- 提前
reserve(1024)又会造成短字符串内存浪费。
解决思路
引入固定缓冲区 char buff[256]:
-
字符先存入缓冲区,攒满 255 个再批量
+=; -
循环结束后处理缓冲区剩余字符;
-
大幅减少扩容次数,兼顾长短串性能。
std::istream& operator>>(std::istream& in, string& s)
{
s.clear();
char buff[256]; // 缓冲区,批量缓存字符
int i = 0;
char ch;
ch = in.get();
while (ch != '\n'&&ch!=' ')
{
buff[i++] = ch;
if (i == 255) // 缓冲区存满,批量追加
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0) // 处理剩余字符
{
buff[i] = '\0';
s += buff;
}
return in;
}
getline函数使用了完全相同的缓冲区方案,修复逻辑一致。