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

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

目录
相关文章
|
6月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
163 2
|
7月前
|
存储 安全 C语言
C++ String揭秘:写高效代码的关键
在C++编程中,字符串操作是不可避免的一部分。从简单的字符串拼接到复杂的文本处理,C++的string类为开发者提供了一种更高效、灵活且安全的方式来管理和操作字符串。本文将从基础操作入手,逐步揭开C++ string类的奥秘,帮助你深入理解其内部机制,并学会如何在实际开发中充分发挥其性能和优势。
|
3月前
|
对象存储 C++ 容器
c++的string一键介绍
这篇文章旨在帮助读者回忆如何使用string,并提醒注意事项。它不是一篇详细的功能介绍,而是一篇润色文章。先展示重载函数,如果该函数一笔不可带过,就先展示英文原档(附带翻译),最后展示代码实现与举例可以直接去看英文文档,也可以看本篇文章,但是更建议去看英文原档。那么废话少说直接开始进行挨个介绍。
78 3
|
6月前
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
311 73
|
3月前
|
存储 算法 安全
c++模板进阶操作——非类型模板参数、模板的特化以及模板的分离编译
在 C++ 中,仿函数(Functor)是指重载了函数调用运算符()的对象。仿函数可以像普通函数一样被调用,但它们实际上是对象,可以携带状态并具有更多功能。与普通函数相比,仿函数具有更强的灵活性和可扩展性。仿函数通常通过定义一个包含operator()的类来实现。public:// 重载函数调用运算符Add add;// 创建 Add 类的对象// 使用仿函数return 0;
111 0
|
3月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
78 0
|
3月前
|
存储 安全 编译器
c++入门
c++作为面向对象的语言与c的简单区别:c语言作为面向过程的语言还是跟c++有很大的区别的,比如说一个简单的五子棋的实现对于c语言面向过程的设计思路是首先分析解决这个问题的步骤:(1)开始游戏(2)黑子先走(3)绘制画面(4)判断输赢(5)轮到白子(6)绘制画面(7)判断输赢(8)返回步骤(2) (9)输出最后结果。但对于c++就不一样了,在下五子棋的例子中,用面向对象的方法来解决的话,首先将整个五子棋游戏分为三个对象:(1)黑白双方,这两方的行为是一样的。(2)棋盘系统,负责绘制画面。
41 0
|
6月前
|
编译器 C++
模板(C++)
本内容主要讲解了C++中的函数模板与类模板。函数模板是一个与类型无关的函数家族,使用时根据实参类型生成特定版本,其定义可用`typename`或`class`作为关键字。函数模板实例化分为隐式和显式,前者由编译器推导类型,后者手动指定类型。同时,非模板函数优先于同名模板函数调用,且模板函数不支持自动类型转换。类模板则通过在类名后加`&lt;&gt;`指定类型实例化,生成具体类。最后,语录鼓励大家继续努力,技术不断进步!
|
7月前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
6月前
|
存储 分布式计算 编译器
C++入门基础2
本内容主要讲解C++中的引用、inline函数和nullptr。引用是变量的别名,与原变量共享内存,定义时需初始化且不可更改指向对象,适用于传参和返回值以提高效率;const引用可增强代码灵活性。Inline函数通过展开提高效率,但是否展开由编译器决定,不建议分离声明与定义。Nullptr用于指针赋空,取代C语言中的NULL。最后鼓励持续学习,精进技能,提升竞争力。