目录
string类的模拟实现
经典的string类问题
浅拷贝
深拷贝
写时拷贝(了解)
构造函数
string的全缺省的构造函数:
string的拷贝构造函数
传统写法
现代写法
string的赋值重载函数
传统写法
现代写法
string的无参构造函数:
遍历函数
operator[ ]
迭代器
迭代器的底层实现begin和end:
范围for的使用
修改字符串相关函数:
reserve
push_back
append
operator+=
insert(字符的版本)
insert(字符串常量)
erase
resize
find(查找单个字符)
find(查找子串)
substr
比较运算符的重载:
流插入cout
流提取cin
优化写法
clear
前言:
?个人博客:Dream_Chaser
?博客专栏:C++
?本篇内容:string类通用函数的模拟实现
string类的模拟实现
经典的string类问题
上面已经对string类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。大家看下以下string类的实现是否有问题?
浅拷贝
说明:上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构 造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块 空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共 享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为 还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
写时拷贝(了解)
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。
构造函数
string的全缺省的构造函数:
我们对string成员函数进行模拟实现的时候,首先要定义一个自定义的命名空间,避免与库里面的冲突:
让我们来看看错误的案例:
①注意常量字符串的用法,权限不能放大:
②尝试给_str声明加上const,这样就会导致初始化的时候出现随机值:
③需要注意的是"string.h"要定义在std的下方:
那么带参的构造可以怎么写呢?初始化列表的顺序应该对应着成员变量声明的顺序
string带参的构造是否可以再优化一下呢?
注意new的时候的+1是为了给'\0'预留的空间:
代码实现:
此构造函数接受一个可选参数(默认为空字符串),用于初始化一个字符串对象,根据传入的C风格字符串(const char* 类型)计算其长度,并据此分配内存空间,最后,将传入的字符串内容复制到新分配的内存区域,以便在C++字符串类中进行管理。
//构造函数,用于初始化一个字符串类对象string(const char* str = ""):_size(strlen(str))// 初始化_size成员变量,存储传入C风格字符串(char数组)str的长度, _capacity(_size)// 初始化_capacity成员变量,初始容量与传入字符串长度相同{_str = new char[_capacity + 1];// 动态分配内存,为字符串对象分配一个新的字符数组strcpy(_str, str); // 使用strcpy将传入的字符串str复制到新分配的字符数组中}
string的拷贝构造函数
传统写法
//传统写法 -- 拷贝构造//s2(s1)string(const string& s){_str = new char[s._capacity + 1];//仅仅开空间strcpy(_str, s._str);//_size = s._size;_capacity = s._capacity;}
现代写法
// 定义交换函数,将当前字符串对象与输入参数s的内部数据(字符数组、长度及容量)进行交换void swap(string& s) { // 使用STL中的std::swap交换三个关键成员变量 std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_size, s._capacity);}// 构造函数,复制输入参数s的字符串内容// 使用临时字符串对象tmp存储s的内容,然后通过调用swap函数交换数据string(const string& s) : _str(nullptr), _size(0), _capacity(0){ string tmp(s._str); // 创建临时字符串tmp,拷贝s的内容 swap(tmp); // 交换当前对象与tmp的数据,实现深拷贝}
string的赋值重载函数
传统写法
//传统写法--赋值重载// 使得可以使用 "s2 = s3" 的形式,将 s3 字符串对象的值赋给 s2string& operator=(const string& s){ // 检查是否为自我赋值,即检查是否同一个对象 if (this != &s) { // 创建一个临时字符数组 tmp,大小为 s 的容量加一(包含结束符 '\0') char* tmp = new char[s._capacity + 1]; // 使用 strcpy 函数将 s 的内部字符串复制到临时数组 tmp 中 strcpy(tmp, s._str); // 删除当前对象已有的内部字符串数组 delete[] _str; // 将临时数组 tmp 的地址赋给当前对象的内部字符串指针 _str _str = tmp; // 将源字符串对象 s 的大小赋给当前对象的大小属性 _size _size = s._size; // 同样将源字符串对象 s 的容量赋给当前对象的容量属性 _capacity _capacity = s._capacity; } // 返回当前对象的引用,以支持连续赋值如 "s1 = s2 = s3" return *this;}
现代写法
// 重载赋值运算符,实现字符串对象之间的深拷贝赋值操作string& operator=(const string& s){ // 检查是否自我赋值(即源对象与目标对象相同),避免不必要的资源释放与重新分配 if (this != &s) { // 创建临时字符串对象tmp,并使用s的内容初始化 string tmp(s); // 调用自定义的swap函数,将当前对象的数据与临时对象tmp的数据进行交换 // 这样可以确保原对象的资源被正确释放,并且新内容被赋值给当前对象 swap(tmp); // 注:这里原本可能是 "this->swap(tmp);",但因为 "swap" 是成员函数, // 在成员函数内部可以直接调用,无需 "this->"。 } // 返回当前对象的引用,以便支持连续赋值操作 return *this;}
现代写法优化
//现代写法优化string& operator=(string tmp){swap(tmp);return *this;}
string的无参构造函数:
经过尝试之后,可以发现以下问题:
无参的构造应该这样写:
代码实现: 建议只写一个全缺省的构造函数即可,因为默认构造函数只能存在一个。
String():_str(new char[1]{'\0'})//数组的初始化方式,_size(0),_capacity(0){}
遍历函数
c_str函数
简要说明
const char* c_str()const{return _str;}
operator[ ]
这里要演示的话就得遍历这个字符串,需要重载一下operator[ ]:
void test_string1(){string s1("hello world");cout << s1.c_str() << endl;string s2;cout << s2.c_str() << endl;for (size_t i = 0; i < s1.size(); i++){cout << s1[i] << " ";}cout << endl;}
需要区分只能读和可读可写的版本:
//可读可写char& operator[](size_t pos){assert(pos < _size);return _str[pos];}//只能读const char& operator[](size_t pos)const{assert(pos < _size);return _str[pos];}
代码执行:
既然说到遍历数组的话,那就少不了迭代器。
迭代器
迭代器声明:
//声明typedef char* iterator;//可读可写typedef const char* const_iterator;//只可读
迭代器的底层实现begin和end:
begin:函数的作用就是返回字符串中第一个字符的地址
//实现:iterator begin(){return _str;}const_iterator begin()const{return _str;}
end函数的作用就是返回字符串中最后一个字符的后一个字符的地址(即’\0’的地址)
//实现:iterator end(){return _str + _size;//指向'\0'}const_iterator end()const{return _str + _size;//指向'\0'}
迭代器外层调用:
范围for的使用
同时为了方便理解,把范围for的实现列举出来:
当咱们将begin改为Begin时,发现会出错,还有一个点:范围for的本质是迭代器
迭代器与范围for的应用:
怎么能够让改变后的字符串,不再变回原来的字符串呢?使用引用即可:
实例代码:
void test_string1(){string s1("hello world");cout << s1.c_str() << endl;string s2;cout << s2.c_str() << endl;/*for (size_t i = 0; i < s1.size(); i++){cout << s1[i] << " ";}cout << endl;*/string::iterator it = s1.begin();while (it != s1.end()){(*it)++;cout << *it << " ";++it;}cout << endl;for (auto& ch : s1)//使用引用{ch++;//让字符串里每个字符的ascll码值+1cout << ch << " ";}cout << endl;cout << s1.c_str() << endl;}
修改字符串相关函数:
reserve
其主要作用是预先为容器分配足够的未使用容量,以应对未来可能的元素添加,而不立即改变容器的大小(元素数量)。使用 reserve()
可以有策略地控制容器的内存分配,提高连续添加元素时的效率
void reserve(size_t n){// 检查请求的容量n是否大于当前内部缓冲区的实际容量(_capacity)if (n > _capacity){ // 如果是,则需要重新分配更大的内存空间。新分配的内存大小为n+1,+1用于存储结尾的空字符'\0 char* tmp = new char[n + 1];//+1存“\0' strcpy(tmp,_str);//将当前字符串内容(包括结尾的'\0')复制到新分配的tmp缓冲区中 delete[] _str;// 释放原有的内部缓冲区(_str),以避免内存泄漏 _str = tmp;// 更新内部缓冲区指针,指向新分配的tmp缓冲区 _capacity = n;// 更新内部记录的缓冲区容量为请求的值n }}
push_back
push_back()
是序列容器(如 std::vector
, std::list
, std::deque
等)的成员函数,用于在容器末尾添加新元素。它自动处理内存管理,确保新元素顺利加入,且保持原有元素顺序不变。对于动态扩容容器(如 std::vector
),在必要时会自动增大容量。
尝试:可以写成这样吗?不行,当_capacity = 0 时,空间还没分配到,此时如果访问就属于越界访问。
代码实现:
void push_back(char ch){// 检查当前内部字符串长度(_size)是否已达到内部缓冲区容量(_capacity)if (_size == _capacity){ // 若已满,调用reserve函数预分配新的内存,扩大容量。// 首次扩容时容量设为4,后续扩容按当前容量翻倍。reserve(_capacity == 0 ? 4 : 2 * _capacity);}// 将待添加字符ch 存储在 内部字符串缓冲区的当前长度索引处(_size) _str[_size] = ch; ++_size;// 更新内部字符串长度(_size),增1以包含新添加的字符 _str[_size] = '\0';// 在新添加字符之后添加空字符('\0'),确保字符串正确终止}
append
对于std::string:
append()用于在字符串末尾追加其他字符串、字符数组或单个字符,可以指定追加的起始位置和长度。
void append(const char* str){int len = strlen(str);// 获取输入字符串str长度// 检查是否需扩大内部缓冲区容量以容纳追加的strif (_size + len > _capacity){reserve(_size + len);}//这个地方要是忘记 +_size,从头到尾覆盖新字符串strcpy(_str+_size,str);// 将str追加到内部字符串缓冲区末尾_size += len;// 更新内部字符串长度}
push_back 和 append 的区别在于一个追加字符,另一个追加字符串
示例:
operator+=
+=运算符的重载:分别复用了 push_back 和 append, 实现字符串与字符,字符串与字符串之间能够直接使用+=运算符进行尾插。
字符串追加字符:
string& operator+=(char ch){push_back(ch);return *this;}
字符串追加字符串:
string& operator+=(const char* str){append(str);return *this;}
示例运行:
void test_string2 (){string s1("hello world");s1 += '#';s1 += "css";cout << s1.c_str() << endl;string s2;s2 += '#';s2 += "csgo";cout << s2.c_str() << endl;}
insert(字符的版本)
尝试写一下insert:
测试一下代码的可行性:
可以发现头插会出错:
头插:
那就换成有符号,此时pos为0,end为-1,此时end<pos,但是依然进入了循环,明显的不对吧,这里是发生了类型转换,end变成无符号整型了。
怎么解决呢:以下两种写法都是正确的:
此时函数的功能:
在动态字符数组 _str
的指定位置 pos
插入一个字符 ch
。首先,它验证插入位置的有效性(必须小于等于当前数组大小_size
)。
若当前数组容量已满,函数会自动扩容,初始容量为4或者当前容量的两倍。
然后,函数通过循环将 pos
位置之后的所有元素向右移动一位,为新字符腾出空间。
最后,函数在指定位置pos
插入字符ch
,并将数组大小_size
加一,表示数组元素数量增加。
//上图的版本二void 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++;}
insert(字符串常量)
该函数的作用:
用于在动态字符数组(或类似字符串结构)的指定位置pos
插入一个C风格字符串str
。首先,它通过断言确保插入位置的有效性。
接着计算待插入字符串的长度,并检查现有容量是否足够容纳插入后的完整字符串,不足则调用reserve
进行扩容。
然后,函数将插入位置之后的所有字符向右移动适当距离,为新字符串腾出空间。最后,使用strncpy
函数将待插入字符串复制到目标位置,并更新整个字符串的大小,反映出新增字符的影响。
首先要注意:
所以需要强转:
// pos:插入位置,从0开始计数// str:要插入的C风格字符串void insert(size_t pos, const char* str){ // 断言检查,确保插入位置pos不大于当前字符串的大小 assert(pos <= _size); // 获取要插入字符串str的长度,即需要移动的字符数量 size_t len = strlen(str); // 检查插入后字符串总长度是否超过当前容量,如果超过则进行扩容 if (_size + len > _capacity) { reserve(_size + len); // 调用reserve函数来增加内部缓冲区的容量 } // 数据挪动部分: // 将插入位置pos之后的所有字符向右移动len个位置 int end = static_cast<int>(_size); // 使用int类型方便进行减操作,注意这里假设_size不会超出int表示范围 while (end >= static_cast<int>(pos)) { _str[end + len] = _str[end]; // 将原字符串中下标为end的字符移动到end + len的位置 --end; // 移动指针至下一个待移动的字符 } // 在指定位置pos处插入字符串str strncpy(_str + pos, str, len); // 使用 strncpy 函数将str复制到目标字符串相应位置 // 更新字符串的大小 _size += len; // 插入操作完成后,字符串的新长度应增加len}
erase
作用是从动态字符数组(或类字符串结构)的特定位置pos
开始删除指定长度len
的子串。
首先,它通过断言确保删除起始位置的有效性(小于当前字符串长度_size
)。根据传入的参数,函数会判断是否需要删除从pos
到结尾的所有字符,
如果是,则将pos
位置之后的字符置为结束符\0
,并将字符串大小更新为pos
;否则,将从pos+len
开始到结尾的字符依次向前移动len
个位置以覆盖待删除区域,并相应减少字符串大小_size
。
简而言之,该函数实现了对字符串的指定范围擦除操作。
// 函数:删除指定位置起始的子串,若未指定长度,默认删除到字符串末尾void erase(size_t pos, size_t len = npos){ // 检查删除起始位置是否合法(必须在当前字符串内) assert(pos < _size); // 如果未指定长度或指定长度使删除范围超出字符串末尾,则删除从pos到字符串末尾的部分 if (len == npos || pos+len >= _size) { // 截断字符串,将pos位置置空字符,字符串长度变为pos _str[pos] = '\0'; _size = pos; } // 否则,按指定长度删除子串 else { // 计算删除范围结束后的新起始位置 size_t begin = pos + len; // 将删除范围后的字符逐个向前移动len位,覆盖待删除部分 while (start <= _size) { _str[begin - len] = _str[begin]; ++begin; } // 更新字符串长度,减少len _size -= len; }}
resize
作用: 改变动态字符数组(或类字符串结构)的大小,并可选地用指定字符ch
填充新添加的空间。
若新指定长度n
小于等于当前长度,函数将截断字符串,使其长度变为n
;
若新长度大于当前长度且可能超过当前容量时,函数先调用reserve
进行扩容,然后使用给定字符ch
填充新增的字符位置,直到字符串长度达到指定的n
,并在字符串末尾添加结束符\0
,以确保字符串的完整性和正确性。
总之,此函数灵活地调整了字符串的长度,并保持其内容的有效性。
void resize(size_t n, char ch = '\0'){ // 当新长度n小于等于当前字符串长度时,执行删除操作 if (n <= _size) { // 将第n个字符设置为空字符,相当于截断字符串,并更新字符串的实际长度为n _str[n] = '\0'; _size = n; } // 当新长度n大于当前字符串长度但小于等于当前容量时,或新长度n大于当前容量时 else { // 先调用reserve函数确保容量足够容纳新长度的字符串 reserve(n); // 循环填充字符,直至字符串长度达到n while (_size < n) { _str[_size] = ch; // 使用指定字符ch填充 ++_size; } // 最后,在新字符串末尾添加结束符'\0' _str[_size] = '\0'; }}
find(查找单个字符)
// 定义一个成员方法,用于在当前字符串对象中查找指定字符 ch 的首次出现位置//ch -- 要查找的字符// 查找的起始位置,默认从字符串开头(索引0)开始size_t find(char ch, size_t pos = 0){ // 使用一个循环遍历从 pos 位置开始到字符串结尾的所有字符 for (size_t i = pos; i < _size; i++) { // 检查当前遍历到的字符是否与要查找的字符 ch 相同 if (_str[i] == ch) { // 如果相同,则返回该字符在字符串中的索引位置 return i; } } // 若遍历完整个指定区间后仍未找到字符 ch,则返回 npos // npos 是一个特殊的值,通常表示“未找到”或“无效位置” return npos;}
find(查找子串)
size_t find(const char* sub, size_t pos = 0){ // 使用C语言库函数strstr查找子串sub在当前字符串(从pos开始的部分)中首次出现的位置 const char* p = strstr(_str + pos, sub); // 如果找到了子串sub,则返回子串首字符在当前字符串中的相对索引(即下标) if (p) { return p - _str; } // 若找不到子串,则返回特殊值npos,表示子串不在当前字符串中 else { return npos; }}
substr
// 定义一个方法,用于从原始字符串中提取子串string substr(size_t pos, size_t len /* 默认为最大长度 */) { // 创建一个新字符串 s 来保存子串 string s; // 计算子串的结束位置(默认取到原始字符串结尾) size_t end = pos + len; if (len == npos || pos + len >= _size) { // 若指定长度超过原始字符串剩余长度,则取剩余全部字符 len = _size - pos; end = _size; } // 为新字符串预分配足够内存以存放子串 s.reserve(len); // 循环遍历原始字符串,从 pos 位置开始,复制到新字符串 s 中,直到结束位置 end for (size_t i = pos; i < end; i++) { s += _str[i]; } // 返回包含子串的新字符串 s return s;}
比较运算符的重载:
bool operator<(const string& s)const{return strcmp(_str,s._str);}bool operator==(const string& s)const{return strcmp(_str, s._str)==0;}//<= 复用<bool operator<=(const string& s)const{return *this < s || *this == s;}//>复用 !(<=)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);}
流插入cout
这里的范围for可以访问吗?不可以:这里要用const迭代器,因为是一个被const修饰的对象,const迭代器指针本身是可以++(修改)的,指针指向的内容不可以被修改:
所以要这样做:
代码:
class string{public:typedef char* iterator;typedef const char* const_iterator;//const迭代器}ostream& operator<<(ostream& out, const string& s){/*for (size_t i = 0; i < s.size(); i++){ out << s[i];}*/ for (auto ch : s) out << ch;return out;}
流提取cin
需要注意的地方:
那么应该这样去写:
代码:
istream& operator>>(istream& in, string& s){s.clear();//清理原来的字符数据,不然就变成尾插了char ch;//in >> ch;//拿不到 空格 或者 换行ch = in.get();//一个字符一个字符的拿while (ch != ' ' && ch != '\n'){s += ch;//in >> ch;ch = in.get();}return in;}
优化写法
// 重载输入流提取运算符 >>,使得可以从输入流(如cin)中读取一串连续的非空格和非换行符字符到string对象s中istream& operator>>(istream& in, string& s){// 清空目标string对象s,准备接收新的输入s.clear();// 定义一个固定大小的缓冲区buff,用于暂存从输入流中读取的字符 char buff[129]; size_t i = 0; // 初始化缓冲区索引为0// 从输入流in中获取第一个字符 char ch; ch = in.get();// 当读取到的字符不是空格也不是换行符时,继续循环while (ch != ' ' && ch != '\n'){ // 将字符放入缓冲区 buff[i++] = ch; // 如果缓冲区已满(达到128个字符) if (i == 128) { // 在缓冲区末尾添加结束符'\0',将其视为一个C风格字符串添加到目标string对象s中 buff[i] = '\0'; s += buff; // 更新s的内容 // 重置缓冲区索引为0,以便继续填充下一个子字符串 i = 0; } // 继续从输入流中获取下一个字符 ch = in.get();}// 若缓冲区中仍有剩余字符(循环结束后)if (i != 0){ // 添加结束符'\0',将剩余的子字符串添加到目标string对象s中 buff[i] = '\0'; s += buff;}// 返回输入流in的引用,以支持链式输入操作 return in;}
clear
不清理之前的数据就变成尾插了:
void clear(){_str[0] = '\0';_size = 0;}
本篇结束。
?本文修改次数:0
?更新时间:2024年4 月 24 日