在上一课中,我们学习了类与对象的构成,理解了如何通过封装、构造与析构等来管理单个对象的状态。但当对象的数量增多,或者数据以集合的形式存在时,仅靠类自身已难以高效管理。
于是,C++标准模板库(STL)登场,它提供了一整套容器化、模板化的数据管理机制。从本节开始,我们将从最基础、最常用的容器——string入手,逐步理解STL的设计思想与实际用法。
#C++参考文档——C++标准委员会官网:isocpp.org
但作为初学者推荐以这两个网站开始查阅学习: cppreference.com
一、STL容器的整体概念
STL是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
STL通过模板技术,将数据结构(如数组、链表)和算法(如排序、查找)独立出来,让一套代码能适用于各种数据类型,让我们避免了重复造轮子(无需自己实现链表、栈、排序等)。提高了效率。并实现了标准化——统一的接口和约定等。
STL的核心作用,是建立了一套通用的编程范式,它将数据结构和算法之间进行了解耦,并通过一套精密的约定将它们重新连接起来。这套范式具体由六大组件协同实现,它们分别是:
-
容器 - 存数据(如 vector, list, map)
-
算法 - 操作数据(如 sort, find, copy)
-
迭代器 - 用来在容器中移动和访问元素的工具
-
仿函数 - 通过重载
()运算符,使类的对象能够像函数一样被调用。它主要用于定制算法的操作逻辑(如排序规则、条件判断) -
适配器 - 转换接口(如 stack, queue)
-
分配器 - 管理容器的内存分配(通常透明,很少直接使用)
在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::iterator、string::const_iterator、string::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) | 调整字符串长度为n | s.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();
}
这就像每次打一勺水就跑回家,大部分时间都花在"跑来跑去"上。
即此方案存在两大性能瓶颈:
-
内存分配频繁:可能涉及
realloc和内存拷贝 -
I/O操作密集:每次
get()都是潜在的系统调用
缓冲区优势:
// 高效写法:批量处理
while (ch != delim) {
buff[i++] = ch; // 先在栈上积累
if (缓冲区满) {
s += buff; // 批量追加,减少内存分配次数
}
}
这就像用水桶积累很多勺水,等水桶满了再一次性倒出去,大大减少了"跑来跑去"的次数。
核心技术优势:
-
空间换时间:通过栈上静态数组避免动态内存管理开销,到当前第五节内存还没有细讲,这里也做一个补充——
栈内存:像“快餐店取餐台”,尺寸固定、存取极快,自动管理(buff[128]就放在这里)
堆内存:像“大型中央厨房”,按需分配、空间灵活,但需要手动管理(new char[]在这里)
注:实际内存布局还包括静态区、常量区等,我们后续在内存管理章节会系统学习
这里的关键思路是:我们提前在栈上准备好固定大小的“取餐台”(缓冲区),虽然占用了一些固定空间,但换来了极快的操作速度,避免了反复向“中央厨房”申请的耗时。这就是“用空间换时间”的经典策略
-
批处理效应:将n次O(k)操作转换为n/128次O(1)操作
-
预取优化:利用局部性原理,提高CPU缓存命中率
3.缓冲区大小选择
char buff[128]; // 为什么是128?
性能平衡:太小的缓冲区→频繁追加;太大的缓冲区→栈内存浪费
常见选择:128、256、512、1024 都是常见缓冲区大小
内存对齐:2的幂次方有利于内存对齐和性能优化

1171

被折叠的 条评论
为什么被折叠?



