C++: STL简介与string类核心技术解析及其模拟实现

发布于:2025-06-02 ⋅ 阅读:(22) ⋅ 点赞:(0)

一.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异常)

有了operator;我们就可以像数组一样访问遍历或修改数据了
在这里插入图片描述

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