【C++】string类的增删改查模拟实现(图例超详细解析!!!)

目录

一、前言

二、string类的模拟实现

✨前情提要

[✨Member functions ------ 成员函数](#✨Member functions —— 成员函数)

⚡构造函数

⚡拷贝构造函数

⚡赋值运算符重载

⚡析构函数

[✨Element access ------ 元素访问](#✨Element access —— 元素访问)

[⚡operator[ ]](#⚡operator[ ])

[⚡Iterator ------ 迭代器](#⚡Iterator —— 迭代器)

[✨Capacity ------ 容量](#✨Capacity —— 容量)

⚡size

⚡capacity

⚡clear

⚡empty

⚡reserve

⚡resize

[✨Modifiers ------ 修改器](#✨Modifiers —— 修改器)

⚡push_back

⚡append

[⚡operator+=(char ch)](#⚡operator+=(char ch))

[⚡perator+=(const char* s)](#⚡perator+=(const char* s))

⚡insert

⚡erase

⚡swap

[✨String Operations ------ 字符串操作](#✨String Operations —— 字符串操作)

[⚡ find --- 寻找一个字符](#⚡ find --- 寻找一个字符)

[⚡find --- 寻找一个字符串](#⚡find --- 寻找一个字符串)

⚡substr

✨非成员函数重载

[⚡relational operators](#⚡relational operators)

[⚡operator<< 流插入](#⚡operator<< 流插入)

[⚡operator>> 流提取【⭐】](#⚡operator>> 流提取【⭐】)

⚡getline

[三、string 类的模拟实现整体代码](#三、string 类的模拟实现整体代码)

[🥝 string.h](#🥝 string.h)

🍇string.cpp

🍍test.cpp

四、共勉


一、前言

在经过漫长的类和对象与STL学习之后,对于 STL中的 string类有了一个基本的认识,如果还有不太了解string 类的老铁,可以看看这篇文章:string类的详解

本模块呢,我将会带大家一起从 0~1去模拟实现一个STL库中的 string类,当然模拟实现的都是一些常用的接口,以便于让大家更好的巩固之前学习过的 缺省参数、封装、类中的6大默认成员函数等,代码量大概在 600行左右。

二、string类的模拟实现

✨前情提要

  • 首先第一点,为了不和库中的string类****发生冲突,我们可以包上一个名称为xas_string的命名空间,此时因为作用域的不同,就不会产生冲突了,如果这一块有点忘记的同学可以再去看看 namespace命名空间

    #pragma once

    #include <iostream>
    #include <assert.h>
    using std::ostream;
    using std::istream;
    using std::cout;
    using std::cin;
    using std::endl;

    // 为了不和 std库 中的 string类 发生冲突,创建我们自己的作用域xas_string
    namespace xas_string
    {
    class string
    {
    public:
    // .....
    private:
    char* _str; // 指向字符数组的指针
    size_t _size; // 字符数组的有效数据的长度
    size_t _capacity; // 字符串数组的容量
    };
    }

  • 接下去呢,就在string.cpp中进行定义,在test.cpp中进行测试即可。其中需要包含一下这个头文件,此时我们才可以在自己实现的类中去调用一些库函数

    #include "string.h"

✨Member functions ------ 成员函数

⚡构造函数

好,首先第一个我们要来讲的就是**【构造函数】**

  • 首先我们从无参的构造函数开始讲起,看到下面的代码,你是否有想起了 C++初始化列表,我们默认给到**_size** 和**_capacity** 的大小为,然后给字符数组开了一个大小的空间,并且将其初始化为**\0** string.cpp定义中可以写为:

    // 无参构造函数
    // 在 xas_string作用域中的string类 的string()函数
    xas_string::string::string()
    :_str(new char[1])
    , _size(0)
    , _capacity(0)
    {
    _str[0] = '\0';
    }

  • 然后我们立即在test.cpp中测试一下,因为我们自己实现的 string类 是包含在了命名空间xas_string中的,那么我们在使用这个类的时候就要使用到 域作用限定符::

    xas_string::string s1;

    // 测试初始化,与循环打印 迭代器
    void test1()
    {
    xas_string::string s1;
    cout << s1 << endl;
    }

    int main()
    {
    test1();
    return 0;
    }

  • 开始运行,结果出现了报错的现象。这个是什么原因呢?

  • 对于输入输出流运算符,我们后面会讲到,目前,我们先用 c_str()函数来代替输出

    const char* c_str() const // 内部不进行修改的文件,可以加上 const 防止权限放大
    {
    return _str;
    }

  • 然后打印一下这个string对象发现是一个空串

  • 有无参,那一定要有带参的,可以看到这里我们在初始化**_size的时候先去计算了字符串str的长度,因为_size取的就是到\0**为止的有效数据个数(不包含\0),那么【strlen】刚好可以起到这个功能

  • 然后在**_str这一块,我们为其开出的空间就是容量的大小 + 1(+1的作用是为'\0'开空间)**,最后的话还要在把有效的数据拷贝到这块空间中,使用到的是【strcpy】

    // 有参构造函数
    string(const char* str) // "" --- 为空的字符串
    {
    _str = new char[strlen(str) + 1]; // strlen 计算的是字符产的长度 ,不计算'\0' 所以要+1
    _size = strlen(str);
    _capacity = strlen(str); // capacity 不包括 '\0'
    strcpy(_str, str);
    }

  • 同样地来进行一个测试

⚡拷贝构造函数

马上,我们就来聊聊有关**【拷贝构造函数】**的内容

  • 拷贝构造函数详解 中我们有提到过若是一个类在没有显示定义拷贝构造对于内置类型不做处理,而对于自定义类型会去调用 类中默认提供的拷贝构造函数 此时就会造成浅拷贝的问题
  • 浅拷贝:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间。其中一个对象的改动会对另一个对象造成影响。
  • 深拷贝:深拷贝是指源对象与拷贝对象互相独立。其中任何一个对象的改动不会对另外一个对象造成影响。
  • 很明显,我们并不希望拷贝出来的两个对象之间存在相互影响,因此,我们这里需要用到深拷贝。下面提供深拷贝的两种写法

写法一:传统写法

**传统写法的思想简单:**先开辟一块足以容纳源对象字符串的空间,然后将源对象的字符串拷贝过去,接着把源对象的其他成员变量也赋值过去即可。因为拷贝对象的_str与源对象的_str指向的并不是同一块空间,所以拷贝出来的对象与源对象是互相独立的。

//拷贝构造
string(const string& s)
{
	_str = new char[s._capacity + 1];
	strcpy(_str, s._str);
	_size = s._size;
	_capacity = s._capacity;
}
  • 通过调试再去观察的话,我们可以发现,此时 对象s1 和 对象s2 中的数据存放在不同的空间中,此时去修改或者是析构的话都不会受到影响

写法二:现代写法

现代写法与传统写法的思想不同:先根据源字符串的C字符串调用构造函数构造一个tmp对象,然后再将tmp对象与拷贝对象的数据交换即可。拷贝对象的_str与源对象的_str指向的也不是同一块空间,是互相独立的。

//拷贝构造
string(const string& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
	swap(tmp); //交换这两个对象
}

**注:**swap成员函数的模拟实现在文章的后面。

  • 同样地来进行一个测试

⚡赋值运算符重载

对于赋值运算符重载这一块我们知道它也是属于类的默认成员函数,如果我们自己不去写的话类中也会默认地生成一个

  • 但是呢默认生成的这个也会造成一个 浅拷贝 的问题。看到下面图示,我们要执行s1 = s3,此时若不去开出一块新空间的话,那么s1和s3就会指向一块同一块空间,此时便造成了下面这些问题
  • 在修改其中任何一者时另一者都会发生变化;
  • 在析构的时候就也会造成二次析构的;
  • 原先s1所指向的那块空间没人维护了,就造成了内存泄漏的问题
  • 那么此时我们应该自己去开出一块新的空间,将s3里的内容先拷贝到这块空间中来,然后释放掉s1所指向这块空间中的内容,然后再让s1指向这块新的空间。那么这个时候,也就达成了我们所要的【深拷贝】,不会让二者去共同维护同一块空间
  • 最后的话不要忘记去修改一下s1的【_size】和【_capacity】,因为大小和容量都发生了改变

写法一:传统写法

// 赋值运算符重载
		string& operator=(const string& s)
		{
			if (this != &s)
			{
				char* tmp = new char[s._capacity + 1];
				strcpy(tmp, s._str);
				delete[] _str;

				_str = tmp;
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}

写法二:现代写法

但是呢,就上面这一种写法并不是最优的,我们来看看下面的这种写法

  • 很多同学非常地震惊,为何这样子就可以做到【深拷贝】呢?

    string& operator=(const string& s)
    {
    if (this != &s) //防止自己给自己赋值
    {
    string tmp(s); //用s拷贝构造出对象tmp
    swap(tmp); //交换这两个对象
    }
    return *this; //返回左值(支持连续赋值)
    }

  • 有关这个swap()函数,本来是应该下面讲的,既然这里使用到了,那就在这里讲吧,这个接口我在上面并没有介绍到,但是在讲 C++模板 的时候有提到过库中的这个swap() 函数,它是一个函数模版,可以 根据模版参数的自动类型推导去交换不同类型的数据

  • 可以看到在我们自己实现的这个swap(string& s)函数中就去调用了std标准库中的函数然后交换一个string对象的所有成员变量

    void swap(string& s)
    {
    std::swap(_str, s._str);
    std::swap(_size, s._size);
    std::swap(_capacity, s._capacity);
    }

  • 接下去来解释一下这里的原理,我们在这个赋值重载的函数内部调用了拷贝构造去获取到一个临时对象tmp,然后再通过swap()函数去交换当前对象和tmp的指向,此时s1就刚好获取到了赋值之后的内容,而tmp呢则是一个临时对象,出了当前函数的作用域后自动销毁,那么原本s1所维护的这块空间刚好就会销毁了,也不会造成内存泄漏的问题

  • 透过上面这个图解读者应该对新的这种拷贝构造有了一定的理解:反正你这个tmp对象出了作用域也要销毁的,你手上呢刚好有我想要的东西,那我们换一下吧,此时我得到了我想要的东西,你呢拿到了我的东西,这块地址中的内容刚好就是要销毁的,那tmp在出了作用域后顺带就销毁了,这也就起到了**【一石二鸟】**的效果

⚡析构函数

最后的话就是析构函数这一块,前面在调试的过程中我们已经看到很多遍了,此处不再细述

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

✨Element access ------ 元素访问

基本的成员函数我们已经讲完了,string对象也构造出来了,接下去我们来访问一下对象里面的内容吧

⚡operator[ ]

  • 首先最常用的就是这**【下标 + [ ]】**的形式去进行一个访问,那很简单,我们通过当前所传入的下标值去访问对应的数据即可

  • 下面的话有两种实现形式,一个是可读可写的,一个则是可读不可写

    // 可读可写
    char& operator[](size_t pos)
    {
    assert(pos < _size);
    return _str[pos];
    }

    // 可读不可写
    const char& operator[](size_t pos) const
    {
    assert(pos < _size);
    return _str[pos];
    }

  • 里面我们就通过循环来访问一下,这里的**size()**函数我们会在下面讲到

  • 但是呢,如果我在定义这个对象的时候在前面加上一个const的话此时这个对象就具有常性了,在调用**operator[]**的时候调用的便是 可读不可写 的那一个,所以此刻我们去做一个修改操作的话就会出问题了

    const xas_string::string s4("hello C++");

⚡Iterator ------ 迭代器

那经过上面的学习我们可以知道,要去遍历访问一个string对象的时候,除了**【下标 + []】**的形式,我们还可以使用迭代器的形式去做一个遍历

  • 而对于迭代器而言我们也是要去实现两种,一个是非const的 ,一个则是const的

  • string类中的迭代器实际上就是字符指针,只是给字符指针起了一个别名叫iterator而已。

    typedef char* iterator;
    typedef const char* const_iterator;

注:不是所有的迭代器都是指针。

  • 这里的话我就实现一下最常用的【begin】和【end】,首位的话就是**_str所指向的这个位置,而末位的话则是_str + _size**所指向的这个位置

    iterator begin()
    {
    return _str;
    }

    iterator end()
    {
    return _str + _size;
    }

  • 实现了普通版本的迭代器之后,我们再来看看常量迭代器。很简单,只需要修改一下返回值,然后在后面加上一个【const成员】,此时就可以构成函数重载了

    const_iterator begin() const
    {
    return _str;
    }

    const_iterator end() const
    {
    return _str + _size;
    }

  • 首先我们来看一下这个普通的迭代器,成功地遍历了这个string对象

  • 那么对于常对象来说的话,就要使用常量迭代器来进行遍历

    // 针对------const 对象的访问
    // 打印这个字符串 --- 不能修改
    void print_str(const string& s)
    {
    for (size_t i = 0; i < s.size(); i++)
    {
    std::cout << s[i] << " ";
    }
    std::cout << std::endl;

      	string::const_iterator it = s.begin();
      	while (it != s.end())
      	{
      		// 内容不能修改
      		std::cout << *it << " ";
      		// 指针可以修改
      		it++;
      	}
      	std::cout << std::endl;
      }
    

✨Capacity ------ 容量

下面六个接口我们一起来看看,然后一同测试

⚡size

  • 首先是 size() ,这里的话我们直接返回**_size即可,因为不会去修改成员变量,所以我们可以加上一个【const成员】(因为它是不可被修改滴)**

  • size函数用于获取字符串当前的有效长度(不包括'\0')

    size_t size() const
    {
    return _size;
    }

注意:这里的 const 是用来修饰 this指针滴

⚡capacity

  • capacity函数用于获取字符串当前的容量

    size_t capacity() const
    {
    return _capacity;
    }

⚡clear

  • 对于 clear() 而言就是去清除当前对象的数据,我们直接在**_str[0]这个位置放上一个\0即可,并且再去修改一下它的_size = 0**即可

  • 不过这个接口来说我们不要去加【const成员】,因为修改了其成员变量**_size**

    void clear()
    {
    _str[0] = '\0';
    _size = 0;
    }

⚡empty

  • 对于 empty() 来说呢就是对象中没有数据,那么使用0 == _size即可

    bool empty() const
    {
    return 0 == _size;
    }

💬 然后我们来测试一下

⚡reserve

然后我们来看**【reserve】**扩容

reserve规则:

  1. 扩容n大于对象当前的capacity时,将capacity扩大到n或大于n
  2. 扩容n小于对象当前的capacity 时,什么也不做
  • 很明显,只有当这个 新容量大于旧容量的时候,才会去选择去开空间,这里的扩容逻辑和我们在实现旧版本的拷贝构造函数时类似的:也是先开出一块新的空间(这里主要使用这个newCapacity 去开),然后再将原本的数据拷贝过来,释放旧空间的数据后让**_str**指向新空间即可。最后的话不要忘了去更新一下容量大小

    void reserve(size_t newCapacity)// 扩容(修改_capacity)
    {
    // 当新容量大于旧容量的时候,就开空间
    if (newCapacity > _capacity)
    {
    // 1.以给定的容量开出一块新空间
    char* tmp = new char[newCapacity + 1]; //多开一个空间用于存放'\0'
    // 2.将原本的数据先拷贝过来
    strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0')
    // 3.释放旧空间的数据
    delete[] _str;
    // 4.让_str指向新空间
    _str = tmp;
    // 5.更新容量大小
    _capacity = newCapacity;
    }
    }

  • **注意:**代码中使用strncpy进行拷贝对象C字符串而不是strcpy,是为了防止对象的C字符串中含有有效字符'\0'而无法拷贝(strcpy拷贝到第一个'\0'就结束拷贝了)。

💬 马上来做一个测试

⚡resize

然后我们再来讲讲**【resize】**,博主觉得下面的这个算法是比较优的,读者可以参考一下

  • 首先我们来分析一下,对于【resize】而言主要对对象中的数据去做一个变化,那就需要去进行分类讨论
  1. 如果这个 newSize < _size 的话,那我们要选择去删除数据
  2. 如果这个 newSize > _size,但是呢 newSize < _capacity 的话,此时要做的就是新增数据但是呢不去做扩容
  3. 如果这个 newSize > _size 并且 newSize > _capacity,我们便要选择去进行扩容了

**_size = 10****,_capacity = 15时**

  • 在分析完了之后,我们立即来实现一下相关的代码。可以看到,一上来我就直接去判断了newSize 是否大于**_size**,然后在内部又做了一层判断,只有当newSize > _capacity时,才去执行【reserve】的扩容逻辑
  • 如果newSize并没有超过容量大小的话我们要做的事情就是去填充数据,这里用到的是一个内存函数【memset
    • 我们从**_str + _size** 的位置开始填充;
    • 填充的个数是newSize - _size个;
    • 填充的内容是c
  • 若是newSize <= _size的话,我们所要做的就是去截取数据,到newSize为止直接设置一个 \0 ,然后更新一下当前对象的**_size**大小

    // 改变大小
    void resize(size_t newSize, char c = '\0')
    {
    // 1.当新的_size比旧的_size来得小的话,则进行删除数据
    if (newSize > _size)
    {
    // 只有当新的size比容量还来的大,才去做一个扩容
    if (newSize > _capacity)
    {
    reserve(newSize);
    }
    // 如果newSize <= _capacity,填充新数据即可
    memset(_str + _size, c, newSize - _size);
    }

      // 如果 newSize <= _size,不考虑扩容和新增数据
      _size = newSize;
      _str[newSize] = '\0';
    

    }

💬 马上我们就来分类测试一下

  • 首先是resize(8),可以看到这里发生了一个数据截断的情况,_size也相对应地发生了一个变化
  • 接下去的话是resize(12),这并没有超过其容量值,但是却超出了**_size**大小,所以我们要去做一个增加数据
  • 最后一个则是resize(18),此时的话就需要去走一个扩容逻辑了,并且在扩完容之后还要再进一步去填充数据

✨Modifiers ------ 修改器

好,接下去我们来讲讲修改器这一块

⚡push_back

  • 首先第一块的话简单一点,我们去追加一个字符,那首先要考虑到的也是一个扩容逻辑,因为我们是一个字符一个字符去进行插入的,那么当这个**_size == _capacity的时候,就要去执行一个扩容的逻辑了,这边的话是运用到了这个三目运算符,若是容量的大小为0的话,默认开个大小为4的空间就可以了;其他的情况都是以2倍**的形式去进行扩充
  • 最后在扩完容之后我们就在末尾去增加数据了,因为**_size指向的就是 \0 的位置,所以就把字符放在这个位置上就可以了,顺带地记得去后移一下这个_size**,再放上一个 \0

    // 追加一个字符
    void push_back(char ch)
    {
    // 如果数据量大于容量的话,则需要进行扩容
    if (_size == _capacity)
    {
    reserve(_capacity == 0 ? 4 : _capacity * 2);
    }
    _str[_size++] = ch;
    _str[_size] = '\0';
    }

💬 马上我们就来分类测试一下

⚡append

  • 接下去的话是【append】,要追加的是一个字符串,所以我们要先去算出它的长度,接下去判断一下在加上这个长度后是否要去做一个扩容,最后的话还是通过我们熟悉的【memcpy】通过字节的形式一一拷贝到**_str + _size的位置(注意拷贝len + 1个,带上最后 \0),最后再把大小_size**给增加一下即可

    // 追加一个字符串
    void append(const char* s)
    {
    int len = strlen(s); // 获取到待插入字符串的长度
    // 若是加上len长度后超出容量大小了,那么就需要扩容
    if (_size + len > _capacity)
    {
    reserve(_size + len);
    }

      // 将字符串拷贝到末尾的_size位置
      memcpy(_str + _size, s, len + 1);
      // 大小增加
      _size += len;
    

    }

💬 马上我们就来分类测试一下

读者一定会觉得上面的函数调用太过于冗余,不过没关系,我们还有**【+=】**呢

⚡operator+=(char ch)

  • 首先的话是去【+=】一个字符,这里我们直接复用前面的*push_back()接口即可,最后因为【+=】改变的是自身,所以我们return this,那么返回一个出了作用域不会销毁的对象,可以采取 引用返回 减少拷贝

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

⚡perator+=(const char* s)

  • 而对于【+=】一个字符串,我们则是去复用前面的**append()**即可

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

💬 马上我们就来分类测试一下

⚡insert

接下去我们就要来实现一下【insert】这个接口了-- 从 pos 位置 开始插入n 个字符

  • 不过在这之前呢我们先要去声明并初始化一个静态的成员变量npos ,它是最大的无符号整数值。但是对于 静态的成员变量 来说我们需要 ++在类内声明并且在类外进行初始化++

    // 类内声明
    const static size_t npos;

    // 类外初始化
    const static size_t npos = -1;

  • 首先第一个的话就是要在pos位置插入n个字符

    void insert(size_t pos, size_t n, char ch)

  • 因为这里会传入一个pos位置,所以第一步我们就是要去考虑这个pos位置是否合法

    assert(pos <= _size);

  • 接下去第二步的话就是去考虑过扩容的问题了,如果**_size + n之后的大小大于_capacity**的话那就要调用【reserve】接口去实现一个扩容的逻辑了

    // 考虑扩容
    if (_size + n > _capacity)
    {
    reserve(_size + n);
    }

  • 第三步呢并不是直接去插入数据,而是要先给需要插入的n个字符腾出位置。从**_size**位置开始,让字符以n个单位地从后往前挪即可,若是从前往后挪的话就会造成覆盖的问题

// 挪动数据
size_t end = _size;
while (end >= pos)
{
	_str[end + n] = _str[end];
	--end;
}
  • 不过呢,我们在这里还要考虑一种极端的情况,如果这个pos == 0的话,也就是在这个位置开始插入数据,那也就相当于头插,此时需要将全部的数据向后进行挪动,可是呢当这个end超出pos的范围时,也就减到了-1,但是呢这个end的数据类型则是【size_t】,为一个无符号整数,我们知道对于无符号整数来说是不可能为负数的,那么这个时候就会发生一个轮回,变成最大的无符号正数
  • 我们可以来看看当这个end在不断减少直至减到0的时候就会突然变成一个很大的数字,这个其实就是npos的值了,此时就会造成一个死循环,导致程序崩溃

    // 字符插入测试
    void test6()
    {
    xas_string::string s1("abcdefghijk");
    s1.insert(0, 3, '#');
    cout << s1.c_str() << endl;

    }

    int main()
    {
    test6();
    return 0;
    }

  • 所以我们应该将无符号该有 有符号类型 size_t ----> int

    // 挪动数据
    int end = _size;
    while (end >=(int)pos)
    {
    _str[end + n] = _str[end];
    --end;
    }

  • 当这个挪动的逻辑结束后,我们就可以从pos这个位置去插入n个字符了。最后再去更新一下这个**_size**的大小即可

    // 插入n个字符
    for (size_t i = 0; i < n; i++)
    {
    _str[pos + i] = ch;
    }
    _size += n;

从pos位置开始插入一个字符串

void erase(size_t pos, int len = npos)
  • 对于在【pos位置插入一个字符串】来说,其他逻辑和上面这个接口都是一样,也是要经过 扩容移位放数据 这些操作,只是这里在放数据的时候换成了字符串而言

    // 插入字符串
    for (size_t i = 0; i < len; i++)
    {
    _str[pos + i] = s[i];
    }
    _size += len;

整体的代码为:

void insert(size_t pos, const char* str)
{
	assert(pos <= _size);

	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}

	int end = _size;
	// 向后挪动
	while (end >= (int)pos)
	{
		_str[end + len] = _str[end];
		end--;
	}
	// 这里不能用 strcpy 会把 '\0' 拷贝过来
	strncpy(_str + pos, str, len);
	_size += len;

}

⚡erase

删除从pos位置开始的len个有效长度字符

  • erase函数的作用是删除字符串任意位置开始的n个字符。删除字符前也需要判断pos的合法性,进行删除操作的时候分两种情况:

1、pos位置及其之后的有效字符都需要被删除

  • 这时我们只需在pos位置放上'\0',然后将对象的size更新即可。

2、pos位置及其之后的有效字符只需删除一部分。

  • 这时我们可以用后方需要保留的有效字符覆盖前方需要删除的有效字符,此时不用在字符串后方加'\0',因为在此之前字符串末尾就有'\0'了。
// 删除  -- 从 pos 位置 删除 长度为 len 的字符串 
		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;
			}
		}

💬 马上我们就来分类测试一下

⚡swap

  • 对于【swap】函数我们在上面已经有讲解过了,此处不再过度赘述

    void swap(string& s)
    {
    std::swap(_str, s._str);
    std::swap(_size, s._size);
    std::swap(_capacity, s._capacity);
    }

✨String Operations ------ 字符串操作

然后再来讲讲有关字符串的一些操作

⚡ find --- 寻找一个字符

  • 这个很简单,就是去遍历一下当前对象中的**_str**,若是在遍历的过程中发现了字符ch的话就返回这个位置的下标,如果遍历完了还是没有找到的话就返回npos这个最大的无符号数

    size_t find(char ch, size_t pos) const
    {
    assert(pos <= _size);
    for (size_t i = 0; i < _size; i++)
    {
    if (_str[i] == ch)
    {
    return i;
    }
    }
    return npos;
    }

⚡find --- 寻找一个字符串

  • 我直接使用的是C语言中的库函数 strstr函数详解,这个的话我们在 字符串函数与内存函数解读 的时候也有讲解并模拟过,如果找到了的话就会返回子串第一次出现在主串中的指针。那我们如果要去计算这个指针距离起始位置有多远的话使用指针 - 指针的方式即可。那如果没找到的话我们返回【npos】即可

    size_t find(const char* s, size_t pos) const
    {
    assert(pos < _size);
    char* tmp = strstr(_str, s);
    if (tmp)
    {
    // 指针相减即为距离
    return tmp - _str;
    }
    return npos;
    }

💬 马上我们就来分类测试一下

⚡substr

上面是去匹配子串,现在我们要将这个子串给取出来,要如何去取呢?

string substr(size_t pos, size_t len = npos)
  • 首先要考虑到的是,如果我们从pos位置开始所要取的子串长度大于剩余的串长,那最多能取到的有效范围也就是从pos位置开始的到末尾的**_size**结束这段距离,所以当这个所取长度过长的话,我们就要考虑去更新一下取子串长度的有效范围
  • 可以看到,我以这个n作为可取的子串长度,一开始得让其等于传入进来的len长,因为如果这个所取长度没有超出有效范围的话,我们所用的还是len

  • 但是如果呢这个长度超出了有效范围后,我们便要去更新这个n = _size - pos

    size_t n = len;
    if (len == npos || pos + len > _size)
    {
    // 就算要取再大的长度,也只能取到pos - _size的位置
    n = _size - pos;
    }

  • 那接下去的话我们就可以去取这个子串了,使用循环的方式从pos位置开始取,取【n】个即可,然后追加到这个临时的 string对象 中去,最后呢再将其返回即可,那我们返回一个出了作用域就销毁的临时对象,只能使用【传值返回】,而不能使用【传引用返回】

    string tmp;
    tmp.reserve(n);
    for (size_t i = pos; i < pos + n; i++)
    {
    tmp += _str[i];
    }
    return tmp;

✨非成员函数重载

最后的话再来模拟一些【非成员函数重载】,使用到的也是非常多

⚡relational operators

① 小于

//< 运算符重载
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)
{
	return *this < s || *this == s;
}

④ 大于

bool operator>(const string& s)
{
	return !(*this <= s);
}

大于等于

bool operator>=(const string& s)
{
	return !(*this < s);
}

⑥ 不等于

bool operator!=(const string& s)
{
	return !(*this == s);
}

💬 马上我们就来分类测试一下

⚡operator<< 流插入

  • 那有认真学习过【类和对象】的话,就可以知道为了不让this所指向的对象默认成为第一个参数的话,我们需要将这个函数实现到类外来,如果要访问类内私有成员的话,就可以使用到【友元】这个东西,不过呢我们不建议使用这个,会破坏类的封装性

    // 流插入
    ostream& operator<<(ostream& out, const string& s)
    {
    for (size_t i = 0; i < s.size(); i++)
    {
    out << s[i];
    }
    return out;
    }

  • 还有一点要提醒的是对于这个流插入来说我们是一定要进行引用返回的,这样就不会去调用拷贝构造了。因为在库中对这个函数是做了一个 防拷贝 的效果,即在后面加上一个**= delete**

💬 好,那到这里的话,我们是时候来讲讲这个cout << s.c_str()cout << s 的区别了

  • c的字符数组, 以\0为终止算长度
  • string不看\0, 以size为终止算长度

⚡operator>> 流提取【⭐】

接下去再来看看这个【>>流提取】

  • 重载**>>运算符是为了让string对象能够像内置类型一样使用>>运算符直接输入。输入前我们需要先将对象的C字符串置空,然后从标准输入流读取字符,直到读取到' '或是'\n'**便停止读取。

    //>>运算符的重载
    istream& operator>>(istream& in, string& s)
    {
    s.clear(); //清空字符串
    char ch = in.get(); //读取一个字符
    while (ch != ' '&&ch != '\n') //当读取到的字符不是空格或'\n'的时候继续读取
    {
    s += ch; //将读取到的字符尾插到字符串后面
    ch = in.get(); //继续读取字符
    }
    return in; //支持连续输入
    }

💬 马上我们就来分类测试一下

⚡getline

getline函数用于读取一行含有空格的字符串。实现时于>>运算符的重载基本相同,只是当读取到'\n'的时候才停止读取字符。

//读取一行含有空格的字符串
istream& getline(istream& in, string& s)
{
	s.clear(); //清空字符串
	char ch = in.get(); //读取一个字符
	while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取
	{
		s += ch; //将读取到的字符尾插到字符串后面
		ch = in.get(); //继续读取字符
	}
	return in;
}

💬 马上我们就来分类测试一下

三、string 类的模拟实现整体代码

🥝 string.h

#pragma once
#include <iostream>
#include <assert.h>
using std::ostream;
using std::istream;
using std::cout;
using std::cin;
using std::endl;

// 为了不和 std库 中的 string类 发生冲突,创建我们自己的作用域
namespace xas_string
{
	class string
	{
	public:

		typedef char* iterator;             // 迭代器某种意义上就是 指针
		typedef const char* const_iterator; 


		//  默认成员函数
		string(const char* str = "");       // 有参构造函数
		string(const string& s);            // 拷贝构造
		string& operator=(const string& s); // 赋值运算符重载
		~string();                          // 析构函数


		//迭代器相关函数
		iterator begin();
		iterator end();
		const_iterator begin() const;
		const_iterator end() const;


		//容量和大小相关函数
		size_t size()  const;                 // 返回目前 字符串的有效字符个数
		size_t capacity() const;              // 返回目前 字符串的容量
		void clear();                         // 清空字符串
		bool empty() const;                   // 判断字符串是否为空
		void reserve(size_t newCapacity = 0); // 扩容(修改_capacity)
		void resize(size_t newSize, char c = '\0'); // 改变大小


		// 修改字符串相关函数
		void push_back(char ch);               // 追加一个字符
		void swap(string& s);                  // 交换 --- 交换两个字符串
		void append(const char* s);            // 追加一个字符串
		string& operator+=(char ch);           // 追加一个字符
		string& operator+=(const char* s);     // 追加一个字符串
		void insert(size_t pos, const char* str); // 插入n个字符
		void erase(size_t pos, size_t len = npos); // 删除  -- 从 pos 位置 删除 长度为 len 的字符串 



		// 访问字符串相关函数
		char& operator[](size_t pos);             // 可读可写
		const char& operator[](size_t pos) const; // 可读不可写
		const char* c_str()const;                 // 用 C语言的方式返回
		size_t find(char ch, size_t pos = 0);     // 寻找字符
		size_t find(const char* str, size_t pos = 0); // 寻找 字符串
		string substr(size_t pos = 0, size_t len = npos); // 截取字符串 从某个位置 取 len 个字符


        // 关系运算符重载
		bool operator<(const string& s)const;      // < 运算符重载 
		bool operator==(const string& s)const;     // ==运算符重载
		bool operator<=(const string& s);          // <=运算符重载
		bool operator>(const string& s);          // >运算符重载
		bool operator>=(const string& s);         // >=运算符重载
		bool operator!=(const string& s);         // !=运算符重载


	private:
		char* _str;         // 指向字符数组的指针
		size_t _size;       // 字符数组的有效数据的长度
		size_t _capacity;   // 字符串数组的容量
		const static size_t npos = -1; //静态成员变量(整型最大值)
	};

	// 流插入
	ostream& operator<<(ostream& out, const string& s);
	//>>运算符的重载
	istream& operator>>(istream& in, string& s);
	//读取一行含有空格的字符串
	istream& getline(istream& in, string& s);



	void print_str(const string& s);    // const对象的输出 
}

🍇string.cpp

#include "string.h"


// 有参构造函数
xas_string::string::string(const char* str)    // ""  --- 为空的字符串
{
	_str = new char[strlen(str) + 1];   // strlen 计算的是字符产的长度 ,不计算'\0' 所以要+1
	_size = strlen(str);
	_capacity = strlen(str);           // capacity 不包括 '\0'
	strcpy(_str, str);
}


//拷贝构造
xas_string::string::string(const string& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
	swap(tmp); //交换这两个对象
}

// 赋值运算符重载
// 传统写法
//xas_string::string& xas_string::string::operator=(const string& s)
//{
//	if (this != &s)
//	{
//		char* tmp = new char[s._capacity + 1];
//		//memcpy(tmp, s._str, s._size + 1);
//		strcpy(tmp, s._str);
//		delete[] _str;
//
//		_str = tmp;
//		_size = s._size;
//		_capacity = s._capacity;
//	}
//	return *this;
//}

//现代写法2
xas_string::string& xas_string::string::operator=(const string& s)
{
	if (this != &s) //防止自己给自己赋值
	{
		string tmp(s); //用s拷贝构造出对象tmp
		swap(tmp); //交换这两个对象
	}
	return *this; //返回左值(支持连续赋值)
}


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


char& xas_string::string::operator[](size_t pos)        // 可读可写
{
	assert(pos <= _size);
	return _str[pos];
}

const char& xas_string::string::operator[](size_t pos) const  // 可读不可写
{
	assert(pos < _size);
	return _str[pos];
}


xas_string::string::iterator xas_string::string::begin()
{
	return _str;
}

xas_string::string::iterator xas_string::string::end()
{
	return _str + _size;
}

xas_string::string::const_iterator xas_string::string::begin() const
{
	return _str;
}

xas_string::string::const_iterator xas_string::string::end() const
{
	return _str + _size;
}


// 针对------const 对象的访问
// 打印这个字符串  --- 不能修改
void xas_string::print_str(const string& s)
{
	for (size_t i = 0; i < s.size(); i++)
	{
		std::cout << s[i] << " ";
	}
	std::cout << std::endl;


	string::const_iterator it = s.begin();
	while (it != s.end())
	{
		// 内容不能修改
		std::cout << *it << " ";
		// 指针可以修改
		it++;
	}
	std::cout << std::endl;
}

void xas_string::string::push_back(char ch)   // 追加一个字符
{
	// 如果数据量大于容量的话,则需要进行扩容
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	_str[_size++] = ch;
	_str[_size] = '\0';
}

// 返回目前 字符串的有效字符个数
size_t xas_string::string::size()  const    // 内部不进行修改的文件,可以加上 const 防止权限放大
{
	return _size;
}

size_t  xas_string::string::capacity() const  // 内部不进行修改的文件,可以加上 const 防止权限放大
{
	return _capacity;
}

bool xas_string::string::operator<=(const string& s)          // <=运算符重载
{
	return *this < s || *this == s;
}


// 清空字符串
void xas_string::string::clear()
{
	_str[0] = '\0';
	_size = 0;
}
// 判断字符串是否为空
bool xas_string::string::empty() const
{
	return 0 == _size;
}
void xas_string::string::reserve(size_t newCapacity)// 扩容(修改_capacity)
{
	// 当新容量大于旧容量的时候,就开空间
	if (newCapacity > _capacity)
	{
		// 1.以给定的容量开出一块新空间
		char* tmp = new char[newCapacity + 1]; //多开一个空间用于存放'\0'
		// 2.将原本的数据先拷贝过来
		strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0')
		// 3.释放旧空间的数据
		delete[] _str;
		// 4.让_str指向新空间
		_str = tmp;
		// 5.更新容量大小
		_capacity = newCapacity;
	}
}

void xas_string::string::resize(size_t newSize, char c) // 改变大小
{
	// 1.当新的_size比旧的_size来得小的话,则进行删除数据
	if (newSize > _size)
	{
		// 只有当新的size比容量还来的大,才去做一个扩容
		if (newSize > _capacity)
		{
			reserve(newSize);
		}
		// 如果newSize <= _capacity,填充新数据即可
		memset(_str + _size, c, newSize - _size);
	}

	// 如果 newSize <= _size,不考虑扩容和新增数据
	_size = newSize;
	_str[newSize] = '\0';
}

void xas_string::string::append(const char* s)             // 追加一个字符串
{
	int len = strlen(s);	// 获取到待插入字符串的长度
	// 若是加上len长度后超出容量大小了,那么就需要扩容
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}

	// 将字符串拷贝到末尾的_size位置
	memcpy(_str + _size, s, len + 1);
	// 大小增加
	_size += len;
}

xas_string::string& xas_string::string::operator+=(char ch)            // 追加一个字符
{                                      
	push_back(ch);
	return *this;
}

xas_string::string& xas_string::string::operator+=(const char* s)    // 追加一个字符串
{
	append(s);
	return *this;
}

void xas_string::string::insert(size_t pos, const char* str)
{
	assert(pos <= _size);

	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}

	int end = _size;
	// 向后挪动
	while (end >= (int)pos)
	{
		_str[end + len] = _str[end];
		end--;
	}
	// 这里不能用 strcpy 会把 '\0' 拷贝过来
	strncpy(_str + pos, str, len);
	_size += len;

}

void xas_string::string::erase(size_t pos, size_t len) // 删除  -- 从 pos 位置 删除 长度为 len 的字符串 
{
	assert(pos < _size);

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

size_t xas_string::string::find(char ch, size_t pos)      // 寻找字符
{
	for (size_t i = 0; i < _size; i++)
	{
		if (_str[i] == ch)
		{
			return i;
		}
	}
	return npos;
}

size_t xas_string::string::find(const char* str, size_t pos) // 寻找字符串
{
	const char* ptr = strstr(_str + pos, str);
	if (ptr == nullptr)
	{
		return npos;
	}
	else
	{
		return ptr - _str;
	}
}
xas_string::string xas_string::string::substr(size_t pos, size_t len ) // 截取字符串 从某个位置 取 len 个字符
{
	assert(pos < _size);
	size_t end = pos + len;
	if (len == npos || pos + len >= _size)
	{
		end = _size;
	}

	string str;
	str.reserve(end - pos);
	for (size_t i = pos; i < end; i++)
	{
		str += _str[i];
	}
	return str;
}

bool xas_string::string::operator<(const string& s)const       // < 运算符重载 
{
	return strcmp(_str, s._str) < 0;
}

bool xas_string::string::operator==(const string& s)const      //==运算符重载
{
	return strcmp(_str, s._str) == 0;
}
bool xas_string::string::operator>(const string& s)           // >运算符重载
{
	return !(*this <= s);
}
bool xas_string::string::operator>=(const string& s)
{
	return !(*this < s);
}
bool xas_string::string::operator!=(const string& s)
{
	return !(*this == s);
}

// 用 C语言的方式返回
const char* xas_string::string::c_str() const     // 内部不进行修改的文件,可以加上 const 防止权限放大
{
	return _str;
}

// 流插入
ostream& xas_string::operator<<(ostream& out, const xas_string::string& s)
{
	for (size_t i = 0; i < s.size(); i++)
	{
		out << s[i];
	}
	return out;
}
//>>运算符的重载
istream& xas_string::operator>>(istream& in, xas_string::string& s)
{
	s.clear(); //清空字符串
	char ch = in.get(); //读取一个字符
	while (ch != ' ' && ch != '\n') //当读取到的字符不是空格或'\n'的时候继续读取
	{
		s += ch; //将读取到的字符尾插到字符串后面
		ch = in.get(); //继续读取字符
	}
	return in; //支持连续输入
}
//读取一行含有空格的字符串
istream& xas_string::getline(istream& in, xas_string::string& s)
{
	s.clear(); //清空字符串
	char ch = in.get(); //读取一个字符
	while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取
	{
		s += ch; //将读取到的字符尾插到字符串后面
		ch = in.get(); //继续读取字符
	}
	return in;
}


// 交换
void xas_string::string::swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

🍍test.cpp

#include "string.h"


//  测试初始化,与循环打印 迭代器
void test1()
{
	// 初始化测试
	xas_string::string s1("hello string");
	cout << s1.c_str() << endl;

	cout << endl;

	// 拷贝构造测试
	xas_string::string s2(s1);
	cout << s2.c_str() << endl;

	cout << endl;

	//赋值运算符重载
	xas_string::string s3("hello world!");
	s1 = s3;
	cout << s1.c_str() << endl;


	// 引用返回 是可以进行修改的
	for (size_t i = 0; i < s3.size(); i++)
	{
		s3[i]++;
	}
	cout << s3.c_str() << endl;
	cout << endl;

	// 迭代器
	xas_string::string::iterator it = s1.begin();
	while (it != s1.end())
	{
		std::cout << *it << " ";
		it++;
	}
	std::cout << std::endl;

	// 范围 for
	for (auto ch : s1)
	{
		std::cout << ch << " ";
	}
	cout << endl;
	cout << endl;

	// 常量对象
	print_str(s1);

}


// 测试容量相关的函数
void test2()
{
	xas_string::string s1("hello string");

	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
	cout << s1.empty() << endl;
	s1.clear();
	cout << s1.empty() << endl;

	cout << endl;

	cout << s1.size() << endl;
	cout << s1.capacity() << endl;

	s1.reserve(100);
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
}

// 测试 resize()函数
void test3()
{
	xas_string::string s1("abcdefghijk");
	s1.reserve(15);
	cout << s1.c_str() << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;

	cout << endl;

	s1.resize(18,'x');
	cout << s1.c_str() << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
}

// 测试修改函数
void test4()
{
	xas_string::string s1("abcdefghijk");
	cout << s1.c_str() << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;

	cout << endl;
	cout << "追加字符:xxxxx" << endl;
	cout << endl;

	s1.append("xxxxx");
	cout << s1.c_str() << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
}

void test5()
{
	xas_string::string s1("abcdefghijk");
	s1 += 'x';
	cout << s1.c_str() << endl;

	s1 += "yyyy";
	cout << s1.c_str() << endl;
}


// 字符插入测试
void test6()
{
	xas_string::string s1("abcdefghijk");
	cout << s1.c_str() << endl;
	cout << endl;
	s1.insert(0,"###");
	cout << s1.c_str() << endl;

}

// 字符串删除测试
void test7()
{
	xas_string::string s1("abcdefghijk");
	cout << s1.c_str() << endl;
	cout << endl;
	s1.erase(3, 3);
	cout << s1.c_str() << endl;

}


// 访问字符串相关函数测试
void test8()
{
	xas_string::string s1("abcdefghijk");
	cout << s1.c_str() << endl;

	size_t pos1 = s1.find('c', 0);
	cout << pos1 << endl;

	size_t pos2 = s1.find("def", 0);
	cout << pos2 << endl;
	cout << endl;

	xas_string::string s2 = s1.substr(5, 5);
	cout << s2.c_str() << endl;
}

// 非成员函数重载
void test9()
{
	xas_string::string s1("hello string");
	cout << s1.c_str() << endl;
	xas_string::string s2("hello world");
	cout << s2.c_str() << endl;


	cout << (s1 < s2) << endl;
	cout << (s1 > s2) << endl;
	cout << (s1 == s2) << endl;
	cout << (s1 != s2) << endl;
}

// 输入输出流测试
void test10()
{
	xas_string::string s1("hello");
	s1 += '\0';
	s1 += "*******";

	cout << s1.c_str() << endl;
	cout << s1 << endl;

	xas_string::string s2;
	getline(cin,s2);
	cout << s2;
}


int main()
{
	test10();
	return 0;
}

四、共勉

以下就是我对 string类的模拟实现 的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对 C++vector类模拟实现 的理解,请持续关注我哦!!!

相关推荐
数据小小爬虫几秒前
Python爬虫获取AliExpress商品详情
开发语言·爬虫·python
小爬虫程序猿1 分钟前
利用Python爬虫速卖通按关键字搜索AliExpress商品
开发语言·爬虫·python
Kenneth風车7 分钟前
【机器学习(九)】分类和回归任务-多层感知机(Multilayer Perceptron,MLP)算法-Sentosa_DSML社区版 (1)11
算法·机器学习·分类
一朵好运莲8 分钟前
React引入Echart水球图
开发语言·javascript·ecmascript
最后一个bug12 分钟前
rt-linux中使用mlockall与free的差异
linux·c语言·arm开发·单片机·嵌入式硬件·算法
EleganceJiaBao14 分钟前
【C语言】结构体模块化编程
c语言·c++·模块化·static·结构体·struct·耦合
Eiceblue20 分钟前
使用Python获取PDF文本和图片的精确位置
开发语言·python·pdf
xianwu54329 分钟前
反向代理模块。开发
linux·开发语言·网络·c++·git
xiaocaibao77734 分钟前
Java语言的网络编程
开发语言·后端·golang
brhhh_sehe1 小时前
重生之我在异世界学编程之C语言:深入文件操作篇(下)
android·c语言·网络