Bootstrap

C++学习25、标准模板库(STL)的简介和使用

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)。

  1. 容器(Containers):容器是用来存储数据的对象,如vector、list、deque、set、map等。它们提供了不同的数据组织方式(如数组、链表、集合、映射等)和不同的访问特性(如顺序访问、随机访问等)。
  2. 迭代器(Iterators):迭代器是STL中用于遍历容器中元素的工具,它提供了一种统一的方式来访问不同容器中的数据。迭代器的行为类似于指针,可以进行自增、自减、解引用等操作,但其设计更加抽象,能适应各种容器类型。
  3. 算法(Algorithms):STL包含了大量的通用算法,如排序(sort)、查找(find)、复制(copy)等,这些算法都是模板化的,可以应用于任何支持适当迭代器的容器。
  4. 函数对象(Function Objects 或 Functors):函数对象是重载了函数调用操作符(operator())的类对象,可以像函数一样被调用。它们常用于定制算法的行为,比如在排序时定义比较规则。
  5. 适配器(Adapters):适配器允许你修改容器或算法的行为,使其符合特定的需求,而无需从头创建新的容器或算法。适配器主要有容器适配器(如stack、queue、priority_queue基于现有容器实现)和算法适配器(如bind用于调整函数参数或返回值)。
  6. 分配器(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::dequestd::vector作为其底层容器。
  • std::queue:一个先进先出(FIFO)的数据结构,通常使用std::dequestd::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::not1std::not2:用于对谓词(Predicate)进行逻辑非操作。
  • std::bind1ststd::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::not1std::not2(在C++11及更高版本中,它们被更现代的std::logical_not和其他组合函数所取代,但概念相似)是函数适配器,它们接受一个谓词(即返回布尔值的函数或函数对象)并返回一个新的函数对象,该对象对输入值应用逻辑非操作。

虽然“算法适配器”不是C++标准库中的正式术语,但我们可以将以下概念视为与之相关的实践:

  • 算法组合:通过组合多个算法来创建新的算法。例如,可以使用std::transformstd::accumulate等算法的组合来计算容器中元素的加权和。

  • 函数适配器与算法结合:使用函数适配器(如std::bindstd::placeholdersstd::partial_sum中的lambda表达式等)来修改算法的行为或参数。

  • 自定义算法:通过封装现有的算法逻辑,并添加额外的功能或参数,来创建自定义的算法适配器。这通常涉及到编写自己的函数对象或使用模板来参数化算法行为。

  • 迭代器适配器:虽然它们不是直接针对算法的,但迭代器适配器(如std::reverse_iterator)可以改变遍历容器的方式,从而间接影响算法的行为。

  • 范围库(Ranges Library,C++20引入):这是C++20标准库中的一个新特性,它提供了一套用于处理容器和范围(range)的算法和适配器。这些算法和适配器可以以更灵活和链式的方式组合使用,从而形成一种类似于“算法适配器”的概念。

6.分配器

分配器(Allocator)是一种用于内存管理的抽象,它定义了对象分配和释放的接口。C++标准库中的容器(如std::vectorstd::list等)默认使用标准分配器(std::allocator),但也可以通过模板参数指定自定义的分配器。

a.标准分配器(std::allocator

std::allocator是C++标准库提供的默认分配器,它封装了底层的内存分配和释放操作。std::allocator与C语言中的mallocfree函数类似,但提供了类型安全和对象构造/析构的支持。

使用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++标准库容器可以与自定义分配器结合使用,从而在不修改容器实现的情况下改变内存管理行为。
;