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;
 }

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

目录
相关文章
|
23天前
|
编译器 C语言 C++
【c++丨STL】list模拟实现(附源码)
本文介绍了如何模拟实现C++中的`list`容器。`list`底层采用双向带头循环链表结构,相较于`vector`和`string`更为复杂。文章首先回顾了`list`的基本结构和常用接口,然后详细讲解了节点、迭代器及容器的实现过程。 最终,通过这些步骤,我们成功模拟实现了`list`容器的功能。文章最后提供了完整的代码实现,并简要总结了实现过程中的关键点。 如果你对双向链表或`list`的底层实现感兴趣,建议先掌握相关基础知识后再阅读本文,以便更好地理解内容。
29 1
|
1月前
|
算法 C语言 C++
【c++丨STL】list的使用
本文介绍了STL容器`list`的使用方法及其主要功能。`list`是一种双向链表结构,适用于频繁的插入和删除操作。文章详细讲解了`list`的构造函数、析构函数、赋值重载、迭代器、容量接口、元素访问接口、增删查改操作以及一些特有的操作接口如`splice`、`remove_if`、`unique`、`merge`、`sort`和`reverse`。通过示例代码,读者可以更好地理解如何使用这些接口。最后,作者总结了`list`的特点和适用场景,并预告了后续关于`list`模拟实现的文章。
51 7
|
2月前
|
存储 编译器 C语言
【c++丨STL】vector的使用
本文介绍了C++ STL中的`vector`容器,包括其基本概念、主要接口及其使用方法。`vector`是一种动态数组,能够根据需要自动调整大小,提供了丰富的操作接口,如增删查改等。文章详细解释了`vector`的构造函数、赋值运算符、容量接口、迭代器接口、元素访问接口以及一些常用的增删操作函数。最后,还展示了如何使用`vector`创建字符串数组,体现了`vector`在实际编程中的灵活性和实用性。
102 4
|
2月前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
101 5
|
2月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
78 2
|
2月前
|
安全 编译器 C++
【C++11】可变模板参数详解
本文详细介绍了C++11引入的可变模板参数,这是一种允许模板接受任意数量和类型参数的强大工具。文章从基本概念入手,讲解了可变模板参数的语法、参数包的展开方法,以及如何结合递归调用、折叠表达式等技术实现高效编程。通过具体示例,如打印任意数量参数、类型安全的`printf`替代方案等,展示了其在实际开发中的应用。最后,文章讨论了性能优化策略和常见问题,帮助读者更好地理解和使用这一高级C++特性。
88 4
|
2月前
|
算法 编译器 C++
【C++】模板详细讲解(含反向迭代器)
C++模板是泛型编程的核心,允许编写与类型无关的代码,提高代码复用性和灵活性。模板分为函数模板和类模板,支持隐式和显式实例化,以及特化(全特化和偏特化)。C++标准库广泛使用模板,如容器、迭代器、算法和函数对象等,以支持高效、灵活的编程。反向迭代器通过对正向迭代器的封装,实现了逆序遍历的功能。
40 3
|
2月前
|
存储 算法 Linux
【c++】STL简介
本文介绍了C++标准模板库(STL)的基本概念、组成部分及学习方法,强调了STL在提高编程效率和代码复用性方面的重要性。文章详细解析了STL的六大组件:容器、算法、迭代器、仿函数、配接器和空间配置器,并提出了学习STL的三个层次,旨在帮助读者深入理解和掌握STL。
83 0
|
1月前
|
存储 编译器 C语言
【c++丨STL】vector模拟实现
本文深入探讨了 `vector` 的底层实现原理,并尝试模拟实现其结构及常用接口。首先介绍了 `vector` 的底层是动态顺序表,使用三个迭代器(指针)来维护数组,分别为 `start`、`finish` 和 `end_of_storage`。接着详细讲解了如何实现 `vector` 的各种构造函数、析构函数、容量接口、迭代器接口、插入和删除操作等。最后提供了完整的模拟实现代码,帮助读者更好地理解和掌握 `vector` 的实现细节。
47 0
|
2月前
|
编译器 C++
【c++】模板详解(1)
本文介绍了C++中的模板概念,包括函数模板和类模板,强调了模板作为泛型编程基础的重要性。函数模板允许创建类型无关的函数,类模板则能根据不同的类型生成不同的类。文章通过具体示例详细解释了模板的定义、实例化及匹配原则,帮助读者理解模板机制,为学习STL打下基础。
40 0