C++: STL简介与string类核心技术解析及其模拟实现
目录:
- 一.STL
- 二.string类
- 一、创建对象的6种构造方式
- 二、常用接口解析
- 1. 容量操作
- 2. 元素访问
- 3. 修改操作
- 4. 字符串操作
- 三.string模拟实现
- 一、设计基础:类结构与资源管理
- 二、拷贝控制:深拷贝的三种实现
- 1. 传统深拷贝
- 2. 现代写法(推荐)
- 三、关键功能实现
- 1. 容量管理
- 2. 访问操作
- 3. 数据修改操作
- 4. 字符串操作
- 四、运算符重载与IO
- 1. 关系运算符
- 2. 流操作符
- 四.总结
一.STL
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
1. STL核心架构
STL(标准模板库)由三大核心组件构成:
- 容器(数据结构):存储数据的模板类(如vector/list/map)
- 算法:通用算法(如sort/find),可适配不同容器
- 迭代器:连接容器与算法的桥梁,支持统一遍历方式
2. 重要版本对比
- SGI版:GCC采用,可读性最佳,适合源码学习
- P.J.版:VS采用,闭源但性能优异
- HP原始版:所有版本始祖,开源自由(没错就是你知道的那个的惠普)
3. 六大组件应用
在这里推荐一本书: 侯捷老师的《STL源码剖析》,里面系统的讲解STL的源码
二.string类
一、创建对象的6种构造方式
构造方式 | 语法示例 | 功能描述 |
---|---|---|
默认构造 | std::string s1; | 创建空字符串(长度为0) |
拷贝构造 | std::string s2(s1); | 复制另一个字符串的内容 |
C字符串构造 | std::string s3("Hello"); | 用C风格字符串初始化(需以\0 结尾) |
子串构造 | std::string s4(s3, 1, 3); | 从s3 的第1个字符开始取3个字符("ell" ) |
部分构造 | std::string s5("Hello", 4); | 取C字符串的前4个字符("Hell" ) |
填充构造 | std::string s6(5, 'A'); | 生成5个重复字符("AAAAA" ) |
✅ 注意:
- 子串构造中
s4(s3, pos, len)
:
pos
超出范围会抛out_of_range
异常,len
超长时自动截断。- 所有构造支持链式操作:
std::string s = std::string("Hi") + " there!";
二、常用接口解析
1. 容量操作
接口 | 参数 | 返回值 | 功能 | 示例 |
---|---|---|---|---|
resize(len) | len :新长度 | void | 调整字符串长度(默认用\0 填充) | s.resize(3); // "Hel" |
resize(len, c) | c :填充字符 | void | 指定填充字符扩展/缩短 | s.resize(5, '!'); // "Hello!!" |
reserve(size) | size :预留容量 | void | 预分配内存避免多次扩容 | s.reserve(100); |
size() | 无 | size_t | 返回字符串的有效长度 | s.size(); |
capacity() | 无 | size_t | 返回空间总大小 | `s.capacity(); |
⚡ 性能提示:
reserve()
减少动态分配次数,适合已知大小时提前调用,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。resize()
扩展时未指定字符则填充空字符(\0
),如果resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变`。
注意在vs底下reserve会可能多开一些空间,而Linux下你要多少它开多少
2. 元素访问
接口 | 参数 | 返回值 | 行为说明 |
---|---|---|---|
operator[](i) | i :索引位置 | char& | 不检查越界(访问越界=未定义行为) |
at(i) (不常用) | i :索引位置 | char& | 检查越界(越界抛out_of_range 异常) |
范围for
- for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
- 范围for可以作用到数组和容器对象上进行遍历
- 范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
//范围for 底层是迭代器,只能正序遍历for (auto e : str2)//值拷贝{cout << e << " ";}cout << endl;for (auto& e : str2)//引用会修改原字符串{e++;cout << e << " ";}
auto关键字
作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期
推导而得
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际
只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
auto不能直接用来声明数组
typeid().name()可以查看数据的类型
迭代器
迭代器(Iterator)是C++ STL中至关重要的抽象机制,其意义远超出简单的遍历容器元素。
- 迭代器为不同类型容器(如vector、list、map)提供了统一的元素访问方式,隐藏了底层存储结构的差异。
//迭代器
void func(const string& str)
{string::const_iterator it = str.begin();//从前向后打印,不可修改值while (it != str.end()){cout << *it << " ";++it;}cout << endl;//string::const_reverse_iterator itr = str.rbegin();//从后向前打印,且不可修改值auto itr = str.rbegin();while (itr != str.rend()){cout << *itr << " ";++itr;}}
int main()
{
string str1;
string str2("hello world");cout << endl;
string::iterator it = str2.begin();
while (it != str2.end())
{cout << *it << " ";++it;
}cout << endl;
it = str2.begin();
while (it != str2.end())
{(*it)-- ;cout << (*it) << " ";++it;
}cout << endl;
string::reverse_iterator it1 = str2.rbegin();
while (it1 != str2.rend())
{cout << *it1 << " ";++it1;
}cout << endl;
func(str2);
}
暂且先记住是这样的,以后会讲,而且string类不喜欢使用迭代器
it 可能是指针也可能不是
begin+ end: begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
rbegin + rend :begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
3. 修改操作
接口 | 参数 | 返回值 | 功能 |
---|---|---|---|
append(str) | str :追加的字符串 | string& | 尾部添加字符串 |
push_back(c) | c :单个字符 | void | 尾部添加字符(等价于+= ) |
insert(pos, str) | pos :插入位置,str :内容 | string& | 在pos 处插入字符串 |
erase(pos, len) | pos :起始位置,len :长度 | string& | 删除子串(默认删到结尾) |
operator+=(str) | str :追加内容 | string& | 追加字符串或字符(最常用) |
clear() | 无 | void | 清空内容(长度=0,容量不变) |
示例:
std::string s = "Hello";
s.append(" World"); // "Hello World"
s.insert(5, " C++"); // "Hello C++ World"
s.erase(5, 4); // "Hello World"
s += '!'; // "Hello World!"
避免循环中反复调用
str += ch
,改用reserve()
+ 循环更高效
频繁拼接字符串时,优先用+=
或append()
而非+
(减少临时对象)。
4. 字符串操作
接口 | 参数 | 返回值 | 功能 |
---|---|---|---|
find(str, pos) | str :查找内容,pos :起始位置 | size_t | 返回首次出现的位置(未找到返回npos ) |
substr(pos, len) | pos :起始位置,len :长度 | string | 提取子串(默认到结尾) |
c_str() | 无 | const char* | 返回C风格字符串(只读) |
c_str()
返回的指针在字符串修改后可能失效,需立即使用。
查询操作(如find
)时间复杂度为O(n),避免在大循环中调用。
示例:
std::string s = "Hello World";
size_t pos = s.find("World"); // pos = 6
std::string sub = s.substr(6, 5); // "World"
const char* cstr = s.c_str(); // 用于C接口函数
场景:从路径中提取文件名(不含扩展名)
#include <iostream>
#include <string>int main() {std::string path = "/home/user/file.txt";// 1. 从末尾找到最后一个'.'size_t dot_pos = path.rfind('.');// 2. 从开头到'.'之前截取std::filename = path.substr(0, dot_pos);// 3. 从末尾找到最后一个'/'size_t slash_pos = path.rfind('/');// 4. 从'/'后截取到'.'前std::string name = path.substr(slash_pos + 1, dot_pos - slash_pos - 1);std::cout << name; // 输出 "file"
}
完整接口列表见C++ Reference(非官方)。
三.string模拟实现
一、设计基础:类结构与资源管理
核心成员变量:
class string {
private:size_t _size; // 当前字符串长度size_t _capacity; // 当前内存容量char* _str; // 动态内存指针
};
构造与析构关键点:
// 默认构造(支持空字符串)
string(const char* str = "") {_size = strlen(str);//计算字符串的大小_capacity = _size;_str = new char[_capacity + 1]; // +1存储'\0'memcpy(_str, str, _size + 1); // 高效内存拷贝
}// 析构函数
~string() {delete[] _str; // 释放动态内存_str = nullptr;_size = _capacity = 0;
}
设计陷阱:若使用编译器生成的默认拷贝构造,会导致浅拷贝问题——多个对象共享同一内存,析构时重复释放。这也是为什么必须手动实现深拷贝。
二、拷贝控制:深拷贝的三种实现
1. 传统深拷贝
// 拷贝构造
string(const string& str) {_str = new char[str._capacity + 1];memcpy(_str, str._str, str._size + 1);_size = str._size;_capacity = str._capacity;
}// 赋值重载
string& operator=(const string& s) {if(this != &s) { // 防止自赋值char* tmp = new char[s._capacity + 1];memcpy(tmp, s._str, s._size + 1);delete[] _str; // 释放旧内存_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;
}
2. 现代写法(推荐)
void swap(string& tmp) {std::swap(_str, tmp._str);std::swap(_size, tmp._size);std::swap(_capacity, tmp._capacity);
}// 拷贝构造
string(const string& str): _str(nullptr) {string tmp(str._str); // 调用构造函数string(const char* str),创建一个内容与str完全相同的临时对象tmpswap(tmp); //调用成员函数swap()交换当前对象和临时对象的内容 //交换后,当前对象获得tmp的资源(即拷贝的数据)//tmp获得当前对象初始时的空状态(nullptr) //当函数结束时,临时对象tmp离开作用域//调用tmp的析构函数,释放其持有的资源(此时是nullptr,安全释放)
}// 赋值重载
string& operator=(string tmp)
{ // 传值产生临时副本swap(tmp); // 交换资源return *this; // tmp离开作用域自动析构旧资源
}
方法对比:
实现方式 | 内存安全 | 异常安全 | 代码简洁度 |
---|---|---|---|
传统深拷贝 | ✅ | ⚠️ | 中等 |
现代写法 | ✅ | ✅ | 高 |
现代写法的核心优势:异常安全与自赋值安全。传参过程自动完成拷贝,避免手动检查。
三、关键功能实现
1. 容量管理
size_t size()const
{return _size;//返回个数大小
}size_t capacity()const
{return _capacity;//返回容量大小
}// 扩容(不影响数据)
void reserve(size_t n) {if (n > _capacity) {char* tmp = new char[n + 1];memcpy(tmp, _str, _size + 1);delete[] _str;_str = tmp;_capacity = n; // 更新容量}
}// 调整大小(影响数据)
void resize(size_t n, char ch = '\0') {if (n < _size) { // 缩小,删数据_size = n;_str[_size] = '\0';//直接在_size位置截断} else { // 扩大reserve(n); // 确保容量for (size_t i = _size; i < n; i++)_str[i] = ch; // 填充字符_size = n;_str[_size] = '\0';}
}
容量操作对比:
方法 | 功能 | 时间复杂度 | 是否影响数据 |
---|---|---|---|
reserve() | 预分配内存 | O(N) | ❌ |
resize() | 调整字符串长度 | O(N) | ✅ |
2. 访问操作
//[]运算符重载
char& operator[](size_t pos)
{assert(pos < _size);return _str[pos];}const char& operator[](size_t pos)const//不可修改
{assert(pos < _size);return _str[pos];
}//迭代器
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;
}
我们自己实现的迭代器可以用原生指针完成,但是并不是所有的迭代器都是指针(虽然底层依旧还是指针)
这是vs底下string迭代器的类型,你会发现他用一个类封装了(考虑到各种原因)
typeid().name()可以看到变量的类型
我们屏蔽掉end(),发现编译器报错“此基于范围的"for“语句需要适合的"end"函数,但未找到”,间接证明范围for容器遍历实际就是替换为迭代器
3. 数据修改操作
// 尾部追加字符
void push_back(char c) {if (_size == _capacity)reserve(_capacity ? _capacity*2 : 4); // 指数扩容_str[_size++] = c;_str[_size] = '\0';
}void append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}memcpy(_str + _size, str, len + 1);_size += len;}
// 插入字符串
void 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;//要注意end是size_t类型,当end = -1 ->42以多的数字恒大于pos//把end改为int也不行,关系运算符两边类型不同会出现整型提升while (end >= pos && end != npos)//nops是公共成员变量 = -1{_str[end + len] = _str[end];end--;}// 插入新内容memcpy(_str + pos, str, len); // 避免循环_size += len;
}// 删除操作
void erase(size_t pos = 0, size_t len = npos) {assert(pos < _size);if (len == npos || pos + len >= _size) { _str[pos] = '\0'; // 截断至pos处_size = pos;} else {memmove(_str + pos, _str + pos + len, _size - pos - len + 1);_size -= len;}
}string& operator+=(char ch)
{push_back(ch);return *this;
}string& operator+=(const char* str)
{append(str);return *this;
}
性能技巧:使用
memmove
而非循环移位,处理内存重叠更安全。
4. 字符串操作
// 查找字符
size_t find(char ch, size_t pos = 0) const {for (; pos < _size; pos++){if (_str[pos] == ch){ return pos;}} return npos;
}// 查找子串
size_t find(const char* s, size_t pos = 0) const {const char* ptr = strstr(_str + pos, s); // 库函数优化return ptr ? ptr - _str : npos;
}// 获取子串
string substr(size_t pos = 0, size_t len = npos) const {if (len == npos || pos + len > _size)len = _size - pos; // 自动调整长度string result;result.reserve(len);for (size_t i = 0; i < len; i++)result += _str[pos + i]; // 支持链式追加return result; // 返回值优化(RVO)
}
四、运算符重载与IO
1. 关系运算符
// 字典序比较
bool operator<(const string& str) const {size_t i = 0;while (i < _size && i < str._size) {if (_str[i] != str[i]) return _str[i] < str[i];i++;}return _size < str._size; // 前缀相同比长度
}// 基于<和==实现其他运算符
bool operator==(const string& str) const { return _size == str._size && memcmp(_str, str._str, _size) == 0;
}bool operator!=(const string& str) const { return !(*this == str);
}
// <=, >, >= 类似实现...
bool string::operator<=(const string& str)const
{return (*this < str) || (*this == str);
}bool string::operator>(const string& str)const
{return !(*this <= str);
}
bool string:: operator>=(const string& str)const
{return !(*this < str);
}
operator<()
“hello” “hello” --false
“hellox” “hello” – false
“hello” "hellox’ --true
2. 流操作符
// 输出(支持含'\0'的字符串)ostream& operator<<(ostream& out, const string& str) {for (size_t i = 0; i < str.size(); i++) out << str[i]; // 避免c_str()的'\0'截断return out;
}// 输入(带缓冲区优化)istream& operator>>(istream& in, string& str) {str.clear(); // 清空目标char buff[128]; // 减少扩容次数size_t i = 0;char ch = in.get();while (isspace(ch)) ch = in.get(); // 跳过空白while (!isspace(ch)) {buff[i++] = ch;if (i == 127) { // 缓冲区满buff[i] = '\0';str += buff; // 批量追加i = 0; // 重置}ch = in.get();}if (i > 0) { // 处理剩余buff[i] = '\0';str += buff;}return in;
}
<<和>> 不要在类里重载,因为会有个隐藏的this指针占据第一个位置
输入优化:缓冲区减少+=
操作次数,降低扩容开销。
四.总结
通过系统的学习string类,我们学会了使用,在面对字符串或字符的时候就可以用string类,提高效率,并且通过完整的模拟实现可深入理解:动态内存管理、深/浅拷贝、操作符重载、迭代器设计等C++核心概念。需知我们模拟实现不是为了造一个更好的轮子,已经有人给我们造出来了,我们直接使用就行了,关键在于学习人家的思路和想发,以及了解底层是如何工作的。