作者:@小萌新
专栏:@C++初阶
作者简介:大二学生 希望能和大家一起进步
本篇博客介绍:本篇博客会收尾String类的使用以及模拟String类的实现
模拟String类的实现
- 本章目标
- String类的使用(补)
* - 1. String类中字符串比较
– - 2. String对象类型转换
– - 模拟String类的实现
* - 默认成员函数
– - 迭代器相关函数
– - 容器和大小相关函数
– - 修改字符串相关函数
– - 访问字符串相关函数
– - 关系运算符重载
- 输入输出运算符重载
- 总结
; 本章目标
- 学会剩下的String类使用方法
- 模拟String类的实现
让我们进入愉快的学习吧~
String类的使用(补)
大部分String类的使用放在前面一篇博客中咯
由于老师讲课篇幅的限制所以说只能在这篇博客中补上剩下的一部分
1. String类中字符串比较
比较方式一
函数使用是compare
compare的使用方式如下
使用规则如下
比较字符实际上是比较字符的ASCII码大小
前面字符大于后面的字符返回大于0的值 (一般情况下是1)
前面字符小于后面的字符返回小于0的值 (一般情况下是-1)
如果相同就返回0
那么比较字符串呢?
实际上就是按照字符的顺序一个一个进行比较
那么如果其中一个字符串结束了 还有一个没有结束怎么办呢?
我们都知道字符串的结束标志是 ‘/0’ 事实上它的ASCII码是0
那么还有没有结束的那个字符串肯定是大于结束的这个字符串的
代码实例如下
string s1("hello world");
string s2 = "hello Ntu";
string s3(s1);
cout << s1.compare(s2) << endl;
cout << s1.compare(s3) << endl;
我们可以发现完全正确
比较方式二
比较语法方式二 我们可以在前面指定开始比较的位置还有需要比较的个数(被比较字符串的)
代码表示如下
int main()
{
string s1("hello world");
string s2 = "hello Ntu";
string s3(s1);
cout << s1.compare(1,3,s2) << endl;
cout << s1.compare(1,3,s3) << endl;
return 0;
}
运行结果如下
我们发现可以完美运行
那么上面的代码是什么意思呢?
截取s1字符串从1位置开始 截取三个字符 然后和s2比较
比较方式三
比较语法三 我们指定两个字符串中需要比较的内容来比较
代码表示如下
int main()
{
string s1("hello world");
string s2 = "hello Ntu";
string s3(s1);
cout << s1.compare(1, 3, s2 ,1,3) << endl;
cout << s1.compare(1, 3, s3,1 , 1) << endl;
return 0;
}
运行结果如下
也完美符合我们的预期
这个是什么意思呢?
我们在前面被比较字符串需要比较的位置以及要比较的字符个数
在指定比较字符串后继续指定比较字符串的位置以及比较字符个数
2. String对象类型转换
String对象转化成其他类型(以Int为例)
函数名称 stoi
这里的三个参数分别是什么意思呢?
第一个参数我们应该传进去一个字符串 这里应该没什么说法
第二个参数我们应该传进去一个pos指针 指向我们开始要转化成int类型的位置(默认是0 也就是全部转换)
第三个参数是进制 转化后是一个什么进制的数字 默认是10
下面我们来看看代码表示
int main()
{
string s1 = "7";
int a = stoi(s1, 0, 10);
cout << a << endl;
return 0;
}
表示结果如下
我们可以发现 这里的a确实被赋值成数字7了
后面又诸多类似的函数 比如说 stos stold stolld 我们这里只要记住这一个的用法
后面需要用到其他的时候改变 stoi(int)最后的i就可以
其他类型转化成String类型
这个函数的用法就很简单了 不用过多的讲解 我们直接看代码和效果
代码表示如下
int main()
{
string s1 = to_string(123123123);
string s2 = to_string(123.123+1.11);
cout << s1 << endl;
cout << s2 << endl;
return 0;
}
实现效果如下
模拟String类的实现
默认成员函数
构造函数
还记得构造函数的格式是什么嘛?
函数名和类名相同 无返回值
我们现在模拟这个类的成员有三个 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);
swap(_str,tmp._str);
_size = s._size;
_capacity = s._capacity;
return *this;
}
随着时间的发展这里诞生出一种现代写法更加简洁
String& operator=(String 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;
begin
代码表示如下
iterator begin()
{
return _str;
}
iterator begin() const
{
return _str;
}
end
代码表示如下
iterator end()
{
return _str + _size;
}
iterator end() const
{
return _str + _size;
}
接下来我们来试验下 使用迭代器遍历数组
代码表示如下
String::iterator it = s1.begin();
while (it!=s1.end())
{
cout << *it << " ";
it++;
}
运行结果如下
所以我们发现这里是不是没有什么新东西啊
只不过是把我们以前学的东西换了个名字而已
范围for
在我们前面的String类中介绍过了范围for
实际上它的底层原理是什么呢?
实际上就是使用了我们的一个迭代器
for (auto x : s1)
{
cout << x << " ";
}
运行结果如下
假设我们把把迭代器屏蔽掉 这里就运行不了了
容器和大小相关函数
size
因为我们这里的Size和Capacity是私有成员
所以说我们要写出函数来返回它们的大小
int size()
{
return _size;
}
实现效果如下
capacity
代码表示如下
int capacity()
{
return _capacity;
}
运行结果如下 这里也没有什么好讲的
reserve
开空间
reserve的执行规则如下
- 如果要开的空间大于capacity的时候 将capacity扩大
- 如果要开的空间小于capacity的时候 什么都不做
代码表示如下
void reserve(int n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strncpy(tmp, _str, _size + 1);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
resize
resize的执行规则如下
- 如果重置的大小大于当前的大小并且小于容量 那就增加差值个’/0′
- 如果重置的大小大于容量 那么久扩容后 后面全部赋值’/0′
- 如果重置的大小小于当前的大小 那么就缩小大小 并且后面的内容截断
代码表示如下
void resize(int n)
{
if (n_size)
{
_size = n;
_str[_size] = '\0';
}
if (n > _size)
{
if (n>_capacity)
{
reserve(n);
int i = _size;
while (i_capacity)
{
_str[i] = '\0';
i++;
}
_size = n;
}
else
{
int i = _size;
while (in)
{
_str[i] = '\0';
i++;
}
_size = n;
}
}
}
运行结果如下
empty
判断是否为空 这个很简单
判断下字符串和空串是否相等就好了
bool empty()
{
return strcmp(_str, "") == 0;
}
修改字符串相关函数
凡是修改字符串的函数 都需要考虑是否要扩容
push_back
这里往后插入一个字符 一定记得后面要加上一个’\0′
代码表示如下
void push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = ch;
_str[_size + 1] = '\0';
_size++;
}
运行结果如下
append
这个函数的作用就是在字符串后面尾插一个字符串 尾插前需要判断字符串的容量是否可以容纳这么多的
字符
代码表示如下
void append(const char* str)
{
if (_size+strlen(str)>_capacity)
{
reserve(_capacity + strlen(str));
}
strcpy(_str + _size, str);
_size = _size + strlen(str);
}
我们来看看最后的效果是什么样子的
这里符合预期
operator +=
这里我们直接复用上面两个实现的函数就好啦
为了符合String内置函数的写法 我们这里写成两个重载函数
代码分别如下
String& operator+=(char ch)
{
push_back(ch);
return *this;
}
String& operator+=(const char* str)
{
append(str);
return *this;
}
显示效果如下
insert
insert函数的作用是选择一个位置插入字符或者是字符串
所以说这里有两个参数 我们将一个设置为pos 一个设置为字符/字符串
首先我们要考虑pos的合法性 它一定是大于等于0的
之后我们来看后面的字符 就要考虑扩容问题啦 移位问题啦
首先我们按照这个思路来设计插入字符的代码
代码表示如下
void append(const char* str)
{
if (_size+strlen(str)>_capacity)
{
reserve(_capacity + strlen(str));
}
strcpy(_str + _size, str);
_size = _size + strlen(str);
}
void insert(int pos, char ch)
{
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++;
}
接下来我们来看看字符串应该怎么插入
代码表示如下
void insert(int pos, const char* str)
{
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(_str + pos, str, strlen(str));
}
运行结果如下
这里尤其要注意的一点就是 最后必须要使用strncpy 很多人都是忽略这个细节导致出错
erase
这里还是我们给出两个参数
一个位置还有要删除字符的个数
void earse(int pos, size_t n = -1)
{
assert(pos >= 0);
assert(pos _size);
if (n >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + n);
_size+=n;
}
}
运行结果如下
clear
这个很简单啦 直接将字符串大小变成0 然后赋值空字符串就可以
void clear()
{
_size = 0;
_str[_size] = '\0';
}
很简单 这样子两行代码搞定
c_str
这里也很简单 返回一个字符串就可以
const char* c_str() const
{
return _str;
}
访问字符串相关函数
operator[]
重载这个运算符只是为了符合我们使用喜欢
想想看 当我们要使用这个运算符的时候是为了什么?
访问数组中的某个元素是吧 那么怎么写这个函数是不是很清晰了
char& operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
const char& operator[](size_t i ) const
{
assert(i < _size);
return _str[i];
}
使用效果如下
find
这个函数的意思很明确 找到第一个出现的字符 如果找到 返回下标 如果找不到 返回一个-1就可以
我们这里再加一个功能 就是用户可以设置从默认的第n个位置开始查找 默认为0
那么代码表示如下
int find(char ch , int pos = 0)
{
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);
if (ret)
{
return ret - _str;
}
else
{
return -1;
}
}
rfind
顾名思义 反向查找
那么我们这里转换下思路就好了 指针从右边开始找
代码表示如下
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;
}
代码运行结果如下
我们发现可以完美运行
关系运算符重载
关系运算符有 >、>=、
Original: https://blog.csdn.net/meihaoshy/article/details/127813812
Author: Aniyaaaaa_
Title: C++ String类 (下) String类的模拟实现
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/660991/
转载文章受原作者版权保护。转载请注明原作者出处!