C++:4d1泛型、模板与STL
C++:3d1泛型、模板与STL
[TOC]
泛型与模板
C++中的泛型编程主要通过模板(Template)实现,它允许编写与类型无关的代码,从而提高代码的复用性和灵活性。泛型编程的核心思想是编写通用的代码,适用于多种数据类型,而不需要为每种类型单独实现。
1. 函数模板
函数模板允许定义一个通用的函数,适用于多种类型。编译器会根据调用时传入的参数类型自动生成对应的函数实例。
1 | template <typename T> |
template <typename T>
声明了一个模板,T
是一个占位符,表示任意类型。template<template T>
每次使用时都要声明。其中T即为泛型也即编译时自动确定类型的类型。- 编译器会根据传入的参数类型自动推导
T
的具体类型(隐式调用),也可在函数后加<>并添加类型(显示调用)。
2. 类模板
类模板允许定义一个通用的类,适用于多种类型。与函数模板类似,编译器会根据使用时指定的类型生成对应的类实例。
1 | template <typename T> |
template <typename T>
声明了一个类模板,T
是类型参数。- 使用时需要显式指定类型,如
Box<int>
。
3. 模板特化
模板特化(Template Specialization)它允许为特定的类型或值提供模板的定制实现。通过模板特化,可以为某些特殊类型或条件提供优化的或不同的行为,而不是使用通用的模板实现。
1 | template <typename T> |
template <>
表示这是一个特化版本。- 特化版本可以为特定类型(如
int
)提供不同的实现。
模板特化分为两种:
- 全特化(Full Specialization):为模板的所有参数指定具体的类型或值。
- 偏特化(Partial Specialization):为模板的部分参数指定具体的类型或值(仅适用于类模板)。
1. 全特化(Full Specialization)
全特化是指为模板的所有参数提供具体的类型或值。全特化可以用于函数模板和类模板。
函数模板全特化
1 |
|
1 | Generic template: double |
类模板全特化
1 |
|
1 | Generic Box |
2. 偏特化(Partial Specialization)
偏特化是指为模板的部分参数提供具体的类型或值。偏特化仅适用于类模板,函数模板不支持偏特化。
类模板偏特化
1 |
|
1 | Partial Specialization for Pair<T, T> |
3. 非类型模板参数的特化
模板参数不仅可以是类型,还可以是值(如整数、指针等)。非类型模板参数也可以特化。
1 |
|
1 | Generic Array with size 5 |
5. 注意事项
- 函数模板不支持偏特化:如果需要偏特化功能,可以通过重载函数实现。
- 特化的接口必须与通用模板一致:特化版本的接口(如成员函数)必须与通用模板一致,否则会导致编译错误。
- 特化版本必须在使用之前定义:特化版本的声明必须在调用之前,否则编译器可能无法找到特化版本。
4. 非类型模板参数
模板参数不仅可以是类型,还可以是常量值(如整数、指针等)。
1 | template <typename T, int size> |
int size
是一个非类型模板参数,表示数组的大小。
5. 可变参数模板
C++11 引入了可变参数模板,允许模板接受任意数量的参数。
1 | template <typename... Args> |
typename... Args
表示可变参数模板。- 使用折叠表达式(C++17)可以方便地处理可变参数。
6.函数模板与模板函数
模板函数是函数模板在具体类型实例化后生成的函数。它是函数模板的一个具体实现。
SFINAE 简介
SFINAE(Substitution Failure Is Not An Error)是C++模板元编程中的一个重要概念。它指的是在模板参数替换过程中,如果替换导致了无效的代码(即替换失败),编译器不会将其视为错误,而是简单地排除该模板实例化的可能性,并继续尝试其他可能的模板或重载函数。这种机制允许程序员编写更加灵活和复杂的模板代码。
当编译器试图实例化一个模板时,会尝试将实际类型或值替换到模板参数中。如果在这个替换过程中出现了非法操作(例如类型不匹配、不存在的成员等),按照SFINAE规则,这不会导致编译错误,而是会导致该特定模板实例化被忽略。
SFINAE常用于:
- 条件性启用模板:根据某些条件决定是否启用某个模板。
- 检测类型特征:确定某个类型是否具有特定成员函数或类型。
- 函数重载选择:基于不同的类型特性选择最合适的函数重载版本。
下面是一个使用SFINAE来检测一个类是否有特定成员函数的例子:
1 |
|
在上述代码中,
has_check_function
类模板用于检查给定类型T
是否包含一个名为check
的成员函数。test
函数有两个重载版本:- 第一个版本尝试获取
T::check
成员函数的地址,并将其作为默认模板参数传递。如果T
没有这样的成员函数,这个版本的函数定义就会失败,但由于SFINAE原则,这不会导致编译错误,而是会选择第二个版本的test
函数。 - 第二个版本是一个可变参数模板(variadic template),它总是可用的,返回
std::false_type
表示没有找到所需的成员函数。
- 第一个版本尝试获取
最终,通过
decltype(test<T>(0))::value
来判断T
是否拥有check
成员函数,并将结果存储在静态常量value
中。
结论
SFINAE 是一种强大的工具,允许开发者编写更智能、更灵活的模板代码。通过巧妙利用模板参数替换失败不会产生编译错误这一特性,可以实现诸如类型特征检测、条件性启用模板等功能,极大地增强了C++的表达能力和灵活性。在现代C++编程中,尤其是模板元编程领域,SFINAE 是一个非常重要的概念和技术。
STL
STL(Standard Template Library)是C++标准库的一部分,提供了一系列通用的模板类和函数,包括容器、算法、迭代器和函数对象,用于高效地处理数据结构和算法。
容器
在C++中,容器是用于存储和管理数据集合的类模板。它们提供了各种数据结构,如数组、链表、栈、队列等,以便开发者能够高效地操作数据。C++标准库(STL,Standard Template Library)提供了多种容器,主要分为以下几类:
1. 序列容器(Sequence Containers)
序列容器以线性方式存储元素,元素的顺序由插入顺序决定。
std::vector
:动态数组,支持快速随机访问,尾部插入和删除效率高,中间插入和删除效率较低。std::deque
:双端队列,支持快速随机访问,头部和尾部插入和删除效率高。std::list
:双向链表,支持高效的插入和删除操作,但不支持随机访问。std::forward_list
:单向链表,与std::list
类似,但只支持单向遍历,内存占用更少。
2. 关联容器(Associative Containers)
关联容器通过键(key)来存储和访问元素,通常基于二叉搜索树实现,元素按键排序。
std::set
:存储唯一键的集合,元素按键自动排序。std::multiset
:与std::set
类似,但允许重复键。std::map
:存储键值对,键唯一且按键排序。std::multimap
:与std::map
类似,但允许重复键。
3. 无序关联容器(Unordered Associative Containers)
无序关联容器通过哈希表实现,元素不按顺序存储,访问速度通常较快。
std::unordered_set
:存储唯一键的集合,元素无序。std::unordered_multiset
:与std::unordered_set
类似,但允许重复键。std::unordered_map
:存储键值对,键唯一且无序。std::unordered_multimap
:与std::unordered_map
类似,但允许重复键。
4. 容器适配器(Container Adapters)
容器适配器是基于其他容器实现的特殊数据结构,提供特定的接口。
std::stack
:栈,后进先出(LIFO)数据结构,默认基于std::deque
实现。std::queue
:队列,先进先出(FIFO)数据结构,默认基于std::deque
实现。std::priority_queue
:优先队列,元素按优先级出队,默认基于std::vector
实现。
5. 其他容器
std::array
:固定大小的数组,与C风格数组类似,但提供了STL容器的接口。std::bitset
:用于存储和操作位序列的容器。
选择容器的考虑因素
- 访问模式:是否需要随机访问、顺序访问或键值访问。
- 插入/删除操作:是否需要频繁在中间、头部或尾部插入/删除元素。
- 内存使用:不同容器的内存布局和占用不同。
- 性能要求:不同容器的操作时间复杂度不同,需根据具体需求选择。
示例代码
1 |
|
迭代器
在C++中,迭代器(Iterator)是一种用于遍历容器(如std::vector
、std::list
、std::map
等)中元素的对象。迭代器提供了一种统一的方式来访问容器中的元素,而不需要关心容器的具体实现细节。
迭代器的类型
C++中的迭代器分为几种类型,每种类型支持不同的操作:
输入迭代器(Input Iterator):
- 只能单向移动(从前往后)。
- 只能读取元素,不能修改。
- 支持
++
(前缀和后缀)和*
(解引用)操作。
输出迭代器(Output Iterator):
- 只能单向移动(从前往后)。
- 只能写入元素,不能读取。
- 支持
++
(前缀和后缀)和*
(解引用)操作。
前向迭代器(Forward Iterator):
- 可以单向移动(从前往后)。
- 可以读取和修改元素。
- 支持
++
(前缀和后缀)和*
(解引用)操作。
双向迭代器(Bidirectional Iterator):
- 可以双向移动(从前往后或从后往前)。
- 可以读取和修改元素。
- 支持
++
(前缀和后缀)、--
(前缀和后缀)和*
(解引用)操作。
随机访问迭代器(Random Access Iterator):
- 可以随机访问容器中的任意元素。
- 可以读取和修改元素。
- 支持
++
、--
、+
、-
、+=
、-=
、[]
(下标访问)和*
操作。
迭代器的使用
迭代器通常通过容器的成员函数来获取,例如begin()
和end()
。begin()
返回指向容器第一个元素的迭代器,end()
返回指向容器末尾(最后一个元素之后的位置)的迭代器。
1 |
|
迭代器的优点
- 通用性:迭代器提供了一种通用的方式来访问不同类型的容器。
- 灵活性:通过迭代器,可以在不修改容器的情况下实现复杂的遍历和操作。
- 安全性:使用迭代器可以避免直接操作容器内部数据结构的风险。
注意事项
- 失效问题:在对容器进行插入或删除操作时,某些迭代器可能会失效,导致未定义行为。
- 范围检查:使用迭代器时,确保不要越界访问,尤其是在使用
end()
迭代器时。
C++11及以后的改进
C++11引入了基于范围的for循环,使得遍历容器更加简洁:1
2
3for (int val : vec) {
std::cout << val << " ";
}
此外,C++11还引入了auto
关键字,可以简化迭代器的声明:1
2
3for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
总之,迭代器是C++中非常强大且灵活的工具,能够有效地处理容器中的元素。
STL算法
2. STL算法的类别
STL算法可以分为以下几类:
2.1 非修改算法
- 查找:如
find
、find_if
,用于查找特定元素。 - 计数:如
count
、count_if
,用于计数字符或元素的个数。 - 比较:如
equal
,用于比较两个范围内的元素是否相等。
2.2 修改算法
- 复制:如
copy
,将元素从一个范围复制到另一个范围。 - 交换:如
swap
,交换两个元素的值。 - 修改:如
fill
、transform
,用于修改范围内的元素。
2.3 排序和重排算法
- 排序:如
sort
、stable_sort
,对容器中的元素进行排序。 - 重排列:如
reverse
,将元素的顺序反转。
2.4 集合操作
- 并集、交集、差集:如
set_union
、set_intersection
、set_difference
,用于处理集合运算。
3. 使用STL算法的优势
- 代码简洁:STL算法通常比手写的循环和条件语句更简洁。
- 效率高:STL算法经过优化,通常比自定义实现的算法更高效。
- 可读性强:使用标准算法可以提高代码的可读性,易于维护。
以下是一个使用STL算法的简单示例,展示了如何使用vector
和sort
算法:
1 |
|
选择题
函数模板的实例化发生在哪个阶段?
A. 编译时
B. 运行时
C. 链接时
D. 预处理时以下哪种模板特化允许保留部分模板参数未指定?
A. 全特化
B. 偏特化
C. 非类型模板参数特化
D. 可变参数模板std::vector<int>::iterator
属于哪种迭代器类型?
A. 输入迭代器
B. 前向迭代器
C. 双向迭代器
D. 随机访问迭代器以下哪种容器不支持快速随机访问?
A.std::vector
B.std::deque
C.std::list
D.std::array
类模板的成员函数在何时被实例化?
A. 类模板定义时
B. 成员函数被调用时
C. 类模板实例化时
D. 预处理阶段以下代码的错误是什么?
1
2
3template<typename T>
void f(T a, T b) {}
f(1, 2.0);A. 模板参数推导失败
B. 函数未实例化
C. 语法错误
D. 无错误std::unordered_map
的底层实现基于什么数据结构?
A. 红黑树
B. 哈希表
C. 双向链表
D. 动态数组以下哪种迭代器可以反向遍历容器?
A.std::forward_list::iterator
B.std::vector::reverse_iterator
C.std::unordered_set::iterator
D.std::stack::iterator
关于可变参数模板,以下说法正确的是?
A. 只能用于函数模板
B. 参数包必须放在模板参数列表末尾
C. 参数包展开必须使用递归
D. 可以用于类模板和函数模板以下代码的输出是什么?
1
2
3
4
5
6
7template<int N>
struct Factorial {
static const int value = N * Factorial<N-1>::value;
};
template<>
struct Factorial<0> { static const int value = 1; };
std::cout << Factorial<5>::value;A. 120
B. 编译错误
C. 0
D. 1
选择题答案
A
解析:模板实例化在编译时完成,编译器根据具体类型生成代码。B
解析:偏特化允许部分模板参数保留未指定,全特化需指定所有参数。D
解析:std::vector
的迭代器支持随机访问(如operator[]
)。C
解析:std::list
是双向链表,仅支持双向迭代器。B
解析:类模板的成员函数在被调用时才会实例化(惰性实例化)。A
解析:T
推导为int
和double
冲突,需显式指定类型。B
解析:无序哈希映射std::unordered_map
基于哈希表实现。map
、multimap
、set
和multiset
容器是基于红黑树实现的。红黑树是一种自平衡二叉查找树,它确保了树的高度保持在对数级别,从而保证了插入、删除和查找操作的时间复杂度都为 O(log n)。B
解析:reverse_iterator
用于反向遍历。D
解析:可变参数模板可用于类和函数模板。A
解析:递归计算阶乘,结果为 5! = 120。 这段代码展示了如何使用C++模板元编程来计算一个数的阶乘。具体来说,它定义了一个模板结构体Factorial
来递归地计算给定整数N
的阶乘,并通过特化处理了基础情况N=0
。1
2
3
4template<int N>
struct Factorial {
static const int value = N * Factorial<N-1>::value;
};这里定义了一个名为
Factorial
的模板结构体,它接受一个整型参数N
。结构体内声明了一个静态常量value
,其值定义为N
乘以Factorial<N-1>::value
。这意味着对于每个N
,value
将等于N
乘以其前一个数(即N-1
)的阶乘值。这个过程是递归定义的。1
2template<>
struct Factorial<0> { static const int value = 1; };由于阶乘的定义中
0!
等于 1,因此这里对Factorial
结构体进行了全特化处理,专门针对N=0
的情况,直接返回value = 1
。这避免了递归调用达到N=0
时继续递归导致的无限循环或编译错误。1
std::cout << Factorial<5>::value;
这行代码将输出
Factorial<5>
的value
,即 5 的阶乘值。根据上述定义,编译器会递归展开计算:Factorial<5>::value
= 5 *Factorial<4>::value
Factorial<4>::value
= 4 *Factorial<3>::value
- …
- 直到
Factorial<0>::value
= 1
因此,
Factorial<5>::value
最终计算结果为 5 4 3 2 1 = 120。
填空题
- 函数模板的全特化需要在模板声明中使用
________
关键字。 std::priority_queue
的默认底层容器是________
。- 模板参数可以是类型参数或
________
参数。 - 在C++11中,可变参数模板的参数包展开可以通过
________
表达式实现。 std::list
的迭代器属于________
迭代器类型。- 类模板的偏特化必须至少保留一个
________
模板参数。 - 若需在模板中匹配任意类型的指针,应使用
________
特化形式。 std::map
的键类型必须支持________
操作以保持有序。- 非类型模板参数的类型只能是
________
、枚举或指向对象的指针。 std::forward_list
不支持________
操作(如反向遍历)。
填空题答案
template<>
std::vector
解析:优先队列priority_queue是一种类堆结构容器(堆是一种特殊的完全二叉树结构,分为最大堆和最小堆两种类型),并会进行堆排序。其使用方法为:
std::priority_queue<int, std::vector<int>, MinHeapCompare>pq
- 第一个模板参数
int
表示存储在优先队列中的元素类型。 - 第二个模板参数
std::vector<int>
指定了用于底层存储的容器类型。默认情况下,std::priority_queue
使用std::vector
作为其底层容器,因此这里显式指定为std::vector<int>
,实际上与默认行为一致。 - 第三个模板参数
MinHeapCompare
是一个比较类或函数对象,用来定义优先队列中元素之间的比较规则。通过提供这个自定义比较器,可以改变优先队列的行为,默认情况下是最大堆(即最大的元素位于队列顶部),而在这里通过MinHeapCompare
实现最小堆(即最小的元素位于队列顶部)。
- 第一个模板参数
pq
: 这是你创建的优先队列对象的名字。通过这个对象,你可以执行优先队列的各种操作,如插入元素、访问顶部元素、移除顶部元素等。
非类型
解析:类型参数:这是最常见的模板参数形式,用于指定将要使用的数据类型。例如,在函数模板或类模板中使用 T
来表示一个未知的数据类型。
1 | template<typename T> |
非类型参数:除了类型参数外,C++ 模板还允许使用非类型的模板参数。这些参数可以是整型值、指针、引用等编译时常量表达式。它们允许模板在实例化时根据传入的具体数值进行不同的行为。
1 | template<int N> |
通过这种方式,模板机制提供了极大的灵活性,使得可以根据类型以及特定的数值来定制代码的行为,而无需重复编写相似的代码逻辑。
折叠(Fold) 解析:
... ags
用于可变长模板。双向
- 未特化(或原始)
- 指针特化(如
template<typename T> class A<T*> {};//偏特化
) - operator<或排序 解析:运算符重载<以实现有序键值对
- 数组大小 template
- 反向迭代器与随机迭代器(或
rbegin()
/rend()
) 解析:forward_list前向链表
改错题
X
修正以下代码的错误:
1
2
3template<typename T, typename U>
T add(T a, U b) { return a + b; }
auto result = add(3, 4.5);指出以下代码的问题:
1
2
3
4template<int N>
void print() { std::cout << N; }
print<5>();
print<5.0>(); // 错误修正以下代码:
1
2
3
4std::vector<int> vec = {1, 2, 3};
for (auto it = vec.end(); it != vec.begin(); --it) {
std::cout << *it;
}指出以下模板偏特化的错误:
1
2
3
4
5
6
7template<typename T, typename U>
struct Pair;
template<typename U>
struct Pair<int, U> {
T first; // 错误
U second;
};
改错题答案
X
错误:返回值类型
T
由第一个参数决定,可能导致精度丢失。
修正:函数返回值使用共同类型(如auto
)。错误:非类型模板参数
N
必须为整型,5.0
是浮点数。
修正:改为整型值(如print<5>()
)。错误:初始迭代器指向
vec.end()
,解引用会导致未定义行为。
修正:使用反向迭代器rbegin()
和rend()
。错误:偏特化中未定义的
T
。
修正:将T
替换为int
。
问答题
- 解释函数模板与模板函数的区别。
- 什么是非类型模板参数?举例说明其应用场景。
- 全特化和偏特化的主要区别是什么?各举一例。
- 为什么
std::vector
的迭代器是随机访问迭代器,而std::list
是双向迭代器? - 说明
std::deque
(双端队列)的底层实现及其优缺点。 - 什么是迭代器失效?举例说明在
std::vector
中插入元素时迭代器失效的情况。 - 解释可变参数模板中参数包展开的两种方法。
- 为什么关联容器(如
std::set(集合)
)的插入操作时间复杂度是 O(log n)? - 如何实现一个支持任意类型和任意数量参数的模板类?
- 说明
std::forward
和std::move
在模板中的应用场景及区别。 - 什么是 SFINAE?举例说明其在模板元编程中的应用。
- 为什么
std::unordered_map
的键类型需要哈希函数和相等比较函数? - 解释模板元编程中的递归实例化与特化的作用。
- 如何在模板中限制参数类型为整数类型?
- 说明
std::stack
和std::queue
为何被称为容器适配器。
问答题答案
- 函数模板 是模板定义,
template<typename T> void f(T);
;模板函数 是实例化后的具体函数,如f<int>(5);
。 - 非类型模板参数 是值而非类型,如
template<int N> class Array {};
,用于编译时常量(如数组大小)。 - 全特化 指定所有参数,如
template<> class A<int> {};
;偏特化 保留部分参数,如template<typename U> class A<int, U> {};
。 std::vector
内存连续,支持随机访问;std::list
是链表,仅支持双向移动。std::deque
基于分块数组,支持头尾高效插入,但中间操作较慢。- 迭代器失效 指容器修改后迭代器指向无效内存。例如,
std::vector
插入元素可能导致扩容,原迭代器失效。 - 递归展开 和 折叠表达式(C++17)。
- 关联容器基于红黑树(平衡二叉搜索树),插入需维持有序性,时间复杂度 O(log n)。
- 使用可变参数模板:
1
2template<typename... Args>
class Tuple {}; std::move
强制转换为右值;std::forward
保持值类别(完美转发)。 左值是指那些可以位于赋值操作符左侧的表达式,即具有持久性的实体,通常有明确的位置(内存地址)。左值代表了一个可以被引用的数据对象。右值是指那些不能位于赋值操作符左侧的表达式,通常是临时的,没有固定的存储位置。右值主要用于表示临时值或即将被销毁的对象。- SFINAE(替换失败不是错误) 用于条件编译,例如通过
std::enable_if
限制模板匹配。 - 哈希函数确定桶位置,相等比较解决哈希冲突。
- 递归实例化用于编译时计算(如阶乘),特化终止递归。
- 使用
std::is_integral
和static_assert
或 SFINAE。 - 它们基于其他容器(如
std::deque
)实现,提供特定接口(栈的 LIFO、队列的 FIFO)。