【C++】模板(函数、类模板/非类型模板参数/特化)

目录

1、函数模板

基本用法

函数模板的实现原理

函数模板的实例化

模板参数的匹配原则

2、类模板

类模板的定义格式

类模板的实例化

3、非类型模板参数

 4、模板的特化

函数模板特化

类模板特化

5、模板分离编译


1、函数模板

基本用法

template < typename T >

返回值类型 函数名(参数列表){}

template 是模板的意思,typename是用来定义模板参数关键字(也可以是 class),T 是模板参数。

可以一次性定义多个模板参数

template<typename T1, typename T2,......,typename Tn>

T1、T2 实例化后的实际类型可以相同,也可以不同。 

模板参数也可以作为返回值。

示例:

 template<typename T>
 void Swap( T& left,  T& right)
 {
     T temp = left;
     left = right;
     right = temp;
 }

定义模板参数的各种情况

typename 和 class 的区别

void Print(const vector<int>& v)
{
	vector<int>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

将以上代码进行范型编程,使它可以打印任意容器的值

//template<typename Container>
template<class Container> // 这里既可以使用 typename 也可以使用 class
void Print(const Container& v)
{
	// 编译不确定Container::const_iterator是类型还是对象(静态变量)
    // 如果 Container::const_iterator 是对象,就不符合语法规则
	// typename就是明确告诉编译器这里是类型,等模板实例化再去找
	//typename Container::const_iterator it = v.begin(); // 必须要加 typename,
                                                         // class 不行
	auto it = v.begin(); // 这种方式更简便,auto 一定是类型
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

 也就是说,在模板函数里,只要模板参数后跟了 : :(域作用限定符),去取模板参数里的类型,一定要在前面声明 typename,或者使用 auto。

函数模板的实现原理

函数模板是一个蓝图,它本身不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器

函数模板的实例化

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于自定义类型(类)、指针类型也是如此。

模板参数实例化分为:隐式实例化和显式实例化

1. 隐式实例化:让编译器根据实参推演模板参数的实际类型。

template<class T>
 T Add(const T& left, const T& right)
 {
    return left + right;
 }

int main()
 {
    int a1 = 10, a2 = 20;
    double d1 = 10.0, d2 = 20.0;
    Add(a1, a2);
    Add(d1, d2);
    
    Add(a1, d1);

     /*
     Add(a1, d1);不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
     通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
     编译器无法确定此处到底该将T确定为int 或者 double类型而报错
    注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
     Add(a1, d1);
    */
    
    // 此时有两种处理方式:
    
    //1. 用户自己来强制转化 
    Add(a1, (int)d1);
    Add((double)a,d1);
        
    //2. 使用显式实例化

    Add<int>(a1,b1);// b1 隐式类型转换
    Add<double>(a1,b1);// a1 隐式类型转换

    

不管是隐式类型转换还是显式类型转换都会产生临时变量,临时变量具有常性,要传递给常引用的函数参数(const 类型名&) 

Add 函数的形参应该写成:

template<class T>
 T Add(const T& left, const T& right)
 {
    return left + right;
 }

2. 显式实例化:在函数名后的<>中指定模板参数的实际类型

 int main()
 {
     int a = 10;
     double b = 20.0;
    
     // 显式实例化
    
     Add<int>(a, b);
     
     return 0;
 }

使用显式实例化的场景:

template<class T> 

T* Alloc(int n)
{
    return new T[n];
}

int main()
{
    double* p = Alloc<double>(10);

    return 0;
}

 函数模板在类内声明,在类外定义时仍要加 template< typename T>:

模板参数的匹配原则

1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

 // 专门处理int的加法函数
 int Add(int left, int right)
 {
     return left + right;
 }
 
// 通用加法函数
 template<class T>
 T Add(T left, T right)
 {
     return left + right;
 }

 void Test()
 {
     Add(1, 2);// 与非模板函数匹配,编译器不需要实例化
     Add<int>(1, 2);  // 调用编译器实例化的Add版本       
 }

2. 对于非模板函数和同名函数模板,如果模板产生的实例与非模板函数匹配度一样,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配度函数, 那么将选择模板产生的实例。

 void Test()
 {
    Add(1, 2);     // 与非函数模板类型完全匹配,不需要函数模板实例化
    Add(1, 2.0);   // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的函数  
}

2、类模板

类模板的定义格式

template<class T1, class T2, ..., class Tn> 
class 类模板名
{
     // 类内成员定义
};    

示例:

// 动态顺序表
// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
 class Vector
 { 
public :
     Vector(size_t capacity = 10)
        : _pData(new T[capacity])
        ,  _size(0)
        ,  _capacity(capacity)
     {}

    // 使用析构函数演示:模板函数在类中声明,在类外定义。
    ~Vector();

    void PushBack(const T& data);
    void PopBack();
    // ...
    size_t Size() {return _size;}
 
    T& operator[](size_t pos)
    {
        assert(pos < _size);
        return _pData[pos];
    }
    
private:
    T* _pData;
    size_t _size;
    size_t _capacity;
 };

类模板函数在类内声明,在类外定义 

// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
    if(_pData)
        delete[] _pData;
    _size = _capacity = 0;
}

对于普通的类,类名就是类型(如 Date 类,类名是 Date,类型名也是 Date),对于类模板,类名(模板名)不是类型(如上面的 Vector ,类名是 Vector,但类型名不是 Vector),类型名是 vector<T>(如 Vector< int >),在使用 :: 指定访问的域时,:: 左边必须是类型名( Vector<T>::~Vector() )。除此之外,还要再声明一下模板参数。(template <class T>)

类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟 <>,然后将实例化的类型放在 <> 中即可,类模板不是真正的类,而实例化的结果才是真正的类。

 // Vector类名,Vector<int>才是类型
 Vector<int> s1; //存储 int 类型数据的顺序表
 Vector<double> s2; //存储 double 类型数据的顺序表

 类模板实例化的特殊情况:

在 vector 类有 resize 函数,用来指定类对象的大小,函数原型:

resize(size_t n, const T& = T());

T() 是缺省参数,是一个 T 类型的匿名对象,T() 会调用 T 实例化后的类的默认构造函数,但是问题来了,如果 T 实例化后是内置类型呢,比如说 int,难道 int 类型有默认构造函数。

为了针对这种情况,C++ 规定 int 类型等内置类型也有默认构造函数:

int a = int();
int b = int(1); // 等价于 int b = 1;

按需实例化 

如果在程序中实例化了一个类,但该类的一个模板成员函数从未使用过,编译器就不会生成该模板成员函数实例化后的代码,也不会该检查模板成员函数的语法。

3、非类型模板参数

模板参数分为类型形参与非类型形参。

  • 类型形参:出现在模板参数列表中,跟在class或者typename之后的参数类型名称。
  • 非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
namespace My_program
{
     // 定义一个模板类型的静态数组
     template<class T, size_t N = 10>
     class array
     {
     public:
         T& operator[](size_t index){return _array[index];}
         const T& operator[](size_t index)const{return _array[index];}
 
         size_t size()const{return _size;}
         bool empty()const{return 0 == _size;}
 
     private:
         T _array[N];
         size_t _size;
     };
}

使用这个数组:

int mian()
{
    array<int,10> arr1; // 容量为 10,存储int类型的数组
    array<double,100> arr1; // 容量为 100,存储double类型的数组
    return 0;
}

 注意:

1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
2. 非类型的模板参数必须在编译期就能确认结果。并且不能在程序中修改。

 4、模板的特化

概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板

// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
 return left < right;
}
int main()
{
 cout << Less(1, 2) << endl; // 可以比较,结果正确
 Date d1(2022, 7, 7);
 Date d2(2022, 7, 8);
 cout << Less(d1, d2) << endl; // 可以比较,结果正确
 Date* p1 = &d1;
 Date* p2 = &d2;
 cout << Less(p1, p2) << endl; // 可以比较,结果错误
 return 0;
}

可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化

函数模板特化

函数模板的特化步骤:

1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。

// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
     return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
    return *left < *right;
}
int main()
{
     cout << Less(1, 2) << endl;
     Date d1(2022, 7, 7);
     Date d2(2022, 7, 8);
     cout << Less(d1, d2) << endl;
     Date* p1 = &d1;
     Date* p2 = &d2;
     cout << Less(p1, p2) << endl; // 调用特化之后的版本,而不走模板生成了
     return 0;
}

 注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。

bool Less(Date* left, Date* right)
{
     return *left < *right;
}

该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。

类模板特化

类模板的特化步骤:

1. 必须要先有一个基础的类模板
2. 关键字template后面的<>里面写不需要特化的模板参数
3. 类名名后跟一对尖括号,尖括号中写模板参数和特化的类型,相对顺序与基础的类模板声明的模板参数顺序一致。

全特化
全特化即是将模板参数列表中所有的参数都确定化。

// 基础的类模板
template<class T1, class T2>
class Data
{
public:
 Data() {cout<<"Data<T1, T2>" <<endl;}
private:
 T1 _d1;
 T2 _d2;
};

// 如果 T1 实例化为 int,T2 实例化为 char,假设要特殊处理
template<>
class Data<int, char>
{
public:
 Data() {cout<<"Data<int, char>" <<endl;}
private:
 int _d1;
 char _d2;
};

偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:

// 基础的类模板
template<class T1, class T2>
class Data
{
public:
 Data() {cout<<"Data<T1, T2>" <<endl;}
private:
 T1 _d1;
 T2 _d2;
};

偏特化有以下两种表现方式:
部分特化
将模板参数类表中的一部分参数特化。

// 将第二个参数特化为int
// 如果 T2 实例化为 int,假设要特殊处理
template <class T1>
class Data<T1, int>
{
public:
 Data() {cout<<"Data<T1, int>" <<endl;}
private:
 T1 _d1;
 int _d2;
};

参数更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。

// 如果两个模板参数都实例化为指针类型,假设要特殊处理
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{ 
public:
 Data() {cout<<"Data<T1*, T2*>" <<endl;}
 
private:
 T1 _d1;
 T2 _d2;
};

// 如果两个模板参数都实例化为引用类型,假设要特殊处理
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
 Data(const T1& d1, const T2& d2)
 : _d1(d1)
 , _d2(d2)
 {
 cout<<"Data<T1&, T2&>" <<endl;
 }
 
private:
 const T1 & _d1;
 const T2 & _d2; 
 };

void test2 () 
{
 Data<double , int> d1; // 调用特化的int版本
 Data<int , double> d2; // 调用基础的模板 
 Data<int *, int*> d3; // 调用特化的指针版本
 Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}

5、模板分离编译

什么是分离编译

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

在 C++ 中,如果将 模板的声明和定义分离到不同的文件(例如 .h 声明,.cpp 定义),通常会导致 链接错误(Linker Error),原因如下:


1. 模板的编译机制:两阶段查找

C++ 模板采用 "两阶段编译"(Two-Phase Lookup) 机制:

  • 阶段 1(模板定义时):检查模板本身的语法是否正确(如 template<typename T> 是否合法)。

  • 阶段 2(模板实例化时):在 调用点 检查模板参数是否合法(如 T 是否支持 operator+)。

由于 模板代码必须在使用时实例化,如果定义和实现分离,编译器在 编译调用代码时 看不到模板的实现,导致 无法生成具体的实例化代码


2. 为什么分离 .h 和 .cpp 会报错?

假设我们有以下文件结构:

// mytemplate.h
template<typename T>
void myFunc(T x);  // 声明

// mytemplate.cpp
#include "mytemplate.h"
template<typename T>
void myFunc(T x) {  // 定义
    std::cout << x << std::endl;
}

// main.cpp
// 由于各个文件是分开编译的,
// 编译器在编译 main.cpp 时,展开 mytemplate.h 
// 编译器只看到函数模板的声明,无法生成具体的函数。
#include "mytemplate.h"
int main() {
    myFunc(42);  // 调用
}

编译过程分析

mytemplate.cpp 编译

  • 编译器看到 template<typename T> void myFunc(T x) { ... },但 没有实际调用,所以 不会生成任何实例化代码myFunc<int> 不会生成)。

main.cpp 编译

  • 编译器看到 myFunc(42),知道需要 myFunc<int>,但 mytemplate.cpp 里没有实例化,所以它 期望链接器在其他地方找到 myFunc<int> 的实现

链接阶段

  • 链接器发现 myFunc<int> 没有实现,报错:无法解析的外部符号

3. 如何解决?

(1) 推荐方法:直接在头文件定义模板

// mytemplate.h
template<typename T>
void myFunc(T x) {  // 声明 + 定义都在头文件
    std::cout << x << std::endl;
}

✅ 优点:编译器在调用点能直接看到定义,可以正确实例化。
❌ 缺点:可能导致头文件变大,编译时间增加(但现代编译器优化后影响不大)。


(2) 显式实例化(Explicit Instantiation)

如果坚持分离 .h 和 .cpp,可以在 .cpp 里 显式实例化 需要的类型:

// mytemplate.h
template<typename T>
void myFunc(T x) {  // 声明 + 定义都在头文件
    std::cout << x << std::endl;
}

✅ 优点:保持分离,链接时能找到实现。
❌ 缺点:必须提前知道所有可能的 T,不够灵活。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值