C++作为现代编程语言中的佼佼者,其强大的功能和灵活性得到了广泛认可。标准模板库(STL)作为C++的核心组件之一,更是为C++程序员提供了丰富的数据结构和算法支持,极大地提高了编程效率和代码复用性。本文将详细介绍C++ STL的基本概念、主要组件、常见容器和算法,并通过示例展示其使用方法。
一、C++ STL简介
C++的STL,即Standard Template Library(标准模板库),是C++编程语言中一个非常重要的组成部分。它由Alexander Stepanov、Mikhail Leeptin等在20世纪80年代末至90年代初开发,并最终被纳入C++标准库中。STL的设计理念强调泛型编程,即通过模板来实现代码的复用,使得同一段代码可以应用于多种数据类型。
STL最初并不是随着C++语言本身一起发布的,而是作为一个独立的库存在。直到1998年,C++标准ISO/IEC 14882:1998(通常称为C++98)发布时,STL才正式成为C++标准库的一部分。此后,随着C++语言的更新,STL也得到了相应的改进和扩展。
- C++98/03:STL首次成为C++标准的一部分,奠定了基础。
- C++11:这次更新对STL做了大量改进,包括引入了右值引用支持的移动语义、范围基础的for循环、lambda表达式等,极大地增强了STL的功能和使用便利性。
- C++14/17/20:这些后续版本继续优化和完善STL,增加了更多容器(如std::unordered_map的heterogeneous lookup)、算法、迭代器支持,以及并行算法等高级特性,使得STL更加高效和现代。
STL主要由六大组件构成,分别是容器(Containers)、迭代器(Iterators)、算法(Algorithms)、函数对象(Function Objects,也称为Functors)、适配器(Adapters)和分配器(Allocators)。
- 容器(Containers):容器是用来存储数据的对象,如vector、list、deque、set、map等。它们提供了不同的数据组织方式(如数组、链表、集合、映射等)和不同的访问特性(如顺序访问、随机访问等)。
- 迭代器(Iterators):迭代器是STL中用于遍历容器中元素的工具,它提供了一种统一的方式来访问不同容器中的数据。迭代器的行为类似于指针,可以进行自增、自减、解引用等操作,但其设计更加抽象,能适应各种容器类型。
- 算法(Algorithms):STL包含了大量的通用算法,如排序(sort)、查找(find)、复制(copy)等,这些算法都是模板化的,可以应用于任何支持适当迭代器的容器。
- 函数对象(Function Objects 或 Functors):函数对象是重载了函数调用操作符(operator())的类对象,可以像函数一样被调用。它们常用于定制算法的行为,比如在排序时定义比较规则。
- 适配器(Adapters):适配器允许你修改容器或算法的行为,使其符合特定的需求,而无需从头创建新的容器或算法。适配器主要有容器适配器(如stack、queue、priority_queue基于现有容器实现)和算法适配器(如bind用于调整函数参数或返回值)。
- 分配器(Allocators):分配器负责管理STL容器中元素的内存分配和释放。默认情况下,STL使用全局的来分配和释放内存,但用户也可以自定义分配器以满足特殊需求,如使用特定内存池或在硬件上优化内存分配。
所有组件都是高度模板化的,旨在提供类型安全、高效且灵活的编程工具。它们共同构成了一个模块化、可组合的编程框架。容器关注数据的存储和组织方式;迭代器提供访问容器元素的方式;算法则定义了对这些元素的操作,三者紧密协作,但各自独立。函数对象为算法提供策略或定制行为,增加了算法的灵活性。
二、C++ STL的主要组件
1. 容器(Containers)
容器是用来存储数据的对象,STL提供了多种类型的容器,每种容器都有其特定的用途和特性。容器可以分为三类:顺序容器、关联容器和容器适配器。
-
顺序容器(Sequence Containers):顺序容器强调值的排序,每个元素由固定的位置(除非用删除、插入的方式改变其位置)。常见的顺序容器有vector、deque和list。
- vector:动态数组,支持随机访问,能够在运行时动态地增加和减少元素。vector是最常用的容器之一,类似于C语言中的数组,但更加灵活和强大。
- deque:双端队列,支持在两端高效插入和删除元素。与vector不同,deque并没有将所有元素存储在连续的内存中,因此它不能保证随机访问的常数时间复杂度。
- list:双向链表,不支持随机访问,但插入和删除元素非常高效。list的元素在内存不连续存放,在任何位置增删元素都能在常数时间完成。
-
关联容器(Associative Containers):关联容器是非线性的树结构(通常是平衡二叉树),元素间没有严格的物理上的顺序关系。关联容器的一个显著特点是有一个值作为关键字key,起到索引作用。常见的关联容器有set、multiset、map和multimap。
- set:集合,不允许重复元素。set中的每个元素只能出现一次,元素会自动按键排序。
- multiset:允许存在相同元素的集合。与set类似,multiset也包含按键排序的元素,但multiset允许元素重复出现。
- map:映射,存储的元素是键值对,并且按键进行排序。map中的每个元素都有且仅有两个成员变量,一个名为first(键),另一个名为second(值),map根据first值对元素进行从小到大排序,并可快速地根据first来检索元素。
- multimap:允许存在相同键的映射。与map不同,multimap允许具有相同first值的元素存在。
-
容器适配器(Container Adapters):容器适配器是将不适用的序列式容器(包括vector、deque和list)变得适用。通过封装某个序列式容器,并重新组合该容器中包含的成员函数,使其满足某些特定场景的需要。常见的容器适配器有stack、queue和priority_queue。
- stack:栈,后进先出(LIFO)的容器适配器。stack提供了栈的基本操作,如push(入栈)、pop(出栈)和top(查看栈顶元素)。
- queue:队列,先进先出(FIFO)的容器适配器。queue提供了队列的基本操作,如push(入队)、pop(出队)、front(查看队头元素)和back(查看队尾元素)。
- priority_queue:优先级队列,最高优先级元素总是第一个出列。priority_queue通常用于实现具有优先级的任务调度等场景。
2. 迭代器(Iterators)
迭代器(Iterators)是一种泛化的指针,用于访问容器中的元素。迭代器提供了一种统一的方式来遍历不同类型的容器,如数组、链表、向量等。C++标准库(STL)为各种容器提供了丰富的迭代器支持。
a.迭代器类型:
- 输入迭代器(Input Iterator):只能向前移动,可以读取但不能修改元素。
- 输出迭代器(Output Iterator):只能向前移动,可以写入但不能读取元素。
- 前向迭代器(Forward Iterator):可以向前移动,可以读取和修改元素。
- 双向迭代器(Bidirectional Iterator):可以向前和向后移动,可以读取和修改元素。
- 随机访问迭代器(Random Access Iterator):可以进行任意位置的访问,支持加减运算和比较运算,可以读取和修改元素。
b.常见容器的迭代器类型:
- std::array, std::vector, std::deque:随机访问迭代器
- std::list, std::forward_list:双向迭代器
- std::set, std::map, std::multiset, std::multimap:双向迭代器
- std::unordered_set, std::unordered_map, std::unordered_multiset, std::unordered_multimap:前向迭代器
c.迭代器的基本操作:
获取迭代器:
- begin():返回指向容器第一个元素的迭代器。
- end():返回指向容器最后一个元素之后的位置的迭代器。
- cbegin() 和 cend():返回常量迭代器,用于只读访问。
迭代器的移动:
- ++it:前移一位。
- --it:后移一位(双向迭代器和随机访问迭代器)。
- it + n 和 it - n:随机访问迭代器支持加减运算。
- it[n]:随机访问迭代器支持索引访问。
迭代器的比较:
- it1 == it2:检查两个迭代器是否相等。
- it1 != it2:检查两个迭代器是否不相等。
- it1 < it2, it1 > it2, it1 <= it2, it1 >= it2:随机访问迭代器支持这些比较运算。
迭代器的解引用:
- *it:解引用迭代器,获取迭代器指向的元素。
- it->member:解引用迭代器并访问成员。
d.示例代码:
- 使用 vector 的迭代器:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 获取迭代器
auto it = vec.begin();
auto end = vec.end();
// 遍历容器
while (it != end) {
std::cout << *it << " ";
++it;
}
std::cout << std::endl;
// 使用范围for循环
for (const auto& elem : vec) {
std::cout << elem << " ";
}
std::cout << std::endl;
return 0;
}
- 使用 list 的迭代器
#include <iostream>
#include <list>
int main() {
std::list<int> lst = {1, 2, 3, 4, 5};
// 获取迭代器
auto it = lst.begin();
auto end = lst.end();
// 遍历容器
while (it != end) {
std::cout << *it << " ";
++it;
}
std::cout << std::endl;
// 反向遍历容器
for (auto rit = lst.rbegin(); rit != lst.rend(); ++rit) {
std::cout << *rit << " ";
}
std::cout << std::endl;
return 0;
}
e.迭代器的安全性:
迭代器失效:
- 当容器发生某些操作(如插入、删除)时,迭代器可能会失效。例如,对于 std::vector,在插入或删除元素时,如果导致内存重新分配,所有迭代器都会失效。
- 对于 std::list 和 std::forward_list,只有删除迭代器指向的元素时,该迭代器会失效。
迭代器检查:
- C++标准库不提供内置的迭代器有效性检查。在使用迭代器时,需要确保迭代器的有效性,避免未定义行为。
3. 算法(Algorithms)
STL提供了大量的通用算法,这些算法都是模板化的,可以应用于任何支持适当迭代器的容器。算法通常独立于容器类型,通过迭代器操作数据。常见的算法有排序(sort)、查找(find)、复制(copy)、变换(transform)等。
- sort:排序算法,可以对数组或容器进行排序。sort算法通常使用快速排序、堆排序或归并排序等高效排序算法实现。
- find:查找算法,用于在容器中查找特定元素。find算法返回一个迭代器,指向找到的元素或容器末尾(如果未找到)。
- copy:复制算法,用于将容器中的元素复制到另一个容器中。copy算法可以接收三个迭代器参数:源容器起始迭代器、源容器结束迭代器和目标容器起始迭代器。
- transform:变换算法,用于对容器中的每个元素应用一个函数,并产生一个新的容器。transform算法可以接收四个迭代器参数和两个函数参数:源容器起始迭代器、源容器结束迭代器、目标容器起始迭代器和变换函数。变换函数可以是一个函数对象或一个lambda表达式。
①sort 算法
用于对容器中的元素进行排序。以下是一些常见的 sort 操作:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {5, 3, 8, 1, 2};
// 排序
std::sort(vec.begin(), vec.end());
// 打印排序后的vector
for (int i : vec) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
②stable_sort 算法
用于稳定排序,保持相等元素的相对顺序。以下是一些常见的 stable_sort 操作:
#include <iostream>
#include <vector>
#include <algorithm>
struct Person {
std::string name;
int age;
};
bool compare_age(const Person& a, const Person& b) {
return a.age < b.age;
}
int main() {
std::vector<Person> people = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 30}};
// 稳定排序
std::stable_sort(people.begin(), people.end(), compare_age);
// 打印排序后的vector
for (const auto& person : people) {
std::cout << person.name << ": " << person.age << std::endl;
}
return 0;
}
③find 算法
用于查找指定值的第一个出现位置。以下是一些常见的 find 操作:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 查找元素
auto it = std::find(vec.begin(), vec.end(), 3);
if (it != vec.end()) {
std::cout << "Found 3 at position " << std::distance(vec.begin(), it) << std::endl;
} else {
std::cout << "3 not found" << std::endl;
}
return 0;
}
④binary_search
算法用于在有序范围内查找指定值。以下是一些常见的 binary_search 操作:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 二分查找
bool found = std::binary_search(vec.begin(), vec.end(), 3);
if (found) {
std::cout << "3 found" << std::endl;
} else {
std::cout << "3 not found" << std::endl;
}
return 0;
}
⑤copy 算法
用于将一个范围内的元素复制到另一个位置。以下是一些常见的 copy 操作:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dest(src.size());
// 复制元素
std::copy(src.begin(), src.end(), dest.begin());
// 打印复制后的vector
for (int i : dest) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
⑥copy_if 算法
用于根据条件复制元素。以下是一些常见的 copy_if 操作:
#include <iostream>
#include <vector>
#include <algorithm>
bool is_even(int x) {
return x % 2 == 0;
}
int main() {
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dest(src.size());
// 根据条件复制元素
auto new_end = std::copy_if(src.begin(), src.end(), dest.begin(), is_even);
// 打印复制后的vector
for (auto it = dest.begin(); it != new_end; ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
⑦replace 算法
用于替换指定值。以下是一些常见的 replace 操作:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 2, 4};
// 替换元素
std::replace(vec.begin(), vec.end(), 2, 9);
// 打印替换后的vector
for (int i : vec) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
⑧transform 算法
用于对每个元素应用某个操作。以下是一些常见的 transform 操作:
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::vector<double> result(vec.size());
// 对每个元素应用平方操作
std::transform(vec.begin(), vec.end(), result.begin(), [](int x) { return std::sqrt(x); });
// 打印变换后的vector
for (double d : result) {
std::cout << d << " ";
}
std::cout << std::endl;
return 0;
}
4. 函数对象(Function Objects 或 Functors)
函数对象(Function Object)是一个能够像函数那样被调用的对象。它可以是任何重载了operator()
的类或结构体实例,或者是一个函数指针、lambda表达式,以及C++11及以后版本中的std::function
。
a.函数对象的类型
-
重载了
operator()
的类或结构体:这是最常见的函数对象类型。通过重载
operator()
,类或结构体的实例可以像函数那样被调用。
struct MyFunctionObject {
void operator()(int x) const {
// 函数体
}
};
-
函数指针:
函数指针本身也可以视为一种函数对象,因为它们可以被调用,并且具有特定的调用签名。
void myFunction(int x) {
// 函数体
}
void (*funcPtr)(int) = myFunction;
funcPtr(5); // 调用函数指针
-
Lambda表达式:
C++11引入了lambda表达式,它们可以捕获外部变量并定义一个可以调用的表达式。Lambda表达式在编译时会被转换为一个匿名的函数对象类型。
auto lambda = [](int x) {
// lambda体
};
lambda(5); // 调用lambda
-
std::function
:C++11还引入了
std::function
,它是一个通用的、类型安全的函数封装器。std::function
可以存储、调用或复制任何可以调用的目标,包括普通函数、Lambda表达式、函数对象以及成员函数指针。
std::function<void(int)> func = [](int x) {
// lambda体
};
func(5); // 调用std::function
b.示例
函数对象在C++中非常有用,特别是在需要传递可调用实体作为参数或返回值的场景中。它们提供了比函数指针更灵活和强大的机制,因为函数对象可以携带状态(即成员变量),而函数指针则不能。
下面是一个使用函数对象的简单示例:
#include <iostream>
#include <vector>
#include <algorithm>
// 定义一个函数对象,用于打印整数
struct PrintInt {
void operator()(int x) const {
std::cout << x << std::endl;
}
};
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 使用函数对象作为算法的参数
std::for_each(numbers.begin(), numbers.end(), PrintInt());
return 0;
}
在这个示例中,PrintInt
是一个函数对象,它的operator()
被重载以打印整数。然后,我们使用std::for_each
算法和PrintInt
函数对象来遍历并打印numbers
向量中的每个元素。
函数对象是C++中一种非常强大且灵活的特性,它们允许开发者定义可重用的、可携带状态的调用实体,从而提高了代码的模块化和可维护性。
5. 适配器(Adapters)
a.容器适配器(Container Adapters):
std::stack
:一个后进先出(LIFO)的数据结构,通常使用std::deque
或std::vector
作为其底层容器。std::queue
:一个先进先出(FIFO)的数据结构,通常使用std::deque
或std::list
作为其底层容器。std::priority_queue
:一个基于优先级的队列,通常使用std::vector
作为其底层容器,并可以使用自定义的比较函数或仿函数来定义优先级。
示例代码:
#include <iostream>
#include <stack>
#include <vector>
#include <queue>
int main() {
// 使用vector作为底层容器的stack
std::stack<int, std::vector<int>> st;
st.push(1);
st.push(2);
std::cout << st.top() << std::endl; // 输出2
st.pop();
std::cout << st.top() << std::endl; // 输出1
// 使用deque作为底层容器的queue
std::queue<int, std::deque<int>> q;
q.push(1);
q.push(2);
std::cout << q.front() << std::endl; // 输出1
q.pop();
std::cout << q.front() << std::endl; // 输出2
return 0;
}
b.迭代器适配器(Iterator Adapters):
std::reverse_iterator
:将正向迭代器转换为反向迭代器,使得可以反向遍历容器。std::insert_iterator
:用于在指定位置插入元素的迭代器适配器。std::back_inserter
:用于在容器的末尾插入元素的迭代器适配器。std::front_inserter
:用于在容器的开头插入元素的迭代器适配器。std::istream_iterator
:用于从输入流中读取元素的迭代器适配器。std::ostream_iterator
:用于向输出流中写入元素的迭代器适配器。
示例代码:
#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用reverse_iterator反向遍历vector
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
// 使用back_inserter在vector末尾插入元素
std::vector<int> another_vec;
std::copy(vec.begin(), vec.end(), std::back_inserter(another_vec));
for (int val : another_vec) {
std::cout << val << " ";
}
std::cout << std::endl;
return 0;
}
c.函数适配器(Function Adapters):
std::not1
和std::not2
:用于对谓词(Predicate)进行逻辑非操作。std::bind1st
和std::bind2nd
(C++11之前):分别绑定二元函数的第一个和第二个参数,但C++11之后被std::bind
取代。std::ptr_fun
:将函数指针转换为可调用对象(Callable Object),但在C++11之后已不推荐使用,因为lambda
表达式和std::function
提供了更灵活和现代的替代方案。std::bind
:用于绑定函数调用的参数,生成新的可调用对象。std::mem_fn
:用于生成一个可调用对象,该对象调用成员函数。
示例代码:
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
bool is_even(int n) {
return n % 2 == 0;
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用std::not1对谓词进行逻辑非操作
auto not_even = std::not1(std::ptr_fun(is_even));
std::copy_if(vec.begin(), vec.end(), std::ostream_iterator<int>(std::cout, " "), not_even);
std::cout << std::endl;
// 使用std::bind绑定函数调用的参数
auto add_five = std::bind(std::plus<int>(), std::placeholders::_1, 5);
for (int val : vec) {
std::cout << add_five(val) << " ";
}
std::cout << std::endl;
return 0;
}
d.算法适配器(Algorithm Adapters):
在C++编程中,算法适配器(Algorithm Adapters)并不是标准术语,但我们可以将其理解为一种设计思想,即通过封装和转换来适配和扩展算法的功能。这种思想并不局限于C++,而是广泛应用于各种编程语言中,以实现算法之间的灵活组合和重用。
然而,在C++标准库中,更常见的是函数对象(Function Objects)、函数适配器(Function Adapters)以及算法(Algorithms)本身,而不是直接称为“算法适配器”的概念。函数适配器可以视为一种特殊的工具,它们能够修改或扩展其他函数或可调用对象的行为。
例如,C++标准库中的std::not1
和std::not2
(在C++11及更高版本中,它们被更现代的std::logical_not
和其他组合函数所取代,但概念相似)是函数适配器,它们接受一个谓词(即返回布尔值的函数或函数对象)并返回一个新的函数对象,该对象对输入值应用逻辑非操作。
虽然“算法适配器”不是C++标准库中的正式术语,但我们可以将以下概念视为与之相关的实践:
-
算法组合:通过组合多个算法来创建新的算法。例如,可以使用
std::transform
和std::accumulate
等算法的组合来计算容器中元素的加权和。 -
函数适配器与算法结合:使用函数适配器(如
std::bind
、std::placeholders
、std::partial_sum
中的lambda表达式等)来修改算法的行为或参数。 -
自定义算法:通过封装现有的算法逻辑,并添加额外的功能或参数,来创建自定义的算法适配器。这通常涉及到编写自己的函数对象或使用模板来参数化算法行为。
-
迭代器适配器:虽然它们不是直接针对算法的,但迭代器适配器(如
std::reverse_iterator
)可以改变遍历容器的方式,从而间接影响算法的行为。 -
范围库(Ranges Library,C++20引入):这是C++20标准库中的一个新特性,它提供了一套用于处理容器和范围(range)的算法和适配器。这些算法和适配器可以以更灵活和链式的方式组合使用,从而形成一种类似于“算法适配器”的概念。
6.分配器
分配器(Allocator)是一种用于内存管理的抽象,它定义了对象分配和释放的接口。C++标准库中的容器(如std::vector
、std::list
等)默认使用标准分配器(std::allocator
),但也可以通过模板参数指定自定义的分配器。
a.标准分配器(std::allocator
)
std::allocator
是C++标准库提供的默认分配器,它封装了底层的内存分配和释放操作。std::allocator
与C语言中的malloc
和free
函数类似,但提供了类型安全和对象构造/析构的支持。
使用std::allocator
可以手动管理内存,但更常见的做法是将其与C++标准库容器结合使用,让容器自动处理内存管理。
b.自定义分配器
自定义分配器允许开发者控制内存分配的策略,以满足特定的性能需求或实现特定的内存管理策略(如池分配、对齐分配等)。
自定义分配器需要实现一组特定的接口,这些接口由C++标准库定义。这些接口包括:
allocate
:分配指定数量的对象所需的内存。deallocate
:释放之前分配的内存。construct
:在已分配的内存上构造对象。destroy
:销毁对象并调用其析构函数(如果需要)。
此外,自定义分配器通常还需要实现一些其他类型定义和成员函数,以与C++标准库容器兼容。
c.分配器的使用
在使用C++标准库容器时,可以通过模板参数指定自定义分配器。例如:
#include <vector>
#include <memory> // 包含std::allocator
// 假设MyAllocator是一个自定义分配器类
// ...
// MyAllocator的定义和实现...
// ...
int main() {
// 使用自定义分配器创建vector
std::vector<int, MyAllocator<int>> vec;
// 现在vec将使用MyAllocator进行内存管理
// ...
return 0;
}
需要注意的是,自定义分配器通常比较复杂,需要仔细处理内存对齐、对象构造/析构、异常安全性等问题。因此,在实现自定义分配器时,建议参考C++标准库中的std::allocator
实现,以确保正确性和性能。
d.分配器的重要性
- 性能优化:通过自定义分配器,可以减少内存碎片、提高内存分配和释放的效率。
- 内存管理策略:自定义分配器允许实现特定的内存管理策略,如池分配、对齐分配等。
- 与容器结合:C++标准库容器可以与自定义分配器结合使用,从而在不修改容器实现的情况下改变内存管理行为。