👉find👈
size_t find(char ch, size_t pos) const { assert(pos < _size); while (pos < _size) { if (_str[pos] == ch) { return pos; } ++pos; } return npos; } size_t find(const char* str, size_t pos) const { assert(pos < _size); const char* ptr = strstr(_str + pos, str); if (ptr == nullptr) { return npos; } else { return ptr - _str; } }
注:strstr函数是暴力匹配的查找算法,除了这种算法外,还有KMP算法。如果想要了解KMP算法的话,可以看一下这篇文章。需要注意的是,strstr函数的返回值是指针,而find函数的返回值为下标,所以我们要将指针进行相减转换成下标。
👉流插入和流提取重载👈
<< 运算符重载
ostream& operator<<(ostream& out, const string& s) { for (size_t i = 0; i < s.size(); i++) { out << s[i]; } return cout; }
<< 运算符是将类对象里的字符一个个打印出来的,所以它跟 c.str() 有一点区别。
clear 函数清空类对象的数据
void clear() { _size = 0; _str[0] = '\0'; }
>> 运算符重载
istream& operator>>(istream& in, string& s) { s.clear(); // 第一种方式 /*char ch = in.get(); while (ch != ' ' && ch != '\n') { s += ch; //in >> ch; ch = in.get(); } return in;*/ // 第二种方式 char buff[128] = { '\0' }; size_t i = 0; char ch = in.get(); while (ch != ' ' && ch != '\n') { if (i == 127) { s += buff; i = 0; } buff[i++] = ch; ch = in.get(); } if (i > 0) { buff[i] = '\0'; s += buff; } return in; }
以上两种方式,都能实现流插入。但是第二种方式相较于第一种方式不需要频繁地扩容。注:流提取是以空格或者换行结束的。
注:以上的流插入和流提取重载可以用友元实现,友元的话就可以直接访问类对象的数据了,而我实现的方式是通过函数接口来访问类对象的数据。
👉拷贝构造和赋值运算符重载的传统写法👈
拷贝构造
//传统拷贝构造写法 s2(s1) string(const string& s) { _str = new char[s._capacity + 1]; _capacity = s._capacity; _size = s._size; strcpy(_str, s._str); }
赋值运算符重载
// 传统赋值运算符重载 s2 = s1 string& operator=(const string& s) { if (this != &s) { char* tmp = new char[s._capacity + 1]; strcpy(tmp, s._str); delete[] _str; _str = tmp; _size = s._size; _capacity = s._capacity; } return *this; }
注:拷贝构造和赋值运算符重载都是先申请或释放空间,然后再把数据拷贝到对象中去。
👉拷贝构造和赋值运算符重载的现代写法👈
拷贝构造
交换类对象的数据
void swap(string& s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); }
库里面也有交换的函数模板,但是这个函数接口会涉及三次深拷贝,效率不高。所以我们就自己实现一个交换类对象数据的函数接口,string 类中的交换函数也是这样实现的,效率较高。
// 现代拷贝构造写法 s2(s1) string(const string& s) : _str(nullptr) , _size(0) , _capacity(0) { string tmp(s._str); // 构造函数 //this->swap(tmp); swap(tmp); }
拷贝构造现代写法说明
先初始化列表,初始化对象 s2
然后调用构造函数string tmp(s._str),构造一个对象出来
最后将 tmp 的数据和 s2 的数据进行交换
注:s2 一定要走初始化列表,如果不走初始化列表,s2 的数据将会是随机值,随机指向一块空间。将 tmp 和 s2 的数据交换后,tmp 会被销毁,那么随机指向的空间将会被delete掉,程序会崩溃。
未写初始化列表
赋值运算符重载
交换类对象的数据
void swap(string& s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); }
// 现代赋值运算符重载 s2 = s1 /*string& operator=(const string& s) { if (this != &s) { //string tmp(s._str); string tmp(s); swap(tmp); } return *this; }*/ // s2 = s1 string& operator=(string s) { swap(s); return *this; }
赋值运算符重载现代写法有两种:第一种现代赋值运算符重载的参数是类对象的引用,可以减少拷贝构造。当 s2 != s1 时,才进行调用拷贝构造函数构造对象tmp,再将tmp的数据和s2的数据进行交换。第二种现代赋值运算符重载的参数类对象,传参需要调用拷贝构造函数构造s,然后将s的数据和s2的数据进行交换。
注:拷贝构造和赋值运算符重载的现代写法并不是追求效率,而是追求简洁。深拷贝不要求_capacity相同。
有了模板之后,自定义类型也需要有构造函数和析构函数。见下图代码:
为什么要支持内置类型的构造函数和析构函数呢?因为有了模板,要支持泛型编程。比如下图:如果 T1 和 T2 为内置类型,就要去调用构造函数和析构函数了。
👉浅拷贝和深拷贝👈
浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共
享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为
还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
就像一个家庭中有两个孩子,但父母只买了一份玩具,两个孩子愿意一块玩,则万事大吉,万一不想分享就 你争我夺,玩具损坏。
可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩
子都买一份玩具,各自玩各自的就不会有问题了。
深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情
况都是按照深拷贝方式提供。
👉总结👈
其实 string 类的函数接口远不止这些,还有很多。而我们模拟实现的只是一些常用的、重点的,那些比较字符串大小的函数接口不经常使用,我们就不实现了。那么,以上就是本篇博客的全部内容,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️