C++入门第六篇—STL模板---string【下】string模板的实现(上)

简介: C++入门第六篇—STL模板---string【下】string模板的实现(上)

前言:

在上一篇文章中,我们详细介绍了string模板库的一系列函数,为了进一步加深我们的理解以及我们的代码能力,我们接下来来实现一下我们自己的string模板库,模拟实现的过程中我们要对很多细节进一步的把控理解,包括很多新的概念和方法,不仅仅是要熟练掌握string库,同时也要对C++的知识点的细节运用更加精确。这便是我们模拟实现的目的。

string模拟实现:

1.第一部分:实现string库的基础功能:

既然是string库,我们首先就要先能构建一个字符串为主体的类,由前面的知识点我们知道,string的模板本质上是一种类,故在这里我们模拟实现的时候,我们也将其按照一种类处理。

首先先让我们考虑一个类的成员变量应该都有什么,字符串的本质是什么?是数组,那么对于数组,我们就可以让其按照顺序表的方式去处理,在顺序表中,我们首先定义了一个指针来指向一个字符串数组,同时创建了两个整型size和capacity,其中size用来统计数组中元素的个数,capacity用来统计我们为这个数组开辟的内存的大小,故我们这样构建类的成员:

1.类成员:

class my_string
{
private:
  char* _str;
  size_t _size;
  size_t _capacity;
  const static size_t npos;
}

注意,由于我们的string类的参数经常给一个缺省值npos,实际上它代表-1,由于存储的是补码的原因,本质上它是一个全1的很大的数,这样保证了我们字符串的后续的处理是全面的,而不会出现遗漏的现象。

对于static修饰的变量,我们要让其在类外全局修饰如下:

const size_t string::npos = -1;//全局变量在类外定义,作为static使用

2.构造函数:

在有了成员之后,我们就要为类创建它的构造函数,由于我们需要在堆区动态开辟数组内存,故我们的构造函数是需要我们手动去书写动态开辟。但是,在写之前让我们先想好我们构造字符串时可能会出现的情况,我们可能会构造一个空的字符串或者默认给值的字符串,所以,我们就要针对这两种情况去构建我们的构造函数,首当其冲的便是初始化的问题,我们在这里可以使用初始化列表来实现如下:

string(const char* str="")//构造函数,我们默认构造函数最好是给全缺省,我们让其缺省值为空字符串,这样倘若什么也不传也不会是空指针,而是直接空字符串返回
  :_size(strlen(str)),//这里要注意细节,注意变量定义的顺序,
  _capacity(_size)
{
  _str = new char[_capacity + 1];//永远要留一个给\0
  strcpy(_str, str);
}

由之前的知识可以知道,在书写构造函数时,我们一般都给构造函数赋全缺省值,而在这里我们只需要传入的便是一个const的字符串作为参数,由于前面我们已经考虑到我们会有创建空字符串的情况,故我们在这里直接给其缺省值为” “,这样,倘若我们不传字符,则默认创建的字符串是空字符串,倘若传,则直接以我们传入的字符串作为次字符串的元素。在初始化列表这里,我们同样有一个细节,那就是我们的初始化顺序是怎样的?

由前面学到的知识可以知道,初始化列表的顺序并不是从上到下依次排列的,而是按照我们成员变量的顺序从上到下依次初始化,所以,我们在这里一定要注意数据流的传输顺序,比如,我们的指针变量是第一位,倘若我们初始化的时候先动态开辟,由于capacity此时还没有被初始化,编译器默认给一个随机值,这样我们的动态开辟的空间就炸了,同理,我们的capacity为其初始化的size的数据,但是size在第二位,故倘若capacity先被创建,此时的size也是随机值,这样我们后续的扩容就会出现很大的问题,故在这里我们有两种解决方案

1.第一种是调整成员变量的顺序,按照初始化列表的顺序调整
2.第二种是根据成员变量的顺序调整初始化列表和构造函数体

在这里我比较倾向于第二种,因为我们在构建一个类成员的时候是不会去思考其顺序的,没法做到精确的调整,不如随意调整顺序,然后根据顺序去调整初始化的顺序,**在这里,我思考到str为第一个成员,故比不可能让其首先初始化,故我们将动态开辟的过程写在构造函数体内,构造函数一定是先完成初始化列表后才能进入构造函数的函数体,然后,我们首先将字符串的长度传给size,然后再初始化capacity,这样,我们初始化的过程就没有随机值的问题了。**如上面的程序,还需要强调的一点,我们动态开辟我们的数组的时候,由于我们capacity是没有考虑到\0的,故我们要capacity+1,这一位是为\0开辟的,千万别忘了,否则字符串没法判断结束。

3.返回字符串长度和内存大小:

程序如下,由于我们之前学到的知识点,类内私有的成员是不能在类外直接访问的,所以我们只能通过类成员函数来访问类的的私有数据,如下:

//返回字符串的长度
void size()
{
  return _size;
}
//返回对象的内存大小
void capacity()
{
  return _capacity;
}

4.原始版本的打印字符串:

由于有了我们的size()函数,我们可以写一个最为简化的字符串遍历如下:

void c_str()
{
   int i=0;
   for(i=0;i<size();i++)
   {
      cout<<_str[i];
   }
}

但后续我们会通过迭代器去实现一个标准的字符串打印函数。

5.标准流输出赋值运算符重载:

首先,让我们先考虑到一个问题,我们的cout<<变量这种写法,要求我们的ostream流作为参数是一定要在我们的字符串前面的,但是在成员函数中,由于我们的this指针的默认性,它是作为第一个参数进入函数的,这样我们的参数顺序就和正好相反了,故我们的输出流函数就要作为全局变量写在类的外面,且参数要求第一个参数为osteam流,第二个参数是我们的string类的参数引用,如下:

ostream& operator<<(ostream& out, const string& s)//流输出要写成全员函数,写在里面自己带一个this了直接
{
  for (auto ch : s)//这里传迭代器必须要在前面的函数上加const,否则迭代器不匹配
  {
    out << ch;
  }
  return out;
}

在上一篇文章中,我们详解过迭代器的知识点,迭代器的本质可以理解为一种指针,再使用迭代器变量的时候我们也确实涉及到指针的移动以及指针的解引用问题,我们使用范围for来遍历字符串,这就需要我们有一个迭代器,也就是一个匹配上我们传入参数字符串的迭代器,在这里,我们的字符串是const 类型的,故我们对应的迭代器也就是通过char*指针包装起来的,故我们使用类型重定义将其包装起来

typedef char* iterator;//模拟实现迭代器访问容器,迭代器本质上就是指针实现的
typedef const char* const_iterator;//迭代器的类型要对应,本质上就是指针要对应,const和非const要分开

在这里我们提前准备好分别对应读写的迭代器,防止后续的传参出现问题。

由此我们就能使用范围for来打印字符串了,如上面的程序。

6.[ ]赋值运算符重载:

const char& operator[](size_t pos)const //重载[](只可读不可写),用了const修饰
{
  assert(pos < _size);//断言输入的下标合法性
  return _str[pos];
}

我们采取的思路,就是将指针对应的字符串的下标对应返回,我们这里使用了const修饰,故证明了这里只可读不可写也不可更改。

7.字符串最开头位置begin,字符串的结尾位置end的获取:

在这里,为了模仿string库中的beign cbegin end cend,我们采取了读写分离的函数书写方式:

const_iterator begin()const//是可以修改的,故不要加const
{
  return _str;
}
 const_iterator end()const
{
  return _str + _size;
}
 iterator begin()//这种情况下是可以修改的,对应的类会调用对应的迭代器,注意迭代器是很智能的,它会去匹配对应的指针
 {
   return _str;
 }
 iterator end()
 {
   return _str + _size;
 }

用对应的迭代器去接受相应的返回值,从而实现对字符传头尾位置的获取,然后通过操作迭代器,我们就能实现字符串的遍历,我们的范围for本质上就是在调用这个利用迭代器遍历的过程,其实现方式是相同的,但是是由编译器自己实现的,我们只需要为其准备好对应的迭代器和函数封装即可,编译器会自己封装。

8.strcmp字符串比较的运算符重载一系列函数:

**由于比较的逻辑性是可以互通的,故我们可以通过逻辑的复用使这一类的函数书写的非常快捷和准确,在进行date日期类函数的书写时,我们就已经使用了这种思路来进行比较运算符重载的书写,同时我们的比较的底层依旧使用C语言的strcmp函数来实现,**如下:

//对于运算符问题就是直接复用
 bool operator>(const string& d2)const//运算符>重载
 {
   return strcmp(_str, d2._str) > 0;
 }
 bool operator==(const string& d2)const//运算符==重载
 {
   return strcmp(_str, d2._str) == 0;
 }
 bool operator>=(const string& d2)const//运算符>=重载
 {
   return *this==d2||*this>d2;
 }
 bool operator<(const string& d2)const//运算符<重载
 {
   return !(*this >= d2);
 }
 bool operator<=(const string& d2)const//运算符<=重载
 {
   return !(*this > d2);
 }
 bool operator!=(const string& d2)const//运算符!=重载
 {
   return !(*this == d2);
 }

如上面,我们只需要写出== >,剩下的< != >= >=就都可以直接通过逻辑操作直接复用出来,学会这种思路,真的很关键,会让我们的代码非常简便。

9.尾插字符:

尾插数据,和顺序表一样,我们首先最需要关注的问题依旧是扩容的问题,我们在这里已经先进行一个判断,即我们size是否等于capacity,倘若等于,我们就进入扩容函数进行扩容,扩容的函数如下:

void reserve(size_t n)//调整容量
 {
   if (n > _capacity)
   {
     char* tmp = new char[n + 1];//注意要多开一个空间给\0
     strcpy(tmp, _str);//注意,C++没有类似C语言那样的realloc函数,C++只能手动开空间然后拷贝给过去后,释放掉原空间后,再把指针转移到新空间的位置
     delete[] _str;
     _str = tmp;
     _capacity = n;//别忘了扩容要改变_capacity
   }
 }

对于扩容的问题,我们可能首选到C语言的realloc函数,但是C++并没有提供这样的函数,如果用realloc就没法对数据进行初始化,所以我们使用new来开辟一块新的空间作为我们的新的字符串空间,然后将数据拷贝过去,delete掉之前的空间即可,然后调整capacity即可,如上面的程序。

由此,我们就可以进行我们的尾插字符的函数:

void push_back(char ch)//尾插字符
{
 if (_size == _capacity)
 {
   reserve(_capacity==0 ? 4:_capacity*2);//注意这里是有bug,倘若capacity为0的话即使扩大2倍依旧是0,故这里的问题很大,所以和当初一样,我们在开辟顺序表的时候使用了三目操作符,我们这里也使用三目操作符
 }
 _str[_size++] = ch;
 _str[_size] = '\0';//对于单个字符的尾插,别忘了\0
}

10.尾插字符串:

其大致的思路和尾插字符差不多,只不过调整为字符串罢了:

不过,我们首先要考虑一下加上字符串长度后能否超过我们的capacity,倘若超过就要扩容

void append(const char* str)//尾插一个字符串
 {
   size_t len = strlen(str);
   if (_size + len > _capacity)
   {
     reserve(_size + len);
   }
   strcpy(_str+_size, str);//注意开始拷贝的位置
   _size += len;
 }

11.+=赋值运算符重载:

string& operator+=(char c)//+=运算符重载,单字符
 {
   push_back(c);
   return *this;
 }
 string& operator+=(const char* arr1)//+=运算符重载,字符串
 {
   append(arr1);
   return *this;
 }

在这里包括字符和字符串两种,本质上他们就是复用了尾插的函数,但是+=赋值运算符是最常用的,故这个很关键。

目录
相关文章
|
1天前
|
算法 安全 编译器
【C++进阶】模板进阶与仿函数:C++编程中的泛型与函数式编程思想
【C++进阶】模板进阶与仿函数:C++编程中的泛型与函数式编程思想
|
1天前
|
存储 算法 程序员
【C++进阶】深入STL之 栈与队列:数据结构探索之旅
【C++进阶】深入STL之 栈与队列:数据结构探索之旅
|
1天前
|
存储 缓存 编译器
【C++进阶】深入STL之list:模拟实现深入理解List与迭代器
【C++进阶】深入STL之list:模拟实现深入理解List与迭代器
|
1天前
|
C++ 容器
【C++进阶】深入STL之list:高效双向链表的使用技巧
【C++进阶】深入STL之list:高效双向链表的使用技巧
|
1天前
|
编译器 C++ 容器
【C++进阶】深入STL之vector:深入研究迭代器失效及拷贝问题
【C++进阶】深入STL之vector:深入研究迭代器失效及拷贝问题
|
3天前
|
编译器 数据安全/隐私保护 C++
c++primer plus 6 读书笔记 第十三章 类继承
c++primer plus 6 读书笔记 第十三章 类继承
|
1天前
|
编译器 C++
【C++初阶】—— 类和对象 (下)
【C++初阶】—— 类和对象 (下)
|
1天前
|
存储 编译器 C++
【C++初阶】—— 类和对象 (中)
【C++初阶】—— 类和对象 (中)
|
1天前
|
存储 编译器 C语言
【C++初阶】—— 类和对象 (上)
【C++初阶】—— 类和对象 (上)
|
2天前
|
程序员 C语言 C++
【C++语言】继承:类特性的扩展,重要的类复用!
【C++语言】继承:类特性的扩展,重要的类复用!