【C++】——string的模拟实现

简介: 【C++】——string的模拟实现

前言:

在之前的学习中,我们已经对string类进行了简单的介绍,大家只要能够正常使用即可。但是在面试中,面试官总喜欢让学生自己 来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。因此,接下来我将带领大家手动模拟实现一下。


(一)成员函数

1、构造函数

刚开始时,如果我们要实现构造函数,可能就需要分别实现带参的构造函数和无参的构造函数,但是有没有简单方法可以做到一步到位呢?

💨  因此,为了更加的灵活方便,我们直接把带参的构造函数和无参构造函数集合,形成全缺省的构造函数,这样就省得再去写两个构造函数。

代码如下:

//全缺省的构造函数
//string(const char* str = nullptr)  //不可以,对其解引用如果遇到空指针就报错
//string(const char* str = '\0')      //类型不匹配,char 不能匹配为指针
//string(const char* str = "\0")      //可以
string(const char* str = "") 
  :_size(strlen(str))
{
  _capacity = _size == 0 ? 5 : _size;
  _str = new char[_capacity + 1];
  strcpy(_str, str);
}

2、拷贝构造

编译器默认的实现的是浅拷贝,但是浅拷贝存在问题:

  • 如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

因此为了解决上述的问题,可以采用深拷贝解决浅拷贝问题:

  • 每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。

代码如下:

//深拷贝
// str3(str2)
string(const string& STR)
  :_size(STR._size)
  , _capacity(STR._capacity)
{
  _str = new char[STR._capacity + 1];
  strcpy(_str, STR._str);
}

3、赋值重载

注意:

  1. 当以拷贝的方式初始化一个对象时,会调用拷贝构造函数;
  2. 当给一个对象赋值时,会调用重载过的赋值运算符。

即使我们没有显式的重载赋值运算符,编译器也会以默认地方式重载它。默认重载的赋值运算符功能很简单,就是将原有对象的所有成员变量一一赋值给新对象,这和默认拷贝构造函数的功能类似。

代码如下:

string& operator=(const string& STR)
{
  if (this != &STR)
  {
    char* tmp = new char[STR._capacity + 1];
    strcpy(tmp, STR._str);
    delete[] _str;
    _str = tmp;
    _size = STR._size;
    _capacity = STR._capacity;
  }
  return *this;
}

4、析构函数

析构函数的实现就比较简单,只需将指针所指的空间进行释放并把置空即可(防止野指针) ,最后把剩余的两个成员置为0即可。

代码如下:

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

(二)容量

1、size()

顾名思义返回字符串的长度(以字符数为单位)

代码如下:

size_t size() const
{
  return _size;
}

2、capacity()

返回当前为basic_string分配的存储空间的大小,以字符表示。

代码如下:

size_t capacity() const
{
  return _capacity;
}

3、reserve()

表示请求更改容量,使字符串容量适应计划的大小更改为最多 n 个字符。

注意是有效字符,不包含标识字符,而在具体实现的时候,我们在底层多开一个空间给\0。

代码如下:

//扩容操作
void reserve(size_t N)
{
  if (N > _capacity)
  {
    char* tmp = new char[N + 1];
    strcpy(tmp, _str);
    delete[] _str;
    _str = tmp;
    _capacity = N;
  }
}

4、resize()

其实它的情况大体上可以分为插入数据和删除数据两种情况。

  • 1.对于插入数据来说直接调用【reserve】提前预留好空间,然后搞一个for循环将字符ch尾插到数组里面去,最后再在数组末尾插入一个\0标识字符;
  • 2.对于删除数据就比较简单了,如果 n 小于当前字符串长度,则当前值将缩短为其第一个 n 个字符,删除第 n 个字符以外的字符然后重置一下_size的大小为n即可。

代码如下:

//扩容+初始化
    void resize(size_t n, char STR = '\0')
    {
      if (n < _size)
      {
        // 删除数据--保留前n个
        _size = n;
        _str[_size] = '\0';
      }
      else if (n > _size)
      {
        if (n > _capacity)
        {
          reserve(n);
        }
        size_t end = _size;
        while (end < n)
        {
          _str[end] = STR;
          end++;
        }
        _size = n;
        _str[_size] = '\0';
      }
    }

5、clear()

顾名思义就是清除字符串,擦除basic_string的内容,该内容变为空字符串长度为 0 个字符)。

代码如下:

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

(三)元素访问

1、 operator[]

元素访问操作相对来说用的最多的就是operator[] ;

  1. 对它进行调用时可能进行的是写操作,也可能进行读操作,所以为了适应const和非const对象,operator[]应该实现两个版本的函数;
  2. 并且这个函数处理越界访问的态度就是assert直接断言,而at对于越界访问的态度是抛异常。

代码如下:

const char& operator[](size_t pos) const
{
  assert(pos < _size);
  return _str[pos];
}
char& operator[](size_t pos)
{
  assert(pos < _size);
  return _str[pos];
}

 


(四)修改

1、 operator+=

追加到字符串,通过在当前值的末尾附加其他字符来扩展

在这里我们只实现添加字符和字符串的操作;

我们可以直接复用【push_back】的操作来实现。

代码如下:

//+=
string& operator+=(char STR_1)
{
  push_back(STR_1);
  return *this;
}
string& operator+=(const char* STR_2)
{
  append(STR_2);
  return *this;
}

 

2、append()

追加到字符串通过在当前值的末尾附加其他字符来扩展。

  1. 我们可以直接调用strcpy接口来进行字符串的尾插,但是需要注意一点,那就是【string】类的字符串函数是不会进行自动扩容的,所以我们需要判断一下是否需要进行扩容,在空间预留好的情况下进行字符串的尾插即可实现;
  2. 其次,如果已经实现了【insert】函数的情况下。我们可以直接复用【insert】函数也可实现对应的操作。

代码如下:

//追加字符串
void append(const char* STR)
{
  size_t len = strlen(STR);
  if (len + _size > _capacity)
  {
    reserve(_size + len);
  }
  strcpy(_str + _size, STR);
  _size += len;
  //insert(_size, STR);
}

3、push_back()

注意:

  1. 首先对于【push_back】有一个特别需要注意的地方就是当容量不够时的扩容操作。如果是一个空对象进行push_back的话,这时如果我们采取的二倍扩容就有问题,因为0*2还是0,所以对于空对象的情况我们应该给他一个初始的capacity值,所以上述构造函数的时候我给成了【5】,其他情况下进行二倍扩容即可;
  2. 其次,就是在尾插字符之后,要记得进行补【\0】操作,,否则在打印的时候就会有麻烦了。
  3. 最后跟【append】一样,如果已经实现了【insert】函数的情况下。我们可以直接复用【insert】函数也可实现对应的操作。

代码如下:

//尾插操作
void push_back(char STR)
{
  if (_size + 1 > _capacity)
  {
    reserve(_capacity * 2);
  }
  _str[_size] = STR;
  ++_size;
  _str[_size] = '\0';
  //insert(_size, STR);
}

4、insert()

插入到字符串中,在 pos(或 p)指示的字符之前将其他字符插入

注意:

  1. 对于【insert】函数,有经常会引出错误的地方,那就是对于while循环里面的操作;
  2. 可能很多的小伙伴在while循环里面都是这样写的:_str[end + 1] = _str[end] ,那么这样写有没有问题呢?答案是会出问题的;
  3. 我们的end是size_t定义的,因为size_t是无符号数,那么-1会被认为是无符号整数,进行隐式类型转换,由于-1的补码是全1,此时就是恒大于0,程序会陷入死循环。所以我们可以不用size_t来定义end,防止发生隐式类型转换;
  4. 那么是不是只要把【size_t end = _size + len;】中的【end】用 int 定义就可以解决了呢?答案当然不是的 (是不是觉得很坑了呀!!!);
  5. 因为-1在和size_t定义的pos进行比较时,又会发生隐式类型转换。这是因为比较运算符也是运算符,只要进行运算就有可能出现隐式类型转换,因此此时又可能出现上述那样的情况,-1就又会被转为无符号整型,程序就又陷入死循环;
  6. 那么有没有解决方法呢?当然是有的,我们只需在比较时将【size_t】的pos强转为【int】类型,此时再去比较就没得问题了;
  7. 但当我们就想使用size_t类型,通过把【end-1】位置的元素挪到【end】位置上去,在while循环条件的判断位置,我们用end来和pos位置进行比较,end应该大于pos的位置,一旦end=pos我们就跳出循环,这样就可以了。

代码如下:

//插入字符操作
    string& insert(size_t pos, char STR_1)
    {
      assert(pos < _size);
      if (_size + 1 > _capacity)
      {
        reserve(2 * _capacity);
      }
      size_t end = _size + 1;
      while (end > pos)
      {
        _str[end] = _str[end - 1];
        --end;
      }
      _str[pos] = STR_1;
      ++_size;
      return *this;
    }
    //插入字符串
    string& insert(size_t pos, const char* STR_2)
    {
      assert(pos < _size);
      size_t len = strlen(STR_2);
      if (_size + len > _capacity){
        reserve(_size + len);
      }
      // 挪动数据
      size_t end = _size + len;
      while (end > pos + len - 1)
      {
        _str[end] = _str[end - len];
        --end;
      }
      // 拷贝插入
      strncpy(_str + pos, STR_2, len);
      _size += len;
      return *this;
    }

5、erase()

意思很简单,就是从字符串中删除字符

对于删除,思路很简单,分为两种情况下的删除:

  • 1.如果当前位置加上要删除的长度大于字符串的长度,即【 pos + len >= _size】,此时的意思即为删除pos之后的所有元素;
  • 2.除了上述情况,就是在字符串内正常删除操作。我们只需利用strcpy来进行,将pos+len之后的字符串直接覆盖到pos位置,这样实际上就完成了删除的工作。

注意:

  1. 对于【npos】这个参数,首先我们知道对于静态成员变量,它的规则是在类外定义,类里面声明,定义时不加static关键字
  2. 但如果静态成员变量有const修饰,这时它可以在类内直接进行定义,这样的特性只针对于整型,对于其他类型则是不适用的;
  3. npos就是const static修饰的成员变量,可以直接在类内进行定义。

代码如下:

//删除操作
    string& 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;
      }
      return *this;
    }

6、swap()

至于交换,这个就没有必要再多说什么了很简单,我相信大家肯定也会这个。

代码如下:

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

 


(五)字符串操作

1、c_str()

获取等效的 C 字符串,返回指向一个数组的指针,该数组包含以 null 结尾的字符序列(即 C 字符串),表示basic_string对象的当前值

代码展示:

const char* c_str()
    {
      return _str;
    }

2、find()

查找字符串中的第一个匹配项basic_string中搜索由其参数指定的序列的第一个匹配项。

对于这个函数不用多说,就是对其进行遍历查找即可。

代码展示:

//查找
    size_t find(char STR, size_t pos = 0)
    {
      assert(pos < _size);
      for (size_t i = pos; i < _size; ++i)
      {
        if (_str[i] == STR)
        {
          return i;
        }
      }
      return npos;
    }
    size_t find(const char* STR, size_t pos = 0)
    {
      assert(pos < _size);
      char* p = strstr(_str + pos, STR);
      if (p == nullptr)
      {
        return npos;
      }
      else
      {
        return p - _str;
      }
    }

 


(六)非成员函数重载

1、relational operators()

basic_string的关系运算符,以ascll码的方式比较大小

这个实现的过程,跟之前日期类的时间如出一辙,基本上都是一样的。

代码如下:

//比较大小
    bool operator >(const string& STR) const
    {
      return strcmp(_str, STR._str) > 0;
    }
    bool operator == (const string & STR)const
    {
      return strcmp(_str, STR._str) == 0;
    }
    bool operator >= (const string & STR)const
    {
      return *this > STR || *this == STR;
    }
    bool operator <  (const string & STR)const
    {
      return !(*this >= STR);
    }
    bool operator <= (const string& STR)const
    {
      return !(*this > STR);
    }
    bool operator!=(const string& STR) const
    {
      return !(*this == STR);
    }

2、operator<<

将字符串插入流将符合 str 值的字符序列插入到 os 中。

代码如下:

//operator<<
  ostream& operator<<(ostream& out, const string& STR)
  {
    for (auto e : STR)
    {
      out << e;
    }
    return out;
  }

3、operator>>

从流中提取字符串输入流中提取字符串,将序列存储在 str 中,该序列被覆盖(替换 str 的先前值)

注意:

  • 流提取是以空格和\n作为间隔标志的 ,而【getline】则是以【\0】就停止。

代码如下:

//operator>>
  istream& operator>>(istream& in, string& STR)
  {
    STR.clear();
    char ch = in.get();
        //如果输入到缓冲区里的字符串非常非常的长,那么+=就需要频繁的扩容,则效率就会降低
        //因此,在这里可以使用开辟一个数组,先将有效数据放入数组中,在进行操作,可有效提高效率
    char buff[128];   
    size_t i = 0;
    while (ch != ' ' && ch != '\0')
    {
      buff[i++] = ch;
      if (i == 127)      //最后得留一个位置给\0
      {
        buff[127] = '\0';
        STR += buff;
        i = 0;
      }
      ch = in.get();
    }
    if (i != 0)
    {
      buff[i] = '\0';
      STR += buff;
    }
    return in;
  }

 

(七)代码汇总

代码汇总如下:

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;
    }
    
    //全缺省的构造函数
    //string(const char* str = nullptr)  //不可以,对其解引用如果遇到空指针就报错
    //string(const char* str = '\0')      //类型不匹配,char 不能匹配为指针
    //string(const char* str = "\0")      //可以
    string(const char* str = "") 
      :_size(strlen(str))
    {
      _capacity = _size == 0 ? 5 : _size;
      _str = new char[_capacity + 1];
      strcpy(_str, str);
    }
    //深拷贝
    // str3(str2)
    string(const string& STR)
      :_size(STR._size)
      , _capacity(STR._capacity)
    {
      _str = new char[STR._capacity + 1];
      strcpy(_str, STR._str);
    }
      //赋值操作  
    string& operator=(const string& STR)
    {
      if (this != &STR)
      {
        // str1 = str1 的情况不满足
        /*delete[] _str;
        _str = new char[s._capaicty + 1];
        strcpy(_str, s._str);
        _size = s._size;
        _capaicty = s._capaicty;*/
        char* tmp = new char[STR._capacity + 1];
        strcpy(tmp, STR._str);
        delete[] _str;
        _str = tmp;
        _size = STR._size;
        _capacity = STR._capacity;
      }
      return *this;
    }
    //析构函数
    ~string()
    {
      delete[] _str;
      _str = nullptr;
      _size = _capacity = 0;
    }
    const char* c_str()
    {
      return _str;
    }
    const char& operator[](size_t pos) const
    {
      assert(pos < _size);
      return _str[pos];
    }
    char& operator[](size_t pos)
    {
      assert(pos < _size);
      return _str[pos];
    }
    size_t size() const
    {
      return _size;
    }
    size_t capacity() const
    {
      return _capacity;
    }
    //比较大小
    bool operator >(const string& STR) const
    {
      return strcmp(_str, STR._str) > 0;
    }
    bool operator == (const string & STR)const
    {
      return strcmp(_str, STR._str) == 0;
    }
    bool operator >= (const string & STR)const
    {
      return *this > STR || *this == STR;
    }
    bool operator <  (const string & STR)const
    {
      return !(*this >= STR);
    }
    bool operator <= (const string& STR)const
    {
      return !(*this > STR);
    }
    bool operator!=(const string& STR) const
    {
      return !(*this == STR);
    }
    //扩容+初始化
    void resize(size_t n, char STR = '\0')
    {
      if (n < _size)
      {
        // 删除数据--保留前n个
        _size = n;
        _str[_size] = '\0';
      }
      else if (n > _size)
      {
        if (n > _capacity)
        {
          reserve(n);
        }
        size_t end = _size;
        while (end < n)
        {
          _str[end] = STR;
          end++;
        }
        _size = n;
        _str[_size] = '\0';
      }
    }
    //扩容操作
    void reserve(size_t N)
    {
      if (N > _capacity)
      {
        char* tmp = new char[N + 1];
        strcpy(tmp, _str);
        delete[] _str;
        _str = tmp;
        _capacity = N;
      }
    }
    //尾插操作
    void push_back(char STR)
    {
      if (_size + 1 > _capacity)
      {
        reserve(_capacity * 2);
      }
      _str[_size] = STR;
      ++_size;
      _str[_size] = '\0';
      //insert(_size, STR);
    }
    //追加字符串
    void append(const char* STR)
    {
      size_t len = strlen(STR);
      if (len + _size > _capacity)
      {
        reserve(_size + len);
      }
      strcpy(_str + _size, STR);
      _size += len;
      //insert(_size, STR);
    }
    //+=
    string& operator+=(char STR_1)
    {
      push_back(STR_1);
      return *this;
    }
    string& operator+=(const char* STR_2)
    {
      append(STR_2);
      return *this;
    }
    //插入字符操作
    string& insert(size_t pos, char STR_1)
    {
      assert(pos < _size);
      if (_size + 1 > _capacity)
      {
        reserve(2 * _capacity);
      }
      size_t end = _size + 1;
      while (end > pos)
      {
        _str[end] = _str[end - 1];
        --end;
      }
      _str[pos] = STR_1;
      ++_size;
      return *this;
    }
    //插入字符串
    string& insert(size_t pos, const char* STR_2)
    {
      assert(pos < _size);
      size_t len = strlen(STR_2);
      if (_size + len > _capacity){
        reserve(_size + len);
      }
      // 挪动数据
      size_t end = _size + len;
      while (end > pos + len - 1)
      {
        _str[end] = _str[end - len];
        --end;
      }
      // 拷贝插入
      strncpy(_str + pos, STR_2, len);
      _size += len;
      return *this;
    }
    //删除操作
    string& 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;
      }
      return *this;
    }
    //交换
    void swap(string& STR)
    {
      std::swap(_str, STR._str);
      std::swap(_capacity, STR._capacity);
      std::swap(_size, STR._size);
    }
    //查找
    size_t find(char STR, size_t pos = 0)
    {
      assert(pos < _size);
      for (size_t i = pos; i < _size; ++i)
      {
        if (_str[i] == STR)
        {
          return i;
        }
      }
      return npos;
    }
    size_t find(const char* STR, size_t pos = 0)
    {
      assert(pos < _size);
      char* p = strstr(_str + pos, STR);
      if (p == nullptr)
      {
        return npos;
      }
      else
      {
        return p - _str;
      }
    }
    void clear()
    {
      _str[0] = '\0';
      _size = 0;
    }
    //operator<<
  ostream& operator<<(ostream& out, const string& STR)
  {
    for (auto e : STR)
    {
      out << e;
    }
    return out;
  }
  //operator>>
  istream& operator>>(istream& in, string& STR)
  {
    STR.clear();
    char ch = in.get();
    char buff[128];
    size_t i = 0;
    while (ch != ' ' && ch != '\0')
    {
      buff[i++] = ch;
      if (i == 127)
      {
        buff[127] = '\0';
        STR += buff;
        i = 0;
      }
      ch = in.get();
    }
    if (i != 0)
    {
      buff[i] = '\0';
      STR += buff;
    }
    return in;
  }

(八)总结

到此,关于string的模拟实现,在这里我们主要实现的是经常用得到的,对于其他的,我们并没有一一列举。如果后面有机会再给大家展示。

接下来,我们简单总结一下本文:

  1. 我们从文档的先后顺序入手,依次对各个板块的常用接口进行了模拟实现;
  2. 大家在上手操作的时候,一定要想明白为什么,做到真正的掌握string类它是非常重要的。在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数

到此,便于string类的模拟实现便讲解完毕了。希望本文对大家有所帮助,感谢各位的观看!!!

 

相关文章
|
20天前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
46 5
|
20天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
30 2
|
2月前
|
C++ 容器
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
26 1
|
2月前
|
C++ 容器
|
2月前
|
C++ 容器
|
2月前
|
存储 C++ 容器
|
2月前
|
安全 C语言 C++
【C++篇】探寻C++ STL之美:从string类的基础到高级操作的全面解析
【C++篇】探寻C++ STL之美:从string类的基础到高级操作的全面解析
41 4
|
2月前
|
存储 编译器 程序员
【C++篇】手撕 C++ string 类:从零实现到深入剖析的模拟之路
【C++篇】手撕 C++ string 类:从零实现到深入剖析的模拟之路
73 2
|
2月前
|
编译器 C语言 C++
【C++】C++ STL 探索:String的使用与理解(三)
【C++】C++ STL 探索:String的使用与理解