C++的STL容器(STL Container in Cpp)

PL-TD 发布于 2025-04-05 29 次阅读


String类

  • 简介:在C语言中,字符串可以认为是以'\0'作为结尾的字符集合,而要使用字符串的一些方法,则需要单独导入string.h这个头文件,这就带来极大的不便,而且用户对字符串操作时还要自己注意管理字符串内存\,以防稍不注意的访问越界行为,这不符合面向对象的思想,所以在C++的STL库中就支持了string类以专门表示字符串

  • 注意事项:string类的底层是一个char*类型数组*;在C++中使用模板basic_string定义,要使用string类,需要导入std库(或者使用作用域限定符std::)和string***文件

String的常用成员

  • String类框架搭建
class string
{
private:
    char* _str;
    size_t _size;
    size_t _capacity;

    // 静态成员npos
    static size_t npos;
};

npos = -1;

默认的4个成员函数

  • 按照惯例,我们先介绍一个类中最重要的4个默认函数,即:构造函数拷贝构造函数析构函数,还有operator=重载
  1. 构造和拷贝构造函数:在C++98中一共有7个构造函数

    • 其中常用的有如下几种:

      1. string():默认构造函数,创建一个不包含任何字符的string对象
      2. string(const string& str):拷贝构造函数,用str复制\构造一个新的string对象
      3. string(const char* s):利用C-string[即字符数组]来构造string对象
      4. string(size_t n, char c):使用n个相同的字符c\构造一个string对象
      // 无参构造
      string()
       : _str(new char[1])
       , _size(0)
       , _capacity(0)
       {_str[0] = '\0';}
      /* 细节:在无参构造时为_str开辟了一个空间,而不是置为nullptr,这是为了防止在使用某些需要解引用_str的操作时产生解引用空指针而导致崩溃 */
      
      // C-string构造函数
      string(const char& str = "")
       ; _str(new char[strlen(str) + 1])
       , _size(strlen(str))
       , _capacity(_size)
      {
       for (int i = 0; i < _size + 1 ;i++)
       {
           _str[i] = str[i];
       }
      }
      /* 细节:在C-string式构造函数的参数后加""空字符串作为全缺省参数,也是可以作为默认构造函数的*/
      
      // 拷贝构造(介绍现代写法)
      string(const string& s)
       : _str(nullptr)
       , _size(0)
       , _capacity(0)
      {
       string tmp(s._str); // 通过C-string的方式构造一个字符串
       ::swap(tmp._str, _str); // 使用C++内置的swap函数(深拷贝)
      }
      /* 什么是现代写法:就是通过设计好的函数,减少程序员的代码量,让函数逻辑书写和实现更简单的过程,下列是传统写法的拷贝构造*/
      //// 拷贝构造(深拷贝) - 传统写法
      //string(const string& s)
      // : _str(new char [strlen(s._str) + 1]) // TODO:在类里访问不受访问限制符的限制
      // , _size(s._size)
      // , _capacity(s._capacity)
      //{
      // for (int i = 0; i < _size + 1; i++)
      // {
      //     _str[i] = s._str[i];
      // }
      //}
      
      // n个字符c构造
      string(size_t n, char c)
       : _str(new char[n + 1])
       , _size(n)
       , _capacity(n)
      {
       for(int i = 0; i < n; i++)
       {
           _str[i] = c;
       }
      }
    • 这是上述构造函数的测试结果:

      image-20250318212540148

    • 余下的:

      1. string(const char* s, size_t n):使用一个C-string对象的前n个字符\构造一个string对象(即使前n个字符包含'\0',也会继续继续拷贝)
      2. string(const string& str, size_t pos, size_t n = npos):使用str从pos位置开始的n个字符构成的字串构造新的string对象
        • npos:是size_t默认的最大值 (size_t npos = -1;)
      3. string(InputIterator first, InputIterator last):使用迭代器first至last范围内字符构造一个新的string对象
        • 迭代器:可以暂时理解为指针,但是对比指针,迭代器有更强的可迁移性,只要是内置类型都有迭代器,详情见下文
  2. 析构函数

    • 由于string是一个给予char类型数组的类型,所以会有动态内存开辟,所以用户需要自己实现析构函数以释放存储在堆中的空间,这样对象在销毁时就会自动释放堆内存中的空间了

      • string():用于清空string对象申请的内存
      ~string()
      {
       if(_str != nullptr)
       {
           delete[] _str;
           _str = nullptr;
       }
      }
  3. ==operator=

    • 和析构的原因一样,由于string需要进行动态内存开辟,所以要手动实现=运算符的重载

      string& operator=(const string& s)
      {
       if(&s != this) // 防止重复赋值(!=符号在下文有定义)
       {
           delete[] _str;
           _str = new char[strlen(s._str) + 1];
           for(int i = 0; i < s._size; i++)
           {
               _str[i] = s._str[i];
           }
           _size = s._size;
           _capacity = s._capacity;
           return *this;
       }
      }

string的容器函数

  • string的函数容器用于返回与string性质相关的数据

基础容器

  • size():返回容器的有效字符个数
  • capacity():返回容器的游戏字符上限数
  • c_str():返回C-string
  • substr(size_t pos = 0, size_t len = npos) const:从string对象的pos位置开始拷贝len长度(默认全拷贝)并创建一个全新的子字符串
// 获取有效字符个数
size_t size()
{
    return _size;
}

// 获取容量(可容纳有效字符的最大个数)
size_t capacity()
{
    return _capacity;
}

// 获取C-string
const char* c_str()
{
    return _str; // 返回首地址
}

// 获取子字符串
string substr(size_t pos = 0, size_t len = npos) const
{
    Checker(pos < _size);
    if (len > _size - pos) // 如果长度过长
    {
        len = _size - pos;
    }
    char* tmp = new char[len + 1];
    for (size_t i = 0; i < len; i++)
    {
        tmp[i] = _str[i + pos];
    }
    tmp[len] = '\0';

    return string(tmp);
}

迭代器

  • 下面就是字符串的迭代器(实质上是char指针)了
  • 什么是迭代器:迭代器是一种检查容器内元素并遍历元素的数据类型,不同容器有不同的迭代器
// 先定义一下string类的迭代器和只读迭代器
typedef char* iterator;
typedef char* const_iterator;

// 迭代器开头(名字一定要定义为begin)
iterator begin()
{
    return _str; // 返回头
}

// 迭代器结尾(名字一定要定义为end)
iterator end()
{
    return _str + _size;
}

const_iterator cbegin() const
{
    return _str;
}

const_iterator cend() const
{
    return _str + _size;
}
  • 要是要使用反转迭代器,则需要先定义反转迭代器这个类,并实现它的运算符重载
// 定义反转迭代器
struct __reverse_iterator
{
    typedef __reverse_iterator Self;
    char* _str;

    __reverse_iterator(char* str)
        : _str(str)
    { }

    char& operator*() 
    {return *_str;}
    // 先++
    Self& operator++() 
    { 
        --_str; 
        return *this; 
    }
    // 后++
    Self operator++(int)
    { 
        Self tmp = *this;
        --_str;
        return tmp;
    }
    // 先--
    Self& operator--() 
    { 
        ++_str;
        return *this;
    }
    // 后--
    Self operator--(int)
    {
        Self tmp = *this;
        ++_str;
        return tmp;
    }
    bool operator(const Self& it) const
    {return _str  it._str;}
    bool operator!=(const Self& it) const
    {return _str != it._str;}   
};

typedef __reverse_iterator reverse_iterator;

// 反转迭代器开头
reverse_iterator rbegin()
{
    return reverse_iterator(_str + _size - 1);    
}

// 反转迭代器结尾
reverse_iterator rend()
{
    return reverse_iterator(_str - 1);
}

string的功能函数

  • string的功能函数符合string类型的结构特征,这也是用户为什么要使用string对象的原因

容量管理

  • reserve(size_t newcap):如果newcap大于string对象的原容量,那么就会将原来的空间扩容至newcap大小
  • resize(size_t n, char ch):重新设置string对象的size大小,如果size比原来大,就会将没有数据的空间填充为ch字符
// 预留空间/扩充容量
void reserve(size_t newcap)
{
    if(newcap > _capacity)
    {
        char* tmp = new char[newcap + 1];
        for (int i = 0; i < _size + 1; i++)
        {
            tmp[i] = _str[i];
        }
        delete[] _str; // 删除_str原来指向的空间
        _str = tmp; // _str指向新空间
        _capacity = newcap;
    }
}

// 重新调整size大小
void resize(size_t n, char ch = '\0') // 默认ch填充'\0'
{
    // 1.n < _size
    if(n < _size)
    {
        _str[n] = '\0'; // 从n位置将字符串截断
        _size[n] = n;
    }
    // 2.n  _size
    else if(n  _size)
    {
        return;
    }
    // 3.n > size
    else
    {
        // 4.n > capacity(扩容)
        if (n > _capacity)
        {
            reserve(n);
        }

        for (int i = _size; i < n; i++)
        {
            _str[i] = ch;
        }
        _size = n;
        _str[_size] = '\0';
    }
}

增删查改

    • push_back(char ch):在字符串尾部插入一个字符
    • append(const char* str):在字符串尾部插入一段字符串
    • pop_back():删除string对象的最后一位有效字符
    • erase(size_t pos, size_t len = npos):删除string对象从pos位置开始len长度的子字符串
    • find(char ch, size_t pos = 0):从string对象的pos(默认为0)位置开始查找直至找到第一个ch字符的位置,若未找到,则返回npos
    • find(const char* str, size_t pos = 0):从string对象的pos(默认为0)位置开始查找直至找到第一个出现子串str的位置[使用strstr函数],若未找到,则返回npos
    • insert(size_t pos, char ch):在string对象的pos位置处插入一个字符ch
    • insert(size_t pos, const char* str):在string对象的pos位置处插入一个字符串str
// 增
void push_back(char ch)
{
    if(_size  _capacity)
    {
        size_t newcapctiy = _capacity  0 ? 4 : _capacity * 2;
        reserve(newcapcity); // 如果空间不够要扩容
    }
    _str[_size] = ch;
    _str[_size + 1] = '\0'; // 一定要注意在字符串的尾部一定要加上'\0'结束字符
    ++_size;
}
void append(const char* str)
{
    size_t len = strlen(str);
    // 如果容量不够
    if(_size + len > _capacity)
    {
        reserve(_size + len); // *2倍不知道空间够不够,所以直接扩大到_size + len即可
    }

    for(size_t i = 0; i < len; i++)
    {
        _str[_size] = str[i];
        ++_size;
    }

    _str[_size] = '\0';
}

// 删
void pop_back()
{
    if(_size  0)
    {
        exit(-1);
    }
    --_size;
    _str[_size] = '\0';
}
string& erase(size_t pos, size_t len = npos)
{
    if(!(pos < _size)) // pos要在合法范围内
    {
        exit(-1);
    }
    // 情况1:删掉len个字符之前就没有有效字符了
    if (pos + len >= _size)
    {
        _str[pos] = '\0';
        _size = pos; // 只剩pos个了
    }
    else
    {
        // 情况2:删掉len个字符后还有有效字符
        for (size_t i = pos; i <= _size - len; i++)
        {
            _str[i] = _str[i + len]; // 顺便把'\0'也移过来
        }
        _size -= len;
    }

    return *this;
}

// 查
size_t find(char ch, size_t pos = 0) // 从pos位置开始查找
{
    if(!(pos < _size)) // pos要在合法范围内
    {
        exit(-1);
    }
    for (size_t i = pos; i < _size; i++)
    {
        if (_str[i]  ch)
        {
            return i;
        }
    }

    return npos; // 没有找到就返回npos
}
size_t find(const char* str, size_t pos = 0) // 从pos位置开始查找
{
    char* p = strstr(_str, str); // 从_str找到符合str的子串,找到就返回对应的指针,否则返回nullptr
    if (p != nullptr)
    {
        size_t index = p - _str; // _str计算时退化
        return index >= pos ? index : npos; // 索引值index应该大于等于pos
    }

    return npos; // 没找到也返回npos
}

// 改
string& insert(size_t pos, char ch)
{
    if(!(pos < _size)) // pos要在合法范围内
    {
        exit(-1);
    }
    // 检查空间
    if (_size  _capacity);
    {
        size_t newcapacity = _capacity  0 ? 4 : _capacity * 2;
        reserve(newcapacity);
    }
    int index = _size - 1; // 尾指针
    while (index >= (int)pos)
    {
        _str[index + 1] = _str[index]; // 后面等于前面的
        --index;
    }
    _str[pos] = ch; // 插入值
    _str[_size + 1] = '\0'; // 结束符号
    ++_size;

    return *this;
}
string& insert(size_t pos, const char* str)
{
    if(!(pos < _size)) // pos要在合法范围内
    {
        exit(-1);
    }
    int len = strlen(str);
    if (_size  _capacity);
    {
        size_t newcapacity = _capacity + len; // 防止内存不足
        reserve(newcapacity);
    }

    int index = _size - 1; // 尾指针
    while (index >= (int)pos) // 防止size_t被赋值为-1 (如果index时一个int类型,为了防止数据截断,int和size_T其中一个会向范围更大的类型转[无符号的类型的绝对范围更大],如果还是要这样比较,就要把size_t强制转
    {
        _str[index + len] = _str[index]; // 后面等于前面的
        --index;
    }
    for (int i = 0; i < len; i++)
    {
        _str[pos + i] = str[i];
    }

    _str[_size + len] = '\0';
    _size += len;

    return *this;
}
  • 测试结果

    image-20250319212225128

其他函数

  • 还有一些除上述基本功能外的一些函数
    • swap(string s):交换函数,即将两个string对象的成员变量进行交换
    • copy(char* dest, size_t len, size_t pos = 0) const:将string对象的部分拷贝赋值给C-string上,但是需要调用者提前安排好空间,且要调用者在C-string的末尾加上'\0'
// 交换
void swap(string s)
{
    ::swap(_str, s._str);
    ::swap(_size, s._size);
    ::swap(_capacity, s._capacity);
}

// 将字符串的一部分复制到C-string中(调用者要提前分配好内存)
size_t copy(char* dest, size_t len, size_t pos = 0) const
{
    if (pos >= _size)
        return 0;
    len = len + pos > _size ? _size - pos : len;
    for (size_t i = 0; i < len; i++) {
        dest[i] = _str[pos + i];
    }

    return len;
    // 返回实际复制的字符数
}

运算符重载

  • 运算符重载是也是很重要的成员函数,能够大大方便一些对容器的操作
    • []:是指针访问操作,相当于C-string中的[]的作用,可读可写
    • +=:相当于push_back或者append函数
    • 等比较运算符:返回_str比较的结果(左比较大返回1,右比较大返回-1,相同返回0)
    • 如何比较:逐个比较两个字符串的每个字符的ASCII码(英文字母)或者Unicode码(汉字等等语言) [但是Unicode的比较效果可能不符合预期,因为编译器会将2个,3个或者字符组成的Unicode编码拆开],当前面的码值都相同时,就比较两个字符串的长度
// []运算符
char& operator[](int i)
{
    if (i > _size - 1)
    {
        cout << "索引值不存在!" << endl;
        exit(-1);
    }
    return _str[i];
}

// +=运算符
string& operator+=(char ch)
{
    this->push_back(ch);
    return *this;
}
string& operator+=(const char* str)
{
    this->append(str);
    return *this;
}

// 运算符
bool operator(const string& s)
{
    if (strcmp(_str, s._str)  0)
    {
        return true;
    }
    return false;
}

// !=运算符
bool operator!=(const string& s)
{
    return !(*this  s);
}

// >运算符
bool operator>(const string& s)
{
    if (strcmp(_str, s._str) > 0) // strcmp比较的是较前字符的ASCII码
    {
        return true;
    }
    return false;
}

// <运算符
bool operator<(const string& s)
{
    return !(*this  s || *this > s);
}

// >=运算符
bool operator>=(const string& s)
{
    return *this  s || *this > s;
}

// <=运算符
bool operator<=(const string& s)
{
    return *this  s || *this < s;
}

非成员函数(友元)

  • 在string中我们就了解其中3个非成员函数

    • operator<<(ostream& out, const string& s)cout输出的重载操作
    • istream& operator>>(istream& in, string& s)cin输入的重载操作,输入空格和回车键停止输入
    • getline(string& s, istream& in):为了区分Space空格停止输入和\n换行停止输入,所以单独设置一个getline函数用于接收空格输入
    // <<运算符
    ostream& operator<<(ostream& out, const string& s)
    {
    // out << s._str; // 推荐使用循环遍历输出for(size_t t = 0; t < s.size(); t++),这样就不需要使用友元函数
    for (size_t i = 0; i < s._size; i++)
    {
        out << s._str[i]; // 摆脱了传统字符串后面一定要'/0'的限制
    }
    return out;
    }
    
    // >>运算符
    istream& operator>>(istream& in, string& s) // 如果s为const string&,还是可以修改它的_str,即引用本身是常量,但指向的内容可以修改,为了严谨还是要把const去掉
    {
    if (strcmp(s._str, "") != 0) // 非空字符串
    {
        s._str[0] = '\0'; // 置为空串
    }
    while (1)
    {
        char ch; // 由于不知掉要输入多少个字符,所以我们采用死循环逐个地读字符
        // in >> ch; // 内置类型默认遇见'\n'时直接跳过,不会读取
        ch = in.get(); // 使用get函数可以读取'\n'
        if (ch  ' ' || ch  '\n') // 输入结束
        {
            return in;
        }
    
        s += ch;
    }
    }
    
    // 获取一行输入
    istream& getline(string& s, istream& in) // 如果s为const string&,还是可以修改它的_str,即引用本身是常量,但指向的内容可以修改,为了严谨还是要把const去掉
    {
    if (strcmp(s._str, "") != 0) // 非空字符串
    {
        s._str[0] = '\0'; // 置为空串
    }
    while (1)
    {
        char ch; // 由于不知掉要输入多少个字符,所以我们采用死循环逐个地读字符
        // in >> ch; // 内置类型默认遇见'\n'时直接跳过,不会读取
        ch = in.get(); // 使用get函数可以读取'\n'
        if (ch == '\n') // 输入结束
        {
            return in;
        }
    
        s += ch;
    }
    }
此作者没有提供个人介绍
最后更新于 2025-05-19