• 模拟实现string类


    一:四个默认成员函数

    1.private 成员

     private:
            char* _str;
            size_t _size;
            size_t _capacity;
            const static size_t npos;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • _str是存储字符串的指针,_size用来存储当前字符串的结尾位置,_capacity用于存储字符串的容量大小
    • npos的值被定义为-1,又因为npos的类型为size_t,是无符号整型,所以npos实际上的值是size_t类型的最大值4294967295
    • npos做参数时,通常表示字符串的结尾,因为我们认为一个字符串的长度不会超过npos,用做返回值时通常用于查找,表示未找到要查找的值
    • npos的初始化需要在类外,因为npos是静态成员

    2.构造函数

     string(const char* str = "")
            {
                //防止传nullptr
                if (str == nullptr) str = "";
                _size = strlen(str);
                _capacity = _size;
                _str = new char[_capacity + 1];
                strcpy(_str, str);
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 构造函数简单的来说就是传一个字符串用来初始化string,不过写缺省参数时不能写nullptr
    • 因为strlen计算字符串长度时不能传nullptr,所以缺省参数通常写为“”,表示空串
    • 还需要注意的是,_capacity是指存储有效字符的长度,不包括结尾的'\0',所以给_str分配空间时要比_capacity多一字节

    3.析构函数

    ~string()
            {
                delete[] _str;
                _str = nullptr;
                _size = _capacity = 0;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • delete 针对nullptr什么也不做,所以可以不用判断_str是否为空

    4.拷贝构造

    <1> 传统方式

      string(const string& str)//拷贝构造,必须传引用,否则会无限递归
            :_str(new char[strlen(str._str)+1])
            {
               strcpy(_str, str._str);
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 拷贝构造和构造函数很相似,不过是用一个string对象的_str来构造一个新的string对象的_str,就是一个拷贝的过程,调用库函数即可
    • 需要注意的是:参数必须传引用
    • 如果不传引用,会造成无限递归

    <2> 现代方式

    void swap(string& str)
            {
                ::swap(_str, str._str);
                ::swap(_size, str._size);
                ::swap(_capacity, str._capacity);
            }
    string(const string& str)
                :_str(nullptr)
                , _size(0)
                , _capacity(0)
            {
                string tmp(str._str);
                swap(tmp);
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 现在我们定义拷贝构造函数时可以不用strcpy了,我们先初始化一个空string对象,然后用构造函数构造一个和需要拷贝构造的string数值相同的string,再把它们交换,就完成了拷贝构造,并且也是深拷贝
    • 出了拷贝构造函数,我们临时构造的对象tmp会自动调用析构函数析构它
    • 这里注意我们需要对_str进行初始化,否则_str为随机值,交换后出了拷贝构造调用析构函数进行delete时会报错,因为此时_str的空间不是动态分配得来的
    • 这里的swap函数可以不自己写,调用库里的,但是库里的效率没有我们自己重新写的效率高,因为我们仅仅是交换,而库里的swap会新建变量进行三次的深拷贝

    5.赋值运算符重载(operator=)

    <1> 传统方式

     string& operator=(const string& str)
            {
                if (this!=&str)//防止s1=s1
                {
                    delete[] _str;
                    _str = new char[strlen(str._str) + 1];
                    strcpy(_str, str._str);
                }
                return *this;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 和拷贝构造不同的是,operator=需要delete释放原空间再申请和str一样大的空间再进行拷贝(因为要保证它们的空间一样大,小了能扩容,大了的话只能重新申请空间,不如直接重新申请空间)
    • 为了防止自己给自己赋值,加上if判断,当this!=&str时再进行赋值

    <2> 现代方式

    string& operator=(string str)//s1=s1时_str的地址会变
            {
                swap(str);
                return *this;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 和拷贝构造一样,operator=也有现在方式,使代码更为简洁
    • 这里的参数是关键,并没有传引用,所以传参时就进行了拷贝构造,之后直接进行swap就完成了赋值
    • 缺点时是无法防止自己给自己赋值

    6.迭代器与operator[]

      const char& operator[](int pos) const
      //专门重载用于const成员函数,防止修改_str的值
       {
           assert(pos < _size);
           return _str[pos];
       }
       char& operator[](int pos)
       {
           assert(pos < _size);
           return _str[pos];
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
      typedef char* iterator;
      typedef const char* const_iterator;
      iterator begin()
      {
          return _str;
      }
      iterator end()
      {
          return _str + _size;
      }
      const_iterator begin() const
      {
          return _str;
      }
      const_iterator end() const
      {
          return _str + _size;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 编写operator[]时别忘记加上assert防止越界访问
    • string类的迭代器其实就是char和const char指针的封装

    二:reserve与resize

    1.reserve函数

    void reserve(size_t n)
    {
       if (n > _capacity)
        {
           char* tmp = new char[n + 1];
           strncpy(tmp, _str, _size + 1);
           //不能用strcpy,有可能有多的\0没有被拷贝过来,拷贝_size+1个,\0也要拷贝
           delete[] _str;
           _str = tmp;
           _capacity = n;
        }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • n<_capacity时不扩容
    • 不能使用strcpy进行拷贝,因为string类中间可能含有`\0``,string类的结束标志是pos==_size
    • 所以我们使用strncpy进行拷贝,直接拷贝_size+1个字符,\0也要拷贝

    2.resize函数

    void resize(size_t n, char val = '\0')//别忘了缺省参数
            {
                if (n < _size)//分成n<_size和n>=size两种情况
                {
                    _size = n;
                    _str[_size] = '\0';
                }
                else
                {
                    if (n > _capacity)
                    {
                        reserve(n);
                    }
                    while (_size < n)
                    {
                        _str[_size++] = val;
                    }
                    _str[_size] = '\0';
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • resize可以分为两种情况
    • (1) n<_size (2)n>=_size
    • 第一种情况比较简单,直接将第n个位置的字符改为\0,再修改_size的值即可
    • 第二种情况就要从_size一直尾插val字符,并在最后以\0结尾

    三:插入删除和查找

    1.push_back

    void push_back(char c)
    {
         if (_size == _capacity)
         {
             reserve(_capacity == 0 ? 4 : _capacity * 2);
         }
         _str[_size++] = c;
         _str[_size] = '\0';
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 每次新增字符前需要检查空间是否足够,不够需要扩容
    • 如果是空串,则分配4个字节的空间,否则增容2倍

    2.append

     void append(const char* str)
     {
         int len = _size + strlen(str);
         if (len > _capacity)
         {
             reserve(len);
         }
         strcpy(_str + _size, str);
         _size = len;
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 和push_back一样,需要检查是否需要扩容

    3.operator+=

    string& operator+=(char c)
    {
         push_back(c);
         return *this;
    }
    string& operator+=(const char* str)
     {
         append(str);
         return *this;
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 需要重载两个版本的operator+=,分别对应尾插一个字符和尾插一个字符串
    • 复用push_back和append即可

    4.insert

    //pos位置之前插入
     string& insert(size_t pos, char ch)
      {
          assert(pos <= _size);//只能在size及之前位置插入
          if (_size == _capacity)
          {
              reserve(_capacity == 0 ? 4 : _capacity * 2);//capacity可能为0
          }
          char* end = _str + _size;
          while (end >= _str + pos)
          {
              *(end + 1) = *end;
              end--;
          }
          _str[pos] = ch;
          _size++;
          return *this;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
     string& insert(size_t pos, const char* str)//不加const传常量字符串会有问题
     {
           assert(pos <= _size);
           size_t len = _size+strlen(str);
           if ( len > _capacity)
           {
               reserve(len);
           }
           char* end = _str + _size;
           while (end >= _str + pos)
           {
               *(end + len) = *end;
               end--;
           }
           _size += len;
           strncpy(_str + pos, str, len);
           return *this;
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 注意点:(1)插入的位置不能超过_size
    • (2)别忘了扩容
    • (3)如果不使用指针进行字符的移动的话,需要从_size+1进行赋值
    • 如果从_size开始,令_str[end+1]=_str[end]的话,循环条件写成while(end>=pos)
    • pos为0时,end最后会变为-1,但end的类型是size_t,会是一个很大的正数,就会导致无限循环
     string& insert(size_t pos, char ch)
            {
                assert(pos <= _size);//只能在size及之前位置插入
                if (_size == _capacity)
                {
                    reserve(_capacity == 0 ? 4 : _capacity * 2);//capacity可能为0
                }
                 size_t end = _size+1;//将end类型改为int也不行,会发生隐式类型转换
                 while (end > pos)
                 {
                     _str[end] = _str[end-1];
                     end--;
                 }
                _str[pos] = ch;
                _size++;
                return *this;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    5.erase

     string& erase(size_t pos = 0, size_t len = npos)//不给参数默认全删
     {
          assert(pos < _size);//_size位置是\0,不能删
          //1.要删的字符数量大于剩余字符
          //2.要删的字符数量小于剩余字符
          size_t leftLen = _size - pos;
          if (len >= leftLen)//pos及以后全删
          {
              _str[pos] = '\0';
              _size = pos;
          }
          else
          {
              strcpy(_str + pos, _str + pos + len);
              _size -= len;
          }
          return *this;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 注意点:(1)pos必须小于_size,_size位置是\0,也不能删除
    • (2)需要计算删除的长度和pos后剩余长度的大小,如果要删除的长度大于剩余长度,直接将pos后全部删除,否则只删除len长度的字符,可以通过strcpy来完成字符的移动

    6.find

    size_t find(char ch, size_t pos = 0)
    {
         assert(pos < _size);
         for (int i = pos; i < _size; i++)
         {
             if (_str[i] == ch)
             {
                 return i;
             }
         }
         return npos;
     }
            
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    size_t find(const char* str, size_t pos = 0)
    {
        assert(pos < _size);
        const char* ret = strstr(_str + pos, str);
        if (ret)
        {
            return ret - _str;
        }
        return npos;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • find函数也需重载,分为查找一个字符和查找一个字符串
    • 查找字符时遍历查找即可,查找字符串时可以通过调用库函数strstr实现
    • 需要注意的是,find返回的是下标,查找字符串时strstr返回的是指针,需要通过ret-_str计算出下标再返回

    四:operator<<与operator>>

    ostream& operator<<(ostream& out, const string& s)
    {
         for (auto ch : s)
         {
             out << ch;
         }
         return out;
     }
     istream& operator>>(istream& in, string& s)
     {
         s.clear();
         char ch;
         ch = in.get();
         while (ch != ' ' && ch != '\n')
         {
             s += ch;
             ch = in.get();
         }
         return in;
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 这里的operator<<和operator>>不用写成友元函数,因为并没有直接修改string的内部成员
    • 而是通过string的接口间接修改,比如operator>>就是先清除原字符串,再不断进行+=操作达成输入的目的
    • 输出则是通过范围for()

    五:比较大小的运算符重载

    	bool operator<(string& s1, string& s2)
        {
            return strcmp(s1.c_str(), s2.c_str()) < 0;
        }
        bool operator==(string& s1, string& s2)
        {
            return strcmp(s1.c_str(), s2.c_str()) == 0;
        }
        bool operator<=(string& s1, string& s2)
        {
            return s1 < s2 || s1 == s2;
        }
        bool operator>(string& s1, string& s2)
        {
            return !(s1 <= s2);
        }
        bool operator>=(string& s1, string& s2)
        {
            return !(s1 < s2);
        }
        bool operator!=(string& s1, string& s2)
        {
            return !(s1 == s2);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 在对string类进行排序时需要比较string类的大小,所以我们需要对>,<,==等运算符进行重载
    • 在stl库中这些重载没有写成成员函数,所以编写时也可以写在类外面
    • 可以通过调用strcmp库函数快速实现
  • 相关阅读:
    JavaWeb之异常处理
    软件架构生态化-多角色交付的探索实践
    Spring事务里required里多线程调用required_new方法到底符不符合预期
    聚观早报|阿里、京东首次未公布双十一GMV;苹果开发搜索引擎
    在 el-table 中嵌入 el-checkbox el-input el-upload 多组件,实现复杂业务场景
    对已经关闭的 channel 进行读写关闭操作会发生什么?
    深入理解Handler(上)
    React函数组件和类组件生命周期比较
    神舟电脑怎么清理缓存文件?介绍几种简单有效方法
    【动态规划】是泰波那契数,不是斐波那契数
  • 原文地址:https://blog.csdn.net/m0_63445149/article/details/126491810