『C++成长记』string模拟实现
小王又困了 2024-07-11 10:05:04 阅读 66
🔥博客主页:小王又困了
📚系列专栏:C++
🌟人之为学,不日近则日退
❤️感谢大家点赞👍收藏⭐评论✍️
目录
一、存储结构
二、默认成员函数
📒2.1构造函数
📒2.2析构函数
📒2.3拷贝构造
📒2.4赋值重载
三、容量操作
📒3.1获取有效字符长度
📒3.2获取对象空间大小
📒3.3使用reserve扩容
四、字符串的遍历
📒4.1下标访问
📒4.2迭代器访问
五、修改操作
📒5.1尾插字符
📒5.2尾插字符串
📒5.3任意位置插入字符
📒5.4任意位置插入字符串
📒5.5+=重载
六、其他操作
📒6.1删除操作
📒6.2查找操作
📒6.3交换操作
📒6.4获取字符串
📒6.5运算符重载
📒6.6清理字符串
📒6.7流操作
🗒️前言:
在上一篇中我们对string类进行了简单的介绍,介绍了各个接口的作用和使用方法,今天我们将为大家介绍string常用接口的模拟实现。
一、存储结构
string本质上是一个char类型的顺序表,所以结构上和顺序表类似。
<code>namespace bit
{
class string
{
public:
private:
char* _str;
size_t _size;
size_t _capacity;
const static size_t npos;
};
}
结构上使用命名空间 bit 进行封装,防止与库冲突,其中:
_str :指向存放字符串存空间的指针_size :表示当前所存储的有效字符个数_capacity :表示当前可存储的最大容量nops:此值设置为 -1,无符号整型转换就是42亿,且此值为const和静态参数具有全局效应,这个值常常被用来当作循环结束判断条件和查找时未找到的标志,某些函数设置其为缺省参数。
nops的初始化:
#include"string.h"
namespace bit
{
const size_t string::nops = -1;
}
小Tips:我们使用声明与定义分离实现,nops只能在CPP文件中定义,因为类里面的静态成员变量相当于全局变量,在.h文件中定义会出现链接错误。我们还要通过类名::成员(函数/变量) 定义和实现函数!
二、默认成员函数
📒2.1构造函数
<code>string.h
string(const char* str = ""); //给缺省值 构造空串
string.cpp
string::string(const char* str)
:_size(strlen(str))
{
_str = new char[_size + 1];
_capacity = _size;
strcpy(_str, str);
}
构造函数的思路:
构造函数可以接收字符串,如果没有参数,默认构造一个空串通过strlen先计算出字符串的长度,并通过初始化列表初始化_size使用new开辟空间,这里我们要多开一个空间存放‘\0’最终将字符串中的字符拷贝到我们所开的空间中
小Tips:因为_size的大小没有包含‘\0’,所以我们要多开辟一个空间。
📒2.2析构函数
我们开辟内存是使用 new[ ] 申请的,所以对应使用 delete[ ]释放。
string::~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
📒2.3拷贝构造
拷贝构造函数,如果我们不写,编译器会默认生成一个,但是默认生成的拷贝构造函数只支持浅拷贝,新构造的对象只是拷贝了_str的指针地址,两个对象都指向同一块空间,最终两个对象析构时释放同一片空间的资源势必会导致程序崩溃!
我们需要新构造的对象通过拷贝构造开辟一片新的空间将数据复制过去,也就是深拷贝,需要我们自己写一个拷贝构造。
🌟传统写法:
<code>string::string(const string& s)
{
_str = new char[s._size + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
🌟现代写法:
string::string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
//string s2(s1);
通过复用构造函数,构造出tmp对象,在将两个对象进行交换,就可以实现拷贝构造。
📒2.4赋值重载
赋值重载需要注意自己给自己赋值这种冗余的行为,同时也要控制空间大小
🌟传统写法:
string& string::operator=(const string& s)
{
if(this != &s)
{
char* tmp = new char[s._size + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
🌟现代写法:
string& string::operator=(const string& s)
{
if (this != &s)
{
string tmp(s._str);
swap(tmp);
}
return *this;
}
string& string::operator=(string tmp)
{
swap(tmp);
return *this;
}
三、容量操作
📒3.1获取有效字符长度
size_t string::size() const
{
return _size;
}
小Tips:
这个函数比较小,可以写在类中形成内联函数。 对于不涉及对字符串的增删查改的函数,使用const修饰this增强安全性。
📒3.2获取对象空间大小
与size函数规则一致。
size_t string::capacity() const
{
return _capacity;
}
📒3.3使用reserve扩容
void string::reserve(size_t n)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str); //将原字符串的数据拷贝到新空间上
delete[] _str; //释放原字符串的空间
_str = tmp;
_capacity = n;
}
四、字符串的遍历
📒4.1下标访问
下标访问是通过重载 [ ] 运算符实现的,在下标pos正确的情况下,返回当前下标字符的引用,否则assert报错。
char& string::operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
📒4.2迭代器访问
现在我们可以简单认为迭代器是指针对象,就是对指针的封装。
//迭代器的声明
typedef char* iterator;
typedef const char* const_iterator; //对数据无法修改
迭代器的begin返回字符串的地址,end返回字符串末端的下一个即‘\0’。
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;
}
五、修改操作
📒5.1尾插字符
在插入字符前,先要判断是否需要扩容。
void string::push_back(char ch)
{
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4: _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
_str[_size + 1] = '\0';
_size++;
}
📒5.2尾插字符串
void string::append(char* s)
{
size_t len = strlen(s);
if (_size +len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, s);
_size += len;
}
📒5.3任意位置插入字符
我们要考虑头插的位置,end和pos的类型都是size_t,代码会陷入死循环,这里我们提供两种解决方法。
🌟方法一:
将end的类型定为int,同时将pos强转为int型。
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
int end = _size;
while (end >= (int)pos)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
++_size;
}
🌟方法二:
将end定位到‘\0’的下一位。
<code>void 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;
}
我们可以通过复用来实现尾插
<code>void string::push_back(char ch)
{
insert(_size, ch);
}
📒5.4任意位置插入字符串
void string::insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
//方法一:
//int end = _size;
//while (end >= (int)pos )
//{
// _str[end + len] = _str[end];
// --end;
//}
//方法二:
size_t end = _size+len;
while (end > pos+len-1)
{
_str[end] = _str[end - len];
--end;
}
memcpy(_str + pos, s, len);
_size += len;
}
我们也可以通过复用来实现尾插字符串
void string::append(const char* str)
{
insert(_size, str);
}
📒5.5+=重载
+=运算符可以在当前字符串尾部追加字符或字符串,我们可以通过复用push_back和append函数来实现。
//追加字符
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
//追加字符串
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
六、其他操作
📒6.1删除操作
erase从pos下标开始删除len个字符,其中len是一个缺省参数,默认是npos。如果没有传值或len超过从pos位置开始到字符串尾部的字符个数则默认从pos位置开始删除后面的所有字符,且不允许在空串的情况下进行删除!
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
📒6.2查找操作
查找函数是find,是从pos位置开始查找,可以查找一个字符或一个子串,查找到后字符返回下标,字符串返回首字符的地址,如果有多个重复的字符或字符串,返回查找到的第一个字符的下标或字符串首的下标;如果没找到则返回npos。
🌟查找一个字符:
size_t string::find(char ch, size_t pos)
{
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
return i;
}
return npos;
}
🌟查找一个子串
size_t string::find(const char* str, size_t pos )
{
char* p = strstr(_str + pos, str);
return p - _str;
}
📒6.3交换操作
void string::swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capaicty, s._capaicty);
}
📒6.4获取字符串
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (len > _size - pos)
{
string sub(_str + pos);
return sub;
}
else
{
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i];
}
return sub;
}
}
📒6.5运算符重载
逻辑判断运算符只需要实现两个,其余的通过复用就可以全部实现。
bool string::operator<(const string& s) const
{
return strcmp(_str, s._str) < 0;
}
bool string::operator>(const string& s) const
{
return !(*this <= s);
}
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 strcmp(_str, s._str) == 0;
}
bool string::operator!=(const string& s) const
{
return !(*this == s);
}
📒6.6清理字符串
clear函数支持清空一个字符串,但不是释放对象,区别于析构函数。clear函数清理字符串并不会引起缩容,只是在下标0位置置为 \0 ,_size置为0即可。
void string::clear()
{
_str[0] = '\0';
_size = 0;
}
📒6.7流操作
流操作属于iostream中的对象,所以不需要定义在类中作为成员函数,也不需要声明为友元,因为使用流体去和流插入需要ostream和istream对象作为左操作参数。
🌟流插入
ostream& operator<< (ostream& os, const string& str)
{
for (size_t i = 0; i < str.size(); i++)
{
os << str[i];
}
return os;
}
🌟流提取
istream& Mystring::operator>>(istream& in, string& s)
{
s.clear();
char buff[256] = {0};
char ch = in.get();
size_t sub = 0;
while (ch != ' ' && ch != '\n') //当缓冲区中有空格和换行就结束提取
{
buff[sub++] = ch;
if (sub == 255)
{
buff[sub] = '\0';
s += buff;
sub = 0;
}
ch = in.get();
}
if (sub != 0)
{
buff[sub] = '\0';
s += buff;
}
return is;
}
小Tips:我们定义一个缓冲区buff,先将字符串输入到缓冲区中,如果字符串很长则分批写入string字符串中,每次写入string后就刷新缓冲区再继续接收,这样就避免了频繁开辟空间。
🎁结语:
本次的内容到这里就结束啦。希望大家阅读完可以有所收获,同时也感谢各位读者三连支持。文章有问题可以在评论区留言,博主一定认真认真修改,以后写出更好的文章。你们的支持就是博主最大的动力。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。