【C++ STL】适配器简单介绍

2 阅读10分钟

适配器

适配器(Adapter)本质上是一种“包装器”,它对已有的容器(vector、list、deque)或迭代器、函数对象进行封装,改变其接口形式,以用来满足特定场景的使用需求。

特点:

  1. 依赖底层容器/迭代器/函数对象,自身不提供独立的存储结构;
  2. 只修改接口,不改变底层对象的核心功能;
  3. 属于模板类,具备高度的通用性和灵活性。

分类:

  • 容器适配器:将顺序容器(如 vector、deque、list)封装为栈、队列、优先队列等抽象数据结构。
  • 迭代器适配器:对迭代器进行包装,改变其遍历行为或生成新的迭代器类型(如反向迭代器、插入迭代器、流迭代器等)。
  • 函数适配器:将函数或函数对象进行组合、绑定参数或转换,使其能够适配到算法所需的接口(现代 C++ 中多由 lambda 和 std::bind 替代)。 核心思想是复用:通过组合现有组件,快速构建出新的功能,而不需要重新实现底层逻辑。

容器适配器

容器适配器是对STL基础容器(如deque、vector、list)进行封装,隐藏其部分接口,只暴露符合特定数据结构逻辑的接口,实现栈、队列、优先级队列等常用数据结构。 STL提供了三种常用容器适配器:

  • stack:栈适配器,遵循“后进先出”(LIFO)的原则;
  • queue:队列适配器,遵循“先进先出”(FIFO)的原则;
  • priority_queue:优先级队列适配器,元素按优先级排序,优先级最高的元素先出队。

[! 注意] 容器适配器的底层容器可指定(部分适配器有默认底层容器),但需满足特定条件(如stack需要支持push_back、pop_back、back等操作)。

栈适配器

stack默认使用deque作为底层容器,也可指定vectorlist作为底层容器(需支持push_back、pop_back、back、empty、size操作)。stack隐藏了底层容器的除“尾部操作”外的所有接口,只允许从尾部插入(push)、尾部删除(pop)、访问尾部元素(top)。

常用操作:

  • push(const T& val):在栈顶插入元素;
  • pop():删除栈顶元素(无返回值,需先判断非空);
  • top():返回栈顶元素的引用(非空时使用);
  • empty():判断栈是否为空,返回bool;
  • size():返回栈中元素个数。
// 1、默认底层使用deque容器
stack<int> s1;
s1.push(1);
s1.push(2);
s1.push(3);

cout << "栈顶元素" << s1.top() << endl;		// 3
cout << "栈大小" << s1.size() << endl;		// 3

// 删除栈顶元素,无返回值
s1.pop();

// 2、指定栈底层容器
stack<int, vector<int>> s2;
s2.push(10);
s2.push(20);
s2.push(30);
cout << "栈顶元素" << s2.top() << endl;	// 30
cout << "栈大小" << s2.size() << endl;	// 3

使用时注意点:

  1. stack的pop()操作只删除元素,不返回元素,若需获取栈顶元素,需先调用top();
  2. 不能直接遍历stack,若需遍历,需先将元素弹出并暂存,遍历后再重新入栈(会破坏原栈结构);
  3. 底层容器若为vector,pop_back()效率较低(需移动后续元素),适合元素个数较少的场景;底层为deque时,效率更均衡。

队列适配器

queue默认使用deque作为底层容器,也可指定list作为底层容器(需支持push_back、pop_front、front、back、empty、size操作)。queue遵循“先进先出”原则,隐藏底层容器的其他接口,只允许从队尾插入(push)、队头删除(pop)、访问队头(front)和队尾(back)元素。

[! 注意] vector头部删除效率不高,因此队列不采用

常用操作:

  • push(const T& val):在队尾插入元素;
  • pop():删除队头元素(无返回值,需先判断非空);
  • front():返回队头元素的引用;
  • back():返回队尾元素的引用;
  • empty():判断队列是否为空;
  • size():返回队列中元素个数。
// 队列适配器
queue<int> q;

// 队尾插入元素
q.push(10);
q.push(20);
q.push(30);

cout << "队尾元素" << q.back() << endl;		// 30
cout << "队首元素" << q.front() << endl;		// 10

// 删除队首元素
q.pop();

cout << "队首元素" << q.front() << endl;		// 20

使用注意:

  1. queue的pop()操作只删除队头元素,不返回元素,需先调用front()获取;
  2. 不能直接遍历queue,遍历需弹出元素暂存,会破坏原队列结构;
  3. 底层容器不能使用vector,因为vector不支持pop_front()(效率极低,STL不推荐)。

优先级队列适配器(priority_queue

priority_queue默认使用vector作为底层容器,也可指定deque作为底层容器(需支持随机访问迭代器、push_back、pop_back、front等操作)。其核心是“堆排序”,底层容器会被维护成一个大根堆(默认),队头元素始终是优先级最高的元素(数值最大的元素)。 优先级可自定义:通过指定比较函数(仿函数),可实现小根堆(优先级最低的元素先出队)。

常用操作:

  • push(const T& val):插入元素,自动调整堆结构,维持优先级;
  • pop():删除队头(优先级最高)元素,自动调整堆结构;
  • top():返回队头元素的引用(非空时使用);
  • empty():判断队列是否为空;
  • size():返回队列中元素个数。

使用注意:

  1. priority_queue的top()返回优先级最高的元素,默认是大根堆(数值大的优先);
  2. 插入和删除元素时,底层会自动调整堆结构,时间复杂度为O(log n);
  3. 自定义优先级时,需重载<运算符(默认)或使用仿函数,确保堆结构能正确维护;
  4. 不能直接遍历priority_queue,遍历需弹出元素,破坏原堆结构。

容器适配器的总结

  • 不提供迭代器,不能使用 STL 算法直接操作。
  • 底层容器必须支持某些操作:stack 要求 back()push_back()pop_back()queue 要求 front()back()push_back()pop_front()priority_queue 要求随机访问迭代器和 push_back()pop_back(),以及堆算法。
  • 通过封装,实现了数据结构的隔离,使代码意图更清晰。

迭代器适配器

迭代器适配器是对已有的迭代器(如vector::iterator、list::iterator)进行封装,改变其遍历方向、访问方式,使其满足特定的遍历需求。STL提供的迭代器适配器不改变迭代器的底层遍历逻辑,只修改其接口行为。 常用的迭代器适配器有3种:

  • reverse_iterator:反向迭代器,将正向迭代器的遍历方向反转(从尾到头);
  • insert_iterator:插入迭代器,将赋值操作(=)转化为插入操作,用于向容器中插入元素;
  • stream_iterator:流迭代器,将输入/输出流(如cin、cout)封装为迭代器,实现流与容器的快速交互。

反向迭代器(reverse_iterator)

reverse_iterator基于正向迭代器实现,通过重载++、--运算符,将“正向遍历”转化为“反向遍历”。 使迭代器反向遍历容器。通过 rbegin() 和 rend() 获得。

[! 注意] 反向迭代器的++操作,等价于正向迭代器的--操作;反向迭代器的--操作,等价于正向迭代器的++操作。

vector<int> v = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// 反向遍历
for (vector<int>::reverse_iterator it = v.rbegin(); it != v.rend(); ++it)
{
	cout << *it << " ";		// 9 8 7 6 5 4 3 2 1 0
}
cout << endl;

插入迭代器(insert_iterator)

insert_iterator将赋值操作(*it = val)转化为容器的插入操作,避免直接使用 insert()方法的繁琐,常用于算法(如copy)中向容器插入元素。根据插入位置的不同, insert_iterator分为3种:

  • back_insert_iterator:在容器尾部插入元素,要求容器支持push_back()(如vector、deque、list);
  • front_insert_iterator:在容器头部插入元素,要求容器支持push_front()(如deque、list);
  • insert_iterator:在容器指定位置插入元素,要求容器支持insert()方法(所有顺序容器均可)。 辅助函数:back_inserter()front_inserter()inserter() 方便创建对应迭代器
vector<int> src = { 0,1,2,3 };
vector<int> dst;

// back_insert_iterator
copy(src.begin(), src.end(), back_inserter(dst));

for (int i : dst)
{
	cout << i << endl;
}

### 流迭代器(stream_iterator)
stream_iterator将输入流(如cin)或输出流(如cout)封装为迭代器,实现“迭代器操作”与“流操作”的无缝衔接。
- 输入流迭代器(istream_iterator)用于从流中读取数据;
- 输出流迭代器(ostream_iterator)用于向流中写入数据。
## 函数适配器
函数适配器(也叫函数对象适配器)是对STL函数对象(仿函数)进行封装、组合或修改,改变其参数个数、参数类型或返回值,使其满足算法(如sort、find_if)的参数要求。

STL提供的函数适配器主要依赖于\<functional\>头文件,常用的有: 
- 绑定适配器:bind1st、bind2nd(C++11前),bind(C++11及以后,推荐使用);
- 否定适配器:not1、not2,用于否定函数对象的返回值; 
- 函数指针适配器:ptr_fun,将普通函数指针转化为函数对象。

>[! 注意]
>C++11及以后,bind适配器功能更强大,可替代bind1st、bind2nd
### 绑定适配器
bind适配器的核心是“绑定函数对象/普通函数的参数”,可以固定部分参数的值,或将参数的顺序调换,返回一个新的函数对象。其语法格式为: bind(函数对象/函数指针, 参数1, 参数2, ..., 参数n)

参数中可以使用placeholders::\_1、placeholders::\_2、...(占位符),表示新函数对象的参数位置(\_1表示第一个参数,\_2表示第二个参数,以此类推)。
```c++
// 判断元素是否大于threshold
bool greaterTan(int val, int threshold)
{
	return val > threshold;
}

int main()
{
	vector<int> v = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

	int threshold = 5;

	// 函数适配器,绑定greaterTan函数的第二个参数,生成一元谓词
	// using namespace std::placeholders; // _1, _2
	auto bind_fun = bind(greaterTan, placeholders::_1, threshold);

	// 统计元素值大于5的个数
	int ret = count_if(v.begin(), v.end(), bind_fun);
	cout << ret << endl;		// 4

	return EXIT_SUCCESS;
};

否定适配器

not1和not2用于否定函数对象的返回值,其中: - not1:用于否定“一元函数对象”(接收1个参数,返回bool值); - not2:用于否定“二元函数对象”(接收2个参数,返回bool值)。

常与STL内置函数对象(如less、greater、equal_to、less_equal)配合使用,改变算法的判断条件。 注意:部分环境中not1可能存在兼容问题,C++11及以后更推荐使用bind适配器替代not1,代码兼容性更强、可读性更好。

 vector<int> v = { 5, 12, 8, 15, 3, 20 };

 // 替代not1:用bind实现“找不小于10的元素(大于等于10)”
 // 等价于否定 x < 10,即 x >= 10,用bind绑定less<int>的参数顺序实现
 auto it1 = find_if(v.begin(), v.end(), bind(less_equal<int>(), 10, placeholders::_1));
 cout << "第一个不小于10的元素:" << *it1 << endl; // 输出12

函数指针适配器

ptr_fun用于将“普通函数指针”转化为“函数对象”,以便适配STL算法中对“函数对象”的要求(部分算法只接受函数对象,不直接接受函数指针)。C++11及以后,bind可替代ptr_fun,但了解其用法有助于理解函数适配器的核心逻辑。

// 普通函数:判断一个数是否为偶数 
bool isEven(int x) 
{ 
	return x % 2 == 0; 
} 

int main() 
{ 
vector<int> v = {1, 2, 3, 4, 5, 6}; 
	// ptr_fun将isEven函数指针转化为函数对象,配合find_if找第一个偶数 
	auto it = find_if(v.begin(), v.end(), ptr_fun(isEven)); 
	if (it != v.end()) 
	{ 
		cout << "第一个偶数:" << *it << endl; // 输出2 } 
		return 0; 
	}

使用lambda表达式

Lambda 可以直接捕获变量、绑定参数,简洁灵活。

vector<int> v = { 5, 12, 8, 15, 3, 20 };

// 统计元素大于10的个数
int threshold = 10;

int ret = count_if(v.begin(), v.end(), [threshold](int val) {return val > threshold; });

std::cout << "大于10的元素个数: " << ret << std::endl; // 3