1. STL标准模板库概述
作为一名C++开发者,STL(Standard Template Library)是我们日常工作中不可或缺的利器。它就像是一个功能强大的工具箱,里面装满了各种现成的数据结构和算法,让我们能够专注于业务逻辑的实现,而不必重复造轮子。
STL的核心思想是泛型编程,通过模板技术实现了数据结构和算法的分离。这种设计使得我们可以用同样的方式操作不同类型的数据,大大提高了代码的复用性。STL主要由六大组件组成:容器(Containers)、算法(Algorithms)、迭代器(Iterators)、函数对象(Function objects)、适配器(Adapters)和分配器(Allocators)。
在实际开发中,合理选择和使用STL容器可以显著提升代码质量和执行效率。比如,当我们需要频繁在序列中间插入删除元素时,list会比vector更高效;当我们需要快速查找时,set/map会比线性容器更适合。理解每种容器的特性和适用场景,是高效使用STL的关键。
2. string字符串处理
2.1 创建和初始化string对象
string是C++中用于处理字符串的类,它封装了字符数组的操作,提供了丰富的成员函数。创建string对象有多种方式:
cpp复制string s1; // 默认构造,空字符串
string s2("hello"); // 使用C风格字符串初始化
string s3(5, 'a'); // 使用5个'a'字符初始化
string s4(s2); // 拷贝构造
在实际项目中,我更喜欢使用显式初始化方式,这样代码意图更清晰。特别是在处理用户输入或文件内容时,明确指定初始值可以避免很多潜在问题。
2.2 字符串赋值操作
string提供了多种赋值方式,最常用的是=操作符:
cpp复制string s;
s = "hello"; // C风格字符串赋值
s = 'a'; // 单个字符赋值
s = otherString; // 另一个string对象赋值
需要注意的是,string的赋值操作会执行深拷贝,这意味着每个string对象都拥有自己独立的存储空间。这在多线程环境下是安全的,但也意味着频繁的赋值可能会影响性能。
2.3 字符串拼接技巧
字符串拼接是日常开发中最常用的操作之一。除了使用+操作符外,string还提供了append()成员函数:
cpp复制string s1 = "hello";
string s2 = "world";
s1 += " "; // 追加单个字符
s1 += s2; // 追加另一个string
s1.append("!"); // 使用append函数
在处理大量字符串拼接时,我发现预先使用reserve()预留足够空间可以显著提高性能,避免频繁的内存重新分配。
2.4 查找和替换操作
string提供了多种查找方法,最常用的是find():
cpp复制string s = "hello world";
size_t pos = s.find("world"); // 返回首次出现的位置
if(pos != string::npos) {
// 找到子串
}
替换操作使用replace()函数:
cpp复制s.replace(pos, 5, "cpp"); // 从pos开始替换5个字符
在实际开发中,我经常使用这些函数来处理文本解析和格式化输出。需要注意的是,这些操作都是基于索引的,使用时要注意边界检查。
2.5 字符串比较策略
字符串比较可以使用==操作符,也可以使用compare()成员函数:
cpp复制string s1 = "apple";
string s2 = "banana";
if(s1 == s2) { /* ... */ }
int result = s1.compare(s2); // 返回-1,0,1
compare()函数提供了更细致的比较控制,可以指定比较的起始位置和长度。在处理文件名排序等场景时特别有用。
2.6 字符存取与修改
string支持类似数组的[]操作符和at()成员函数来访问单个字符:
cpp复制char c = s[0]; // 不检查边界
char c2 = s.at(1); // 会检查边界,越界抛出异常
s[0] = 'H'; // 修改字符
在性能敏感的代码中,我倾向于使用[]操作符,因为它没有边界检查开销。但在处理用户输入等不可信数据时,使用at()更安全。
2.7 插入与删除操作
insert()和erase()函数提供了灵活的字符串修改能力:
cpp复制s.insert(5, " dear"); // 在位置5插入
s.erase(5, 5); // 从位置5开始删除5个字符
这些操作会改变字符串长度,可能引发内存重新分配。在大字符串上频繁操作时需要注意性能影响。
2.8 子串提取方法
substr()函数可以方便地提取子串:
cpp复制string sub = s.substr(6, 5); // 从位置6开始提取5个字符
在处理日志分析或数据解析时,这个函数非常实用。我经常用它来分割和提取关键信息。
3. vector容器详解
3.1 vector的基本特性
vector是C++中最常用的序列容器,它提供了动态数组的功能,能够根据需要自动扩展容量。与普通数组相比,vector的最大优势在于它能够自动管理内存,同时保持了数组的随机访问特性。
vector的内部实现通常使用连续的内存空间,这使得它支持高效的随机访问(O(1)时间复杂度),但在中间位置插入删除元素效率较低(O(n)时间复杂度)。
3.2 创建和初始化vector
vector提供了多种构造方式:
cpp复制vector<int> v1; // 空vector
vector<int> v2(10); // 10个元素,默认初始化
vector<int> v3(10, 5); // 10个元素,每个初始化为5
vector<int> v4(v3.begin(), v3.end()); // 通过迭代器范围构造
vector<int> v5 = {1,2,3}; // 初始化列表
在实际项目中,我推荐使用初始化列表方式,代码更简洁直观。对于大型vector,预先指定大小可以避免不必要的扩容操作。
3.3 赋值操作与容量管理
vector支持多种赋值方式:
cpp复制v1 = v2; // 拷贝赋值
v1 = {1,2,3}; // 初始化列表赋值
v1.assign(5, 10); // 分配5个10
容量相关操作:
cpp复制v1.size(); // 实际元素数量
v1.capacity(); // 当前容量
v1.reserve(100); // 预留空间
v1.shrink_to_fit(); // 释放多余空间
经验表明,在知道元素数量大致范围的情况下,预先调用reserve()可以显著提高性能,减少内存分配次数。
3.4 插入和删除元素
vector提供了多种插入删除方法:
cpp复制v1.push_back(10); // 尾部插入
v1.pop_back(); // 尾部删除
v1.insert(v1.begin()+2, 20); // 指定位置插入
v1.erase(v1.begin()+1); // 删除指定位置
v1.clear(); // 清空
需要注意的是,在vector中间插入删除元素会导致后续元素移动,性能开销较大。如果需要在序列中间频繁操作,考虑使用list可能更合适。
3.5 数据访问方式
vector支持多种数据访问方式:
cpp复制int a = v1[0]; // 不检查边界
int b = v1.at(1); // 检查边界
int& c = v1.front(); // 首元素引用
int& d = v1.back(); // 尾元素引用
int* p = v1.data(); // 底层数组指针
在性能关键代码中,我通常使用[]操作符,而在处理用户输入等不确定数据时,使用at()更安全。
3.6 元素交换与空间优化
vector提供了swap()成员函数用于快速交换两个vector的内容:
cpp复制vector<int> v6;
v1.swap(v6); // 交换内容
这个操作非常高效,因为它只交换内部指针而不复制数据。我经常用它来快速清空vector并释放内存:
cpp复制vector<int>().swap(v1); // 清空并释放内存
另一个技巧是使用shrink_to_fit()来释放多余容量:
cpp复制v1.shrink_to_fit(); // 减少capacity到size
4. deque双端队列
4.1 deque的特点与实现
deque(双端队列)是一种支持在两端高效插入删除的序列容器。与vector不同,deque不保证元素存储在连续内存中,而是通常实现为多个固定大小的数组的集合。
这种结构使得deque在头部插入删除操作(push_front/pop_front)的时间复杂度为O(1),而vector的头部操作需要移动所有元素,时间复杂度为O(n)。
4.2 创建和初始化deque
deque的构造方式与vector类似:
cpp复制deque<int> d1; // 空deque
deque<int> d2(10); // 10个元素
deque<int> d3(10, 5); // 10个5
deque<int> d4(d3.begin(), d3.end()); // 迭代器范围构造
deque<int> d5 = {1,2,3}; // 初始化列表
在实际应用中,deque特别适合需要频繁在两端操作的场景,比如实现滑动窗口算法或任务队列。
4.3 大小操作与容量管理
deque的大小操作:
cpp复制d1.size(); // 元素数量
d1.empty(); // 是否为空
d1.resize(5); // 调整大小
需要注意的是,deque没有capacity()和reserve()成员函数,因为它的内存管理方式与vector不同。
4.4 插入和删除操作
deque支持两端高效操作:
cpp复制d1.push_front(10); // 头部插入
d1.push_back(20); // 尾部插入
d1.pop_front(); // 头部删除
d1.pop_back(); // 尾部删除
d1.insert(d1.begin()+1, 30); // 指定位置插入
d1.erase(d1.begin()+2); // 指定位置删除
虽然deque支持中间位置的插入删除,但这些操作的效率不如两端操作高。如果需要频繁在中间位置操作,list可能更合适。
4.5 数据访问方式
deque支持随机访问:
cpp复制int a = d1[0]; // 下标访问
int b = d1.at(1); // 带边界检查
int& c = d1.front(); // 首元素
int& d = d1.back(); // 尾元素
deque的随机访问效率比vector略低,因为可能需要计算两次指针解引用,但在大多数应用中这种差异可以忽略不计。
4.6 排序操作
虽然deque本身不维护元素顺序,但可以使用标准算法sort()进行排序:
cpp复制sort(d1.begin(), d1.end()); // 默认升序
sort(d1.rbegin(), d1.rend()); // 降序
需要注意的是,sort算法需要随机访问迭代器,因此不能用于list容器。对于大型deque,排序可能比较耗时,可以考虑使用partial_sort()或nth_element()等部分排序算法。
5. stack和queue容器适配器
5.1 stack栈的特性与使用
stack是一种后进先出(LIFO)的容器适配器,它基于其他容器(默认是deque)实现,提供了受限的接口:
cpp复制stack<int> s1; // 默认基于deque
stack<int, vector<int>> s2; // 基于vector
s1.push(10); // 压栈
int top = s1.top(); // 访问栈顶
s1.pop(); // 出栈
stack特别适合需要后进先出语义的场景,比如函数调用栈、表达式求值、括号匹配等算法。
5.2 queue队列的特性与使用
queue是一种先进先出(FIFO)的容器适配器,默认基于deque实现:
cpp复制queue<int> q1;
q1.push(10); // 入队
int front = q1.front(); // 队首元素
int back = q1.back(); // 队尾元素
q1.pop(); // 出队
queue适合需要先进先出处理的场景,如消息队列、广度优先搜索等。需要注意的是,queue没有提供迭代器接口,无法遍历元素。
6. list链表容器
6.1 list的特点与实现
list是双向链表的实现,支持在任何位置高效插入删除(O(1)时间复杂度),但不支持随机访问。与vector和deque相比,list的每个元素都包含指向前驱和后继的指针,因此内存开销较大。
list特别适合需要频繁在任意位置插入删除的场景,比如实现LRU缓存、编辑缓冲区等。
6.2 创建和初始化list
list的构造方式与其他容器类似:
cpp复制list<int> l1; // 空list
list<int> l2(10); // 10个元素
list<int> l3(10, 5); // 10个5
list<int> l4(l3.begin(), l3.end()); // 迭代器范围构造
list<int> l5 = {1,2,3}; // 初始化列表
6.3 赋值和交换操作
list支持多种赋值方式:
cpp复制l1 = l2; // 拷贝赋值
l1 = {4,5,6}; // 初始化列表赋值
l1.assign(5, 10); // 分配5个10
l1.swap(l2); // 交换内容
list的swap操作非常高效,因为它只需要交换头尾指针,不涉及元素移动。
6.4 插入和删除操作
list提供了丰富的插入删除方法:
cpp复制l1.push_front(10); // 头部插入
l1.push_back(20); // 尾部插入
l1.pop_front(); // 头部删除
l1.pop_back(); // 尾部删除
l1.insert(l1.begin(), 30); // 指定位置插入
l1.erase(l1.begin()); // 指定位置删除
l1.remove(10); // 删除所有值为10的元素
l1.unique(); // 删除连续重复元素
list的插入删除操作不会使迭代器失效(除了被删除元素的迭代器),这在某些场景下非常有用。
6.5 数据访问方式
list不支持随机访问,只能通过迭代器顺序访问:
cpp复制int& first = l1.front(); // 首元素
int& last = l1.back(); // 尾元素
如果需要访问中间元素,必须从头或尾开始遍历。这使得list不适合需要频繁随机访问的场景。
6.6 排序和反转操作
list提供了成员函数sort()和reverse():
cpp复制l1.sort(); // 升序排序
l1.sort(greater<int>()); // 降序排序
l1.reverse(); // 反转链表
与标准算法sort()不同,list的sort()成员函数是专门为链表优化的,效率更高。对于大型list,使用成员函数sort()比先拷贝到vector再排序更高效。
7. set/multiset关联容器
7.1 set的特性与实现
set是一种关联容器,存储唯一键值,并自动按键排序。set通常实现为红黑树,因此插入、删除和查找的时间复杂度都是O(log n)。
set特别适合需要快速查找且元素唯一的场景,比如黑白名单、字典等。
7.2 构造和赋值操作
set的构造方式:
cpp复制set<int> s1; // 空set
set<int> s2 = {1,2,3}; // 初始化列表
set<int> s3(s2.begin(), s2.end()); // 迭代器范围构造
set的赋值操作:
cpp复制s1 = s2; // 拷贝赋值
s1 = {4,5,6}; // 初始化列表赋值
7.3 大小和交换操作
set的大小操作:
cpp复制s1.size(); // 元素数量
s1.empty(); // 是否为空
s1.max_size(); // 最大可能大小
交换操作:
cpp复制s1.swap(s2); // 交换内容
7.4 插入和删除操作
set的插入操作:
cpp复制auto result = s1.insert(10); // 返回pair<iterator, bool>
s1.insert({20,30,40}); // 插入多个元素
删除操作:
cpp复制s1.erase(10); // 删除值为10的元素
s1.erase(s1.begin()); // 删除指定位置
s1.clear(); // 清空set
insert()返回的pair中,second成员表示插入是否成功(对于set,如果元素已存在则插入失败)。
7.5 查找和统计操作
set提供了高效的查找方法:
cpp复制auto it = s1.find(20); // 查找元素,返回迭代器
if(it != s1.end()) { /* 找到 */ }
int count = s1.count(30); // 返回1或0
对于set,count()只能返回0或1,因为元素是唯一的。multiset的count()可以返回大于1的值。
7.6 set与multiset的区别
multiset允许存储重复的键值,而set不允许。除此之外,它们的接口几乎完全相同:
cpp复制multiset<int> ms;
ms.insert(10);
ms.insert(10); // 允许重复
int cnt = ms.count(10); // 可能大于1
在需要统计元素出现次数或允许重复键值的场景中,multiset非常有用。
8. map/multimap关联容器
8.1 map的特性与实现
map是关联容器,存储键值对,按键排序且键唯一。与set类似,map通常也实现为红黑树,提供O(log n)的查找、插入和删除操作。
map适合需要建立键到值映射的场景,比如配置管理、字典、缓存等。
8.2 构造和赋值操作
map的构造方式:
cpp复制map<string, int> m1; // 空map
map<string, int> m2 = {{"a",1},{"b",2}}; // 初始化列表
map<string, int> m3(m2.begin(), m2.end()); // 迭代器范围构造
赋值操作:
cpp复制m1 = m2; // 拷贝赋值
m1 = {{"c",3},{"d",4}}; // 初始化列表赋值
8.3 大小和交换操作
大小操作:
cpp复制m1.size(); // 元素数量
m1.empty(); // 是否为空
m1.max_size(); // 最大可能大小
交换操作:
cpp复制m1.swap(m2); // 交换内容
8.4 插入与删除操作
插入操作:
cpp复制auto result = m1.insert({"e",5}); // 返回pair<iterator, bool>
m1["f"] = 6; // 使用下标操作符插入
删除操作:
cpp复制m1.erase("e"); // 删除指定键
m1.erase(m1.begin()); // 删除指定位置
m1.clear(); // 清空map
使用下标操作符[]时,如果键不存在会自动插入。如果不希望自动插入,可以使用find()或count()先检查键是否存在。
8.5 查找和统计操作
map提供了多种查找方法:
cpp复制auto it = m1.find("a"); // 查找键
if(it != m1.end()) {
int value = it->second; // 获取值
}
int count = m1.count("b"); // 返回1或0
对于map,count()只能返回0或1,因为键是唯一的。multimap的count()可以返回大于1的值。
8.6 multimap的特性与使用
multimap允许键重复,适合一键多值的场景:
cpp复制multimap<string, int> mm;
mm.insert({"a",1});
mm.insert({"a",2}); // 允许重复键
int cnt = mm.count("a"); // 可能大于1
在处理数据库查询结果等需要分组聚合的场景中,multimap非常有用。