Una parte integral de la biblioteca C++STL: cadena (implementación de simulación)

Preámbulo

Hola a todos, este artículo explica principalmente la implementación de simulación de algunas interfaces comunes de string .
Como todos sabemos, en la vida diaria, las cadenas de caracteres son omnipresentes, como "simplemente hazlo", "China", "One Kun Nian" , etc. Si desea mostrar estos caracteres en la computadora , necesita usar To la clase de cadena, y para nosotros los programadores de C++, si podemos simular e implementar una cadena es una prueba importante para nuestras habilidades básicas .
No hay mucho que decir, comencemos a simular la implementación. (Hay un código fuente al final del artículo, debe recogerlo usted mismo)

En primer lugar, la implementación de interfaces comunes

pd: para distinguirlo de la cadena en la biblioteca, creamos un nuevo espacio de nombres llamado mjw , e implementaremos una cadena en él.
Esta variable miembro de simulación se define de la siguiente manera

1.1 Constructor

Como se muestra en la figura, las anteriores son las diversas sobrecargas de funciones del constructor de cadenas en la biblioteca. Las más utilizadas son (1) constructor sin argumentos , (2) constructor de copias y (4) constructor de argumentos .

1.1.1 Constructores con/sin parámetros

Dado que el constructor sin argumentos en realidad pasa el carácter ' ', implementamos (1) y (4) juntos, y (1) se implementará como el parámetro predeterminado de (4) .
Al escribir código, debemos prestar atención a dos puntos:
1. strlen(str) calcula el número de caracteres antes de '\0', por lo que al abrir un espacio, agregue la posición de '\0'
2. Al abrir un espacio , preste atención a Es posible que no se abra, por lo que primero creamos un puntero ptr para abrir el espacio y luego asignamos ptr a _str después de tener éxito. 3. Usamos directamente strcpy
para copiar la cadena . A continuación, se presenta brevemente el uso de estresante
Como se muestra en la figura anterior, la función de strcpy es copiar el contenido en el origen al espacio al que apunta el destino.
        //有参构造函数,无参利用缺省参数实现
        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 Copiar constructor

La lógica del constructor de copias es similar a la del constructor, pero debe tener cuidado de no usar el constructor de copias predeterminado, que parece una copia exitosa, pero de hecho los dos punteros apuntan al mismo espacio.
Esto implica el tema de la copia profunda y superficial .
La copia superficial causará los siguientes problemas: (Usé la clase anterior y el diagrama de objetos, perdóname por ser perezoso)
Por lo tanto, si la gestión de recursos está involucrada en una clase , su constructor de copia, la función de sobrecarga de asignación y el destructor deben mostrarse y proporcionarse en forma de copia profunda.

El código del constructor de copias es el siguiente:

//拷贝构造函数
        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的各个常用接口,希望能够对铁子们有所帮助。

Supongo que te gusta

Origin blog.csdn.net/zcxmjw/article/details/129429042
Recomendado
Clasificación