【C++】STL---string

简介: 【C++】STL---string

一、C语言中的字符串

C语言中,字符串是以 ‘\0’ 结尾的一些字符的集合,为了操作方便,C标准库中提供了一些 str 系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。

二、string类

  1. string 是表示字符串的字符串类
  2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
  3. string 在底层实际是:basic_string 模板类的别名,typedef basic_string<char, char_traits, allocator> string;
  4. 不能操作多字节或者变长字符的序列。
  5. 在使用 string 类时,必须包含 #include 头文件以及 using namespace std;

其中,string 类的许多接口可以点击链接-> string 查看。

在这里需要介绍一下迭代器,在 string 类中,迭代器其实就是原生指针,迭代器的使用如下:

#include <iostream>
    #include <string>
    using namespace std;
    int main()
    {
      string s1("hello,world");
      // 迭代器的使用,iterator 就是迭代器,它需要指定作用域
      string::iterator it = s1.begin();
      while (it != s1.end())
      {
        cout << *it << ' ';
        it++;
      }
      cout << endl;
      return 0;
    }

其中 s1.begin(); 其实就是指向字符串开始的指针,s1.end() 就是指向 ‘\0’ 的指针。

三、模拟实现 string 类

下面我们直接开始模拟实现 string 类的接口,在实现的过程中讲解用法,注意,我们只模拟比较常见和重要的接口。

我们先观察 string 类的声明部分,先预览一下我们需要实现哪些接口:

0. string 类的声明

namespace Young
    {
      class String
      {
      public:
        // 迭代器
        typedef char* iterator;
        typedef const char* const_iterator;
        iterator begin();
        iterator end();
        const_iterator begin() const;
        const_iterator end() const;
        // 构造
        String(const char* str = "")
          :_str(new char [strlen(str) + 1])
          ,_size(strlen(str))
          ,_capacity(_size)
        {
          strcpy(_str, str);
        }
        // 析构
        ~String()
        {
          delete[] _str;
          _str = nullptr;
          _size = _capacity = 0;
        }
        // 交换
        void swap(String& tmp)
        {
          ::swap(_str, tmp._str);
          ::swap(_size, tmp._size);
          ::swap(_capacity, tmp._capacity);
        }
        // s2(s1)
        // 拷贝构造
        String(const String& str)
          :_str(nullptr)
          ,_size(0)
          ,_capacity(0)
        {
          String tmp(str._str);
          swap(tmp);
        }
        // s2 = s1
        // 赋值运算符重载
        String& operator=(const String& str)
        {
          if (this != &str)
          {
            String tmp(str._str);
            swap(tmp);
          }
          return *this;
        }
        // 申请空间 -- 不改变 _size
        void reserve(size_t n);
        // 将空间调整为 n -- 改变 _size
        void resize(size_t n, char c = '\0');
        // 尾插字符
        void push_back(char c);
        String& operator+=(char c);
        // 尾插字符串
        void append(const char* str);
        String& operator+=(const char* str);
        // 清空字符串
        void clear();
        // 获取字符串长度
        size_t size() const;
        // 获取容量
        size_t capacity() const;
        // [] 重载   s[1]
        const char& operator[](size_t index) const;
        char& operator[](size_t index);
        // 比较符号运算符重载
        bool operator>(const String& s) const;
        bool operator==(const String& s) const;
        bool operator>=(const String& s) const;
        bool operator<(const String& s) const;
        bool operator<=(const String& s) const;
        bool operator!=(const String& s) const;
        // 返回它的字符串 -- 返回 char* 类型
        const char* c_str() const;
        // 判断是否为空字符串
        bool empty() const;
        // find -- 从pos位置开始查找字符/字符串
        size_t find(char ch, size_t pos = 0) const;
        size_t find(const char* str, size_t pos = 0) const;
        // 获得从 pos 位置开始到 len 的子字符串;如果 len 不给值,默认到结尾
        String substr(size_t pos, size_t len = npos) const;
        // 在 pos 位置插入插入字符 ch 或字符串 str
        String& insert(size_t pos, char ch);
        String& insert(size_t pos, const char* str);
        // 删除从 pos 位置开始 len 长度的字符串;如果 len 不给值就默认删到末尾 
        String& erase(size_t pos, size_t len = npos);
        // 打印数据
        void Print();
      private:
        char* _str;
        size_t _size;
        size_t _capacity;
      public:
        const static size_t npos = -1;
      };
      // 流插入、流提取      cout << s1;
      ostream& operator<<(ostream& out, const String& s);
      istream& operator>>(istream& in, String& s);
    }

1. 构造函数

构造函数往往是实现一个类首先需要考虑的,而 string 类的成员变量分别有 char* _str; size_t _size; size_t _capacity; ,虽然都是内置类型,但是 char* 类型需要我们手动申请空间,所以需要我们显式写构造函数:

// 构造函数
      String(const char* str = "")
        :_str(new char [strlen(str) + 1])
        ,_size(strlen(str))
        ,_capacity(_size)
      {
        strcpy(_str, str);
      }

我们在构造函数中给了缺省值 "" 即空字符串;我们在申请空间的时候往往是要申请比 str 多一个空间,因为需要存放 '\0';最后我们使用 strcpy 函数将 str 拷贝到 _str 即可。

2. 析构函数

因为 string 类是我们手动申请空间的,所以要我们手动释放,析构函数如下:

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

要注意使用 delete 需要匹配使用。

3. 拷贝构造函数

在这里的拷贝构造函数中,我们就需要回顾一下浅拷贝深拷贝了,我们此前也在 类和对象(中篇) 了解过,现在来回顾一下。

  1. 浅拷贝: 也称值拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

我们可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。

  1. 深拷贝: 如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

下面我们自己显式写一个 string 类的拷贝构造函数:

// String s2(s1);
      // 拷贝构造
      String(const String& str)
        :_str(nullptr)
        ,_size(0)
        ,_capacity(0)
      {
        String tmp(str._str);
        swap(tmp);
      }

注意,上面的拷贝构造中,假设是这样: String s2(s1);_str 就是 s2 对象的,strs1 对象的,我们的思路是首先将 _str 走初始化列表置空,_size_capacity 置零,然后利用构造函数 String tmp(str._str); 实例化一个 tmp 对象,此时 tmp 相当于是 s1 ,最后将 s2tmp 对象中的资源交换,即完成了 s1 拷贝给 s2;而且出了作用域 tmp 还会自动调用析构函数析构。

注意以上的 swap 函数也是 string 类中的,所以也需要我们自己实现,实现如下:

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

在实现中,我们再利用标准库中的 swap 函数帮助我们完成 string 类的swap 函数。

使用和结果如下图:

4. 赋值运算符重载

赋值运算符重载也和拷贝构造差不多,实现如下:

// s2 = s1
      // 赋值运算符重载
      String& operator=(const String& str)
      {
        if (this != &str)
        {
          String tmp(str._str);
          swap(tmp);
        }
        return *this;
      }

使用和结果如下:

5. 迭代器

string 类的迭代器其实就是原生指针,声明在上面的 string 类声明中,下面我们直接实现:

// 迭代器
    Young::String::iterator Young::String::begin()
    {
      return _str;
    }
    Young::String::iterator Young::String::end()
    {
      return _str + _size;
    }
    Young::String::const_iterator Young::String::begin() const
    {
      return _str;
    }
    Young::String::const_iterator Young::String::end() const
    {
      return _str + _size;
    }

由于我们是声明和定义分离写,所以在 iterator/const_iteratorbegin()/end() 前都要指定我们的作用域;其中 iterator/const_iterator 分别是普通对象调用的迭代器和 const 对象调用的迭代器;begin()/end() 分别是指向字符串的头和尾的指针。

6. 元素访问:[] 重载

为了方便访问 string,我们可以重载 [] 可以直接访问下标,实现如下:

const对象:

const char& Young::String::operator[](size_t index) const
    {
      assert(index < _size);
      return _str[index];
    }

普通对象:

char& Young::String::operator[](size_t index)
    {
      assert(index < _size);
      return _str[index];
    }

7. 流插入与流提取重载

在使用 string 的时候,为了方便查看字符串,我们可以重载流插入和流提取,方便打印查看字符串;在以前讲过,我们为了方便我们的使用以及体现流插入和提取的使用价值,我们要在类外面实现,防止 this 指针抢占第一个参数位置,实现如下:

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

流插入中我们只需要将每一个字符打印出来即可;

// 流提取    cin >> s
    istream& Young::operator>>(istream& in, String& s)
    {
      s.clear();
      char buff[129];
      size_t i = 0;
      char ch = in.get();
      while (ch != ' ' && ch != '\n')
      {
        buff[i++] = ch;
        if (i == 128)
        {
          buff[i] = '\0';
          s += buff;
          i = 0;
        }
        ch = in.get();
      }
      if (i != 0)
      {
        buff[i] = '\0';
        s += buff;
      }
      return in;
    }

我们创建一个 buff 数组,存放输入的字符,当 buff 数组满了就一把插入到对象中,避免频繁开辟空间;因为流提取默认遇到 ' ''\0' 就结束,所以我们需要用 cin 的成员函数 get() 提取到 ' ''\0' ,方便我们判断结束条件。

8. 与容量相关的接口

(1)size

获取字符串的有效长度,实现:

size_t Young::String::size() const
    {
      return _size;
    }

(2)capacity

获取字符串的容量,实现:

size_t Young::String::capacity() const
    {
      return _capacity;
    }

(3)clear

清空字符串的内容,实现:

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

清空字符串的内容并不是销毁空间,所以只需要在下标为 0 位置加上 '\0' 即可,并将长度置 0.

(4)empty

判断字符串是否为空字符串,实现:

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

只需要判断 _size 是否为 0.

上面四个接口的使用如下:

(5)reserve

我们可以查看 reserve 接口的相关文档:

其实就是申请 n 个空间,reserve 有保留的意思,就是有保留 n 个空间的意思,n 大于 _capacity 就改变空间,小于则不用改变;注意 reserve 不改变 _size 的值;其实现如下:

// 申请空间
    void Young::String::reserve(size_t n)
    {
      if (n > _capacity)
      {
        char* tmp = new char[n + 1];
        strcpy(tmp, _str);
        delete[] _str;
        _str = tmp;
        _capacity = n;
      }
    }

假设需要申请 n 个空间,就需要申请 n+1 空间,给 '\0' 预留一个空间;然后将原来字符串中的内容拷贝到新开辟的空间中,然后销毁原来的空间 _str,让原来的空间 _str 指向新的空间 tmp

(6)resize

reserveresize 的区别就是 resize 是调整空间的大小,并可以初始化空间,resize 是可以改变 _size 的值的。

// 调整空间+初始化
    void Young::String::resize(size_t n, char c)
    {
      // 如果 n 大于 _size,直接申请 n 个空间,然后从原来的尾部开始初始化 
      if (n > _size)
      {
        reserve(n);
        for (size_t i = _size; i < n; i++)
        {
          _str[i] = c;
        }
        _str[n] = '\0';
        _size = n;
      }
      // 否则,删数据
      else
      {
        _str[n] = '\0';
        _size = n;
      }
    }

初始化的字符如果没有显式传,会使用我们在声明处给的缺省值 '\0

9. 修改字符串的相关接口

(1)push_back

尾插,在字符串尾部插入一个字符,我们先看原文档:

实现如下:

// 尾插字符
    void Young::String::push_back(char c)
    {
      if (_size == _capacity)
      {
        reserve(_capacity == 0 ? 4 : _capacity * 2);
      }
      _str[_size++] = c;
      _str[_size] = '\0';
    }

尾插之前需要判断容量是否已经满了,满了就要扩容;或者容量是 0,我们就默认开 4 个容量。

(2)append

追加字符串,我们先看文档:

文档中重载了许多接口,我们在这里只实现一个接口,就是尾插字符串,也就是上图中的第三个接口,实现如下:

// 尾插字符串
    void Young::String::append(const char* str)
    {
      int len = strlen(str);
      // 空间不够扩容
      if (_size + len > _capacity)
      {
        reserve(_size + len);
      }
      strcpy(_str + _size, str);
      _size += len;
    }

(3)+= 运算符重载

+= 运算符也是追加字符、字符串、string 对象,我们在这里实现追加字符和字符串,也就是尾插,其实现如下:

//尾插字符
    Young::String& Young::String::operator+=(char c)
    {
      push_back(c);
      return *this;
    }

有了之前实现的 push_backappend ,我们只需要复用它们就可以实现了;

// 尾插字符串
    Young::String& Young::String::operator+=(const char* str)
    {
      append(str);
      return *this;
    }

以上四个接口的使用与流插入流提取的使用如下:

(4)insert

insert 是在 pos 位置插入字符 ch 或字符串 str,我们就实现插入字符或字符串的接口,实现如下:

// 插入字符
    Young::String& Young::String::insert(size_t pos, char ch)
    {
      assert(pos < _size);
      // 满了就扩容
      if (_size == _capacity)
      {
        reserve(_capacity == 0 ? 4 : _capacity * 2);
      }
      // 挪动数据
      size_t end = _size + 1;
      while (end > pos)
      {
        _str[end] = _str[end - 1];
        end--;
      }
      // 插入字符
      _str[pos] = ch;
      _size++;
      return *this;
    }

插入字符串:

Young::String& Young::String::insert(size_t pos, const char* str)
    {
      assert(pos < _size);
      // 判断插入字符串的长度是否会满
      size_t len = strlen(str);
      if (_size + len > _capacity)
      {
        reserve(_size + len);
      }
      // 挪动数据
      size_t end = _size + len;
      while (end > pos)
      {
        _str[end] = _str[end - len];
        end--;
      }
      // 拷贝数据1.
      /*for (size_t i = pos; i < len; i++)
      {
        _str[i] = str[i];
      }*/
      // 拷贝数据2.
      strncpy(_str + pos, str, len);
      _size += len;
      return *this;
    }

(5)erase

erase 是删除从 pos 位置开始 len 长度的字符串;如果 len 不给值就默认删到末尾;

到末尾我们需要在声明处定义一个 npos 的静态无符号变量,将它定义为 -1 ,因为是无符号,所以它是整型的最大值,我们在缺省值处给 npos ,即可取到末尾。注意,如果声明和定义分离写,缺省值只能给在声明处。

实现如下:

Young::String& Young::String::erase(size_t pos, size_t len)
    {
      assert(pos < _size);
      // 删到末尾
      if (len == npos || pos + len > _size)
      {
        _str[pos] = '\0';
        _size = pos;
      }
      // 删 len 长度
      else
      {
        strcpy(_str + pos, _str + pos + len);
        _size -= len;
      }
      return *this;
    }

insert 和 erase 的使用如下图:

10. 操作字符串的接口

(1)c_str

返回它的字符串 - 返回 char* 类型,实现:

const char* Young::String::c_str() const
    {
      return _str;
    }

(2)find

find 是查找函数的接口,从 pos 位置开始查找字符/字符串,pos 不给值默认下标从 0 开始找,实现如下:

查找字符:

size_t Young::String::find(char ch, size_t pos) const
    {
      assert(pos < _size);
      for (size_t i = pos; i < _size; i++)
      {
        if (_str[i] == ch)
        {
          // 返回下标
          return i;
        }
      }
      return npos;
    }

查找字符串:

size_t Young::String::find(const char* str, size_t pos) const
    {
      assert(pos < _size);
      assert(str);
      const char* ret = strstr(_str + pos, str);
      if (ret == nullptr)
        return npos;
      // 下标相减,返回下标
      return ret - _str;
    }

strstr 是查找匹配字串的库函数,它的返回值是如果找到就返回匹配字串的开头,否则返回空。

(3)substr

substr 是获得从 pos 位置开始到 len 的子字符串;如果 len 不给值,默认到结尾,即 npos,实现如下:

Young::String Young::String::substr(size_t pos, size_t len) const
    {
      assert(pos < _size);
      // 创建一个临时对象 tmp
      String tmp;
      // end 为取到的子串的结尾的下标
      size_t end = pos + len;
      // 取到末尾
      if (len == npos || end > _size)
      {
        len = _size - pos;
        end = _size;
      }
      // 申请 len 的空间
      tmp.reserve(len);
      // 开始取子串
      for (size_t i = pos; i < end; i++)
      {
        tmp += _str[i];
      }
      return tmp;
    }

一般 findsubstr 一起使用,它们的使用场景可以将一个网址分割成协议、域名、资源名,使用如下:

11. 比较运算符重载

我们也像以前一样,只需要实现 >== 运算符,其它的都复用这两个就可以了,实现如下:

bool Young::String::operator>(const String& s) const
    {
      return strcmp(_str, s._str) > 0;
    }
    bool Young::String::operator==(const String& s) const
    {
      return strcmp(_str, s._str) == 0;
    }
    bool Young::String::operator>=(const String& s) const
    {
      return *this > s || *this == s;
    }
    bool Young::String::operator<(const String& s) const
    {
      return !(*this >= s);
    }
    bool Young::String::operator<=(const String& s) const
    {
      return !(*this > s);
    }
    bool Young::String::operator!=(const String& s) const
    {
      return !(*this == s);
    }

使用和结果如下:

目录
相关文章
|
1月前
|
C++ 容器
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
C++ 容器
|
1月前
|
C++ 容器
|
1月前
|
存储 C++ 容器
|
1月前
|
安全 C语言 C++
【C++篇】探寻C++ STL之美:从string类的基础到高级操作的全面解析
【C++篇】探寻C++ STL之美:从string类的基础到高级操作的全面解析
36 4
|
1月前
|
存储 编译器 程序员
【C++篇】手撕 C++ string 类:从零实现到深入剖析的模拟之路
【C++篇】手撕 C++ string 类:从零实现到深入剖析的模拟之路
64 2
|
1月前
|
编译器 C语言 C++
【C++】C++ STL 探索:String的使用与理解(三)
【C++】C++ STL 探索:String的使用与理解
|
1月前
|
存储 编译器 C++
【C++】C++ STL 探索:String的使用与理解(二)
【C++】C++ STL 探索:String的使用与理解
|
1月前
|
编译器 C语言 C++
【C++】C++ STL 探索:String的使用与理解(一)
【C++】C++ STL 探索:String的使用与理解