🎈个人主页:🎈 :✨✨✨初阶牛✨✨✨
🐻推荐专栏1: 🍔🍟🌯C语言初阶
🐻推荐专栏2: 🍔🍟🌯C语言进阶
🔑个人信条: 🌵知行合一
🍉本篇简介:>:讲解如何模拟实现C++中的string类.
金句分享:
✨你要做多大的事情,就要承受多大的压力!✨
前言
我们先认识一下string
类的框架.
class string
{
public:
//成员函数
private:
char* _str; //字符串指针
size_t _capacity; //容量
size_t _size; //当前字符有效个数
}:
框架图:
目录
- 前言
- 一、构造函数与析构函数
- (1) 无参构造:
- (2) 使用常量字符串构造
- (3) 拷贝构造
- (4) 析构函数
- 二、capacity相关操作
- (1) reserve函数
- (2) resize函数
- (3) empty函数
- (4) size和capacity函数
- 三、访问与遍历
- (1) 迭代器
- (2)下标访问符 方括号`[ ]`重载
- 四、修改与查找
- (1) push_back函数
- (2) append函数
- (3) find函数
- (4) insert函数
- (5) erase函数
- 五、运算符重载
- (1) 流运算符重载
- 流插入运算符
- 流提取运算符
- (2) 比较运算符重载
一、构造函数与析构函数
(1) 无参构造:
我们可以试着看一下库里面是如何赋值的?
std::string s1;
cout << "s1= " << s1 << endl;
所以,对于无参构造,我们只需要将*str
赋值为空串就行了.
注意:
""
(中间没有空格)
(2) 使用常量字符串构造
- 先计算字符串的长度.
- 将长度值赋值给
_size
和_capacity
. - 申请一块为
_capacity+1
大小的空间.(+1是为了存储'\0'
) - 将字符串中的值按字节拷贝至
string
类中的_str
.
代码实现:
//全缺省构造函数,,默认初始化为空字符
string(const char* str = "")//无参也是调用这个
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];//里面其实有一个字符'\0'
// strcpy(_str, str); //遇到'\0'结束拷贝,也会吧'\0'拷贝过去
memcpy(_str, str, _size + 1);
}
(3) 拷贝构造
这里注意实现深拷贝即可.
string(const string& s) //注意这里+const 普通对象可以调用,const对象也可以调用
{
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];//需要多一个字节存放,字符'\0'
memcpy(_str,s._str,s._size+1);
}
(4) 析构函数
析构函数注意释放动态申请的空间即可.
~string()
{
_size = 0;
_capacity = 0;
delete[]_str;
_str = nullptr;
}
二、capacity相关操作
(1) reserve函数
reserve()
:请求改变容量的大小,类似于扩容从操作.
- 向堆区申请一块
n+1
大小的新空间. - 将旧空间的数据拷贝到新空间,
- 释放旧空间
_str
指向新空间- 更新容量
capacity
void reserve(size_t n)
{
if (n > _capacity)
{
//申请一块新空间
char* tmp = new char[n + 1];
memcpy(tmp, _str, _size + 1);//不建议使用strcpy,可能存在中间有'\0'的强开
delete[] _str;//释放旧空间
_str = tmp;
_capacity = n;
}
}
(2) resize函数
resize()
:用于改变字符串的有效字符长度.不够的地方用第二个参数填充.
步骤:
-
如果
n<size
(即n<当前长度):
①直接在n
位置处赋值为’\0’.
②_size
更新为n
-
如果
n>=size
:
①调用扩容函数,扩容至n大小.
②超出部分用字符'c'
填充
③更新_size
,并在最后一个位置设置为’\0’
代码实现:
void resize(size_t n, char c = '\0')
{
if (n < _size)
{
_str[n] = '\0';
_size = n;
}
else
{
reserve(n);//无论需不要扩容,不需要扩容时reserve内部会什么也不做,需要扩容时,reserve会扩容.
memset(_str + _size, c, n - _size);
_size = n;
_str[_size] = '\0';
}
}
(3) empty函数
如果_size
值为0
,则为空,返回true
.
否则返回false
;
bool empty()const
{
//size=0则为空,返回true
return _size == 0 ? true : false;
}
(4) size和capacity函数
这两个函数,直接返回值即可.
size_t size()const
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
三、访问与遍历
(1) 迭代器
迭代器的介绍
C++
迭代器是一个用于遍历容器(如vector、list、set等)中的元素的对象。迭代器的作用类似于指针,可以通过解引用操作符(*)获取容器中的元素值,也可以通过自增操作符(++)移动迭代器指向下一个元素。迭代器可以访问容器中的元素,也可以修改容器中的元素值。
注意迭代器的定义,迭代器是左闭右开的区间.
public:
typedef char* iterator;
typedef const char* const_iterator;
//普通迭代器
iterator begin()
{
return _str; //返回第一个字符位置
}
iterator end()
{
return _str + _size; //返回最后一个有效字符的下一个位置
}
//常类迭代器
const const_iterator begin()const
{
return _str; //返回第一个字符位置
}
const const_iterator end()const
{
return _str + _size; //返回最后一个有效字符的下一个位置
}
(2)下标访问符 方括号[ ]
重载
返回_str
中第index
的位置
char& operator[](size_t index)
{
//判断位置是否合法
assert(index >= 0 && index < _size);
return *(_str + index);
}
const char& operator[](size_t index)const
{
assert(index >= 0 && index < _size);
return *(_str + index);
}
四、修改与查找
(1) push_back函数
push_back
尾部插入一个字符
在进行插入操作之前,要先考虑扩容的情况.
需要注意的是,如果采用无参构造,刚开始容量是0.
这就导致是初次扩容,容量开始是0,所以这里要判断扩容前,容量是否是0,再考虑1.5倍或者二倍扩容.
void push_back(char c)
{
if (_size + 1 > _capacity)
{
//如果capacity是0,则无法进行1.5倍扩容
//reserve(_capacity * 1.5);
reserve(_capacity == 0 ? 4 : _capacity * 1.5);//扩容多少没有标准,2倍或者1.5倍扩容都可,
}
_str[_size] = c;
_size++; //有效数据+1
_str[_size] = '\0';
}
(2) append函数
append
尾部追加字符串
void append(const char* str)
{
int sz=strlen(str);
//如果容量不够,就申请新空间
if (_size + sz > _capacity)
{
reserve(_capacity +sz);
}
//追加新的字符串
memcpy(_str + _size, str, sz);
_size += sz;
_str[_size] = '\0';
}
(3) find函数
在string
中查找目标字符,通过遍历比较.
第二个参数表示从pos
位置开始查找.
顺序查找即可
size_t find(char c, size_t pos = 0) const
{
assert(pos < _size);
for (int i = pos; i < _size; i++)
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
字符串匹配:查找string
类的中的目标字串
字符串匹配算法,这里简化,直接调用库函数strstr
,就不手撕算法了.
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, s);//通过调用库函数strstr找到字符串出现的位置的指针
if (ptr)
{
return ptr - _str;
}
else
{
return npos;
}
}
(4) insert函数
在pos
位置插入一个字符:
老规矩,先扩容,学过数据结构的小伙伴应该知道,需要先移动数据再进行插入数据操作.
顺序表任意位置插入
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& insert(size_t pos, char c)
{
assert(pos >= 0 && pos<= _size);
if (_size + 1 > _capacity)
{
reserve(_capacity * 1.5);
}
int i = 0;
//移动数据
for (i = _size; i > pos - 1; i--)
{
_str[i] = _str[i - 1];
}
_str[pos - 1] = c;
_size++;
return *this;
}
在pos
位置插入一段字符串:
这里在移动数据时,注意0号位置插入时移动.
如果我们只是将前面的数据直接往后移动字符串长度大小的位置,则到插入0号位置时,前面的数据是非法的,此处设计时,需要注意.
string& insert(size_t pos, const char* str)
{
assert(pos >= 0 && pos <= _size);
int sz = strlen(str);
//如果容量不够,就申请新空间
if (_size + sz > _capacity)
{
reserve(_capacity + sz);
}
int i = 0;
//移动数据,这里需要注意0号位置插入时是否移动数据非法.
size_t end = _size;
while (end >= pos && end != npos)
{
_str[end + sz] = _str[end];
--end;
}
//插入字符串
for (i = pos; i <pos+sz; i++)
{
_str[i] = str[i-pos];
}
_size+=sz;
return *this;
}
(5) erase函数
erase
:删除从pos
位置开始往后len
长度的元素,并返回删除后的string
.
// 删除pos位置上的元素,并返回该元素的下一个位置
string& erase(size_t pos, size_t len=npos)
{
assert(pos <= _size);
if (len == npos || pos + len >= _size)//如果要求删除的长度+pos超过了string中有效字符的长度
{
_size = pos;
_str[_size] = '\0';
}
else
{
size_t end = pos + len;
while (end <= _size)
{
_str[pos++] = _str[end++];
}
_size -= len;
}
return *this;
}
五、运算符重载
(1) 流运算符重载
采用友元函数的方式,实现流提取与流插入运算符重载.
流插入运算符
ostream& operator<<(ostream& _cout, const cjn::string& s)//记得包在cjn命名空间里面
{
//在实现了迭代器的情况下,可以使用范围for
for (auto& in : s) //依次取出string类中的全部字符,插入进流
{
_cout << in;
}
return _cout; //返回输出流
}
流提取运算符
istream& operator>>(istream& in, string& s)
{
s.clear();//如果s中本身有数据,先将有效数据清除
char ch = in.get();
//处理缓冲区的空格和换行,因为可能有人先输入了空格或者换行导致读取数据失败
while (ch == ' ' || ch == '\n')
{
ch = in.get();
}
//有效数据插入进s
char buff[128];//为了避免从小的容量一次次扩容
int i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;//将读取到的数据先存放进临时数组
if (i == 127)//只有等数据满127时,才将数据插入进s
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();//继续获取有效数据
}
if (i != 0)//最后,如果buff数组中还有数据,则将这些剩余数据插入
{
buff[i] = '\0';
s += buff;
}
return in;
}
(2) 比较运算符重载
两个字符串比较,我们利用memcmp
函数比较两字符串中较短字符串的长度位数.
然后根据memcmp
返回值进行进一步判断.
bool operator<(const string& s)
{
int length = s._size;
//谁短,length就等于谁
if (_size < length) length = _size;
int ret=memcmp(_str, s._str, length);
if (ret == 0)//如果短的 与长的前半部分相等
{
if (_size< s._size)//比比较的字符串短,则<成立,true
{
return true;
}
return false;
}
if(ret==-1)//表示右操作数大,<满足
return true;
if (ret == 1)//表示左操作数大,<不满足
return false;
}
三目运算符写起来可能不大好理解,但是代码看起来很简洁
bool operator<(const string& s) const
{
int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);//按短的进行比较
//如果ret==0,则比较长度,s长则返回真,否则返回假
//ret!=0则-1表示真,1表示假
return ret == 0 ? _size < s._size : ret < 0;
}
其他运算符直接复用或者比较简单,这里不做解释.
bool operator<=(const string& s)
{
return *this < s || *this == s;
}
bool operator>(const string& s)
{
return !(*this <= s);
}
bool operator>=(const string& s)
{
return !(*this < s);
}
bool operator==(const string& s)
{
if (memcmp(_str, s._str, _size) == 0)
{
if (_size == s._size)
{
return true;
}
return false;
}
}
bool operator!=(const string& s)
{
return !(*this == s);
}
博主能力有限,无法严格按照库中的方法实现,比如采用内存池等技术,还有部分函数并未实现,模拟实现string
的目的只是为了我们更好的理解string
类,而不是真正让我们去写一个库函数.
拜拜了.