【C++】模板从入门到进阶(函数模板 + 类模板 + 特化 + 分离编译)

文章目录


前言

在学习模板之前,想写一个通用交换函数,我们得靠函数重载写 N 个版本:int 版、double 版、char 版...... 逻辑完全重复,加新类型就要手动补代码。

有没有办法写一套通用逻辑,让编译器自动适配所有类型?这就是 C++ 的模板(Template)!


一、为什么需要模板?从函数重载的痛点说起

1. 函数重载的局限

我们想实现通用交换函数,用 C 语言 / 重载的思路得这么写:

cpp 复制代码
void Swap(int& left, int& right) {
    int temp = left;
    left = right;
    right = temp;
}
void Swap(double& left, double& right) {
    double temp = left; 
    left = right; 
    right = temp;
}
void Swap(char& left, char& right) {
    char temp = left; 
    left = right; 
    right = temp;
}

这就带来了两个问题:

复用率极低 :逻辑一模一样,只是类型不同,每加一种类型就要抄一遍代码;

维护成本高:一个逻辑出 bug,所有重载版本都要挨个修改,漏改就可能出 bug。

2. 泛型编程与模板

泛型编程的核心思想:编写与类型无关的通用代码,让编译器根据传入类型自动生成对应版本,实现代码复用。

生活化比喻:

模板就像月饼模具。模具本身不是月饼,但你倒入绿豆沙就出绿豆月饼,倒入莲蓉就出莲蓉月饼。一套模板,就可以适配所有你想要的口味(类型)。

C++ 模板分为两大类:

函数模板 :针对函数,生成不同类型的函数;

类模板:针对类,生成不同类型的类。

二、模板初阶:函数模板与类模板

1. 函数模板

1.1 基本语法:

cpp 复制代码
template<typename T1, typename T2, ...>
返回值类型 函数名(参数列表) {
    // 函数体
}
  • template:声明模板的关键字;
  • typename:定义模板类型参数,也可以用class替代,二者完全等价(注意不能用struct
  • T:虚拟类型名,代表一种待定的类型。.

示例:通用交换函数

cpp 复制代码
#include <iostream>
using namespace std;

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

int main() {
    int a = 10, b = 20;
    Swap(a, b); // 自动生成int版Swap

    double c = 1.1, d = 2.2;
    Swap(c, d); // 自动生成double版Swap

    char e = 'x', f = 'y';
    Swap(e, f); // 自动生成char版Swap
    return 0;
}

1.2 底层原理

模板本身不是真正的函数,只是一张 "蓝图"。编译阶段,编译器会根据实参类型推演出 T 的真实类型,然后生成一份对应类型的具体函数。

比如调用Swap(a,b)时,编译器生成void Swap(int&, int&);调用Swap(c,d)时,生成void Swap(double&, double&)

简单说:我们写一套模板,编译器帮我们生成所有需要的重载。

1.3 函数模板的实例化

用具体类型调用模板、生成对应代码的过程叫实例化,分两种:

隐式实例化(最常用)

编译器根据实参自动推演类型:

cpp 复制代码
template<class T>
T Add(const T& left, const T& right) {
    return left + right;
}

int main() {
    Add(10, 20);    // T推演为int
    Add(1.1, 2.2);  // T推演为double
    return 0;
}

易错点:类型不一致导致推演失败

比如下面代码

cpp 复制代码
Add(10, 20.5); // 编译报错!

第一个实参把 T 推成int,第二个推成double,但模板只有一个 T,编译器无法决策。

模板中编译器一般不会自动做类型转换,避免背锅。

两种解决方法:

1、手动强转:Add(10, (int)20.5);

2、显式实例化

显式实例化

手动在<>中指定类型:

cpp 复制代码
Add<int>(10, 20.5); // 强制T为int,20.5自动转成int

类型不匹配时编译器会尝试隐式转换,转不了才报错。

1.4 模板参数匹配原则

当普通函数和同名模板同时存在时,编译器按三条规则匹配:

  1. 普通函数优先级更高:类型完全匹配时,优先调用普通函数,不会用模板生成;
  2. 模板更匹配则选模板:如果模板能生成更精准的版本(比如两个不同类型参数),则调用模板;
  3. 普通函数支持自动类型转换,模板不支持
cpp 复制代码
#include <iostream>
using namespace std;

// 普通int加法
int Add(int left, int right) {
	cout << "普通int加法" << endl;
	return left + right;
}

// 通用模板
template<class T1, class T2>
T1 Add(T1 left, T2 right) {
	cout << "通用模板" << endl;
	return left + (T1)right;
}

int main() {
	Add(1, 2);      // 参数完全匹配普通函数,优先调用
	Add(1, 2.0);    // 普通函数需要转double,模板能生成更匹配版本 → 调用模板
	return 0;
}

2. 类模板

函数模板解决了通用函数的问题,那类呢?比如栈,想支持 int 栈、double 栈、string 栈,总不能写三个类吧?这就需要类模板

2.1 语法格式

cpp 复制代码
template<class T1, class T2, ...>
class 类名 {
    // 类成员
};

示例:通用栈类模板

cpp 复制代码
template<typename T>
class Stack {
public:
    Stack(size_t capacity = 4) {
        _array = new T[capacity];
        _capacity = capacity;
        _size = 0;
    }
    ~Stack() {
        delete[] _array;
        _array = nullptr;
    }
    void Push(const T& data); // 类内声明
private:
    T* _array;
    size_t _capacity;
    size_t _size;
};

2.2 类外定义成员函数

类模板的成员在类外定义时,必须带上模板声明,作用域写类名<T>::

cpp 复制代码
template<class T>
void Stack<T>::Push(const T& data) {	// 类外定义
    _array[_size] = data;
    ++_size;
}

提醒:模板不建议声明和定义分离到 .h 和 .cpp,会出现链接错误,后面进阶会详细讲。

2.3 类模板的实例化

类模板必须显式指定类型,不能靠编译器推演:

cpp 复制代码
#include <iostream>
using namespace std;

template<typename T>
class Stack {
public:
    Stack(size_t capacity = 4) {
        _array = new T[capacity];
        _capacity = capacity;
        _size = 0;
    }
    ~Stack() {
        delete[] _array;
        _array = nullptr;
    }
    void Push(const T& data); // 类内声明
private:
    T* _array;
    size_t _capacity;
    size_t _size;
};

template<class T>
void Stack<T>::Push(const T& data) { // 类外定义
    _array[_size] = data;
    ++_size;
}

int main() {
    Stack<int> st1;      // int类型的栈
    Stack<double> st2;   // double类型的栈
    st1.Push(10);
    st2.Push(3.14);
    return 0;
}

Stack:是类模板名 ,不是真正的类;

Stack<int>:才是实例化后的具体类型

三、模板进阶:核心特性

1. 非类型模板参数

模板参数分两类:

  • 类型形参 :用typename/class修饰,代表一种类型(前面讲的都是);
  • 非类型形参 :用一个常量作为模板参数,模板内部可以当常量使用。

示例:定义固定大小的静态数组

cpp 复制代码
template<class T, size_t N = 10>
class array {
public:
    T& operator[](size_t index) { 
        return _array[index]; 
    }
    size_t size() const { 
        return N; 
    }
private:
    T _array[N];
};

int main() {
    array<int, 100> arr1; // 大小100的int数组
    array<double> arr2;   // 默认大小10的double数组
    return 0;
}

N 就是非类型模板参数,编译期就能确定数组大小,不用动态开辟。

注意事项:

  1. 浮点数、类对象、字符串不能作为非类型模板参数
  2. 非类型参数必须是编译期就能确定的常量(比如整型、枚举、指针引用等)。

2. 模板特化

2.1 为什么需要特化?

通用模板能处理大多数场景,但遇到特殊类型可能逻辑错误。比如实现一个比较大小的 Less 模板:

cpp 复制代码
#include <iostream>
using namespace std;

class Date
{
public:
	Date(int y = 1, int m = 1, int d = 1)
	{
		_year = y;
		_month = m;
		_day = d;
	}

	// 重载小于运算符,实现日期大小比较
	bool operator<(const Date& date) const
	{
		if (_year != date._year)
			return _year < date._year;
		if (_month != date._month)
			return _month < date._month;
		return _day < date._day;
	}
private:
	int _year, _month, _day;
};
template<class T>
bool Less(T left, T right) {
	return left < right;
}

int main() {
	cout << Less(1, 2) << endl;       // 正确,比较int值

	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less(d1, d2) << endl;     // 正确,比较Date对象

	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl;     // 错误!比较的是指针地址,不是对象内容
	return 0;
}

指针场景下,通用模板不符合预期。这时候就需要模板特化:针对特殊类型,单独写一套特殊实现。

2.2 函数模板特化

特化步骤:

  1. 先有基础函数模板;
  2. template后跟空尖括号<>
  3. 模板函数名后<>中指定要特化的类型;
  4. 形参列表必须和主模板形参类型、个数、顺序完全一致。
cpp 复制代码
// 基础模板
template<class T>
bool Less(T left, T right) { return left < right; }

// 针对Date*的特化版本
template<>
bool Less<Date*>(Date* left, Date* right) {
    return *left < *right; // 解引用比较对象内容
}

调用Less(p1,p2)时,会自动匹配特化 Date* 版本。

函数模板一般不推荐特化,直接写一个对应类型的普通函数更简单直观,可读性更好。

2.3 类模板特化

类模板特化用得更多,分两种:全特化偏特化

全特化

把模板所有参数都确定成具体类型:

cpp 复制代码
#include <iostream>
using namespace std;
// 基础模板
template<class T1, class T2>
class Data {
public:
    Data() { cout << "Data<T1, T2>" << endl; }
private:
    T1 _d1;
    T2 _d2;
};

// 全特化:两个参数都指定为int、char
template<>
class Data<int, char> {
public:
    Data() { cout << "Data<int, char>" << endl; }
private:
    int _d1;
    char _d2;
};

int main() {
    Data<int, int> d1;   // 走基础模板
    Data<int, char> d2;  // 走全特化版本

    return 0;
}
偏特化

对模板参数做进一步限制,不是全部指定。分两种表现形式:

形式 1:部分特化

只特化一部分参数:

cpp 复制代码
// 第二个参数特化为int
template <class T1>
class Data<T1, int> {
public:
    Data() { cout << "Data<T1, int>" << endl; }
private:
    T1 _d1;
    int _d2;
};

形式 2:参数进一步限制

对参数类型加约束,比如特化为指针、引用:

cpp 复制代码
// 两个参数都特化为指针
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;
};

2.4 模板的分离编译

什么是分离编译?

一个项目由多个.cpp源文件组成,每个源文件单独编译生成目标文件,最后链接成可执行文件,这就是分离编译模式。

模板分离编译为什么会报错?

如果把模板声明放 .h,定义放 .cpp,调用时会出现链接错误:

cpp 复制代码
// Add.h
template<class T>
T Add(const T& left, const T& right);

// Add.cpp
#include "Add.h"
template<class T>
T Add(const T& left, const T& right) {
    return left + right;
}

// test.cpp
#include <iostream>
using namespace std;

#include "Add.h"
int main() {
    Add(1, 2);    // 链接报错!
    Add(1.0, 2.0);// 链接报错!

    return 0;
}

原因分析:

  1. 编译阶段:Add.cpp 里只有模板定义,没有实例化调用,编译器不会生成具体的Add<int>Add<double>代码;
  2. 链接阶段:test.cpp 里调用了Add<int>,但找不到对应函数的地址,直接报链接错误。

解决方法:

  1. 推荐做法:把模板的声明和定义放到同一个文件里(比如.hpp 或.h),编译时就能实例化生成代码;
  2. 显式实例化:在定义文件里手动指定实例化类型,不实用,不推荐。

总结

模板是 C++ 泛型编程的核心,也是 STL 模板库的底层基础,后续很多容器模拟都依赖模板实现。

相关推荐
Darkwanderor1 小时前
Linux系统编程实战项目:模拟实现shell
linux·c++
himobrinehacken2 小时前
揭秘Windows程序启动的神秘之旅
c++·安全
lpfasd1232 小时前
编程语言榜单变迁分析
开发语言·后端·scala
名字还没想好☜2 小时前
Go 用 bufio.Scanner 读大文件踩坑:默认 64KB 行上限、Buffer 扩容与按 Token 切分
开发语言·后端·golang·go
库玛西3 小时前
C++ 运行时多态 :核心总结与原理图解
c语言·c++·笔记
Byron Loong3 小时前
【C++】重定向是什么
开发语言·c++
hansang_IR4 小时前
【题解】LC:Z 算法(Z Algorithm)
c++·算法·字符串
techdashen4 小时前
Go设计取舍之六: sync.Mutex正常模式与饥饿模式
开发语言·后端·golang
TunerT_TQ4 小时前
【智能体安全治理|专栏第9期】从“一堆规则”到“数字宪法”:智能体治理的下一个阶段
java·开发语言·安全·开源治理·大模型安全·ai基础设施·智能体安全
Aurorar0rua4 小时前
CS50 x 2024 Notes Algorithms - 07
c语言·开发语言·学习方法