大纲 C++ 模板与泛型 泛型编程
泛型编程是一种以独立于任何特定类型的方式编写代码的编程方式。在使用泛型编程时,需要提供具体程序实例所需的类型、习惯或值。 在 C++ 中,泛型编程(基于模板)可以用于实现静态多态(编译期多态),即在编译阶段根据具体类型生成对应的代码 ;与之对应,虚函数机制用于实现动态多态(运行时多态),通过基类指针或引用在运行时决定调用哪个派生类的实现。模板的作用
模板是泛型编程的基础。它是一种用于创建类或函数的蓝图或公式,开发者只需为这些蓝图提供足够的信息,模板便能真正执行并生成具体的代码。 模板的类型支持
模板支持将类型作为参数进行程序设计,从而直接支持泛型编程过程。也就是说,C++ 的模板机制允许在定义类或函数时,将类型作为参数传递,实现代码的通用性。 模板的两种分类
函数模板函数模板是用于生成特定类型函数的蓝图。它允许在定义函数时,将参数或返回值的类型作为模板参数,从而实现通用操作(如比较、交换等)。 比如,基于函数模板编写一个比较两个值大小的函数,可以适用于 int、double、string 等多种类型。 类模板类模板是用于生成特定类型类的蓝图。它允许在定义类时,将成员变量或成员函数的类型作为模板参数,从而实现通用数据结构或容器(如栈、队列、数组等)。 比如,基于类模板实现一个可存储任意类型元素的栈类,使用时再指定具体类型(如 Stack<int>、Stack<string>)。 模板基础 函数模板 函数模板的概述 模的板定义以 template 关键字开头
后面紧跟 <>,其中称为模板参数列表(或模板实参列表)。 模板参数列表规则
至少包含一个模板参数。 多个参数用逗号分隔。 每个模板参数前需使用 typename 或 class 关键字(此处 class 非类定义用途)。 常见写法示例
单个模板参数template<class T>或者 template<typename T> 多个模板参数:template<class T, class Q>或者 template<typename T, typename Q> 模板参数的含义
表示函数定义中使用的类型或值,类似于函数参数列表的作用。 模板实参的指定方式
显式指定:使用 <> 包裹模板实参,比如 max<int>(3, 5);。 隐式推断:编译器根据函数调用上下文自动推断模板实参,无需显式指定。 类型参数 T 的作用
T 是模板的类型参数,本身只是一个占位符。在模板被使用时,编译器会进行模板实例化,根据调用上下文推导出 T 的具体类型。这个过程通常发生在编译期,而不是运行时 。函数模板的使用 关键点
函数模板的调用与普通函数的调用区别不大。在调用函数模板时,编译器会根据调用时提供的实参,去推断模板参数列表中的参数(即模板形参)的类型。有些时候,光凭函数实参是无法准确推断出模板参数的,这时候就需要使用 <> 来主动提供模板参数了,比如 max<int>(3, 5);。值得注意的是,模板参数有时是由编译器推断出来的,推断的依据是调用函数时传入的实参。当编译器推断出模板的形参类型后,编译器会实例化一个特定版本的函数。函数模板只有被调用(实例化)时,编译器才会生成对应的代码,仅定义模板编译器是不会生成任何代码的。由于编译器在生成代码的时候,需要找到函数的函数体,所以函数模板的定义通常是写在 .h 头文件中。
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 #include <iostream> using namespace std;template <typename T>T maxNum (T a, T b) { return (a > b) ? a : b; } int main () { int result1 = maxNum (10 , 20 ); double result2 = maxNum (3.14 , 2.71 ); double result3 = maxNum<double >(10 , 3.14 ); return 0 ; }
非类型模板参数 关键点
在定义函数模板时,因为 T 前边有一个 typename / class,这表示 T 代表一个类型,是一个类型参数。那么在模板参数列表里面,还可以定义非类型参数,这里的非类型参数代表的是一个值。既然非类型参数代表一个值,那么就不能用 typename / class 这种关键字来修饰这个值,而是必须要用具体的类型名称来指定非类型参数。比如,非类型参数 S 如果是个整型,就可以写成 template<typename T, int S>。当函数模板实例化时,这种非类型参数的值通常需要由用户显式指定(一般情况下编译器不会自动推断),并且这些值都必须是常量表达式,因为函数模板的实例化是在编译阶段完成的。另外,无论是函数模板还是类模板,非类型模板参数都可以指定默认值,比如 template <typename T, int A = 5>,但需要注意非类型模板参数的默认值必须从右向左连续定义。特别注意,浮点型不能作为非类型模板参数,比如 float、double。
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <cstring> #include <iostream> using namespace std;template <int A, int B>int sum () { return A + B; } template <unsigned L1, unsigned L2>int charscomp (const char (&c1)[L1], const char (&c2)[L2]) { return strcmp (c1, c2); } template <typename T, int A = 5 >void repeatPrint (T value) { for (int i = 0 ; i < A; ++i) { cout << value << " " ; } cout << endl; } void test01 () { cout << "========== test01() ======" << endl; int result1 = sum<3 , 5 >(); cout << result1 << endl; } void test02 () { cout << "========== test02() ======" << endl; int result1 = charscomp ("test2" , "test" ); cout << result1 << endl; } void test03 () { cout << "========== test03() ======" << endl; repeatPrint<int , 2 >(10 ); repeatPrint<double , 3 >(3.14 ); repeatPrint<const char *, 4 >("Hi" ); repeatPrint<float >(6.7f ); } int main () { test01 (); test02 (); test03 (); return 0 ; }
程序运行输出的结果如下:
1 2 3 4 5 6 7 8 9 ========== test01() ====== 8 ========== test02() ====== 50 ========== test03() ====== 10 10 3.14 3.14 3.14 Hi Hi Hi Hi 6.7 6.7 6.7 6.7 6.7
类模板 类模板的概述 类模板可以用来实例化一个特定的类。由于编译器不能为类模板自动推断模板参数类型,因此为了使用类模板,必须在模板名后边使用 <> 来提供额外的信息,这些信息其实就是对应着模板参数的具体类型或值。类模板的核心优势在于:同一套代码,可以应付不同的数据类型,代码精简多了,避免了为每种类型重复编写类似的类。在 C++ 模板中,只要一个名称依赖于模板参数,编译器在第一阶段无法判断它是类型还是变量,默认按非类型解析;因此无论是在类模板还是函数模板中,都必须使用 typename 显式声明它是类型。
对比项 函数模板 类模板 模板实参推断 支持自动推断 不支持(C++17 之前),需显式指定 使用方式 max(1, 2)vector<int>实例化时机 调用时实例化(编译期实例化) 使用类型时实例化(编译期实例化) 函数 / 成员函数生成 调用时生成 成员函数按需实例化(懒生成)
类模板的使用 关键点
C++ 类模板的成员函数是指在类模板中定义的函数,这些函数同样依赖于模板参数,因此在类外实现类模板的成员函数时,需要保留模板参数声明(比如 template<typename T>)并使用作用域限定符(如 MyVector<T>::func())进行定义。由于模板是在编译期进行实例化的,成员函数的实现通常需要放在头文件中,以便编译器在使用时能够生成具体类型的代码。此外,在成员函数中如果使用了依赖于模板参数的类型(如 MyVector<T>::iterator),编译器默认无法判断其是类型还是变量,此时必须使用 typename 进行显式说明(如 typename MyVector<T>::iterator)。总体来说,类模板的成员函数与普通类成员函数在语法上类似,但需要额外注意模板参数、依赖名称解析以及编译期实例化机制。
MyVector.hpp,简单模拟实现 STL 的 vector 容器,不考虑扩容、移动语义等实现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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 #pragma once #include <iostream> using namespace std;template <typename T>class MyVector {public : using iterator = T*; MyVector (); ~MyVector (); MyVector (const MyVector& v); MyVector& operator =(const MyVector& v); iterator begin () ; iterator end () ; bool push_back (const T& t) ; private : T* m_data; size_t m_size; size_t m_capacity; }; template <typename T>MyVector<T>::MyVector () { m_size = 0 ; m_capacity = 10 ; m_data = new T[m_capacity]; } template <typename T>MyVector<T>::~MyVector () { if (m_data) { delete [] m_data; m_data = nullptr ; } } template <typename T>MyVector<T>::MyVector (const MyVector<T>& v) { m_size = v.m_size; m_capacity = v.m_capacity; m_data = new T[m_capacity]; for (size_t i = 0 ; i < m_size; ++i) { m_data[i] = v.m_data[i]; } } template <typename T>MyVector<T>& MyVector<T>::operator =(const MyVector<T>& v) { if (this == &v) { return *this ; } T* new_data = new T[v.m_capacity]; for (size_t i = 0 ; i < v.m_size; ++i) { new_data[i] = v.m_data[i]; } delete [] m_data; m_data = new_data; m_size = v.m_size; m_capacity = v.m_capacity; return *this ; } template <typename T>typename MyVector<T>::iterator MyVector<T>::begin () { return m_data; } template <typename T>typename MyVector<T>::iterator MyVector<T>::end () { return m_data + m_size; } template <typename T>bool MyVector<T>::push_back (const T& t) { if (m_size == m_capacity) { return false ; } m_data[m_size] = t; ++m_size; return true ; }
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 #include <iostream> #include "MyVector.hpp" using namespace std;void show (MyVector<int >& v) { for (MyVector<int >::iterator it = v.begin (); it != v.end (); ++it) { cout << *it << " " ; } cout << endl; } int main () { MyVector<int > v1; v1.push_back (1 ); v1.push_back (3 ); v1.push_back (5 ); show (v1); MyVector<int > v2 = v1; show (v2); v2 = v1; show (v2); return 0 ; }
程序运行输出的结果如下:
非类型模板参数 关键点
在定义类模板时,因为 T 前边有一个 typename / class,这表示 T 代表一个类型,是一个类型参数。那么在模板参数列表里面,还可以定义非类型参数,这里的非类型参数代表的是一个值。既然非类型参数代表一个值,那么就不能用 typename / class 这种关键字来修饰这个值,而是必须要用具体的类型名称来指定非类型参数。比如,非类型参数 S 如果是个整型,就可以写成 template<typename T, int S>。当类模板实例化时,这种非类型参数的值通常需要由用户显式提供(一般情况下编译器不会自动推断),并且这些值都必须是常量表达式,因为类模板的实例化是在编译阶段完成的。另外,无论是函数模板还是类模板,非类型模板参数都可以指定默认值,比如 template <typename T, int A = 5>,但需要注意非类型模板参数的默认值必须从右向左连续定义。特别注意,浮点型不能作为非类型模板参数,比如 float、double。
MyVector.hpp,简单模拟实现 STL 的 vector 容器,不考虑扩容、移动语义等实现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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 #pragma once #include <iostream> using namespace std;template <typename T, int C = 10 >class MyVector { public : using iterator = T*; MyVector (); ~MyVector (); MyVector (const MyVector& v); MyVector& operator =(const MyVector& v); iterator begin () ; iterator end () ; bool push_back (const T& t) ; private : T* m_data; size_t m_size; size_t m_capacity; }; template <typename T, int C>MyVector<T, C>::MyVector () { m_size = 0 ; m_capacity = C > 0 ? C : 10 ; m_data = new T[m_capacity]; } template <typename T, int C>MyVector<T, C>::~MyVector () { if (m_data) { delete [] m_data; m_data = nullptr ; } } template <typename T, int C>MyVector<T, C>::MyVector (const MyVector<T, C>& v) { m_size = v.m_size; m_capacity = v.m_capacity; m_data = new T[m_capacity]; for (size_t i = 0 ; i < m_size; ++i) { m_data[i] = v.m_data[i]; } } template <typename T, int C>MyVector<T, C>& MyVector<T, C>::operator =(const MyVector<T, C>& v) { if (this == &v) { return *this ; } T* new_data = new T[v.m_capacity]; for (size_t i = 0 ; i < v.m_size; ++i) { new_data[i] = v.m_data[i]; } delete [] m_data; m_data = new_data; m_size = v.m_size; m_capacity = v.m_capacity; return *this ; } template <typename T, int C>typename MyVector<T, C>::iterator MyVector<T, C>::begin () { return m_data; } template <typename T, int C>typename MyVector<T, C>::iterator MyVector<T, C>::end () { return m_data + m_size; } template <typename T, int C>bool MyVector<T, C>::push_back (const T& t) { if (m_size == m_capacity) { return false ; } m_data[m_size] = t; ++m_size; return true ; }
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> #include "MyVector.hpp" using namespace std;template <int C>void show (MyVector<int , C>& v) { for (typename MyVector<int , C>::iterator it = v.begin (); it != v.end (); ++it) { cout << *it << " " ; } cout << endl; } int main () { MyVector<int , 5 > v1; v1.push_back (1 ); v1.push_back (3 ); v1.push_back (5 ); show (v1); MyVector<int , 5 > v2 = v1; show (v2); v2 = v1; show (v2); return 0 ; }
程序运行输出的结果如下:
模板进阶 typename 的使用场合 在 C++ 中,typename 常见的三种使用场合:
(1) 函数模板的定义:
1 2 3 4 5 template <typename T>T maxNum (T a, T b) { ...... }
(2) 类模板的定义:
1 2 3 4 5 template <typename T>class MyVector { ...... }
(3) 类外访问类模板的类型成员:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 template <typename T>class MyVector {public : using iterator = T*; iterator begin () ; ...... } template <typename T>typename MyVector<T>::iterator MyVector<T>::begin () { ...... }
函数指针的使用方式 函数指针作为其他函数的参数
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> using namespace std;typedef int (*FunType) (int , int ) ;int sum (int a, int b) { return a + b; } int testFunc (int i, int j, FunType func) { return func (i, j); } int main () { int result = testFunc (3 , 5 , sum); cout << result << endl; return 0 ; }
程序运行输出的结果如下:
函数模板的趣味写法 在函数模板中,使用函数指针和函数对象
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 #include <iostream> using namespace std;typedef int (*FunType) (int , int ) ;template <typename T, typename F>int testFunc (const T &i, const T &j, F func) { return func (i, j); } int sum (int a, int b) { return a + b; } class Test {public : Test () { cout << "Test()" << endl; } ~Test () { cout << "~Test()" << endl; } Test (const Test &t) { cout << "Test(const Test&)" << endl; } int operator () (int i, int j) const { return i + j; } }; void test01 () { cout << "------------ test01() ---------------" << endl; int result = testFunc (1 , 3 , sum); cout << result << endl; } void test02 () { cout << "------------ test02() ---------------" << endl; Test obj; int result2 = testFunc (5 , 6 , obj); cout << result2 << endl; } void test03 () { cout << "------------ test03() ---------------" << endl; int result3 = testFunc (7 , 8 , Test ()); cout << result3 << endl; } int main () { test01 (); test02 (); test03 (); return 0 ; }
程序运行输出的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 ------------ test01() --------------- 4 ------------ test02() --------------- Test() Test(const Test&) ~Test() 11 ~Test() ------------ test03() --------------- Test() ~Test() 15
函数指针与函数对象的简单介绍
对比 函数指针 函数对象 是否能存状态 ❌ ✅ 是否可内联优化 一般不行 ✅ 灵活性 低 高
默认模板参数的使用 函数模板使用默认模板参数
模板参数设置默认类型(typename T = int) 1 2 3 4 5 6 7 8 9 10 11 12 13 #include <iostream> using namespace std;template <typename T = int >T add (T a, T b) { return a + b; } int main () { cout << add (1 , 2 ) << endl; cout << add<double >(1.5 , 2.3 ); }
1 2 3 4 5 6 7 8 9 10 11 12 #include <iostream> using namespace std;template <typename T>T add (T a, T b = 10 ) { return a + b; } int main () { cout << add (5 , 3 ) << endl; cout << add (5 ) << endl; }
1 2 3 4 5 6 7 8 9 10 11 12 #include <iostream> using namespace std;template <typename T = int >T add (T a, T b = 10 ) { return a + b; } int main () { cout << add (5 ) << endl; cout << add<double >(5.5 ) << endl; }
类模板使用默认模板参数
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 #include <exception> #include <iostream> using namespace std;template <typename T, size_t size = 10 >class MyArray { public : T& operator [](size_t index) { if (index < 0 || index > size - 1 ) { throw out_of_range ("index out of range" ); } return array[index]; } const T& operator [](size_t index) const { if (index < 0 || index > size - 1 ) { throw out_of_range ("index out of range" ); } return array[index]; } private : T array[size]; }; int main () { MyArray<int > array; array[0 ] = 10 ; array[1 ] = 20 ; cout << array[0 ] << ", " << array[1 ] << endl; MyArray<int , 5 > array2; array2[0 ] = 40 ; array2[1 ] = 50 ; cout << array2[0 ] << ", " << array2[1 ] << endl; return 0 ; }
程序运行输出的结果如下:
成员函数模板的使用 在 C++ 中,无论是普通类还是类模板,其成员函数(包括构造函数)都可以声明为函数模板(称为成员函数模板)。但由于虚函数(virtual)机制依赖于运行时多态,而函数模板是在编译期间实例化的,因此成员函数模板不能被声明为虚函数(virtual)。
特别注意
对于类模板,其成员函数(包括普通成员函数和成员函数模板)通常采用 "按需实例化"(Lazy Instantiation)机制。只有当程序中实际使用(例如发生函数调用)某个成员函数时,编译器才会对该函数进行实例化。如果某个成员函数从未被使用,则不会被实例化。
普通类的成员函数模板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> using namespace std;class Test {public : template <typename T> T sum (T v1, T v2) { return v1 + v2; } }; int main () { Test obj; int result = obj.sum (3 , 5 ); cout << result << endl; return 0 ; }
类模板的成员函数模板 第一种写法(类的声明与定义写在一起)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> using namespace std;template <typename A>class Test {public : template <typename B> void func (A v1, B v2) { cout << v1 << ", " << v2 << endl; } }; int main () { Test<int > obj; obj.func (3 , 5.3 ); return 0 ; }
第二种写法(类的声明与定义分开写)
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> using namespace std;template <typename A>class Test {public : template <typename B> void func (A v1, B v2) ; }; template <typename A>template <typename B>void Test<A>::func (A v1, B v2) { cout << v1 << ", " << v2 << endl; } int main () { Test<int > obj; obj.func (3 , 5.3 ); return 0 ; }
模板显式实例化的使用 为了避免在多个 .cpp 文件中重复实例化同一个模板(包括类模板和函数模板),C++ 提供了 “显式实例化”(Explicit Instantiation)机制。通过在某一个 .cpp 源文件中进行显式实例化,可以将模板实例化集中到单一个翻译单元,从而避免重复生成代码并减少编译和链接开销。实际上,编译器和链接器通常会对重复的模板实例进行合并(如 COMDAT 机制),但显式实例化仍然可以用于控制模板实例化的位置,并在一定程度上减少编译时间和代码膨胀。
特别注意
显式实例化(Explicit Instantiation)既适用于类模板,也适用于函数模板,其本质是为指定类型生成对应的模板实例代码。 函数模板的显式实例化:其本质是在某个 .cpp 源文件中为指定类型(如 int)生成对应的函数实现,其他翻译单元通过 extern template 声明避免函数模板重复实例化。 类模板的显式实例化:其本质是在某个 .cpp 源文件中为某个具体类型(如 MyClass<int>)生成完整的实例代码(包括类结构及其所有已定义的成员函数实现),其他翻译单元通过 extern template 声明避免类模板重复实例化。 类模板 + 显式实例化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #ifndef MYCLASS_H #define MYCLASS_H #include <iostream> using namespace std;template <typename T>class MyClass {public : void print (T value) ; }; extern template class MyClass <int >;#endif
1 2 3 4 5 6 7 8 9 10 #include "MyClass.h" template <typename T>void MyClass<T>::print (T value) { cout << "value: " << value << endl; } template class MyClass <int >;
1 2 3 4 5 6 7 #include "MyClass.h" int main () { MyClass<int > obj; obj.print (42 ); return 0 ; }
函数模板 + 显式实例化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #ifndef FUNC_H #define FUNC_H #include <iostream> using namespace std;template <typename T>void printValue (T value) ;extern template void printValue<int >(int );#endif
1 2 3 4 5 6 7 8 9 10 #include "func.h" template <typename T>void printValue (T value) { cout << "value: " << value << endl; } template void printValue<int >(int );
1 2 3 4 5 6 #include "func.h" int main () { printValue (42 ); return 0 ; }