专业的网站开发建访,seo咨询推广找推推蛙,网站推广哪个平台最好,wordpress群晖套件目录
前言
1.命名空间的使用
2.string的成员变量
3.构造函数
4.析构函数
5.拷贝构造 5.1 swap交换函数的实现
6.赋值运算符重载
7.迭代器部分
8.数据容量控制
8.1 size和capacity
8.2 empty
9.数据修改部分
9.1 push_back
9.2 append添加字符串
9.3 运算符重载…目录
前言
1.命名空间的使用
2.string的成员变量
3.构造函数
4.析构函数
5.拷贝构造 5.1 swap交换函数的实现
6.赋值运算符重载
7.迭代器部分
8.数据容量控制
8.1 size和capacity
8.2 empty
9.数据修改部分
9.1 push_back
9.2 append添加字符串
9.3 运算符重载
9.4 clear函数
9.5 insert
9.6 erase
9.7 substr
9.8 []运算符的重载
10.c_str
11.关系运算符
12. find函数
13.流插入操作符重载
14.流提取
总代码 前言
这里我们就开始介绍我们string的模拟实现了我相信在经过之前给大家介绍的标准库string类的使用后大家对我们的string类都已经有一定的认识心里对该底层实现也有了一定的猜想那么现在我们就为大家打消疑虑给大家揭开我们string神秘的面纱注意小编这里只给大家是实现了一部分我们经常使用的函数。 1.命名空间的使用
首先我们需要使用我们命名空间来避免和我们的库中的string导致冲突
namespace xhj{class string{};};
2.string的成员变量
要知道我们string的成员变量我们要从两个方面入手首先是我们的string的存储结构其次是根据我们string的成员函数。 1.很明显我们的string存储的是一串字符串那么该底层的存储结构用的就是我们的char类型的变长数组数组因此我们确定了第一个变量就是我们的char类型的指针 2.第二根据我们的size返回我们的有效字符个数因此我们要使用一个int类型的变量记录我们的有效字符个数。 3.第三就是我们的capacity了这里我们就需要使用一个int类型变量记录我们的容量大小 4.第四点比较难想到但是小编在之前给大家提到了一个静态变量npos这个在string常用来表示我们无限大因此该是确定的一个size_t类型的常变量。 因此我们的成员变量如下 namespace xhj{class string{private:char* _str;size_t _capacity;size_t _size;const static size_t npos;};const size_t string::npos -1;
};
3.构造函数
这里我们只需要实现我们比较重要的两个即可也就是我们的无参构造和我们的C-string进行的构造但是这里我们可以使用我们的缺省参数将两个合并为一个代码如下 string(const char* str ){_size strlen(str);_capacity _size;_str new char[_capacity 1];//由于多一位需要存储我们的‘/0’,因此要进行1strcpy(_str, str);}
4.析构函数 对于析构函数我们是需要自己实现的因为这里都是内置类型且我们这些内置类型中我们还开辟了新的空间如果我们不自己实现很大程度会造成内存的泄露。
~string()
{delete[] _str;_str nullptr;_size _capacity 0;}
5.拷贝构造
在实现拷贝构造前我们需要确认我们是否需要写我们的拷贝构造很明显我们这里是非常有必要的因为这里会涉及到浅拷贝的问题 因此以下我们需要实现我们的深拷贝。 5.1 swap交换函数的实现
为什么我们在介绍拷贝构造函数之前要先给大家介绍我们的交换函数呢?这里就涉及了我们拷贝构造函数的两种写法。
实现我们的swap函数是非常简单的也就是
void swap(string s){//这里调用的是我们算法库中的函数std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}
那么我们的拷贝构造的两类写法是
传统写法 string(const string s):_str(nullptr),_size(0), _capacity(0){//传统写法_size s._size;_capacity s._capacity;_str new char[_capacity 1];memcpy(_str, s._str,s._size1);//C语言的字符数组是以\0为终止算长度//string不看\0而是以size算终止长度}
现代写法 注意对于传统写法我们的现代写法依赖于编译器对数据的初始化如果编译没有对数据进行初始化操作那么在交换的过程中很可能会出现随机值的情况然后在最后对tmp进行析构的时候就会出现程序崩溃的情况所以这里我们需要先走初始化列表 string(const string s):_str(nullptr),_size(0), _capacity(0){//现代写法string tmp(s._str);swap(tmp);}
对于传统写法我相信大家都能理解对于现代写法这里小编就需要给大家解释一下了我们这里先调用构造函数使用我们s这个对象的_str部分去构造我们的tmp对象这里我们只需要将我们的tmp和我们的当前对象进行交换这也就达成了我们的当前对象的所有成员对象都赋予了我们tmp的值而我们的当前地址空间只需要交给我们的tmp出局部作用域进行销毁也就是 6.赋值运算符重载
对于赋值运算符我们也是需要进行重载的这里也牵涉到我们的浅拷贝带来的问题因此我们这里也是需要重新开辟一段空间进行我们数据的存储的那么这里我们也有我们的两种写法
注意1.我们的原本空间可能小于我们的形参的数据空间因此我们要重新开辟新空间 2.不要使用原空间指针开辟新空间以免开空间失败破坏原空间
传统版本 string operator(const string s){if (this ! s){//传统写法char* temp new char[s._capacity 1];//避免我们原本指针开空间失败导致旧空间被破坏memcpy(temp, s._str, s._size 1);delete[]_str;_str temp;_size s._size;_capacity s._capacity;}return *this;}
现代版本 string operator(string s)//传值传参调用拷贝构造{swap(s);return *this;}对于传统版本相信凭借大家的基础一定是随便掌握这里小编仅给大家介绍一下我们的现代版本这里我们先让此处直接进行传值传参调用我们的拷贝构造那么我们此时的s对象就是我们外部参数的一个拷贝那么我们直接使用老方法将我们s产生的新空间给我们的当前对象我们的旧空间就给我们的s出作用域的时候销毁即可。
7.迭代器部分
对于迭代器部分首先我们要想到该使用方式 string s1(hello world);string::iterator it s1.begin();while (it ! s1.end()){cout *it ;it;}
通过该定义的方式我们可以看出我们的迭代器也是一个类型一个被定义在string类中的类型而对于该使用方式来看我们的迭代器很类似于我们的指针而实际上我们的迭代器就是我们的指针或者是对我们指针进行封装的类那么该打印结果是 那么很明显我们的是从头到尾的一个遍历的过程而我们的begin函数返回的就是我们的数组首元素的地址我们的end()函数返回的就是我们数组末尾的下一个位置的地址。此外我们的迭代器这里仅仅给大家介绍我们的正向迭代器对于反向迭代器小编会在之后的内容给大家介绍)在库中一共分为两个版本 一个是我们的普通版本一个是我们的const版本那么这两者又有着什么不同呢首先对于我们普通迭代器我们即允许了读也允许了写而我们的const版本只允许读而不允许写其次就是我们调用对象的不同我们的iterator是给普通对象调用的我们的const_iterator就需要我们用我们的const修饰我们的this指针虽然按照语法来说该既可以被我们的普通对象调用权限缩小,也可以被我们的const对象调用权限平移但是在iterator版本出现时我们的编译器在每次调用中会给我们最匹配的那个因此也就达到了我们的普通对象调用我们的普通版本const对象调用我们const版本。
那么该具体实现如下 typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}const_iterator begin() const{return _str;}iterator end(){return _str _size;}const_iterator end() const{return _str _size;}
那么之前给大家介绍了范围for实际上我们的范围for就是我们以上正向迭代器的使用放式只不过上层做了一层封装因此只有有迭代器才可以使用我们的范围for
8.数据容量控制
数据的容量控制我们就需要实现以下几个函数
8.1 size和capacity
首先是我们获取我们有效数据和容量大小的函数 size_t size()const{return _size;}size_t capacity()const{return _capacity;}
8.2 empty
其次是我们判断我们的有效数据是否为空 bool empty()const//这里不仅仅要被我们的普通对象调用也要被我们的const对象调用{return _size 0;}
最后比较关键的两个就是我们有效数据控制和我们容量控制的函数
容量控制函数 void reserve(size_t n){if (n _capacity)//只有空间大小大于当前才需要进行扩容{char* temp;temp new char[n 1];//多的一个用于存储/0memcpy(temp, _str,_size1);delete[]_str;_str temp;_capacity n;}}
对于扩容逻辑我想大家并不陌生这里就是我们开辟一个新的扩容后的空间再将我们当前的内容拷贝到我们扩容后的空间最后将我们的当前指针指向新开辟好的空间即可。 有效数据个数控制函数 void resize(size_t n, char c \0){if (n _size){_str[n] \0;_size n;}else{reserve(n);for (int i _size; i n; i){_str[i] c;}_size n;_str[_size] \0;}} 以上我们一共存在三种情况 n_size 直接删除数据只需要我们将有效位置的位置的下一个置为‘/0’,然后改变我们的——_size即可 _sizencapacity 只需要将剩余的空间初始化这里只需要从原先的_size开始依次往后填写直到达到我们有效数据个数即可最后需要改变我们的_size大小然后最后一位存上我们的/0。 ncapacity 扩容初始化我们这里的操作只是比我们的情况二多了一个扩容操作这里小编将情况三和情况二的判断放在了我们的reserve函数中大家可以体会一下。 9.数据修改部分
9.1 push_back
我们的push_back通常只是在后面添加字符但是在添加字符的过程中我们需要注意到的就是我们在添加过后是否需要进行增容代码如下 void push_back(char c){if (_capacity _size){reserve(_capacity 0 ? 4 : _capacity * 2);}_str[_size] c;_size;_str[_size] \0;}
这里我们扩容逻辑是当我们的_capacity和我们的_size相等时就需要进行扩容也就是说当我们的有效数据和我们的容量大小相等时就需要进行扩容这里我们的扩容逻辑就是当我们的容量为0的时候就只开四个空间其余情况按照旧容量的两倍进行扩容最后就是将我们的添加的字符放在我们的数组末然后将我们的_size最后记得将有效数据的下一位赋值上我们的/0即可。
9.2 append添加字符串
这里小编仅仅给大家实现了我们append添加字符串的那个版本在实现的过程中我们任然需要注意的是我们的扩容操作代码如下 void append(const char* str){size_t len strlen(str);if (_size len _capacity)//判断是否需要进行扩容{reserve(_size len);//扩容到能够存储我们新增字符串大小}strcpy(_str _size, str);//从_size位置开始将我们新增字符串复制到该后面_size _size len;//更新我们的_size}
9.3 运算符重载
我们的运算符是我们string类常用于添加我们的字符串或者字符的一个操作符那么该如何同时能添加字符串和我们的操作符的呢很简单那就是我们的函数重载。
字符版本 string operator(char c)//这里我们的*this并没有被销毁所以可以使用引用返回{push_back(c);return *this;//注意我们的需要返回后的值}
这里我们发现实际上这里只是对我们push_back的一个复用那么字符串版本呢相信聪明的小伙伴已经猜到了没错这里就是对我们append的一个复用。
字符串版本 string operator(const char* str){append(str);return* this;}
9.4 clear函数
clear函数的作用就是清除我们string对象中所有有效元素这实际上是非常简单的一种操作只需要更改我们的_size为,以及将我们数组的起始位置填上/0即可。 void clear(){_str[0] \0;_size 0;}
9.5 insert
我们的insert小编这里也给大家实现两个版本,一个是在pos位置前插入我们n个字符c一个是在我们pos位置前插入字符串。
版本一 void insert(size_t pos,size_t n, char c){assert(pos _size);//判断pos位置的合理性if (_size n _capacity)//判断是否需要进行扩容操作{reserve(_size n);}size_t end _size;//end指向我们的数组末尾while (end pos end ! npos)//当我们的end大于我们的pos且我们end值合理时{_str[end n] _str[end];//往后移动数据--end;//pos位置以及该后的得全部要往后移n位}for (size_t i 0; i n; i)//从pos位置写入我们n个c{_str[pos i] c;}_size n;//修改我们的_size的值} 版本二 void insert(size_t pos, const char* str){assert(pos _size);//判断pos位置的合法性size_t len strlen(str);//获取字符串长度方便后续操作if (_size len _capacity)//判断是否需要扩容{reserve(_size len);}size_t end _size;while (end pos end ! npos)//移动元素{_str[end len] _str[end];--end;}for (size_t i 0; i len; i)//写入元素{_str[pos i] str[i];}_size len;}
这里我们的版本二实际上和我们版本一的思路是一样的只不过该插入字符串时需要判断字符串长度才能进行元素的移动和元素的写入。
9.6 erase
删除pos位置开始的len长度的字符
这里我们的删除我们是需要分情况讨论的
当我们的poslensize或者我们的lennpos,那么说明我们pos位置开始的值是要全部删除的也就是。 2.诺poslensize,那么我们只需要删除我们pos位置的len长度的字符即可这里我的 思路是将poslen位置后的值按次序移到我们pos位置以及该后面位置进行覆盖直到我们的/0_size的位置就是我们/0存储位置也被移过来之后就完成了我们的删除。 代码如下 void erase(size_t pos, size_t lennpos){assert(pos _size);//判断pos位置的合法性if (len npos || pos len _size){//全部删除_str[pos] \0;_size pos;}else{//部分删除size_t end pos len;while (end _size)//注意我们是,因为此处需要将/0也移过来{_str[pos] _str[end];}_size _size - len;}}
9.7 substr
该函数的作用是截取从pos位置开始的len长度的字符串注意该函数的返回值是一个我们的string对象该函数也有两类情况
情况一lennpos或者lenpossize,这里就需要将我们pos后面的值全部截取但是对于截取我们部分截取和全部截取的逻辑都是一致的这里我们需要注意的是我们需要修正我们的len值否则就会造成我们的越界截取。
情况二poslensize这里我们只需要做到部分截取即可这里的逻辑是首先构造一个string对象将其空间开辟好然后将pos位置极其以后的值全部写入到该对象即可 string substr(size_t pos 0, size_t len npos){assert(pos _size);size_t n len;if (len npos || pos len _size){//修正lenn _size - pos;}//截取逻辑string tmp;tmp.reserve(n);for (size_t i pos; i pos n; i){tmp _str[i];//复用}return tmp;}
9.8 []运算符的重载
为什么要重载我们的[]呢因为我们的string类是将我们的底层数组封装了外界并不能直接访问因此我们要提供我们的[]接口给大家使用从而间接访问到我们的底层数组但是需要注意的是我们[]涉及到数据的写入和读取因此该要提供给我们的普通对象读取和写入的权力给我们的const对象只提供读取的权力因此这里也就要实现两个版本 char operator[](size_t index){assert(index _sizeindex0);return _str[index];}const char operator[](size_t index)const{assert(index _size index 0);return _str[index];}
10.c_str
之前给大家说过我们这个接口是为了和我们C语言进行配合因此我们返回的就是我们C语言字符串类型也就是我们的字符指针 const char* c_str()const{return _str;}
11.关系运算符 bool operator(const string s) const{int ret memcmp(_str, s._str, _size s._size ? s._size : _size);return ret 0 ? _size s._size : ret 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 _size s._size memcmp(_str, s._str, _size)0;}bool operator!(const string s)const{return !(*this s);}
对于我们的关系运算符我们这里使用的是C语言的memcmp函数去比较我们的大小由于我们的memcmp是按一个字节一个字节去进行比较的因此我们是按string中有效数据个数最短的那个对象去进行我们的比较操作但是最短的字符比较肯会出现以下两类情况 这里我就给大家简单的介绍一下我们的运算符以及运算符的重载逻辑其他的都是对两者的复用
预算符重载 运算符重载
12. find函数
对于find函数我们这里给大家实现了两个版本一个是查找单个字符一个是查找字符串对于查找字符串我们可以使用我们C语言中学习过的strstr函数进行字串的查找
单个字符版本 size_t find(char c, size_t pos 0) const{assert(pos _size);for (size_t i pos; i _size; i){if (_str[i] c){return i;}}return npos;}
字符串版本 size_t find(const char* s, size_t pos 0) const{assert(pos _size);const char* ptr strstr(_str pos, s);if (ptr){return ptr - _str;}else{return npos;}}
对于这里的实现都比较简单大家只需要注意找不到返回我们的npos即可。
13.流插入操作符重载
在给大家介绍友元函数的时候就给大家介绍过一次我们Date类的流插入运算符的重载由于我们的流插入的调用参数的原因我们不得不把我们的该函数写在类外然后又由于我们要直接去访问我们的私有成员变量又不得不去构造我们的友元关系。那么实际上我们也可以通过间接的函数去获得我们的内部成员但是我们C语言是不常使用的但是对于我们string的操作符我们是否可以调用我们的C-str接口去实现我们这个接口呢 答案是不行的原因是我们的C-str返会的是我们C语言的字符串因此遇到\0会自动停止打印但是我们的string类是以我们的size作为结束标志因此这里是不可取的。 因此我们这里是通过构造友元关系实现的具体实现代码如下 ostream operator(ostream out, const string s){for (auto ch : s){out ch;}return out;}
14.流提取
在实现流提取我们需要注意一点就是我们这里的流提取在遇到空格和我们的\n就会停止读取因此我们要在此处加以我们的判断。 istream operator(istream _cin, xhj::string s){char ch;_cin ch;while (ch ! ch ! \n){s buff;_cin ch;}return _cin;}
这里我给大家提供了一个版本不过这个版本是一个错误版本且就算成功该也会带来极大的资源损耗原因在于 _cin在输入数据到缓冲区的时候我们的空格和\n并不能被存储在我们的缓冲区因为这里被认为是我们多个值的间隔会造成死循环这里就算不会造成死循环每次读取一个值就写入我们的对象中就会导致我们空间的扩容过于频繁导致资源损耗。 那么对于以上问题我们各自采用的解决方案是什么呢 首先是解决我们空格和我们\n的读取问题这里就需要我们使用我们的get函数这个函数就会对其进行读取然后扩容过于频繁我们这里的解决方案就是使用一个数组进行写入当这个数组被写满之后直接一次性写入到我们的对象中接下来请看代码 istream operator(istream _cin, xhj::string s){char ch _cin.get();s.clear();//为了每次达到输入后覆盖的效果char buff[128];//这里使用数组首先是避免了每次造成的多次开辟空间而导致效率上的降低//其次避免了我们预先在堆上开辟大量空间而可能造成空间浪费的情况//清理缓冲区while (ch || ch \n){ch _cin.get();}int i 0;while (ch ! ch ! \n){buff[i] ch;if (i 127){ buff[i] \0;s buff;i 0;}ch _cin.get();}if (i ! 0){buff[i] \0;s buff;}return _cin;}
此外我们的clear是对该对象中原先的值进行清理以达到我们后输入的值对其进行覆盖的效果。
总代码
#includeiostream
#includeassert.h
using namespace std;
namespace xhj{class string{friend ostream operator(ostream _cout, const xhj::string s);friend istream operator(istream _cin, xhj::string s);public:typedef char* iterator;typedef const char* const_iterator;public:string(const char* str ){_size strlen(str);_capacity _size;_str new char[_capacity 1];//由于多一位需要存储我们的‘/0’,因此要进行1strcpy(_str, str);}//对于传统写法我们的现代写法依赖于编译器对数据的初始化如果编译没有对数据进行初始化操作那么在交换的过程中很可能会出现随机值的情况//然后在最后对tmp进行析构的时候就会出现程序崩溃的情况所以这里我们需要先走初始化列表string(const string s):_str(nullptr),_size(0), _capacity(0){//传统写法/*_size s._size;_capacity s._capacity;_str new char[_capacity 1];memcpy(_str, s._str,s._size1);*///C语言的字符数组是以\0为终止算长度//string不看\0而是以size算终止长度//现代写法string tmp(s._str);swap(tmp);}/*string operator(const string s){if (this ! s){//传统写法char* temp new char[s._capacity 1];memcpy(_str, s._str, s._size 1);delete[]_str;_str temp;_size s._size;_capacity s._capacity//现代写法拷贝构造一个新的对象让两者进行交换可以将新的值搞到我们对应的对象然后就空间可以让我们的局部对象出了作用域出了析构对象进行销毁string temp(s);std::swap(_str, temp._str);std::swap(_size, temp._size);std::swap(_capacity, temp._capacity);}return *this;}*/string operator(string s)//传值传参调用拷贝构造{swap(s);return *this;}~string(){delete[] _str;_str nullptr;_size _capacity 0;}// iteratoriterator begin(){return _str;}const_iterator begin() const{return _str;}iterator end(){return _str _size;}const_iterator end() const{return _str _size;}// modifyvoid push_back(char c){if (_capacity _size){reserve(_capacity 0 ? 4 : _capacity * 2);}_str[_size] c;_size;_str[_size] \0;}string operator(char c){push_back(c);return *this;}void append(const char* str){size_t len strlen(str);if (_size len _capacity){reserve(_size len);}strcpy(_str _size, str);_size _size len;}string operator(const char* str){append(str);return* this;}void clear(){_str[0] \0;_size 0;}void swap(string s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}const char* c_str()const{return _str;}/// capacitysize_t size()const{return _size;}size_t capacity()const{return _capacity;}bool empty()const{return _size 0;}void resize(size_t n, char c \0){if (n _size){_str[n] \0;_size n;}else{reserve(n);for (int i _size; i n; i){_str[i] c;}_size n;_str[_size] \0;}}void reserve(size_t n){if (n _capacity){char* temp;temp new char[n 1];memcpy(temp, _str,_size1);delete[]_str;_str temp;_capacity n;}}char operator[](size_t index){assert(index _sizeindex0);return _str[index];}const char operator[](size_t index)const{assert(index _size index 0);return _str[index];}///relational operatorsbool operator(const string s) const{int ret memcmp(_str, s._str, _size s._size ? s._size : _size);return ret 0 ? _size s._size : ret 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 _size s._size memcmp(_str, s._str, _size)0;}bool operator!(const string s)const{return !(*this s);}// 返回c在string中第一次出现的位置size_t find(char c, size_t pos 0) const{assert(pos _size);for (size_t i pos; i _size; i){if (_str[i] c){return i;}}return npos;}// 返回子串s在string中第一次出现的位置size_t find(const char* s, size_t pos 0) const{assert(pos _size);const char* ptr strstr(_str pos, s);if (ptr){return ptr - _str;}else{return npos;}}// 在pos位置上插入字符c/字符串str并返回该字符的位置void insert(size_t pos,size_t n, char c){assert(pos _size);if (_size n _capacity){reserve(_size n);}size_t end _size;while (end pos end ! npos){_str[end n] _str[end];--end;}for (size_t i 0; i n; i){_str[pos i] c;}_size n;}void insert(size_t pos, const char* str){assert(pos _size);//判断pos位置的合法性size_t len strlen(str);//获取字符串长度方便后续操作if (_size len _capacity)//判断是否需要扩容{reserve(_size len);}size_t end _size;while (end pos end ! npos)//移动元素{_str[end len] _str[end];--end;}for (size_t i 0; i len; i)//写入元素{_str[pos i] str[i];}_size len;}// 删除pos位置上的元素并返回该元素的下一个位置void erase(size_t pos, size_t lennpos){assert(pos _size);//判断pos位置的合法性if (len npos || pos len _size){_str[pos] \0;_size pos;}else{size_t end pos len;while (end _size){_str[pos] _str[end];}_size _size - len;}}string substr(size_t pos 0, size_t len npos){assert(pos _size);size_t n len;if (len npos || pos len _size){n _size - pos;}string tmp;tmp.reserve(n);for (size_t i pos; i pos n; i){tmp _str[i];}return tmp;}private:char* _str;size_t _capacity;size_t _size;const static size_t npos;};const size_t string::npos -1;ostream operator(ostream out, const string s){for (auto ch : s){out ch;}return out;}istream operator(istream _cin, xhj::string s){char ch _cin.get();s.clear();//为了每次达到输入后覆盖的效果char buff[128];//这里使用数组首先是避免了每次造成的多次开辟空间而导致效率上的降低//其次避免了我们预先在堆上开辟大量空间而可能造成空间浪费的情况//清理缓冲区while (ch || ch \n){ch _cin.get();}int i 0;while (ch ! ch ! \n){buff[i] ch;if (i 127){ buff[i] \0;s buff;i 0;}ch _cin.get();}if (i ! 0){buff[i] \0;s buff;}return _cin;}};