【C++初阶】第八站:string类的模拟实现
Dream_Chaser~ 2024-06-28 08:35:02 阅读 51
目录
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 字符串对象的值赋给 s2
string& 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:
//实现:
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码值+1
cout << 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长度
// 检查是否需扩大内部缓冲区容量以容纳追加的str
if (_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 日
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。