C++第五课:STL容器——5.1string(个人整合学习复习使用,有错误欢迎指出)

在上一课中,我们学习了类与对象的构成,理解了如何通过封装、构造与析构等来管理单个对象的状态。但当对象的数量增多,或者数据以集合的形式存在时,仅靠类自身已难以高效管理。

于是,C++标准模板库(STL)登场,它提供了一整套容器化、模板化的数据管理机制。从本节开始,我们将从最基础、最常用的容器——string入手,逐步理解STL的设计思想与实际用法。

#C++参考文档——C++标准委员会官网:isocpp.org

但作为初学者推荐以这两个网站开始查阅学习: cppreference.com   

or https://cplusplus.com

一、STL容器的整体概念

STL是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。

STL通过模板技术,将数据结构(如数组、链表)和算法(如排序、查找)独立出来,让一套代码能适用于各种数据类型,让我们避免了重复造轮子(无需自己实现链表、栈、排序等)。提高了效率。并实现了标准化——统一的接口和约定等。

STL的核心作用,是建立了一套通用的编程范式,它将数据结构和算法之间进行了解耦,并通过一套精密的约定将它们重新连接起来。这套范式具体由六大组件协同实现,它们分别是:

  1. 容器 - 存数据(如 vector, list, map)

  2. 算法 - 操作数据(如 sort, find, copy)

  3. 迭代器 - 用来在容器中移动和访问元素的工具

  4. 仿函数 - 通过重载 () 运算符,使类的对象能够像函数一样被调用。它主要用于定制算法的操作逻辑(如排序规则、条件判断)

  5. 适配器 - 转换接口(如 stack, queue)

  6. 分配器 - 管理容器的内存分配(通常透明,很少直接使用)

在STL中,容器、算法和迭代器构成了最核心的“铁三角”。理解这三者的协作关系,是掌握STL的关键。

接下来,我们以常用的 string 为例,看看它作为容器,是如何封装字符数据,并通过迭代器与算法协同工作,最终为我们提供一个远比C风格字符数组更安全、更便捷的字符串处理工具。

二、string

string本质上是一个“专门用于存储字符的容器”

它被设计为序列容器,但因为它存储的元素类型固定为char(或wchar_t等),所以有自己独特的接口。(严格来说,string不属于STL标准容器(container),但为行为与vector十分类似,可是做特殊的序列容器,因此常被视为STL容器学习的起点。)

2.1 string的应用场景及其常用接口

为什么需要string?例如:

• 替代 C 风格字符串——string 自动管理内存,不需要手动 malloc/free,避免了缓冲区溢出等风险,更方便。

•处理文本数据——几乎所有程序都需要用户进行信息输入输出,而这些信息最后在大多数情况下都以文本形式存在,这就离不开string。

•构建与拼接字符串——在需要动态拼接内容(如路径、输出、SQL语句)时使用。

了解使用场景后,我们再看看 string 提供了哪些常用接口。(常用的,并不会讲完所有接口,有更多想了解的可以去自己查阅文档~,链接已经在开头贴出)

在使用string类时,必须包含#include头文件以及using namespace std;

1. 构造与赋值

string 提供多种初始化方式,应根据使用场景选择最合适的构造接口。

在这里我们只需要重点了解这四个构造函数:

//1.默认构造
//什么时候用:事先不知道字符串内容,需要后续填充时
string s1;          // 空字符串 ""
s1 = "后来赋值";    // 后续填充内容

//2.C字符串构造 string(const char* s)
//什么时候用:从字面值或C风格字符串创建string
string s2("hello");     // 从字面值
string s3 = "world";    // 等价写法

char cstr[] = "C字符串";
string s4(cstr);        // 从C风格数组

//3.拷贝构造 string(const string& str)
//什么时候用:需要复制一个已存在的string对象
string original = "原字符串";
string copy(original);  // 完整复制一份
// 修改copy不会影响original

//4.4. 填充构造 string(size_t n, char c)
//什么时候用:需要创建由重复字符组成的字符串
string s5(5, 'A');      // "AAAAA"
string s6(3, '*');      // "***"
string spaces(10, ' '); // 10个空格

#string的拷贝构造是深拷贝(自动实现),这是它比C风格字符串安全的核心原因

构造函数用途
string()创建空字符串
string(const char*)从C字符串构造
string(const string&)拷贝构造
string(size_t,char)重复字符构造
2.迭代器

如果说构造函数决定了string从哪里来,那么迭代器就决定了我们如何访问string其中的内容。

C++中访问容器元素的通用方式就是“迭代器”(iterator)。它本质上是一种抽象的指针(底层不一定真的是指针),用于在字符串内部进行顺序访问,使我们能够逐个读取或修改字符,而无需直接以来底层内存结构或下表操作。

用大白话来说:“迭代器就是容器里的‘定位工具’,既能找到具体元素的位置,又能直接拿到那个位置的元素,还能顺着容器的顺序挪到下一个位置”

STL提供了一组成员函数用于定位、遍历与反向遍历(迭代器)

函数说明
begin()返回指向首字符的迭代器
end()返回指向末尾后一位置的迭代器(不存放字符,仅作边界标记)
rbegin()返回指向末尾字符的反向迭代器
rend()返回指向首字符前一位置的反向迭代器(不存放字符,仅作边界标记)

前两个更加常用。

代码示例:

(写的时候用下面优化过的,这里仅为过渡)

string str = "hello";

// 1. 正向迭代器 - 从头到尾遍历
cout << "正向遍历: ";
for (string::iterator it = str.begin(); it != str.end(); ++it) {
    cout << *it << " ";  // 使用 * 解引用访问元素
}
// 输出: h e l l o

// 2. 常量迭代器 - 只读访问
cout << "只读遍历: ";
for (string::const_iterator cit = str.cbegin(); cit != str.cend(); ++cit) {
    cout << *cit << " ";
    // *cit = 'x';  // 错误!不能修改
}

// 3. 反向迭代器 - 从尾到头遍历
cout << "反向遍历: ";
for (string::reverse_iterator rit = str.rbegin(); rit != str.rend(); ++rit) {
    cout << *rit << " ";
}
// 输出: o l l e h

上面的代码中,迭代器类型写起来很冗长:string::iteratorstring::const_iteratorstring::reverse_iterator等等。这些类型名不仅长,而且在模板编程中会变得更加复杂。

正是为了解决类型声明的繁琐问题,C++11引入了auto关键字和基于范围的for循环,让代码更简洁、更易读。

auto关键字

auto让编译器自动推导变量类型,语法非常简单:

// auto的基本用法
auto x = 10;           // x被推导为int
auto y = 3.14;         // y被推导为double  
auto name = "hello";   // name被推导为const char*

// 在迭代器中特别有用
auto it = str.begin(); // 编译器自动知道it是string::iterator类型

基于范围的for循环

提供了更直观的遍历语法:

string str = "hello";

// 基本语法:for (元素类型 变量名 : 容器)
for (char ch : str) {
    cout << ch << " ";
}
// 输出: h e l l o

// 如果需要修改元素,使用引用
for (char& ch : str) {
    ch = toupper(ch);  // 可以修改原字符串
}

// 结合auto使用更通用
for (auto& ch : str) {
    ch = tolower(ch);  // 再转回小写
}

范围for的注意事项:

  • 适用于所有支持begin()end()的容器

  • 遍历时是只读的,除非使用引用

  • 不能在遍历过程中添加或删除元素

用auto和范围for重写迭代器示例
string str = "hello";

// 1. 正向遍历(可读写)
cout << "正向遍历: ";
for (auto& ch : str)          // auto& -> 可修改内容
    cout << ch << " ";
cout << endl;

// 2. 只读遍历(常量)
cout << "只读遍历: ";
for (const auto& ch : str)    // const auto& -> 只读访问
    cout << ch << " ";
cout << endl;

// 3. 反向遍历(范围 for 默认不反向,这里结合反向视图)
cout << "反向遍历: ";
for (auto& ch : string(str.rbegin(), str.rend()))  // 构造临时反向字符串
    cout << ch << " ";
cout << endl;
3.增删查改

理解了如何遍历和访问string中的字符后,我们来看看如何对字符串内容进行实际的修改操作——这就是string的增删查改功能。

3.1增

——向已有字符串中添加新的字符或子串

C++为此提供了多种接口,从简单拼接到插入指定位置,灵活性和很高

常见的“增”操作包括:

操作用法示例说明
operator+=拼接字符或字符串s+="!!!";简洁常用,用于连接字符串
push_back()向字符串尾插单个字符s.push_back('!');常用于逐个字符构造字符串
append()向字符串尾插另一个字符串s.append("Tom");可追加string或char*
insert(pos,str)在指定位置插入字符串s.insert(5,"dear");在位置pos之前插入内容

示例代码:

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "Hello";
    s.push_back('!');
    s.append(" world");
    s.insert(5, " dear");
    s += "!!!";

    cout << s << endl; // 输出:Hello dear! world!!!
}
3.2删

string提供多种删除字符串内容的方式,可以按位置、范围或直接清空

操作用法示例说明
erase(pos,n)从位置pos开始,删除n个字符s.erase(3,2);删除下标2开始的3个字符
erase(it)删除迭代器指向的字符s.erase(s.begin()+1);适合配合遍历使用
erase(first,last)删除[first,last)内的字符s.erase(s.begin(),s.begin()+3);常用于批量删除
clear()清空整个字符串s.clear();长度变为0,但对象仍存在

示例代码:

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "Hello, world!";

    // 1. 删除从下标2开始的3个字符
    s.erase(2, 3);            // "He, world!"
    
    // 2. 删除迭代器指向的字符(第二个字符)
    s.erase(s.begin() + 1);   // "H, world!"
    
    // 3. 删除范围内字符
    s.erase(s.begin(), s.begin() + 2); // " world!"
    
    // 4. 清空字符串
    s.clear();                 // ""
    
    cout << "最终字符串: '" << s << "'" << endl;
    return 0;
}
3.3查

“查”操作指在字符串中查找某个字符或子串的位置

操作用法示例说明
find(str)查找子串第一次出现的位置s.find("world");返回下标,找不到返回string::npos
find(ch)查找字符第一次出现的位置s.find('0');返回字符位置
rfind(str)查找子串最后一次出现的位置s.rfind("l");从右向左查找
rfind(ch)查找字符最后一次出现的位置s.rfind('0');从右向左查找

示例代码:

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "hello world";

    // 查找子串 "world"
    size_t pos1 = s.find("world");     // 6
    cout << "\"world\" 位置: " << pos1 << endl;

    // 查找字符 'o'
    size_t pos2 = s.find('o');         // 4
    cout << "'o' 第一次出现位置: " << pos2 << endl;

    // 查找最后一次出现的 'l'
    size_t pos3 = s.rfind('l');        // 9
    cout << "'l' 最后一次出现位置: " << pos3 << endl;

    // 查找不存在的字符
    size_t pos4 = s.find('x');         // string::npos
    if (pos4 == string::npos)//string::npos 就是字符串操作中 “没找到” 的专用标记。
        cout << "'x' 未找到" << endl;

    return 0;
}

浅浅复习一下——size_t是 C++ 里的一种无符号整数类型(unsigned integer type),专门用来表示 "大小" 或 "位置" 这类非负数值。

3.4改

“改”操作指在字符串中修改已有字符或子串的内容。

string提供了直接修改单个字符、替换子串、或者使用迭代器修改的接口。

操作用法示例说明
operator[]通过下标访问修改字符s[0]='H';下标从0开始,必须保证合法索引
at(pos)带边界检查的下标访问s.at(1) ='i';越界会抛出out_of_range的异常
replace(pos,n,str)从位置pos开始替换n个字符为指定子串s.replace(0,5,"Hello");可替换任意长度的子串
迭代器使用迭代器修改字符for(auto& ch : s)ch =toupper(ch)遍历修改,适合批量操作

示例代码:

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "hello world";

    // 1. 下标修改单个字符
    s[0] = 'H';                  // "Hello world"

    // 2. 使用 at 修改字符
    s.at(6) = 'W';                // "Hello World"

    // 3. 替换子串
    s.replace(6, 5, "C++");       // "Hello C++"

    // 4. 使用迭代器修改(全部字符大写)
    for (auto& ch : s)
        ch = toupper(ch);         // "HELLO C++"

    cout << s << endl;

    return 0;
}

toupper 是 C++ 标准库中的一个字符转换转换函数,作用是将小写字母转换为对应的大写字母。

这些接口让字符串具备“可变性”,区别于 C 风格字符串的固定长度

4.容量与空间管理

string除了能操作文本内容外,还提供了一组与“容量”和“内存”的相关接口。这些接口帮助我们高效地管理字符串的存储空间,避免频繁地内存分配,或在需要时主动调整容量。

4.1查看容量信息
接口用法示例说明
size()/length()获取当前字符串的长度s.size() 现在更推荐使用size(),因为这是所有容器通用的获取长度的接口,length是早期设计的时候保留的,只适用于string返回字符个数(不含\0)
empty()判断字符串是否为空s.empty()空则返回true
capacity()查看当前分配的空间大小s.capacity()通常>=size()
4.2调整容量与长度
接口用法示例说明
resize(n)调整字符串长度为ns.resize(3)若变短则截断
resize(n,ch)调整字符串长度为n,超出部分用ch填充s.resize(10,'*');长则填充
reserve(n)预留至少n字节的存储空间s.reserve(100)避免频繁扩容
shrink_to_fit()尝试释放多余空间s.shrink_to_fit()使容量≈长度,容器不再修改,且之前有过大量元素删除/明确需要优化内存占用
5.字符串比较与拼接操作
5.1比较运算符

C风格字符串比较需要使用strcmp(),而string已经重载了比较运算符:

==,!=,<,>,<=,>=,这些运算符会按字典序比较字符串内容,而不是比较地址.

所谓字典序,就是像查字典一样,从左到右一次比较字符的ASCII或Unicode编码值,直到分出大小为止

运算符含义示例
==内容是否相等"abc" == "abc"
!=是否不相等"abc" != "abc"
</>字典序比较"apple"<"banana"结果为true
<=/>=字典序非严格比较"cat">="car"结果为true

C++的string让比较更直观,也避免了内存越界风险。

5.2字符串拼接

C++支持用+或+=运算符拼接字符串,也可以和字符字面量混用。

拼接时会自动生成新的string对象。

运算符功能说明
+链接两个字符串,生成新的字符串
+=在原字符串后追加内容

5.3substr——提取子串

substr用于从一段完整字符串,按逻辑提取有意义的部分,在实际开发中十分常见。

substr的返回类型是string

用于从字符串中提取一个子串并返回新的std:string

substr(pos =0,n=npos)

第一个参数pos指定其实下标,第二个参数n指定要提取的长度(默认到末尾),若pos超过当前字符串的长度,会抛出std::out_of_range

6.类型转换接口

在实际开发中,我们经常需要再字符串与数值类型之间进行转换,例如:从用户输入读取数字、从配置文件中提取参数或将数字拼接到输出文本中。

为此C++引入了一系列便捷的转换函数:

函数功能
stoi(const string& s)将字符转为int,返回类型为int
stol(const string& s)转换为long,返回类型为long
stoll(const string& s)转换为 long long 返回类型为long long
stof(const string& s)转换为float,返回类型为float
stod(const string& s)转换为double,返回类型为double

若字符中含非法字符,会抛出主动异常:std::invalid_argument

若数值超出对应类型范围,也会主动抛出异常:std::out_of_range

可使用try-catch捕获异常(这里有个概念就行,捕获异常后面会细说)

补充:数值类型->字符串

函数功能
to_string(int n)数值转字符串
to_string(double d)浮点转字符串

to_string()默认保留较多位小数,若需精度控制,应使用std::ostringstream

那么到这里,我们思考一下为什么string的常用接口是这些?是不是反映了string的应用场景呢?

答案就藏在string的本质角色中:

string不仅仅是一个字符数组,而是一个完整的文本处理工具包。

它的接口设计精准对应了现实编程中的核心需求:

构造与赋值:应对文本数据的来源多样性
增删查改:解决文本内容的动态变化性 
容量管理:保障大规模文本的性能可控性
比较拼接:满足字符串操作的语义直观性
类型转换:打通文本与数值的数据互通性

每一个接口都不是凭空设计,而是为了解决实际开发中的痛点。当你下次面对字符串处理问题时,不妨先想想:string已经为你准备好了什么工具?

三、string的模拟实现

​掌握了string丰富的接口用法后,你也许会好奇:这些便捷的功能在底层是如何实现的?为了更好地理解string的设计精髓,让我们亲手模拟实现一个简化版的string类,从开发者的角度深入探索其内部机制。

3.1头文件

基于上面第二部分所提及的内容,我们实现string类的头文件应如以下代码。

#pragma once

#include <iostream>
#include <string.h>
#include <assert.h>

using namespace std;

namespace bit
{
    class string
    {
    private:
     
        char* _str = nullptr;      // 动态分配的字符串指针
        size_t _size = 0;          // 当前字符串长度(不包含\0)
        size_t _capacity = 0;      // 当前分配的总容量

        // 小字符串优化(SSO)缓冲区
        // 当 _size < 16 时,字符串存储在_buff数组中,避免动态内存分配
        // 当 _size >= 16 时,字符串存储在_str指向的堆内存中
        char _buff[16];

    public:
        // 静态常量
        static const size_t npos;  // 表示未找到的特殊值,通常为(size_t)-1

        // 迭代器类型定义
        typedef char* iterator;
        typedef const char* const_iterator;

        // 1. 迭代器接口
        iterator begin();                          // 返回指向字符串首字符的迭代器
        iterator end();                            // 返回指向字符串末尾(\0)的迭代器
        const_iterator begin() const;              // const版本begin
        const_iterator end() const;                // const版本end

        // 2. 构造函数与析构函数
        string(const char* str = "");              // 带参构造函数,支持默认构造
        ~string();                                 // 析构函数
        string(const string& s);                   // 拷贝构造函数
        string& operator=(string s);               // 现代写法:传值+swap
        void swap(string& s);                      // 交换两个字符串内容

        // 3. 容量与访问接口
        size_t size() const;                       // 返回字符串长度
        const char* c_str() const;                 // 返回C风格字符串
        char& operator[](size_t i);                // 下标访问(可修改)
        const char& operator[](size_t i) const;    // const下标访问(只读)

        // 4. 容量管理
        void reserve(size_t n);                    // 预留存储空间
        void clear();                              // 清空字符串内容

        // 5. 增删查改
        void push_back(char ch);                   // 尾插单个字符
        void append(const char* str);              // 追加字符串
        string& operator+=(char ch);               // 重载+=用于追加字符
        string& operator+=(const char* str);       // 重载+=用于追加字符串
        void pop_back();                           // 删除末尾字符
        string& insert(size_t pos, char ch);       // 在指定位置插入字符
        string& insert(size_t pos, const char* str); // 在指定位置插入字符串
        string& erase(size_t pos = 0, size_t len = npos); // 删除子串
        size_t find(char ch, size_t pos = 0) const; // 查找字符
        size_t find(const char* str, size_t pos = 0) const; // 查找子串

        // 6. 子串操作
        string substr(size_t pos, size_t len = npos) const; // 提取子串

        // 7. 比较操作符重载
        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;    // 不等于比较
    };

    // 非成员函数
    ostream& operator<<(ostream& out, const string& s);     // 输出流重载
    istream& operator>>(istream& in, string& s);            // 输入流重载
    istream& getline(istream& is, string& str, char delim = '\n'); // 读取一行
    void swap(string& x, string& y);                        // 全局swap函数
}

接下来就是在确认好要写的功能的基础下逐步进行具体的代码实现

3.2 基础函数实现——构造与析构、拷贝控制

//1.静态成员初始化
const size_t string::npos = -1;

//2.构造函数的实现
string::string(const char* str)
    :_size(strlen(str))  // 初始化列表:设置字符串长度
{
    _capacity = _size;           // 容量初始等于长度
    _str = new char[_size + 1];  // 动态内存指针(+1存放\0),一个char*指针
    memcpy(_str, str, _size + 1); // 拷贝字符串内容(含\0)
}

//3.析构函数的实现
string::~string()
{
    delete[] _str;        // 释放动态内存
    _str = nullptr;       // 防止野指针
    _size = _capacity = 0;// 重置大小和容量
}

//工具人交换函数

	void string::swap(string& s)
	{
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}


//4.拷贝构造函数(现代写法,深拷贝),完全复用已有代码
string::string(const string& s) // s是被拷贝的源对象
{
    cout << "string::string(const string& s)" << endl;
    
    string tmp(s._str);  //这里用了上面手动实现的构造函数,一个类型为string的名为tmp的变量的初始值是_str指针指向的值
// 用源数据创建新的tmp对象(深拷贝)

		
    swap(tmp);           // 交换当前对象与临时对象的内容
}


//了解:传统的深拷贝写法,需要手动完成 “分配内存 → 拷贝数据 → 初始化成员变量”
string::string(const string& s)
{
    _str = new char[s._capacity + 1];  // ← 分配新内存
    memcpy(_str, s._str, s._size + 1); // ← 拷贝内容
    _size = s._size;
    _capacity = s._capacity;
}

补充:memcpy 

它是 C/C++ 标准库中的内存拷贝函数,核心作用是按字节直接拷贝两块内存区域的数据,不关心数据类型,效率极高。它是底层内存操作的常用工具,尤其适合数组、结构体等连续内存的拷贝 其语法为:

void* memcpy(void* destination, const void* source, size_t num);

destination:目标内存位置(拷贝后数据存放的地址),必须是可修改的内存区域(非 const)。 source:源内存位置(要拷贝的数据来源地址),用 const 修饰以保证源数据不被修改。

num:要拷贝的字节数(需明确指定,单位是字节,而非元素个数)

核心操作:从 source 指向的源内存位置,拷贝 num 个字节的数据,到 destination 指向的目标内存位置

3.3容量与空间管理
void string::reserve(size_t n)
{
    if (n > _capacity)  // 只在需要扩容时执行
    {
        // 步骤1:输出调试信息
        cout << "reserve:" << n << endl;
        
        // 步骤2:分配新的、更大的内存空间
        char* tmp = new char[n + 1];  // +1用于存放字符串结束符\0
        
        // 步骤3:将原有数据拷贝到新内存
        memcpy(tmp, _str, _size + 1);  // 拷贝原内容+结束符
        
        // 步骤4:释放旧的内存空间
        delete[] _str;
        
        // 步骤5:更新指针指向新内存
        _str = tmp;
        
        // 步骤6:更新容量记录
        _capacity = n;
    }
}
  • 作用:确保字符串至少有容量n来存储字符

  • 特点:只会扩容,不会缩容(符合STL设计原则)

3.4增删查改
// _size表示字符串的长度

// 1.尾插单个字符:在字符串末尾添加一个字符
void string::push_back(char ch)
{
    // 检查容量,不足时扩容
    if (_size >= _capacity)
    {
        // 初始容量为0时设为4,否则2倍扩容
        size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
        reserve(newcapacity);
    }

    // 在末尾插入字符并更新大小
    _str[_size] = ch;//在之前\0的位置插入字符
    ++_size;
    _str[_size] = '\0';  // 保持C字符串格式,在新的末尾位置添加结束符
}

// 2.追加字符串:在末尾添加C风格字符串
void string::append(const char* str)
{
    size_t len = strlen(str);
    
    // 检查容量,选择更大的扩容策略
    if (_size + len > _capacity)
    {
        // 取2倍容量和所需容量的较大值
        size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
        reserve(newcapacity);
    }

    // 拷贝字符串内容(包含结束符\0)
    memcpy(_str + _size, str, len + 1);
    _size += len;
}

// 3.追加字符运算符:重载+=用于追加单个字符
string& string::operator+=(char ch)
{
    push_back(ch);    // 复用push_back实现
    return *this;     // 支持链式调用
}

// 4.追加字符串运算符:重载+=用于追加C风格字符串
string& string::operator+=(const char* str)
{
    append(str);      // 复用append实现
    return *this;     // 支持链式调用
}

// 5.删除末尾字符:移除字符串的最后一个字符
void string::pop_back()
{
    assert(_size > 0);  // 确保字符串非空

    --_size;             // 减小长度
    _str[_size] = '\0';  // 更新结束符位置
}

// 6.插入单个字符:在指定位置插入一个字符
string& string::insert(size_t pos, char ch)
{
    assert(pos <= _size);  // 检查位置合法性

    // 检查并扩容
    if (_size >= _capacity)
    {
        size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
        reserve(newcapacity);
    }

    // 向后移动数据,为插入腾出空间
    size_t end = _size + 1;
    while (end > pos)
    {
        _str[end] = _str[end - 1];
        --end;
    }

    // 插入字符并更新大小
    _str[pos] = ch;
    ++_size;

    return *this;  // 支持链式调用
}

// 7.插入字符串:在指定位置插入C风格字符串
string& string::insert(size_t pos, const char* str)
{
    assert(pos <= _size);  // 检查位置合法性

    size_t len = strlen(str);
    
    // 检查并扩容
    if (_size + len > _capacity)
    {
        size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
        reserve(newcapacity);
    }

    // 向后移动数据,腾出插入空间
    size_t end = _size + len;
    while (end > pos + len - 1)
    {
        _str[end] = _str[end - len];
        --end;
    }

    // 拷贝插入的字符串内容
    for (size_t i = 0; i < len; i++)
    {
        _str[pos + i] = str[i];
    }

    _size += len;  // 更新大小

    return *this;  // 支持链式调用
}

// 8.删除子串:从指定位置开始删除指定长度的字符
string& string::erase(size_t pos, size_t len)
{
    assert(pos < _size);  // 检查位置合法性

    // 如果要删除的长度超过剩余字符数,直接截断
    if (len == npos || len >= (_size - pos))
    {
        _size = pos;              // 直接调整大小到pos位置
        _str[_size] = '\0';       // 设置新的结束符
    }
    else
    {
        // 移动数据覆盖要删除的部分
        size_t i = pos + len;
        memmove(_str + pos, _str + i, _size + 1 - i);
        _size -= len;  // 更新大小
    }

    return *this;  // 支持链式调用
}

// 9.查找字符:查找字符第一次出现的位置
size_t string::find(char ch, size_t pos) const
{
    // 从pos位置开始线性搜索
    for (size_t i = pos; i < _size; i++)
    {
        if (_str[i] == ch)
        {
            return i;  // 找到返回位置
        }
    }

    return npos;  // 未找到返回npos
}

// 10.查找子串:查找字符串第一次出现的位置
size_t string::find(const char* str, size_t pos) const
{
    // 使用strstr函数(C 标准库函数)进行子串查找
    const char* p1 = strstr(_str + pos, str);//这里是查找范围,strstr会在这个范围内查找str子串,并返回找到的第一个匹配的起始地址
    
    if (p1 == nullptr)
    {
        return npos;  // 未找到返回npos
    }
    else
    {
        return p1 - _str;  // 计算并返回相对位置
    }
    
    // 注:这里使用了简单的strstr,实际STL可能使用更高效的KMP算法,但是有点难理解,以后等我彻底理解了会开一个专门整理kmp算法的
}
补充:memmove

它是 C/C++ 标准库中用于拷贝两块连续内存数据的函数,声明于 <string.h>(C)或 <cstring>(C++),与 memcpy 相比,其核心优势是能正确处理源内存和目标内存重叠的情况

语法:

void* memmove(void* destination, const void* source, size_t num);

destination:目标内存位置(拷贝后数据存放的地址),必须是可修改的内存区域(非 const)。 source:源内存位置(要拷贝的数据来源地址),用 const 修饰以保证源数据不被修改。

num:要拷贝的字节数(需明确指定,单位是字节,而非元素个数)

核心操作是:从 source 指向的源内存位置,拷贝 num 个字节的数据,到 destination 指向的目标内存位置,且能正确处理两块内存区域重叠的情况

3.5提取子串
// 提取子串:从pos位置开始提取len个字符
string string::substr(size_t pos, size_t len) const
{
    // 参数校验:如果len为npos或超过剩余字符数,则提取到字符串末尾
    if (len == npos || len >= _size - pos)
    {
        len = _size - pos;  // 调整len为实际可提取的字符数
    }

    // 创建结果字符串对象
    string ret;
    
    // 预分配足够空间,避免多次扩容
    ret.reserve(len);
    
    // 逐个字符拷贝到结果字符串
    for (size_t i = 0; i < len; i++)
    {
        ret += _str[pos + i];  // 追加字符到结果字符串
    }
    
    // 调试信息:输出结果字符串的地址(通常被注释掉)
    //cout << &ret << endl;
    
    // 返回结果字符串(涉及返回值优化RVO)
    return ret;
}
3.6比较运算符重载

运算符重载是通过自定义运算符函数,让用户定义的类能够像内置类型一样使用直观的运算符语法。

// 1.小于比较:按字典序比较两个字符串
bool string::operator<(const string& s) const
{
    size_t i1 = 0, i2 = 0;
    // 逐个字符比较,直到任一字符串结束
    while (i1 < _size && i2 < s._size)
    {
        if (_str[i1] < s[i2])
        {
            return true;  // 当前字符较小,直接返回true
        }
        else if (_str[i1] > s[i2])
        {
            return false; // 当前字符较大,直接返回false
        }
        else
        {
            // 字符相等,继续比较下一个
            ++i1;
            ++i2;
        }
    }

    // 如果当前字符串已结束,但s还有剩余字符,则当前字符串较小
    return i2 < s._size;
}

//2. 小于等于比较:小于或等于都返回true
bool string::operator<=(const string& s) const
{
    return *this < s || *this == s;  // 复用<和==运算符
}

// 3.大于比较:取反小于等于运算
bool string::operator>(const string& s) const
{
    return !(*this <= s);  // 不是小于等于就是大于
}

//4. 大于等于比较:取反小于运算  
bool string::operator>=(const string& s) const
{
    return !(*this < s);   // 不是小于就是大于等于
}

// 5.等于比较:判断两个字符串内容是否完全相同
bool string::operator==(const string& s) const
{
    size_t i1 = 0, i2 = 0;
    // 逐个字符比较
    while (i1 < _size && i2 < s._size)
    {
        if (_str[i1] != s[i2])
        {
            return false;  // 发现不同字符,立即返回false
        }
        else
        {
            // 字符相同,继续比较
            ++i1;
            ++i2;
        }
    }

    // 两个字符串必须同时结束才相等
    return i1 == _size && i2 == s._size;
}

// 6.不等于比较:取反等于运算
bool string::operator!=(const string& s) const
{
    return !(*this == s);  // 复用==运算符
}
3.7输入输出接口


// 工具:清空字符串:重置字符串为空状态,保留已分配内存
void string::clear()
{
    _str[0] = '\0';  // 设置首字符为结束符
    _size = 0;       // 长度归零
}



// 1.输出流重载:支持使用cout直接输出string对象
ostream& operator<<(ostream& out, const string& s)
{
    // 逐个字符输出,避免依赖C字符串格式
    for (size_t i = 0; i < s.size(); i++)
    {
        out << s[i];
    }
    return out;  // 支持链式输出
}

// 2.输入流重载:从输入流读取单词(遇到空格或换行停止)
istream& operator>>(istream& in, string& s)
{
    s.clear();  // 清空目标字符串

    char buff[128];  // 临时缓冲区
    int i = 0;

    char ch = in.get();  // 读取第一个字符
    // 读取直到空格或换行符
    while (ch != ' ' && ch != '\n')
    {
        buff[i++] = ch;
        // 缓冲区满时批量追加
        if (i == 127)
        {
            buff[i] = '\0';
            s += buff;
            i = 0;
        }
        ch = in.get();
    }

    // 处理缓冲区剩余数据
    if (i > 0)
    {
        buff[i] = '\0';
        s += buff;
    }

    return in;  // 支持链式输入
}

// 3.读取一行:从输入流读取直到指定分隔符(默认换行符)
istream& getline(istream& in, string& s, char delim)//delim表示分隔符,默认分隔符是换行符,但可以自定义
{
    s.clear();

    char buff[128];
    int i = 0;

    char ch = in.get();
    // 读取直到分隔符
    while (ch != delim)
    {
        buff[i++] = ch;
        // 缓冲区满时批量追加
        if (i == 127)
        {
            buff[i] = '\0';
            s += buff;
            i = 0;
        }
        ch = in.get();
    }

    // 处理缓冲区剩余数据
    if (i > 0)
    {
        buff[i] = '\0';
        s += buff;
    }

    return in;
}

getline补充——buff 缓冲区知识点拓展

先看看对上面代码的细化解析

>> 遇空格即停,getline 遇换行才止,后者独享读取含空格完整文本的能力。

getline的独有优势:能完整读取包含空格的整行内容,使其成为处理句子、地址、文件路径等复合文本的唯一选择。

读取流程:
开始读取 → 检查字符 → 不是分隔符 → 存入缓冲区 → 缓冲区满 → 批量追加 → 继续读取
                    ↑                                    ↓
                    ←────────── 是分隔符 ←────────── 停止读取

char ch = in.get();          // 1. 读取第一个字符
while (ch != delim)          // 2. 循环直到遇到分隔符
{
    buff[i++] = ch;          // 3. 字符存入缓冲区
    if (i == 127)            // 4. 缓冲区满(128-1,留位置给\0)
    {
        buff[i] = '\0';      // 5. 添加字符串结束符
        s += buff;           // 6. 批量追加到目标字符串
        i = 0;               // 7. 重置缓冲区索引
    }
    ch = in.get();           // 8. 读取下一个字符
}

那么,缓冲区buff是什么?为什么我们需要buff?

1. 什么是缓冲区?为什么需要它?
大白话版本:想象一下,如果你要从井里打水,有两个选择:
• 一次打一勺水,跑回家倒进水缸,再跑回来打下一勺
• 拿一个水桶装很多勺水,等水桶满了再一次性倒进水缸
显然第二种方式更高效!这里的buff就相当于那个水桶——一个临时的"中转站"。

专业术语版本:缓冲区(Buffer)是一块临时存储区域,用于在数据生产者(输入流)和消费者(目标字符串)之间进行流量整形批处理优化

2.没有缓冲区的问题

// 低效写法:每个字符都直接追加
while (ch != delim) {
    s += ch;          // 每次追加都可能触发内存重分配
    ch = in.get();
}

这就像每次打一勺水就跑回家,大部分时间都花在"跑来跑去"上。

即此方案存在两大性能瓶颈:

  1. 内存分配频繁:可能涉及realloc和内存拷贝

  2. I/O操作密集:每次get()都是潜在的系统调用

缓冲区优势:

// 高效写法:批量处理
while (ch != delim) {
    buff[i++] = ch;   // 先在栈上积累
    if (缓冲区满) {
        s += buff;    // 批量追加,减少内存分配次数
    }
}

这就像用水桶积累很多勺水,等水桶满了再一次性倒出去,大大减少了"跑来跑去"的次数。

核心技术优势:

  1. 空间换时间:通过栈上静态数组避免动态内存管理开销,到当前第五节内存还没有细讲,这里也做一个补充——

    栈内存:像“快餐店取餐台”,尺寸固定、存取极快,自动管理(buff[128]就放在这里)

    堆内存:像“大型中央厨房”,按需分配、空间灵活,但需要手动管理(new char[]在这里)

    注:实际内存布局还包括静态区、常量区等,我们后续在内存管理章节会系统学习

    这里的关键思路是:我们提前在栈上准备好固定大小的“取餐台”(缓冲区),虽然占用了一些固定空间,但换来了极快的操作速度,避免了反复向“中央厨房”申请的耗时。这就是“用空间换时间”的经典策略

  2. 批处理效应:将n次O(k)操作转换为n/128次O(1)操作

  3. 预取优化:利用局部性原理,提高CPU缓存命中率

3.缓冲区大小选择

char buff[128];  // 为什么是128?

性能平衡:太小的缓冲区→频繁追加;太大的缓冲区→栈内存浪费

常见选择:128、256、512、1024 都是常见缓冲区大小

内存对齐:2的幂次方有利于内存对齐和性能优化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值