目录:
一.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完全相同的临时对象tmp
swap(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++核心概念。需知我们模拟实现不是为了造一个更好的轮子,已经有人给我们造出来了,我们直接使用就行了,关键在于学习人家的思路和想发,以及了解底层是如何工作的。