An integral part of the C++STL library—string (simulation implementation)

Preamble

Hello everyone, this article mainly explains the simulation implementation of some common interfaces of string .
As we all know, in daily life, character strings are ubiquitous, such as ''just do it'', ''China'', ''One Kun Nian'' , etc. If you want to display these characters on the computer, you need to use To the string class, and for us C++ programmers, whether we can simulate and implement string is an important test for our basic skills .
Not much to say, let's start to simulate the implementation. (There is source code at the end of the article, you need to pick it up yourself)

First, the implementation of common interfaces

ps: In order to distinguish it from the string in the library, we created a new namespace called mjw , and we will implement string in it.
This simulation member variable is defined as follows

1.1 Constructor

As shown in the figure, the above are the various function overloads of the string constructor in the library. The more commonly used ones are (1) no-argument constructor , (2) copy constructor , and (4) argument constructor .

1.1.1 Constructors with/without parameters

Since the no-argument constructor is actually passing the character ' ', we implement (1) and (4) together, and (1) will be implemented as the default parameter of (4) .
When writing code, we need to pay attention to two points:
1. strlen(str) calculates the number of characters before '\0', so when opening a space, add the position of '\0'
2. When opening a space, pay attention to It may fail to open up, so we create a pointer ptr to open the space first, and then assign ptr to _str after success.
3. We directly use strcpy to copy the string . Here is a brief introduction to the usage of strcpy
As shown in the figure above, the function of strcpy is to copy the content in the source to the space pointed to by the destination
        //有参构造函数,无参利用缺省参数实现
        string(const char* str = "")
            :_size(strlen(str))
        {
            //由于strlen计算的是"/0"前面字符的数量,
            //所以实际空间要留出'/0'的位置,也就是要多开辟一个空间
            _capaicty = strlen(str)==0?3:strlen(str);
            char* ptr = new char[_capacity + 1];
            strcpy(ptr, str);

            _str = ptr;
        }

1.1.1 Copy constructor

The logic of the copy constructor is similar to that of the constructor, but you need to be careful not to use the default copy constructor, which looks like a successful copy, but in fact the two pointers point to the same space.
This involves the issue of deep and shallow copying .
Shallow copy will cause the following problems: (I used the previous class and object diagram, forgive me for being lazy)
Therefore, if a class involves resource management , its copy constructor, assignment overload function, and destructor all need to be displayed and provided in the form of deep copy.

The copy constructor code is as follows:

//拷贝构造函数
        string(const string& s)
            :_size(s.size())
        {
            _capaicty = s._capacity;
            char* ptr = new char[_capacity + 1];
            strcpy(ptr, s._str);
            _str = ptr;
        }

1.2 Destructor

将开辟的空间释放,然后将_str置空即可,一定要 注意开辟和释放所用关键字要配对(new []/delete[])

代码如下:

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

1.3 []运算符重载

由于[]访问字符串比较方便,所以我们为了后续方便测试,我们将[]运算符重载放到第三个实现。
为了应对不同情况的权限问题,所以我们打算完成上面的两个函数重载,这里需要注意的点 就是要保证pos值的合法性,也就是pos<=_size.

代码如下:

        //[]重载
        char& operator[](size_t size)
        {
            assert(!(size > _size));
            return _str[size];
        }
        const char& operator[](size_t size) const//应对只用[]遍历,不修改的权限问题
        {
            assert(!(size > _size));
            return _str[size];
        }

1.4 返回_size/返回_str的地址/返回_capacity

三个个比较简短却又不能缺少的接口,没什么难度就不做赘述了。

代码如下:

        //返回size
        size_t size() const
        {
            return _size;
        }
        //返回_str地址
        const char* c_str()
        {
            return _str;
        }
        //返回capacity
        size_t capacity() const
        {
            return _capacity;
        }

1.5赋值函数重载

如上图所示,如果是 第三种情况两个 长度相等,那么 容量不用变;如果是第一种情况 s1的长度小于s2,要将s1赋值给s2, 直接拷贝即可,但是此时会有一个问题, 那就是有大量空间浪费掉了;第二种情况, s1的长度大于s2,想要将s1赋值给s2, s2就要扩容,但是new不支持扩容, 所以我们只能将s2原来空间释放,重新开辟一个和s1一样大的空间再将s1的内容拷贝过去
综上所述,我们为了满足每一种的情况,采取第二种的应对方法, 就是将原来空间释放掉,重新开辟一个空间进行拷贝

代码如下:

//赋值
        string& operator=(const string& s)
        {
            if (this != &s)//s1=s1的情况
            {
                //new开辟失败的时候,赋值没有实现,但s1却已经被破坏
                /*delete[] _str;
                _str = new char[s._capaicty + 1];
                _size = s._size;
                _capaicty = s._capaicty;
                strcpy(_str, s._str);*/

                char* ptr = new char[s._capaicty + 1];
                strcpy(ptr, s._str);

                delete[] _str;
                _str = ptr;
                _size = s._size;
                _capaicty = s._capaicty;
            }
            return *this;
        }

1.6 迭代器

迭代器(Iterator)是一个对象,它的工作是遍历并选择序列中的对象,它提供了一种访问一个容器(container)对象中的各个元素,而又不必暴露该对象内部细节的方法。
string的迭代器实现方式比较简单,用typedef就可以实现。

代码如下:

//迭代器
        typedef char* iterator;
        typedef const char* const_iterator;

        iterator begin()
        {
            return _str;
        }

        iterator end()
        {
            return _str + _size;
        }
        //const修饰的迭代器
        const_iterator begin() const
        {
            return _str;
        }

        const_iterator end() const
        {
            return _str + _size;
        }
但是由于string中的[]更加方便,所以迭代器用的地方比较少,但是后面的list迭代器用处很大。

1.7 reserve(扩容)

扩容函数接口是我们后面 模拟插入,尾插等必不可少的接口,虽然很重要但是实现还是比较简单的。

reserve接口的实现和赋值函数重载的实现一致,都是把原来的空间销毁,然后新开空间。

代码如下:

        //扩容,和赋值的思路类似
        void reserve(const size_t n)
        {
            if (_capacity < n)
            {
                //开n+1的空间,是要给'/0'留一个空间
                char* ptr = new char[n + 1];
                //防止开空间失败所以先用ptr接收,成功后在赋值给_str
                strcpy(ptr, _str);

                delete[] _str;
                _str = ptr;
                _capacity = n;
            }
            
        }

1.8 insert(重点)

insert接口实现是string模拟中比较重要的一个点,后面的尾插可以复用这个,而且这一部分的细节比较多,需要多注意。

对于intsert部分,我们打算实现两个函数重载:
1.在pos位置插入字符串 2.在pos位置插入字符

1.8.1 insert(插入字符串)

insert:在指定的位置插入字符或者字符串
插入字符串的大体逻辑如下:
首先检查 是否需要扩容,然后在将 pos位置往后的字符往后挪len(要插入的字符串的长度)个位置,给要插入的字符串留出足够的位置,然后 拷贝字符串
注意:最后的拷贝字符串可以手动拷贝,我们这里选择的是用库里的 函数strncpy进行拷贝,相比与strcpy,strncpy的控制更加精准
strncpy简单介绍
函数的作用大致为从source中拷贝num个字符到destination中

代码如下:

//在pos的位置插入字符串s
        string& insert(size_t pos, const string& s)
        {
            assert(pos <= _size);//检查pos是否合法
            int len = s.size();
            //检查扩容
            if (_size + len > _capacity)
            {
                reserve(_size + len);
            }
            size_t end = _size;
            //pos的数据及后面的数据向后挪len个位置
            while (end >= pos)
            {
                _str[end + len] = _str[end];
                end--;
            }
            
            //插入字符串
            //strcpy(_str + pos, s._str);
            strncpy(_str + pos, s._str,len);
            _size += len;
            return *this;
        }
插入的基本功能差不多完成了,但是其中还有一个小bug不知道铁子们发现没有,那就是当 pos为0时,循环会进入死循环
注意此时end为0,按照我们的逻辑来看,下一步为-1,就该跳出循环了。
实际上并不是我们想的那样,end变成-1,而变成了最大值,这是因为什么呢,
因为end和pos的类型都是size_t,而size_t实际上是unsignen int,因此当end为0进行--时就直接变成了最大值.
那么有没有避免这种情况的方法?
答案肯定是有的如:
1. 将end和pos的类型都变成int,但是这样就和库中的参数不同,有违我们模拟的初衷
ps:如果只改变end的类型,在比大小的时候仍会被强制转成size_t,当然也可以在比的时候把pos强制转出int,但是这样可能会导致数据失真。
2.  改变循环逻辑
如上所示,这样以来end的最小值不会再低于0,这样就不会因为是无符号整形,导致永远是正数,从而导致死循环。

改良后的代码:

//在pos的位置插入字符串s
        string& insert(size_t pos, const string& s)
        {
            assert(pos <= _size);//检查pos是否合法
            int len = s.size();
            //检查扩容
            if (_size + len > _capacity)
            {
                reserve(_size + len);
            }
            size_t end = _size+len;
            //pos的数据及后面的数据向后挪len个位置
            /*while (end >= pos)
            {
                _str[end + len] = _str[end];
                end--;
            }*/
            while (end > pos + len - 1)
            {
                _str[end] = _str[end - len];
                end--;
            }
            //插入字符串
            //strcpy(_str + pos, s._str);
            strncpy(_str + pos, s._str,len);
            _size += len;
            return *this;
        }
        

1.8.2 insert(插入字符)

插入字符和插入字符串一样,其实就是把插入字符串中的len变成1就是插入字符。
//在pos的位置插入字符ch
        string& insert(size_t pos, const char ch)
        {
            assert(pos <= _size);//检查pos是否合法
            //检查扩容
            if (_size + 1 > _capacity)
            {
                reserve(_capacity * 2);//二倍扩容
            }
            size_t end = _size+1;
        
            while (end > pos)
            {
                _str[end] = _str[end-1];
                end--;
            }
            _str[pos] = ch;
            _size++;
            return *this;
        }

1.9 erase

erase: 在pos位置往后(包括pos)删除len个字符,当len>=_size时,默认pos后面的数据删完即可
erase情况分三种:len==npos,len>=_size,len<size.因为len类型为size_t,而npos值恒定为-1,所以前两种情况可以归为一种,就是len>=_size.

代码如下:

//erase,在pos位置往后(包括pos)删除n/npos个字符
        string& erase(size_t pos = 0, size_t len = npos)
        {
            assert(pos <= _size);//检查pos是否合法
            if (len == npos || len >= _size)
            {
                _str[pos] = '\0';
                _size = pos;
            }
            else
            {
                //将pos后面的数据都向前挪len个位置
                //1.手动挪
                //size_t cur = pos;
                //while (cur <= _size - len)
                //{
                //    _str[cur] = _str[cur + len];
                //    cur++;
                //}
                //2.strcpy
                strcpy(_str + pos, _str + pos + len);
                _size -= len;
            }

1.10 push_back(尾插字符)和append(尾插字符串)

1.10.1 push_back

实现方法:
1.检查扩容,然后直接插入
2.复用insert(插入字符)
//尾插字符
        void push_back(char ch)
        {
            //1.检查扩容,然后直接插入
            //检查扩容
            //if (_size + 1 > _capacity)
            //{
            //    reserve(_capacity*2);//二倍扩容
            //}
            当前_size指向的是原字符串'\0'的位置,此时赋值'\0'会被覆盖
            所以需要在后面补上'\0'
            //_str[_size] = ch;
            //_size++;
            //_str[_size] = '\0';
            //2.复用insert
            insert(_size, ch);
            
        }

1.10.2 append

我们要实现的是上面的第一个函数重载
实现方法:
1.检查扩容,然后用strcpy拷贝
2. 复用insert(插入字符串)
//尾插字符串
        void append(const string& s)
        {
            //1.检查扩容,然后用strcpy拷贝
            //int len = s._size;
            检查扩容
            //if (_size + len > _capacity)
            //{
            //    reserve(_size + len);//按需扩容
            //}
            //strcpy(_str + _size, s._str);
            //_size += len;
            //2. 复用insert(插入字符串)
            insert(_size, s);
        }

1.11 +=操作符重载

我们要实现上图的第一个和第三个函数重载
实现方式: 复用push_back(尾插字符)和append(尾插字符串)即可
//+=重载 复用尾插和尾插字符串
        //+=字符
        //1.字符
        string& operator+=(const char ch)
        {
            push_back(ch);
            return *this;
        }
        //2.字符串
        string& operator+=(const string& s)
        {
            append(s);
            return *this;
        }

1.12 resize

resize:重新规划_size的大小,注意不是_capacity的大小,而是元素的个数。
resize的实现分为以下情况:

代码实现:

void resize(size_t n, char ch = '\0')
        {
            if (n <= _size)
            {
                _size = n;
                _str[_size] = '\0';
            }
            else
            {
                //判断扩容
                if (n > _capacity)
                {
                    reserve(n);
                }
                for(int i = _size; i < n; i++)
                {
                    _str[i] = ch;
                }
                _size = n;
                _str[_size] = '\0';
            }
        }

1.13 swap

写交换函数的时候 尽量不要直接复用库里的swap函数,下面代码会解释。
//交换函数
        //swap(s1,s2);
        //和上面库中的交换函数比,类中的交换函数效率更高
        //因为库中函数需要调用三次构造函数构造s1,s2
        //而类中的交换函数,可以直接引用传参,不需要调用构造函数
        void swap(string& s)
        {
            //用库中的swap函数,前面要加std
            //不然会优先调用当前类中的swap函数,参数不对会出错
            std::swap(_str, s._str);
            std::swap(_size, s._size);
            std::swap(_capacity, s._capacity);
        }

1.14 <<(流插入)和>>(流提取)重载

流插入流提取都不能作为成员函数实现,因为成员函数中*this永远是第一个参数,所以在成员函数中实现只能实现这样的效果:s1<<cout,所以我们一般是 作为全局函数或者友元函数实现。

1.14.1 <<(流插入)

流插入我们采取一个范围for来实现
//流插入
    ostream& operator<<(ostream& out,string& s)
    {
        for (auto ch : s)
        {
            out << ch;
        }
        return out;
    }

1.14.2 >>(流提取)重载

在写流提取重载前,我们可以看看库中是如何运行的
观察上面程序我们发现,每次进行 流提取,会将字符串的原数据删除,然后输入流提取的内容
ps:在写入字符时,要用istream中的get()函数, 如果直接用>>,库中函数默认空格和'\n'会清除缓存,导致ch无法读取,从而无法停止循环,如下所示

因此需要用in.get()函数提取字符

代码如下:

//流提取
    istream& operator>>(istream& in,string& s)
    {
        char ch = in.get();//直接流提取输入默认' '是单词的间隔
        s.erase();
        while (ch!=' '&&ch != '\n')
        {
            s += ch;
            ch = in.get();
        }
        
        return in;
    }

二,源码

#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
namespace mjw
{
    class string
    {
    public:
        //迭代器
        typedef char* iterator;
        typedef const char* const_iterator;

        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 char* str = "")
            :_size(strlen(str))
        {
            //由于strlen计算的是"/0"前面字符的数量,
            //所以实际空间要留出'/0'的位置,也就是要多开辟一个空间
            _capacity = strlen(str)==0?3:strlen(str);
            char* ptr = new char[_capacity + 1];
            strcpy(ptr, str);

            _str = ptr;
        }
        //拷贝构造函数
        string(const string& s)
            :_size(s.size())
        {
            _capacity = s.capacity();
            char* ptr = new char[_capacity + 1];
            strcpy(ptr, s._str);
            _str = ptr;
        }
        //[]重载
        char& operator[](size_t size)
        {
            assert(!(size > _size));
            return _str[size];
        }
        const char& operator[](size_t size) const//应对只用[]遍历,不修改的权限问题
        {
            assert(!(size > _size));
            return _str[size];
        }
        //返回size
        size_t size() const
        {
            return _size;
        }
        //返回_str地址
        const char* c_str()
        {
            return _str;
        }
        //返回capacity
        size_t capacity() const
        {
            return _capacity;
        }
        //赋值
        string& operator=(const string& s)
        {
            if (this != &s)//s1=s1的情况
            {
                //new开辟失败的时候,赋值没有实现,但s1却已经被破坏
                /*delete[] _str;
                _str = new char[s._capaicty + 1];
                _size = s._size;
                _capaicty = s._capaicty;
                strcpy(_str, s._str);*/

                char* ptr = new char[s.capacity() + 1];
                strcpy(ptr, s._str);

                delete[] _str;
                _str = ptr;
                _size = s._size;
                _capacity = s.capacity();
            }
            return *this;
        }
        //比较大小
        // 对于不修改成员变量的函数尽量用const修饰一下
        //<
        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) || (*this == s);
        }
        // >=
        bool operator>=(const string& s) const
        {
            return !(*this < s) || (*this == s);
        }
        // !=
        bool operator!=(const string& s) const
        {
            return !(*this == s);
        }

        //扩容,和赋值的思路类似
        void reserve(const size_t n)
        {
            if (_capacity < n)
            {
                //开n+1的空间,是要给'/0'留一个空间
                char* ptr = new char[n + 1];
                //防止开空间失败所以先用ptr接收,成功后在赋值给_str
                strcpy(ptr, _str);

                delete[] _str;
                _str = ptr;
                _capacity = n;
            }
            
            
        }

        //尾插字符
        void push_back(const char ch)
        {
            //1.检查扩容,然后直接插入
            //检查扩容
            //if (_size + 1 > _capacity)
            //{
            //    reserve(_capacity*2);//二倍扩容
            //}
            当前_size指向的是原字符串'\0'的位置,此时赋值'\0'会被覆盖
            所以需要在后面补上'\0'
            //_str[_size] = ch;
            //_size++;
            //_str[_size] = '\0';
            //2.复用insert
            insert(_size, ch);
            
        }
        //尾插字符串
        void append(const string& s)
        {
            //1.检查扩容,然后用strcpy拷贝
            //int len = s._size;
            检查扩容
            //if (_size + len > _capacity)
            //{
            //    reserve(_size + len);//按需扩容
            //}
            //strcpy(_str + _size, s._str);
            //_size += len;
            //2. 复用insert(插入字符串)
            insert(_size, s);
        }
        //+=重载 复用尾插和尾插字符串
        //+=字符
        //1.字符
        string& operator+=(const char ch)
        {
            push_back(ch);
            return *this;
        }
        //2.字符串
        string& operator+=(const string& s)
        {
            append(s);
            return *this;
        }
        //
        void resize(size_t n, char ch = '\0')
        {
            if (n <= _size)
            {
                _size = n;
                _str[_size] = '\0';
            }
            else
            {
                //判断扩容
                if (n > _capacity)
                {
                    reserve(n);
                }
                for(int i = _size; i < n; i++)
                {
                    _str[i] = ch;
                }
                _size = n;
                _str[_size] = '\0';
            }
        }
        //insert
        //在pos的位置插入字符ch
        string& insert(size_t pos, const char ch)
        {
            assert(pos <= _size);//检查pos是否合法
            //检查扩容
            if (_size + 1 > _capacity)
            {
                reserve(_capacity * 2);//二倍扩容
            }
            size_t end = _size+1;
        
            while (end > pos)
            {
                _str[end] = _str[end-1];
                end--;
            }
            _str[pos] = ch;
            _size++;
            return *this;
        }
        //在pos的位置插入字符串s
        string& insert(size_t pos, const string& s)
        {
            assert(pos <= _size);//检查pos是否合法
            int len = s.size();
            //检查扩容
            if (_size + len > _capacity)
            {
                reserve(_size + len);
            }
            size_t end = _size+len;
            //pos的数据及后面的数据向后挪len个位置
            /*while (end >= pos)
            {
                _str[end + len] = _str[end];
                end--;
            }*/
            while (end > pos + len - 1)
            {
                _str[end] = _str[end - len];
                end--;
            }
            //插入字符串
            //strcpy(_str + pos, s._str);
            strncpy(_str + pos, s._str,len);
            _size += len;
            return *this;
        }
        
        //erase,在pos位置往后(包括pos)删除n/npos个字符
        string& erase(size_t pos = 0, size_t len = npos)
        {
            assert(pos <= _size);//检查pos是否合法
            if (len == npos || len >= _size)
            {
                _str[pos] = '\0';
                _size = pos;
            }
            else
            {
                //将pos后面的数据都向前挪len个位置
                //1.手动挪
                //size_t cur = pos;
                //while (cur <= _size - len)
                //{
                //    _str[cur] = _str[cur + len];
                //    cur++;
                //}
                //2.strcpy
                strcpy(_str + pos, _str + pos + len);
                _size -= len;
            }
            
            return *this;
        }
        //交换函数
        //swap(s1,s2);
        //和上面库中的交换函数比,类中的交换函数效率更高
        //因为库中函数需要调用三次构造函数构造s1,s2
        //而类中的交换函数,可以直接引用传参,不需要调用构造函数
        void swap(string& s)
        {
            //用库中的swap函数,前面要加std
            //不然会优先调用当前类中的swap函数,参数不对会出错
            std::swap(_str, s._str);
            std::swap(_size, s._size);
            std::swap(_capacity, s._capacity);
        }
        
        //析构函数
        ~string()
        {
            delete[] _str;
            _str = nullptr;
            _size = _capacity = 0;
        }
        
    private:
        char* _str;
        size_t _size;
        size_t _capacity;

        static size_t npos;
        //static const size_t npos;两个是一样的

    };
    size_t string::npos = -1;
    //流插入
    ostream& operator<<(ostream& out,string& s)
    {
        for (auto ch : s)
        {
            out << ch;
        }
        return out;
    }
    //流提取
    istream& operator>>(istream& in,string& s)
    {
        char ch = in.get();//直接流提取输入默认' '是单词的间隔
        s.erase();
        while (ch!=' '&&ch != '\n')
        {
            s += ch;
            ch = in.get();
        }
        
        return in;
    }
    
}

总结

以上就是我们模拟实现的接口,我们 模拟实现string的目的不是造一个更好的轮子,而是更加深入的了解string的各个常用接口,希望能够对铁子们有所帮助。

Guess you like

Origin blog.csdn.net/zcxmjw/article/details/129429042