作者:@小萌新
专栏:@C++初阶
作者简介:大二学生 希望能和大家一起进步
本篇博客介绍:本篇博客会收尾String类的使用以及模拟String类的实现
让我们进入愉快的学习吧~
大部分String类的使用放在前面一篇博客中咯
由于老师讲课篇幅的限制所以说只能在这篇博客中补上剩下的一部分
函数使用是compare
compare的使用方式如下

使用规则如下
比较字符实际上是比较字符的ASCII码大小
前面字符大于后面的字符返回大于0的值 (一般情况下是1)
前面字符小于后面的字符返回小于0的值 (一般情况下是-1)
如果相同就返回0
那么比较字符串呢?
实际上就是按照字符的顺序一个一个进行比较
那么如果其中一个字符串结束了 还有一个没有结束怎么办呢?
我们都知道字符串的结束标志是 ’/0‘ 事实上它的ASCII码是0
那么还有没有结束的那个字符串肯定是大于结束的这个字符串的
代码实例如下
string s1("hello world");
string s2 = "hello Ntu";
string s3(s1);
// 这里写三种初始化方法
// 1. 通过一个对象来调用 看看另外一个对象和自己相不相等
cout << s1.compare(s2) << endl;
cout << s1.compare(s3) << endl;

我们可以发现完全正确
比较语法方式二 我们可以在前面指定开始比较的位置还有需要比较的个数(被比较字符串的)
代码表示如下
int main()
{
string s1("hello world");
string s2 = "hello Ntu";
string s3(s1);
// 这里写三种初始化方法
// 2. 通过一个对象来调用 指定位置还有比较个数
cout << s1.compare(1,3,s2) << endl;
// s1的“hel”和整个s2比较
cout << s1.compare(1,3,s3) << endl;
// s1的“hel”和整个s3比较
return 0;
}
运行结果如下

我们发现可以完美运行
那么上面的代码是什么意思呢?
截取s1字符串从1位置开始 截取三个字符 然后和s2比较
比较语法三 我们指定两个字符串中需要比较的内容来比较
代码表示如下
int main()
{
string s1("hello world");
string s2 = "hello Ntu";
string s3(s1);
// 这里写三种初始化方法
// 3. 通过一个对象来调用 分别指定两个字符串比较的位置和个数
cout << s1.compare(1, 3, s2 ,1,3) << endl;
// s1的“hel”和s2的“hel”比较
cout << s1.compare(1, 3, s3,1 , 1) << endl;
// s1的“hel”和 “h”比较
return 0;
}
运行结果如下

也完美符合我们的预期
这个是什么意思呢?
我们在前面被比较字符串需要比较的位置以及要比较的字符个数
在指定比较字符串后继续指定比较字符串的位置以及比较字符个数
函数名称 stoi

这里的三个参数分别是什么意思呢?
第一个参数我们应该传进去一个字符串 这里应该没什么说法
第二个参数我们应该传进去一个pos指针 指向我们开始要转化成int类型的位置(默认是0 也就是全部转换)
第三个参数是进制 转化后是一个什么进制的数字 默认是10
下面我们来看看代码表示
int main()
{
string s1 = "7"; // 十进制 7
int a = stoi(s1, 0, 10); // 0就是空指针 最后的10可以不写
// int a = stoi(s1) 两段代码效果一样
cout << a << endl;
return 0;
}
表示结果如下

我们可以发现 这里的a确实被赋值成数字7了
后面又诸多类似的函数 比如说 stos stold stolld 我们这里只要记住这一个的用法
后面需要用到其他的时候改变 stoi(int)最后的i就可以

这个函数的用法就很简单了 不用过多的讲解 我们直接看代码和效果
代码表示如下
int main()
{
string s1 = to_string(123123123);
string s2 = to_string(123.123+1.11);
cout << s1 << endl;
cout << s2 << endl;
return 0;
}
实现效果如下

还记得构造函数的格式是什么嘛?
函数名和类名相同 无返回值
我们现在模拟这个类的成员有三个 Size(字符个数) Capacity (能储存的字符个数)_str (指向字符串的指针)
我们来看看效果

可以完美运行
String类中的析构函数需要我们自己来写 因为我们使用了动态内存开辟 所以肯定需要我们手动来释放资
源
当然啦 析构函数也很简单
代码表示如下
~String()
{
delete[] _str; // 释放内存 避免内存泄漏
_str = nullptr;
_size = 0;
_capacity = 0;
}
我们先来试试 如果我们不写拷贝构造函数 使用系统默认的会怎么样

这个错误其实我们讲类和对象中的时候是不是已经遇到过了 这就是对同一空间的连续释放
这个其实就是由于浅拷贝 两个指针指向同一块空间的后果
浅拷贝: 浅拷贝只复制某个对象的引用 而不复制对象本身 新旧对象还是共享同一块内存
深拷贝: 深拷贝会创造一个一摸一样的对象 新对象和原对象不共享内存 修改新对象不会改变原来的对象
所以说 我们这里要写一个深拷贝函数
String(const String& s)
:_str(new char[strlen(s._str)+1])
,_capacity(s._capacity)
,_size(s._size)
{
strcpy(_str, s._str); // 复制里面的内容
}

我们发现 使用深拷贝之后就不会报错了 (因为这个时候都有自己的空间了)
这里我们同样也要使用深拷贝来避免内存错误
类似这样

这里我们首先要明确一点
开辟内存是有可能失败的 而释放内存是一定成功的
所以说我们这里可以先开辟一个tmp内存 开辟成功之后再释放原来的内存(防止释放之后开辟不成功的问题)
写法如下
因为tmp是一个临时变量 所以说出了作用域会自动调用析构函数 所以说我们不用管原来_str指向的区域
String& operator=(const String& s)
{
String tmp(s); // 拷贝构造一个tmp
swap(_str,tmp._str); // 交换指针指向
_size = s._size;
_capacity = s._capacity;
return *this;
}
随着时间的发展这里诞生出一种现代写法更加简洁
String& operator=(String s)
{
// s是一个传值拷贝的参数
swap(_str, s._str);
swap(_size, s._size);
swap(_capacity, s._capacity);
return *this;
}
这里直接使用传值传递 之后我们交换下他们的指针 大小 容量
s会自动销毁 我们返回*this就可以
还记不记得我们上篇博客说的话
我们将这个迭代器默认为一个指针就好了
实际上Stirng类中的迭代器就是一个指针
但是我们要注意的是 并不是所有类中的迭代器都是指针
那么知道它是指针之后就很好办了
注意!!! 这里的格式不能错
typedef char* iterator;
typedef const char* const_iterator;
代码表示如下
iterator begin()
{
return _str; // 返回字符串中第一个字符地址
}
iterator begin() const
{
return _str; // 返回字符串中第一个字符地址
}
代码表示如下
iterator end()
{
return _str + _size; // 返回 /0
}
iterator end() const
{
return _str + _size; // 返回 /0
}
接下来我们来试验下 使用迭代器遍历数组
代码表示如下
String::iterator it = s1.begin();
while (it!=s1.end()) // 实际上就是不等于/0的时候
{
// 这里实际上就是字符串遍历数组的方式
cout << *it << " ";
it++;
}
运行结果如下

所以我们发现这里是不是没有什么新东西啊
只不过是把我们以前学的东西换了个名字而已
在我们前面的String类中介绍过了范围for
实际上它的底层原理是什么呢?
实际上就是使用了我们的一个迭代器
for (auto x : s1)
{
cout << x << " ";
}
运行结果如下

假设我们把把迭代器屏蔽掉 这里就运行不了了
因为我们这里的Size和Capacity是私有成员
所以说我们要写出函数来返回它们的大小
int size()
{
return _size;
}
实现效果如下

代码表示如下
int capacity()
{
return _capacity;
}
运行结果如下 这里也没有什么好讲的

开空间
reserve的执行规则如下
代码表示如下
void reserve(int n) //n 表示要开的空间大小
{
// 首先判断大小
if (n > _capacity)
{
char* tmp = new char[n + 1]; // 这里多开一个空间是为了存放 /0
strncpy(tmp, _str, _size + 1); // 这里之所以使用strncpy是因为防止str里面有/0存在
delete[] _str;
_str = tmp;
_capacity = n; // 容量代表的是可以存放字符的数量 不包括/0 所以说要比真实大小少1
}
}
resize的执行规则如下
代码表示如下
void resize(int n) // n 表示要重置的大小
{
// 小于的话改变下size就好
if (n<=_size)
{
_size = n;
_str[_size] = '\0';
}
// 大于的话 要考虑是否要扩容
if (n > _size)
{
if (n>_capacity)
{
reserve(n); // 扩容
// 后面全部赋值 /0
int i = _size;
while (i<=_capacity)
{
_str[i] = '\0';
i++;
}
_size = n;
}
else
{
int i = _size;
while (i<=n)
{
_str[i] = '\0';
i++;
}
_size = n;
}
}
}
运行结果如下

判断是否为空 这个很简单
判断下字符串和空串是否相等就好了
bool empty()
{
return strcmp(_str, "") == 0;
}

凡是修改字符串的函数 都需要考虑是否要扩容
这里往后插入一个字符 一定记得后面要加上一个‘\0’
代码表示如下
void push_back(char ch)
{
// 首先判断是否需要扩容
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = ch;
_str[_size + 1] = '\0';
_size++;
}
运行结果如下

这个函数的作用就是在字符串后面尾插一个字符串 尾插前需要判断字符串的容量是否可以容纳这么多的
字符
代码表示如下
void append(const char* str)
{
// 判断是否需要扩容
if (_size+strlen(str)>_capacity)
{
reserve(_capacity + strlen(str));
}
// 开始拼接字符串
strcpy(_str + _size, str); // 将_str尾插到字符串末尾
// 重置字符串长度
_size = _size + strlen(str);
}
我们来看看最后的效果是什么样子的

这里符合预期
这里我们直接复用上面两个实现的函数就好啦
为了符合String内置函数的写法 我们这里写成两个重载函数
代码分别如下
String& operator+=(char ch)
{
// 复用就可以
push_back(ch);
return *this;
}
String& operator+=(const char* str)
{
// 复用就可以
append(str);
return *this;
}
显示效果如下

insert函数的作用是选择一个位置插入字符或者是字符串
所以说这里有两个参数 我们将一个设置为pos 一个设置为字符/字符串
首先我们要考虑pos的合法性 它一定是大于等于0的
之后我们来看后面的字符 就要考虑扩容问题啦 移位问题啦
首先我们按照这个思路来设计插入字符的代码
代码表示如下
void append(const char* str)
{
// 判断是否需要扩容
if (_size+strlen(str)>_capacity)
{
reserve(_capacity + strlen(str));
}
// 开始拼接字符串
strcpy(_str + _size, str); // 将_str尾插到字符串末尾
// 重置字符串长度
_size = _size + strlen(str);
}
void insert(int pos, char ch)
{
// 首先考虑pos是否合法
assert(pos >= 0);
assert(pos <= _size);
// 其次考虑是否需要扩容
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2* _capacity );
}
// 之后开始考虑移位 为插入数据腾出位置
for (int i = _size; i >= pos; i--)
{
_str[i + 1] = _str[i];
}
_str[pos] = ch;
// 调整下size的大小
_size++;
}
接下来我们来看看字符串应该怎么插入
代码表示如下
void insert(int pos, const char* str)
{
// assert
assert(pos >= 0);
assert(pos <= _size);
// 判断是否需要扩容
int len = _size + strlen(str);
if (len > _capacity)
{
reserve(len + _capacity);
}
// 之后开始考虑位移
for (int i = _size; i >= pos; i--)
{
_str[i + strlen(str)] = _str[i];
}
// 开始插入 注意这个时候要用strncpy才可以
// 如果这个时候你用了strcpy 后面会被/0覆盖
strncpy(_str + pos, str, strlen(str));
}
运行结果如下

这里尤其要注意的一点就是最后必须要使用strncpy 很多人都是忽略这个细节导致出错
这里还是我们给出两个参数
一个位置还有要删除字符的个数
void earse(int pos, size_t n = -1)
{
// assert
assert(pos >= 0);
assert(pos <= _size);
// 判断要删除的是不是后面所有的值
if (n >= _size - pos)
{
_str[pos] = '\0';
// 改变size的大小
_size = pos;
}
// 否则就不是要删除所有的值 那么我们这个时候来看看被删除的值有多少 n
else // n < _size - pos
{
strcpy(_str + pos, _str + pos + n);
_size+=n;
}
}
运行结果如下


这个很简单啦 直接将字符串大小变成0 然后赋值空字符串就可以
void clear()
{
_size = 0;
_str[_size] = '\0';
}
很简单 这样子两行代码搞定
这里也很简单 返回一个字符串就可以
const char* c_str() const
{
return _str;
}
重载这个运算符只是为了符合我们使用喜欢
想想看 当我们要使用这个运算符的时候是为了什么?
访问数组中的某个元素是吧 那么怎么写这个函数是不是很清晰了
char& operator[](size_t i)
{
assert(i < _size); //检测下标的合法性
return _str[i]; //返回对应字符
}
const char& operator[](size_t i ) const
{
assert(i < _size); //检测下标的合法性
return _str[i]; //返回对应字符
}
使用效果如下

这个函数的意思很明确 找到第一个出现的字符 如果找到 返回下标 如果找不到 返回一个-1就可以
我们这里再加一个功能 就是用户可以设置从默认的第n个位置开始查找 默认为0
那么代码表示如下
int find(char ch , int pos = 0)
{
// assert
assert(pos < _size);
for (size_t i = 0+pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return -1;
}

这里我们可以发现结果没问题
如果我们想要找字符串呢?
很简单 我们使用strstr查找就可以
//正向查找第一个匹配的字符串
int find(const char* str, int pos = 0)
{
assert(pos < _size); //检测下标的合法性
const char* ret = strstr(_str + pos, str); //调用strstr进行查找
if (ret) //ret不为空指针,说明找到了
{
return ret - _str; //返回字符串第一个字符的下标
}
else //没有找到
{
return -1; //返回-1
}
}

顾名思义 反向查找
那么我们这里转换下思路就好了 指针从右边开始找
代码表示如下
int rfind(const char* str, int pos = 0)
{
assert(pos < _size); //检测下标的合法性
pos = _size - 1 - pos;
// 一个个开始往后面找
while (pos)
{
const char* ret = strstr(_str + pos, str);
if (ret==nullptr)
{
pos--;
}
else
{
return ret - _str;
}
}
return -1;
}
代码运行结果如下

我们发现可以完美运行
关系运算符有 >、>=、<、<=、==、!= 这六个,但是对于C++中任意一个类的关系运算符重载,我们均
只需重载其中的两个,剩下的四个关系运算符可以通过复用已经重载好了的两个关系运算符来实现。
我们这里实现 == 和 >
//>运算符重载
bool operator>(const string& s)const
{
return strcmp(_str, s._str) > 0;
}
//==运算符重载
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);
}
//<=运算符重载
bool operator<=(const string& s)const
{
return !(*this > s);
}
//!=运算符重载
bool operator!=(const string& s)const
{
return !(*this == s);
}
重载>>运算符是为了让string对象能够像内置类型一样使用>>运算符直接输入。输入前我们需要先将对象的C字符串置空,然后从标准输入流读取字符,直到读取到’ ‘或是’\n’便停止读取
代码表示如下
//>>运算符的重载
istream& operator>>(istream& in, string& s)
{
s.clear(); //清空字符串
char ch = in.get(); //读取一个字符
while (ch != ' '&&ch != '\n') //当读取到的字符不是空格或'\n'的时候继续读取
{
s += ch; //将读取到的字符尾插到字符串后面
ch = in.get(); //继续读取字符
}
return in; //支持连续输入
}
同样的 流插入运算符表示如下
//<<运算符的重载
ostream& operator<<(ostream& out, const string& s)
{
//使用范围for遍历字符串并输出
for (auto e : s)
{
cout << e;
}
return out; //支持连续输出
}
还记不记得我们之前有一个getline运算符
它其实和我们的流提取差不多 唯一的区别就是它遇到换行符才会停
//读取一行含有空格的字符串
istream& getline(istream& in, String& s)
{
s.clear(); //清空字符串
char ch = in.get(); //读取一个字符
while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取
{
s += ch; //将读取到的字符尾插到字符串后面
ch = in.get(); //继续读取字符
}
return in;
}

本篇博客主要模拟了String类的实现
由于博主水平有限 错误在所难免 希望大佬看到能够及时指正
如果本篇博客帮助了你 别忘记一键三连啊
阿尼亚 哇酷哇酷!