本章目标
1.学会剩下的String类使用方法
2.模拟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); // 这里写三种初始化方法 // 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; }
运行结果如下
也完美符合我们的预期
这个是什么意思呢?
我们在前面被比较字符串需要比较的位置以及要比较的字符个数
在指定比较字符串后继续指定比较字符串的位置以及比较字符个数
2. String对象类型转换
String对象转化成其他类型(以Int为例)
函数名称 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就可以
其他类型转化成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); // 拷贝构造一个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;
begin
代码表示如下
iterator begin() { return _str; // 返回字符串中第一个字符地址 } iterator begin() const { return _str; // 返回字符串中第一个字符地址 }
end
代码表示如下
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++; }
运行结果如下
所以我们发现这里是不是没有什么新东西啊
只不过是把我们以前学的东西换了个名字而已
范围for
在我们前面的String类中介绍过了范围for
实际上它的底层原理是什么呢?
实际上就是使用了我们的一个迭代器
for (auto x : s1) { cout << x << " "; }
运行结果如下
假设我们把把迭代器屏蔽掉 这里就运行不了了