C++:3d1泛型、模板与STL

[TOC]

泛型与模板

C++中的泛型编程主要通过模板(Template)实现,它允许编写与类型无关的代码,从而提高代码的复用性和灵活性。泛型编程的核心思想是编写通用的代码,适用于多种数据类型,而不需要为每种类型单独实现。

1. 函数模板

函数模板允许定义一个通用的函数,适用于多种类型。编译器会根据调用时传入的参数类型自动生成对应的函数实例。

1
2
3
4
5
6
7
8
9
10
11
template <typename T>
T add(T a, T b) {
return a + b;
}


int main() {
int result1 = add(3, 4); // 调用 add<int>(3, 4)
double result2 = add(3.5, 4.2); // 调用 add<double>(3.5, 4.2)
return 0;
}
  • template <typename T> 声明了一个模板,T 是一个占位符,表示任意类型。template<template T>每次使用时都要声明。其中T即为泛型也即编译时自动确定类型的类型。
  • 编译器会根据传入的参数类型自动推导 T 的具体类型(隐式调用),也可在函数后加<>并添加类型(显示调用)。

2. 类模板

类模板允许定义一个通用的类,适用于多种类型。与函数模板类似,编译器会根据使用时指定的类型生成对应的类实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename T>
class Box {
private:
T value;
public:
Box(T v) : value(v) {}
T getValue() const {
return value;
}
};

int main() {
Box<int> intBox(123); // 实例化 Box<int>
Box<double> doubleBox(45.67); // 实例化 Box<double>
return 0;
}
  • template <typename T> 声明了一个类模板,T 是类型参数。
  • 使用时需要显式指定类型,如 Box<int>

3. 模板特化

模板特化(Template Specialization)它允许为特定的类型或值提供模板的定制实现。通过模板特化,可以为某些特殊类型或条件提供优化的或不同的行为,而不是使用通用的模板实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
template <typename T>
class Box {
public:
void print() {
std::cout << "Generic Box" << std::endl;
}
};

// 特化版本
template <>
class Box<int> {
public:
void print() {
std::cout << "Specialized Box for int" << std::endl;
}
};

int main() {
Box<double> doubleBox;
doubleBox.print(); // 输出: Generic Box

Box<int> intBox;
intBox.print(); // 输出: Specialized Box for int
return 0;
}
  • template <> 表示这是一个特化版本。
  • 特化版本可以为特定类型(如 int)提供不同的实现。

模板特化分为两种:

  1. 全特化(Full Specialization):为模板的所有参数指定具体的类型或值。
  2. 偏特化(Partial Specialization):为模板的部分参数指定具体的类型或值(仅适用于类模板)。

1. 全特化(Full Specialization)

全特化是指为模板的所有参数提供具体的类型或值。全特化可以用于函数模板和类模板。

函数模板全特化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <typeinfo>

// 通用模板
template <typename T>
void printType(T value) {
std::cout << "Generic template: " << typeid(T).name() << std::endl;
}

// 全特化版本(针对 int 类型)
template <>
void printType<int>(int value) {
std::cout << "Specialized template for int: " << value << std::endl;
}

int main() {
printType(3.14); // 调用通用模板
printType(42); // 调用全特化版本
return 0;
}
1
2
Generic template: double
Specialized template for int: 42
类模板全特化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

// 通用模板
template <typename T>
class Box {
public:
void print() {
std::cout << "Generic Box" << std::endl;
}
};

// 全特化版本(针对 int 类型)
template <>
class Box<int> {
public:
void print() {
std::cout << "Specialized Box for int" << std::endl;
}
};

int main() {
Box<double> doubleBox;
doubleBox.print(); // 调用通用模板

Box<int> intBox;
intBox.print(); // 调用全特化版本
return 0;
}
1
2
Generic Box
Specialized Box for int

2. 偏特化(Partial Specialization)

偏特化是指为模板的部分参数提供具体的类型或值。偏特化仅适用于类模板,函数模板不支持偏特化。

类模板偏特化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>

// 通用模板
template <typename T, typename U>
class Pair {
public:
void print() {
std::cout << "Generic Pair" << std::endl;
}
};

// 偏特化版本(当 T 和 U 相同时)
template <typename T>
class Pair<T, T> {
public:
void print() {
std::cout << "Partial Specialization for Pair<T, T>" << std::endl;
}
};

// 偏特化版本(当 U 是 int 时)
template <typename T>
class Pair<T, int> {
public:
void print() {
std::cout << "Partial Specialization for Pair<T, int>" << std::endl;
}
};

int main() {
Pair<double, double> pair1;
pair1.print(); // 调用偏特化版本 Pair<T, T>

Pair<double, int> pair2;
pair2.print(); // 调用偏特化版本 Pair<T, int>

Pair<double, char> pair3;
pair3.print(); // 调用通用模板
return 0;
}
1
2
3
Partial Specialization for Pair<T, T>
Partial Specialization for Pair<T, int>
Generic Pair

3. 非类型模板参数的特化

模板参数不仅可以是类型,还可以是值(如整数、指针等)。非类型模板参数也可以特化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

// 通用模板
template <typename T, int size>
class Array {
public:
void print() {
std::cout << "Generic Array with size " << size << std::endl;
}
};

// 全特化版本(针对 int 类型和 size = 10)
template <>
class Array<int, 10> {
public:
void print() {
std::cout << "Specialized Array<int, 10>" << std::endl;
}
};

int main() {
Array<double, 5> array1;
array1.print(); // 调用通用模板

Array<int, 10> array2;
array2.print(); // 调用全特化版本
return 0;
}
1
2
Generic Array with size 5
Specialized Array<int, 10>

5. 注意事项

  1. 函数模板不支持偏特化:如果需要偏特化功能,可以通过重载函数实现。
  2. 特化的接口必须与通用模板一致:特化版本的接口(如成员函数)必须与通用模板一致,否则会导致编译错误。
  3. 特化版本必须在使用之前定义:特化版本的声明必须在调用之前,否则编译器可能无法找到特化版本。

4. 非类型模板参数

模板参数不仅可以是类型,还可以是常量值(如整数、指针等)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T, int size>
class Array {
private:
T arr[size];
public:
T& operator[](int index) {
return arr[index];
}
};

int main() {
Array<int, 5> intArray; // 创建一个大小为5的int数组
intArray[0] = 10;
return 0;
}
  • int size 是一个非类型模板参数,表示数组的大小。

5. 可变参数模板

C++11 引入了可变参数模板,允许模板接受任意数量的参数。

1
2
3
4
5
6
7
8
9
template <typename... Args>
void print(Args... args) {
(std::cout << ... << args) << std::endl; // 折叠表达式
}

int main() {
print(1, 2, 3, "hello", 4.5); // 输出: 1 2 3 hello 4.5
return 0;
}
  • typename... Args 表示可变参数模板。
  • 使用折叠表达式(C++17)可以方便地处理可变参数。

6.函数模板与模板函数

模板函数是函数模板在具体类型实例化后生成的函数。它是函数模板的一个具体实现。

SFINAE 简介

SFINAE(Substitution Failure Is Not An Error)是C++模板元编程中的一个重要概念。它指的是在模板参数替换过程中,如果替换导致了无效的代码(即替换失败),编译器不会将其视为错误,而是简单地排除该模板实例化的可能性,并继续尝试其他可能的模板或重载函数。这种机制允许程序员编写更加灵活和复杂的模板代码。

当编译器试图实例化一个模板时,会尝试将实际类型或值替换到模板参数中。如果在这个替换过程中出现了非法操作(例如类型不匹配、不存在的成员等),按照SFINAE规则,这不会导致编译错误,而是会导致该特定模板实例化被忽略。

SFINAE常用于:

  • 条件性启用模板:根据某些条件决定是否启用某个模板。
  • 检测类型特征:确定某个类型是否具有特定成员函数或类型。
  • 函数重载选择:基于不同的类型特性选择最合适的函数重载版本。

下面是一个使用SFINAE来检测一个类是否有特定成员函数的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <type_traits>

// 检测是否存在名为check的成员函数
template<typename T>
class has_check_function {
private:
// 两个辅助结构体,用于测试T是否拥有名为check的成员函数
template<typename U, void (U::*)() const = &U::check>
static std::true_type test(int);

template<typename>
static std::false_type test(...);

public:
// 如果T有名为check的成员函数,则has_check_function<T>::value为true
static constexpr bool value = decltype(test<T>(0))::value;
};

struct A {
void check() const {}
};

struct B {};

int main() {
std::cout << "A has check function: " << has_check_function<A>::value << "\n"; // 输出1
std::cout << "B has check function: " << has_check_function<B>::value << "\n"; // 输出0
}
  • 在上述代码中,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <vector>
#include <map>
#include <unordered_set>
#include <stack>

int main() {
// 使用vector
std::vector<int> vec = {1, 2, 3, 4};
vec.push_back(5); // 添加元素
for (int v : vec) {
std::cout << v << " "; // 输出: 1 2 3 4 5
}
std::cout << std::endl;

// 使用map
std::map<std::string, int> m = {{"apple", 1}, {"banana", 2}};
m["cherry"] = 3; // 添加键值对
for (const auto& pair : m) {
std::cout << pair.first << ": " << pair.second << " "; // 输出: apple: 1 banana: 2 cherry: 3
}
std::cout << std::endl;

// 使用unordered_set
std::unordered_set<int> uset = {1, 2, 3, 4};
uset.insert(5); // 添加元素
for (int v : uset) {
std::cout << v << " "; // 输出顺序不确定
}
std::cout << std::endl;

// 使用stack
std::stack<int> s;
s.push(1);
s.push(2);
s.push(3);
while (!s.empty()) {
std::cout << s.top() << " "; // 输出: 3 2 1
s.pop();
}
std::cout << std::endl;

return 0;
}

迭代器

在C++中,迭代器(Iterator)是一种用于遍历容器(如std::vectorstd::liststd::map等)中元素的对象。迭代器提供了一种统一的方式来访问容器中的元素,而不需要关心容器的具体实现细节。

迭代器的类型

C++中的迭代器分为几种类型,每种类型支持不同的操作:

  1. 输入迭代器(Input Iterator)

    • 只能单向移动(从前往后)。
    • 只能读取元素,不能修改。
    • 支持++(前缀和后缀)和*(解引用)操作。
  2. 输出迭代器(Output Iterator)

    • 只能单向移动(从前往后)。
    • 只能写入元素,不能读取。
    • 支持++(前缀和后缀)和*(解引用)操作。
  3. 前向迭代器(Forward Iterator)

    • 可以单向移动(从前往后)。
    • 可以读取和修改元素。
    • 支持++(前缀和后缀)和*(解引用)操作。
  4. 双向迭代器(Bidirectional Iterator)

    • 可以双向移动(从前往后或从后往前)。
    • 可以读取和修改元素。
    • 支持++(前缀和后缀)、--(前缀和后缀)和*(解引用)操作。
  5. 随机访问迭代器(Random Access Iterator)

    • 可以随机访问容器中的任意元素。
    • 可以读取和修改元素。
    • 支持++--+-+=-=[](下标访问)和*操作。

迭代器的使用

迭代器通常通过容器的成员函数来获取,例如begin()end()begin()返回指向容器第一个元素的迭代器,end()返回指向容器末尾(最后一个元素之后的位置)的迭代器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <vector>

int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};

// 使用迭代器遍历vector
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;

// 使用const迭代器遍历vector,不能通过这个迭代器修改原始容器中的任何元素
for (std::vector<int>::const_iterator it = vec.cbegin(); it != vec.cend(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;

// 使用反向迭代器遍历vector
for (std::vector<int>::reverse_iterator it = vec.rbegin(); it != vec.rend(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;

return 0;
}

迭代器的优点

  • 通用性:迭代器提供了一种通用的方式来访问不同类型的容器。
  • 灵活性:通过迭代器,可以在不修改容器的情况下实现复杂的遍历和操作。
  • 安全性:使用迭代器可以避免直接操作容器内部数据结构的风险。

注意事项

  • 失效问题:在对容器进行插入或删除操作时,某些迭代器可能会失效,导致未定义行为。
  • 范围检查:使用迭代器时,确保不要越界访问,尤其是在使用end()迭代器时。
C++11及以后的改进

C++11引入了基于范围的for循环,使得遍历容器更加简洁:

1
2
3
for (int val : vec) {
std::cout << val << " ";
}

此外,C++11还引入了auto关键字,可以简化迭代器的声明:

1
2
3
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}

总之,迭代器是C++中非常强大且灵活的工具,能够有效地处理容器中的元素。

STL算法

2. STL算法的类别

STL算法可以分为以下几类:

2.1 非修改算法
  • 查找:如 findfind_if,用于查找特定元素。
  • 计数:如 countcount_if,用于计数字符或元素的个数。
  • 比较:如 equal,用于比较两个范围内的元素是否相等。
2.2 修改算法
  • 复制:如 copy,将元素从一个范围复制到另一个范围。
  • 交换:如 swap,交换两个元素的值。
  • 修改:如 filltransform,用于修改范围内的元素。
2.3 排序和重排算法
  • 排序:如 sortstable_sort,对容器中的元素进行排序。
  • 重排列:如 reverse,将元素的顺序反转。
2.4 集合操作
  • 并集、交集、差集:如 set_unionset_intersectionset_difference,用于处理集合运算。

3. 使用STL算法的优势

  • 代码简洁:STL算法通常比手写的循环和条件语句更简洁。
  • 效率高:STL算法经过优化,通常比自定义实现的算法更高效。
  • 可读性强:使用标准算法可以提高代码的可读性,易于维护。

以下是一个使用STL算法的简单示例,展示了如何使用vectorsort算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
std::vector<int> numbers = {4, 2, 5, 1, 3};

// 排序
std::sort(numbers.begin(), numbers.end());

// 输出结果
for (int num : numbers) {
std::cout << num << " ";
}
return 0;
}

选择题

  1. 函数模板的实例化发生在哪个阶段?
    A. 编译时
    B. 运行时
    C. 链接时
    D. 预处理时

  2. 以下哪种模板特化允许保留部分模板参数未指定?
    A. 全特化
    B. 偏特化
    C. 非类型模板参数特化
    D. 可变参数模板

  3. std::vector<int>::iterator 属于哪种迭代器类型?
    A. 输入迭代器
    B. 前向迭代器
    C. 双向迭代器
    D. 随机访问迭代器

  4. 以下哪种容器不支持快速随机访问?
    A. std::vector
    B. std::deque
    C. std::list
    D. std::array

  5. 类模板的成员函数在何时被实例化?
    A. 类模板定义时
    B. 成员函数被调用时
    C. 类模板实例化时
    D. 预处理阶段

  6. 以下代码的错误是什么?

    1
    2
    3
    template<typename T>  
    void f(T a, T b) {}
    f(1, 2.0);

    A. 模板参数推导失败
    B. 函数未实例化
    C. 语法错误
    D. 无错误

  7. std::unordered_map 的底层实现基于什么数据结构?
    A. 红黑树
    B. 哈希表
    C. 双向链表
    D. 动态数组

  8. 以下哪种迭代器可以反向遍历容器?
    A. std::forward_list::iterator
    B. std::vector::reverse_iterator
    C. std::unordered_set::iterator
    D. std::stack::iterator

  9. 关于可变参数模板,以下说法正确的是?
    A. 只能用于函数模板
    B. 参数包必须放在模板参数列表末尾
    C. 参数包展开必须使用递归
    D. 可以用于类模板和函数模板

  10. 以下代码的输出是什么?

    1
    2
    3
    4
    5
    6
    7
    template<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

选择题答案

  1. A
    解析:模板实例化在编译时完成,编译器根据具体类型生成代码。

  2. B
    解析:偏特化允许部分模板参数保留未指定,全特化需指定所有参数。

  3. D
    解析:std::vector 的迭代器支持随机访问(如 operator[])。

  4. C
    解析:std::list 是双向链表,仅支持双向迭代器。

  5. B
    解析:类模板的成员函数在被调用时才会实例化(惰性实例化)。

  6. A
    解析:T 推导为 intdouble 冲突,需显式指定类型。

  7. B
    解析:无序哈希映射std::unordered_map 基于哈希表实现。 mapmultimapsetmultiset 容器是基于红黑树实现的。红黑树是一种自平衡二叉查找树,它确保了树的高度保持在对数级别,从而保证了插入、删除和查找操作的时间复杂度都为 O(log n)。

  8. B
    解析:reverse_iterator 用于反向遍历。

  9. D
    解析:可变参数模板可用于类和函数模板。

  10. A
    解析:递归计算阶乘,结果为 5! = 120。 这段代码展示了如何使用C++模板元编程来计算一个数的阶乘。具体来说,它定义了一个模板结构体 Factorial 来递归地计算给定整数 N 的阶乘,并通过特化处理了基础情况 N=0

    1
    2
    3
    4
    template<int N>  
    struct Factorial {
    static const int value = N * Factorial<N-1>::value;
    };

    这里定义了一个名为 Factorial 的模板结构体,它接受一个整型参数 N。结构体内声明了一个静态常量 value,其值定义为 N 乘以 Factorial<N-1>::value。这意味着对于每个 Nvalue 将等于 N 乘以其前一个数(即 N-1)的阶乘值。这个过程是递归定义的。

    1
    2
    template<>  
    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。

填空题

  1. 函数模板的全特化需要在模板声明中使用 ________ 关键字。
  2. std::priority_queue 的默认底层容器是 ________
  3. 模板参数可以是类型参数或 ________ 参数。
  4. 在C++11中,可变参数模板的参数包展开可以通过 ________ 表达式实现。
  5. std::list 的迭代器属于 ________ 迭代器类型。
  6. 类模板的偏特化必须至少保留一个 ________ 模板参数。
  7. 若需在模板中匹配任意类型的指针,应使用 ________ 特化形式。
  8. std::map 的键类型必须支持 ________ 操作以保持有序。
  9. 非类型模板参数的类型只能是 ________、枚举或指向对象的指针。
  10. std::forward_list 不支持 ________ 操作(如反向遍历)。

填空题答案

  1. template<>

  2. 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: 这是你创建的优先队列对象的名字。通过这个对象,你可以执行优先队列的各种操作,如插入元素、访问顶部元素、移除顶部元素等。
  3. 非类型

​ 解析:类型参数:这是最常见的模板参数形式,用于指定将要使用的数据类型。例如,在函数模板或类模板中使用 T 来表示一个未知的数据类型。

1
2
3
4
template<typename T>
T add(T a, T b) {
return a + b;
}

非类型参数:除了类型参数外,C++ 模板还允许使用非类型的模板参数。这些参数可以是整型值、指针、引用等编译时常量表达式。它们允许模板在实例化时根据传入的具体数值进行不同的行为。

1
2
3
4
5
6
template<int N>
struct FixedSizeArray {
int data[N]; // 数组大小由模板参数N决定
};
// 使用示例
FixedSizeArray<10> arr; // 固定大小为10的数组

通过这种方式,模板机制提供了极大的灵活性,使得可以根据类型以及特定的数值来定制代码的行为,而无需重复编写相似的代码逻辑。

  1. 折叠(Fold) 解析:... ags用于可变长模板。

  2. 双向

  3. 未特化(或原始)
  4. 指针特化(如 template<typename T> class A<T*> {};//偏特化
  5. operator<或排序 解析:运算符重载<以实现有序键值对
  6. 数组大小 template
  7. 反向迭代器与随机迭代器(或 rbegin()/rend() 解析:forward_list前向链表

改错题

  1. X

  2. 修正以下代码的错误:

    1
    2
    3
    template<typename T, typename U>  
    T add(T a, U b) { return a + b; }
    auto result = add(3, 4.5);
  3. 指出以下代码的问题:

    1
    2
    3
    4
    template<int N>  
    void print() { std::cout << N; }
    print<5>();
    print<5.0>(); // 错误
  4. 修正以下代码:

    1
    2
    3
    4
    std::vector<int> vec = {1, 2, 3};  
    for (auto it = vec.end(); it != vec.begin(); --it) {
    std::cout << *it;
    }
  5. 指出以下模板偏特化的错误:

    1
    2
    3
    4
    5
    6
    7
    template<typename T, typename U>  
    struct Pair;
    template<typename U>
    struct Pair<int, U> {
    T first; // 错误
    U second;
    };

改错题答案

  1. X

  2. 错误:返回值类型 T 由第一个参数决定,可能导致精度丢失。
    修正:函数返回值使用共同类型(如 auto )。

  3. 错误:非类型模板参数 N 必须为整型,5.0 是浮点数。
    修正:改为整型值(如 print<5>())。

  4. 错误:初始迭代器指向 vec.end(),解引用会导致未定义行为。
    修正:使用反向迭代器 rbegin()rend()

  5. 错误:偏特化中未定义的 T
    修正:将 T 替换为 int

问答题

  1. 解释函数模板与模板函数的区别。
  2. 什么是非类型模板参数?举例说明其应用场景。
  3. 全特化和偏特化的主要区别是什么?各举一例。
  4. 为什么 std::vector 的迭代器是随机访问迭代器,而 std::list 是双向迭代器?
  5. 说明 std::deque (双端队列)的底层实现及其优缺点。
  6. 什么是迭代器失效?举例说明在 std::vector 中插入元素时迭代器失效的情况。
  7. 解释可变参数模板中参数包展开的两种方法。
  8. 为什么关联容器(如 std::set(集合))的插入操作时间复杂度是 O(log n)?
  9. 如何实现一个支持任意类型和任意数量参数的模板类?
  10. 说明 std::forwardstd::move 在模板中的应用场景及区别。
  11. 什么是 SFINAE?举例说明其在模板元编程中的应用。
  12. 为什么 std::unordered_map 的键类型需要哈希函数和相等比较函数?
  13. 解释模板元编程中的递归实例化与特化的作用。
  14. 如何在模板中限制参数类型为整数类型?
  15. 说明 std::stackstd::queue 为何被称为容器适配器。

问答题答案

  1. 函数模板 是模板定义,template<typename T> void f(T);模板函数 是实例化后的具体函数,如 f<int>(5);
  2. 非类型模板参数 是值而非类型,如 template<int N> class Array {};,用于编译时常量(如数组大小)。
  3. 全特化 指定所有参数,如 template<> class A<int> {};偏特化 保留部分参数,如 template<typename U> class A<int, U> {};
  4. std::vector 内存连续,支持随机访问;std::list 是链表,仅支持双向移动。
  5. std::deque 基于分块数组,支持头尾高效插入,但中间操作较慢。
  6. 迭代器失效 指容器修改后迭代器指向无效内存。例如,std::vector 插入元素可能导致扩容,原迭代器失效。
  7. 递归展开折叠表达式(C++17)。
  8. 关联容器基于红黑树(平衡二叉搜索树),插入需维持有序性,时间复杂度 O(log n)。
  9. 使用可变参数模板:
    1
    2
    template<typename... Args>  
    class Tuple {};
  10. std::move 强制转换为右值;std::forward 保持值类别(完美转发)。 左值是指那些可以位于赋值操作符左侧的表达式,即具有持久性的实体,通常有明确的位置(内存地址)。左值代表了一个可以被引用的数据对象。右值是指那些不能位于赋值操作符左侧的表达式,通常是临时的,没有固定的存储位置。右值主要用于表示临时值或即将被销毁的对象。
  11. SFINAE(替换失败不是错误) 用于条件编译,例如通过 std::enable_if 限制模板匹配。
  12. 哈希函数确定桶位置,相等比较解决哈希冲突。
  13. 递归实例化用于编译时计算(如阶乘),特化终止递归。
  14. 使用 std::is_integralstatic_assert 或 SFINAE。
  15. 它们基于其他容器(如 std::deque)实现,提供特定接口(栈的 LIFO、队列的 FIFO)。