欢迎各位大佬光临本文章!!!

还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。

本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。

我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog

我的gitee:冰冰棒 (bingbingsupercool) - Gitee.comhttps://gitee.com/bingbingsurercool


系列文章推荐

冰冰学习笔记:《运算符重载---日期类的实现》

冰冰学习笔记:《一步一步带你实现《顺序表》》


目录

系列文章推荐

前言

1.string类的基本结构

2.string类中默认成员函数

2.1构造函数与析构函数

2.2拷贝构造与赋值运算符重载

3.元素访问与容量函数

3.1[ ]的重载与at函数

3.2容量函数

3.3resize函数与reserve函数

4.迭代器的实现

5.插入与删除函数

5.1push_back与pop_back

5.2append与+=

5.3insert与erase

6.find与substr函数

7.运算符重载

总结


前言

string类是C++库中管理字符串的类,里面将字符串的处理函数进行了封装,可以方便我们进行调用。相比于C语言中字符串的表达方式,C++的string类管理起来更加方便,快捷。但是string类的设计也一直受到吐槽,原因很多,比如复杂冗余,很多接口的设计根本用不到,因此很多人都会去自己实现好用的string类,一些面试题中也经常会让我们简单模拟实现string类,今天我们就盘一盘string类的模拟实现。

1.string类的基本结构

string类的内部函数非常的多,总共大约有106个函数,但是常用的也就是十几个,我们平常使用时可以查看文档进行学习。string类是处理字符和字符串的容器,底层的存储与我们之前实现的顺序表一致,一个字符指针指向存储的内容,以及size和capacity记录大小和容量。因此我们模拟实现时就需要完成string类中最基本的功能即可,不需要对其面面俱到。例如在访问string类中存储字符串的内容时,string类提供了[ ]和at函数两种访问形式,都是返回pos位置处的字符,两者都会对越界进行检查,但是at抛出异常,[ ]则直接报错,除此之外并没有什么不同,并且基本都会使用[ ]进行访问。

string类中部分函数接口:

string类中还含有一个静态成员变量npos, 其表示最大可能值,在内部函数的拷贝和删除时通常作为缺省值出现,代表操作范围未指定或者大于字符串本身长度时将“直到字符串结束”。通常将其定义为无符号整型-1。

所以string类的模拟实现的成员就可以实现成下面的样子:

namespace lb
{class string{public:private:char* _str;size_t _size;size_t _capacity;public:const static size_t npos = -1;//const可以给值,当作定义-->定义初始化};
}

这里我们将自己实现的string类放在自己的命名空间中,避免与库中实现的出现冲突。这里还涉及到一个知识点,我们之前学习的静态成员变量需要在类内进行声明,类外实现定义。但是C++允许被const修饰的静态成员变量在类中声明的时候直接给予定义,如上面代码所示,npos=-1此时的n-1不再是缺省值,而是直接就是定义形式。当然这与常规变量看起来格格不入,这明明是声明的地方,结果const成员还能给出定义,这的确也是值得吐槽的地方。

2.string类中默认成员函数

2.1构造函数与析构函数

string类中的构造函数多达七个,但是常用的也就三个构造函数,构造一个空类,使用字符串进行构造,使用string类进行构造。

但是我们要注意到,当我们使用string类创造一个对象时,实际开辟的空间并非是我们字符数量的空间,总是会多开一个空间来存放'\0'作为结束标识。而里面的size指向的也是最后一个字符的下一个位置。所以我们在搭建构造函数时就需要注意空串的情况,对其赋予缺省值来确保存储一个'\0'。

经过讨论我们写出了第一个版本的构造函数:

//构造函数冗余版本
string():_str(new char[1]), _size(0), _capacity(0)
{_str[0] = '\0';
}
string(const char* str ):_str(new char[strlen(str)+1]), _size(strlen(str)), _capacity(strlen(str)){strcpy(_str, str);}

这里使用了函数重载,对于空类型则开辟一个空间,并将'\0'放入_str[0]中。对于字符串类型则调用另一个构造函数。但是此种方式并非最优方式,当传入空字符时,我们需要的是将第一个位置放入'\0',空字符串" "天生带有'\0',因此我们可以将其作为缺省值赋予构造函数。

对于字符串的构造函数,我们连续调用了三次strlen()函数来计算,strlen函数是一个O(N)的算法,多次调用难免会出现效率低下的结果。但是我们不能像下面那样优化:原因在于初始化列表的构建顺序与声明顺序有关,而与列表顺序无关。

//构造函数错误修改版本
string(const char* str ): _size(strlen(str)),_str(new char[_size+1]), _capacity(_size){strcpy(_str, str);}

因此我们不如统统将其放入到函数体中进行初始化,因此改进后的构造函数如下所示

string(const char* str = "")//构造{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);//数据拷贝}

 string类中析构函数的实现比较简单,只需要将开辟的内存进行释放即可。

~string()//析构{delete[] _str;_str = nullptr;_size = _capacity = 0;}

2.2拷贝构造与赋值运算符重载

拷贝构造函数和赋值运算重载是会默认生成的一种成员函数,在日期类的模拟实现中我们也讲解过,对于没有开辟空间的类,使用默认生成的拷贝构造与赋值没有问题,但是在涉及到内存空间分配时,在使用简单的浅拷贝就会出现问题。

浅拷贝只是简单的值拷贝,两个string类对象进行浅拷贝后,原本两个_str指向的不同空间变为指向同一个空间,将会造成两个后果:1)内存泄漏,拷贝对象原先指向的空间没有得到释放,_str指向了被拷贝的空间,原先的地址空间丢失,通常在赋值时会出现。2)析构报错,在调用析构函数时,由于两个类将会指向同一块空间,析构函数将会对同一块空间进行析构两次,程序报错。

        因此在string类中实现的拷贝函数和赋值运算符重载应该是深拷贝,需要开辟一块相同大小的空间并且将原先指向的空间进行释放,然后将被拷贝字符串中的内容赋予拷贝的字符串。

因此,拷贝构造与赋值重载的传统写法如下:

//--传统写法string(const string& s)//拷贝构造--深拷贝:_str(new char[s._capacity + 1]), _size (s._size), _capacity (s._capacity){strcpy(_str, s._str);}string& operator=(const string& s)//赋值{if (&s != this){char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);//先拷贝再释放避免空间开辟失败delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;}

在传统写法中,拷贝函数的逻辑比较好理解,开辟空间并将原先内容一一拷贝即可。赋值重载需要注意以下几点:1)需要避免自己与自己赋值,自己与自己赋值并没有意义,因此需要对其进行条件判断。2)一定要先开辟空间拷贝之后在释放原先空间,如果先释放再拷贝,放拷贝失败时,将导致原先空间被收回,_str变为野指针。

既然称前面的写法为传统写法,那么就有现代写法。现代写法真是将“资本家”思维运用到了极致。我们并没有自己去实现拷贝过程,而是利用“打工仔”tmp去调用构造函数来完成拷贝构造。当打工仔tmp利用构造函数构造完毕后,我们在调用string类中提供的swap交换两个类的内容即可。

string类内部提供了swap函数接口,swap函数交换string类中的内容,例如创建string s1("abc"),s2("123");将s2的内容交换到s1中可以如下调用:s1.swap(s2);

因此,现代写法的拷贝构造实际上是复用了构造函数,tmp调用构造函数来初始化,初始化内容就是需要拷贝的内容s._str。构造完毕后,我们交换对象即完成拷贝构造。

void swap(string& tmp)//--内部调用全局swap完成交换{::swap(_str, tmp._str);::swap(_size, tmp._size);::swap(_capacity, tmp._capacity);}
//--现代写法string(const string& s)//利用别人去拷贝:_str(nullptr), _size(0), _capacity(0){string tmp(s._str);swap(tmp);}

而对于赋值重载的现代写法更是巧妙,在传参的时候使用的是传值传参,直接就会进行一份拷贝构造,然后进行交换,交换过后将this指针指向的内容进行返回,而出了函数作用域后,拷贝构造的临时对象将被释放。因此我们即完成了赋值又完成了空间释放,一举两得。

string& operator=(string s)//--参数采用传值完成拷贝{swap(s);return *this;}

3.元素访问与容量函数

3.1[ ]的重载与at函数

前面就提到了string类可以向数组那样使用[ ]来进行元素访问,这得益于运算符的重载,string类将 [ ]进行重载,可以实现直接利用下标来访问元素。C++库中的[ ]实现了两种,congt类型和普通类型。

at函数和[ ]的重载类似,二者唯一的区别就是报错的方式不同。

因此两个函数的实现如下:此处实现时对于月结的处理均为报错

        char& operator[](size_t pos){assert(pos >= 0 && pos < _size);return _str[pos];}const char& operator[](size_t pos)const{assert(pos >= 0 && pos < _size);return _str[pos];}char& at(size_t pos){assert(pos >= 0 && pos < _size);return _str[pos];}const char& at(size_t pos)const{assert(pos>=0&&pos < _size);return _str[pos];}

3.2容量函数

string类中容量函数具有很多,但是常用的就那么几个。其中length是string类中特有的函数,其与size功能完全相同。

        size_t size()const{return _size;}size_t capacity()const{return _capacity;}void clear(){_str[0] = '\0';_size = 0;}bool empty()const{return _size == 0;}

3.3reserve函数resize函数

这两个函数比较重要,两个函数都具备开辟空间的作用,不同的是resize函数还会对开辟的空间进行初始化为 '\0' 。两个函数都会接受一个空间大小的n,并将空间扩展到n,但是当n<capacity时,reserve函数并不会将空间缩小,resize函数会将空间缩小到n。

因此reserve函数的实现逻辑与以前实现的增容逻辑没有变化,都是开辟新空间,并拷贝内容,然后释放旧空间。

        void reserve(size_t n)//开辟空间{if (n > _capacity)//{char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}

而resize函数就需要分情况来讨论,当n大于capacity时复用reserve进行空间开辟,并将新拓展的空间初始化为'\0';当n小于capacity时我们需要将空间缩小,实际就是控制size的大小。

     void resize(size_t n, char ch = '\0')//开辟空间加初始化{if (n > _size)//增加数据{reserve(n);for (size_t i = _size; i < n; i++){_str[i] = ch;}_str[n] = '\0';_size = n;}else//删除数据{_str[n] = '\0';_size = n;}}

4.迭代器的实现

  迭代器是容器中一种类似于指针访问元素的工具,但是迭代器并不是指针。对于空间连续的string类和vector类我们可以使用指针直接实现迭代器,但是对于空间不连续的链表来说,迭代器就不能使用原生指针。

迭代器一般会提供begin,end,rbegin,rend等函数接口。begin,end等接口还分为只读类型的const_iterator。

        //迭代器---指针typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const typedef char* const_iterator;const_iterator begin()const{return _str;}const_iterator end()const{return _str + _size;}

5.插入与删除函数

string类中对于字符的插入与删除提供的接口也很多,但是常用的接口却不多,+=操作符的重载最常用,该操作符可以实现对字符,字符串,string类进行+=操作。在实现+=运算符时,可以先实现push_back,append等函数,然后复用这些函数进行实现。

5.1push_back与pop_back

push_back,pop_back函数就是在末尾增减一个字符,与顺序表实现的逻辑一样。

     void push_back(char ch)//插入{if (_size == _capacity)//检查空间是否足够{reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}void pop_back(){assert(_size > 0);_size--;_str[_size] = '\0';}

        值得注意的一点是无论插入还是删除,都需要重新在字符串后面显示添加'\0'作为字符串结束标记。 

5.2append与+=

append函数实现的重载比较多,但是常用的并不多。通常使用都是对字符串的增加操作。

对于使用append增加一个字符串,我们首先需要知道字符串的长度,然后检查剩余容量是否能够容纳新增字符串的长度,如果不能还需要扩容处理。剩下的就是将字符连接到原先字符串的后面即可,我们可以使用strcpy函数进行操作。

     void append(const char* str){size_t len = strlen(str);if (_size+len > _capacity){reserve(_size+len);}strcpy(_str + _size, str);//strcat(_str , str);//效率低,因为要遍历找'\0'_size += len;}

这里需要注意,很多人再想到字符串连接时需要使用strcat函数,这样使用是没有问题的,与strcpy函数一样:strcat(_str+_size,str);但是如果我们这样使用效率会很低: strcat(_str,str);原因在于strcat需要从头寻找 '\0' 浪费时间。

在append实现字符串的插入后,就可以顺手实现两个重载函数。

     //插入一个string类void append(const string& s){append(s._str);}//插入n个字符void append(size_t n, char ch){reserve(_size + n);for (size_t i = 0; i < n; i++){push_back(ch);}}

        对于+=操作符的实现,就是将push_back与append进行了合并。

        string& operator+=(char ch){push_back(ch);return *this;}string& operator+=(const char* ch){append(ch);return *this;}string& operator+=(const string s){append(s);return *this;}

5.3insert与erase

insert函数库中也提供很多的重载版本,我们只需要模拟实现常用的即可。insert函数的逻辑就是在pos位置之前插入字符或者字符串。

在模拟实现时我们还是需要先检查pos是否符合条件,并且判断空间是否够用。在满足这些条件后就需要对pos及pos之后的元素向后移动,将pos指向的空间空出来,然后插入新字符。

     string& insert(size_t pos, char ch)//插入字符{assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}size_t end = _size + 1;while (end>pos){_str[end] = _str[end - 1];--end;}_str[pos] = ch;++_size;return *this;}

插入过程如下所示:

而对于插入n个字符的insert就需要先移出n个字符的位置出来,但是我们需要先对n进行判断,如果n等于0,则说明没有插入,直接返回即可,不然将会出现错误。移动完毕后将字符插入进去即可。而插入一个字符串就可以先计算出字符串的长度,然后与插入n个字符时一样挪动数据即可。

        string& insert(size_t pos, size_t n, char ch)//插入n个字符{assert(pos <= _size);if (n == 0)//插入0个字符{return *this;}if (_size+n > _capacity){reserve(_size+n);}size_t end = _size + n;while (end>= pos+n){_str[end] = _str[end-n];--end;}while (pos <= end){_str[pos] = ch;++pos;}_size += n;_str[_size] = '\0';return *this;}string& insert(size_t pos, const char* str){assert(pos <= _size);size_t len = strlen(str);if (len == 0)//极端情况插入空串{return *this;}if (_size+len > _capacity){reserve(_size+len);}size_t end = _size + len;while (end>=pos+len){_str[end] = _str[end - len];end--;}strncpy(_str + pos, str, len);_size += len;return *this;}

erase函数我们只需要实现删除pos位置开始后的n个字符即可,如果n大于字符串长度时则将后面的字符全部删除。

删除时我们使用strcpy函数进行了覆盖。

     void erase(size_t pos, size_t len = npos){assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{strcpy(_str + pos, _str + pos + len);_size -= len;}}

6.find与substr函数

find函数在string类中给出了,但是在库中还有一个全局的find函数。

string类中提供的find函数在找到时返回的是字符的下标或者字符串首字母的下标,找不到返回的是npos。

查找字符我们只需要遍历然后比对即可,找到就返回下标,找不到返回npos,而对于字符串的查找,可以复用库函数strstr,只不过strstr找到返回字符串的指针,找不到返回的是nullptr。

     size_t find(const char ch, size_t pos = 0)const {assert(pos < _size);size_t i = 0;for (i = pos; i < _size; i++){if (_str[i] == ch){return i;}}return npos;}size_t find(const char* sub, size_t pos=0)const{assert(sub);assert(pos < _size);const char* ptr = strstr(_str + pos, sub);if (ptr == nullptr){return npos;}else{return ptr - _str;}}

有时我们还需兼顾C语言的环境,C语言中没有string类,只有字符串,因此我们还需要实现接口c_str。

     const char* c_str()const{return _str;}

substr函数是取出string类中pos之后的n个字符的函数,当n没有显示提供或者大于size时,则将pos之后的字符全部取出并存放在string类中,然后返回。

     string substr(size_t pos, size_t len = npos) const{assert(pos < _size);size_t realen = len;if (len == npos || len + pos > _size){realen = _size - pos;}string ret;for (size_t i = 0; i < realen; i++){ret += _str[pos + i];}return ret;}

7.运算符重载

运算符的重载没有什么难点,和日期类一样,只需要实现==和>运算符的重载后其他的进行复用实现即可。

        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类的友元函数,例如我们将string类在命名空间lb中实现,那我们在使用string类时需要指定命名空间lb,将流提取函数实现为lb空间中的全局函数也可以进行操作。

流插入运算符中我们需要考虑效率问题,为了避免频繁的插入,我们需要实现一个类似缓冲区的容器进行临时存放函数。并且插入字符时通常用空格或者'\r'来作为间隔符,cin是不识别这两个字符的,因此我们需要使用istream中的接口函数get()。

 ostream& operator<<(ostream& out, const string& s){for (size_t i = 0; i < s.size(); ++i){out << s[i];}return out;}//istream& operator>>(istream& in, string& s)//频繁扩容//{//  char ch = in.get();//  while (ch != ' ' && ch != '\n')// {//     s += ch;//        ch = in.get();//   }// return in;//}istream& operator>>(istream& in, string& s)//{s.clear();//清除原有字符char ch = in.get();const size_t N = 32;char buff[N];size_t i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == N - 1){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}buff[i] = '\0';s += buff;return in;}

总结

string类的模拟实现并不难,就是一些基础知识的复用与堆叠,博主实现的string类仅仅是简单的练习版本,函数接口的实现仁者见仁智者见智,大家可以根据自己的想法进行模拟实现。代码链接:完成string的模拟实现 · 5b291e4 · 冰冰棒/C++ - Gitee.com

在拷贝构造和赋值重载时还有一种拷贝方式可以实现,即写时拷贝,有兴趣的可以观看其他博主的文章进行欣赏。

冰冰学习笔记:string类的简单模拟相关推荐

  1. 冰冰学习笔记:list的简单模拟

    欢迎各位大佬光临本文章!!! 还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正. 本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬.帅哥.美女点点支 ...

  2. 冰冰学习笔记:反向迭代器的模拟

    欢迎各位大佬光临本文章!!! 还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正. 本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬.帅哥.美女点点支 ...

  3. 冰冰学习笔记:类与对象(上)

    欢迎各位大佬光临本文章!!! 还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正. 本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬.帅哥.美女点点支 ...

  4. 冰冰学习笔记:类与对象(下)

    欢迎各位大佬光临本文章!!! 还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正. 本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬.帅哥.美女点点支 ...

  5. 《C++ Primer Plus》学习笔记-string类和标准模板库

    第16章 string类和标准模板库(本书附录G有详细介绍) 16.1 string类 string类是由头文件string支持的. string类位于命名空间std中. ctor标识是传统C++中构 ...

  6. 学习笔记——String类常用方法(字符、字节、比较、查找、替换、拆分、截取、结构、格式化、连接、入池、驱除空格、长度计算、大小写转换等)

    1.字符串与字符 在jdk1.9之前提供string数组实现了包装处理,在String类里面提供相应的转换处理方法,这些方法包含构造方法与普通方法两类. public String​(char[] v ...

  7. JavaS学习笔记 - String类

    文章目录 String 创建字符串 拼接 常用方法 空串和null StringBuffer insert delete replace toString reverse StringBuilder ...

  8. 冰冰学习笔记:进程概念

    欢迎各位大佬光临本文章!!! 还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正. 本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬.帅哥.美女点点支 ...

  9. 冰冰学习笔记:二叉树的进阶OJ题与非递归遍历

    欢迎各位大佬光临本文章!!! 还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正. 本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬.帅哥.美女点点支 ...

最新文章

  1. python列表题目_day5.python列表练习题
  2. 如何创建高效的业务连续性计划
  3. nfs自动挂载报错及解决方法
  4. xp系统怎样安装传真服务器,XP系统下如何安装传真机 XP系统安装传真机的详细步骤[图文教程]...
  5. [译] 探究 Swift 中的 Futures Promises
  6. nginx备忘录,错误a duplicate default server for 0.0.0.0:80
  7. 移动互联网时代,学iOS开发好吗
  8. node url模块
  9. 函数式编程与面向对象编程[1]: Lambda表达式 函数柯里化 高阶函数
  10. 服务器主板型号详解,支持Intel Xeon的主板
  11. 蓝桥杯近三年初赛题之一(15年b组)
  12. python再计算无限循环小数的循环节
  13. Windows 10 驱动开发 及 驱动签名
  14. android 网络编程 笔记,【Android 应用开发】Android 网络编程 API笔记
  15. 隧道联系测量高程传递水准数据处理流程及说明
  16. 【深度学习】损失函数详解
  17. linux输入特殊符号密码,linux 输入特殊符号
  18. MAC地址,IP地址和ARP协议
  19. 定义一个学生类Student,包含三个属性姓名、年龄、性别, 创建三个学生对象存入ArrayList集合中。 A:使用迭代器遍历集合。 B:求出年龄最大的学生,然后将该对象的姓名变为:小猪佩奇。
  20. ubuntu 下正确安装android手机驱动

热门文章

  1. C语言实现 输入密码显示星号******
  2. 打印机端口无法创建print spooler无法启动报错1068怎么办
  3. 基于SSM的学生选课系统
  4. 华硕天选3和神舟战神Z8那个好?
  5. Matlab安装MinGW64 Compiler
  6. html5怎么让图片垂直居中显示,css中如何实现图片垂直居中显示?
  7. wince 系统定制
  8. 评测:最好最有效的PDF在线无损转换免费工具Smallpdf
  9. C语言原码,反码和补码转换详解
  10. Cakewalk Z3TA+ 2 v2.2.3.51 win-mac 波塑形合成器加预设合集