重生之从0开始学习c++之模板初级

1. 泛型编程 ------ 为什么需要模板?

如何实现一个通用的交换函数呢?

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;
}

因为c++支持函数重载,所以如果我们想用不同类型的参数,是不是可以这么写啊,但是这样写是不是有点麻烦和冗余啊,因为它们的逻辑完全相同,仅仅是类型不同。

那能否告诉编译器一个模子 ,让编译器根据不同的类型利用该模子来生成代码呢?

就像这样:

cpp 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     模具 (模板)                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                                                     │   │
│  │         void Swap( T& left, T& right )             │   │
│  │         {                                           │   │
│  │             T temp = left;                          │   │
│  │             left = right;                           │   │
│  │             right = temp;                           │   │
│  │         }                                           │   │
│  │                                                     │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
        │                    │                    │
        ▼ 倒入绿色液体        ▼ 倒入蓝色液体        ▼ 倒入红色液体
   ┌─────────┐          ┌─────────┐          ┌─────────┐
   │ Swap    │          │ Swap    │          │ Swap    │
   │ <int>   │          │ <double>│          │ <char>  │
   └─────────┘          └─────────┘          └─────────┘

如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的铸件(即生成具体类型的代码),那将会节省许多头发。巧的是前人早已将树栽好,我们只需在此乘凉。

核心思想:泛型编程------编写与类型无关的通用代码,由编译器根据实际使用时的类型,自动生成针对该类型的代码。

2. 函数模板

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

2.1 函数模板的语法

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

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

cpp 复制代码
template<typename T>   // T 是模板参数,可以是 typename 或 class
void Swap(T& left, T& right) {
    T temp = left;
    left = right;
    right = temp;
}

2.2 函数模板的原理

函数模板本身不是函数,它只是一个蓝图。编译器遇到函数模板的调用时,才会根据实参类型,生成一个具体的函数。这个过程叫做模板实例化。

流程图解:编译器在编译期的推演过程

cpp 复制代码
源代码:
─────────────────────────────────────────────────────────────
int main()
{
    double d1 = 2.0, d2 = 5.0;
    Swap(d1, d2);           //  调用点1

    int i1 = 10, i2 = 20;
    Swap(i1, i2);           //  调用点2

    char a = '0', b = '9';
    Swap(a, b);             //  调用点3
}
─────────────────────────────────────────────────────────────
        │                    │                    │
        ▼                    ▼                    ▼
┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
│ 编译器推演:    │  │ 编译器推演:    │  │ 编译器推演:    │
│ 实参类型 double │  │ 实参类型 int    │  │ 实参类型 char   │
│ 推导 T = double │  │ 推导 T = int    │  │ 推导 T = char   │
└─────────────────┘  └─────────────────┘  └─────────────────┘
        │                    │                    │
        ▼                    ▼                    ▼
┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
│ 生成的函数:    │  │ 生成的函数:    │  │ 生成的函数:    │
│ void Swap(      │  │ void Swap(      │  │ void Swap(      │
│   double& left, │  │   int& left,    │  │   char& left,   │
│   double& right │  │   int& right)   │  │   char& right)  │
│ ) { ... }       │  │ { ... }         │  │ { ... }         │
└─────────────────┘  └─────────────────┘  └─────────────────┘

编译器为每种不同的类型组合,生成一份独立的函数代码。最终的可执行文件中,包含了 Swap<double>Swap<int>Swap<char> 三个具体的函数,就像你手动写了三个重载一样。

2.3 函数模板的实例化

用具体类型使用函数模板,称为实例化。分为两种:

  1. 隐式实例化

让编译器自动根据实参类型推导模板参数。

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

int main() {
    Add(1, 2);      //  两个实参都是 int,推导 T = int
    Add(1.0, 2.0);  //  两个实参都是 double,推导 T = double
    // Add(1, 2.0); //  编译错误!一个 int,一个 double,编译器推导冲突
}

错误图解:

cpp 复制代码
调用 Add(1, 2.0):
        │
        ├─ 实参1 (1) 类型为 int    → 推导 T = int
        ├─ 实参2 (2.0) 类型为 double → 推导 T = double
        │
        └─ 冲突!模板参数列表中只有一个 T,编译器无法确定 T 到底是 int 还是 double。
           编译器不会进行隐式类型转换,因为转换可能丢失数据,编译器不背这个锅。

解决方案:

cpp 复制代码
Add(a, (int)d);          // 方案1:用户手动强转
Add<int>(a, d);          // 方案2:显式实例化(推荐)
  1. 显式实例化

在函数名后用 <类型> 强制指定模板参数。

cpp 复制代码
Add<int>(10, 20.0);   // 强制 T = int,20.0 会被隐式转换为 int

流程图:

cpp 复制代码
调用 Add<int>(a, b):
        │
        ▼
┌─────────────────────────────────────────┐
│ 编译器:用户已指定 T = int,不用推导了  │
│ 实参 a (int) → 匹配                     │
│ 实参 b (double) → 尝试隐式转换为 int    │
│ (如果能转就编译通过,否则报错)        │
└─────────────────────────────────────────┘
        │
        ▼
   生成函数:int Add(const int& left, const int& right)

2.4 模板参数的匹配原则

原则一:非模板函数可以和同名模板函数共存

cpp 复制代码
// 非模板函数(专门处理 int)
int Add(int left, int right)
 {
  	return left + right; 
 }

// 模板函数(通用版本)
template<class T> 
T Add(T left, T right)
 {
 	 return left + right; 
 }

原则二:优先调用非模板函数,除非模板能生成更好的匹配

cpp 复制代码
Add(1, 2);        // 调用非模板函数(完全匹配,且非模板优先)
Add<int>(1, 2);   // 强制调用模板实例化的版本
Add(1, 2.0);      // 非模板不匹配(参数类型不同),模板可以生成更好的匹配(如果模板有两个参数)

决策流程图:

cpp 复制代码
遇到函数调用 Add(1, 2):
        │
        ├─ 查找同名非模板函数 → 找到 int Add(int, int) → 完全匹配 → 调用 
        │
        └─ 即使模板能生成完全相同的函数,也不考虑,非模板优先

遇到函数调用 Add(1, 2.0):
        │
        ├─ 查找同名非模板函数 → int Add(int, int) 不匹配(第二个参数类型不对)
        │
        └─ 查找模板 → 若有 template<class T1, class T2> 版本,可生成匹配函数 → 调用模板实例化版本

原则三:模板函数不允许自动类型转换,普通函数可以

cpp 复制代码
void func(int x, int y) { }   // 普通函数

template<class T>
void func(T x, T y) { }       // 模板函数

func(1, 'a');   // 调用普通函数:'a' 自动转换为 int(ASCII 97)
                // 模板函数不会考虑,因为 char 和 int 推导冲突

3. 类模板

3.1 为什么需要类模板?

以 Stack(栈)为例,我们需要存储 int 的栈,也需要存储 double、string 的栈。如果不用模板,要么为每种类型写一个类,要么用 void* 或继承(不类型安全)。

类模板就是类的模具。

3.2 类模板的定义格式

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

我们来举一个我们很熟练的栈的例子:

cpp 复制代码
template <class Type>
class Stack
{
public:
	Stack(int capacity = 4)
		:_arr(new Type[capacity])
		,_size(0)
		,_capacity(capacity)
	{

	}
	~Stack()
	{
		delete[] _arr;
		_arr = nullptr;
		_size = _capacity = 0;

	}
	void Push(const Type& x)
	{
		if (_capacity == _size)
		{
			Type* tmp = new Type[_capacity*2];
			memcpy(tmp, _arr, sizeof(Type) * _size);
			delete[] _arr;
			_arr = tmp;
			_capacity = _capacity * 2;
		}
		_arr[_size++] = x;

	}
	void Print() const {
		for (int i = 0; i < _size; ++i)
		{
			cout << _arr[i] << " ";
		}
		
	}

private:
	Type* _arr;
	int _size;
	int _capacity;
};

int main()
{
	Stack<int> st1;
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);
	st1.Push(4);
	st1.Print();

	return 0;
}

关键点:

  • 类模板名字是 Stack,但真正的类型是 Stack<int>Stack<double> 等。
  • 成员函数在类外定义时,必须写成 template<class T> void Stack<T>::Push(...)
  • 模板的声明和定义通常不分离到 .h 和 .cpp 两个文件,否则会导致链接错误(原因涉及模板实例化的编译模型,后面会深入)。

3.3 类模板的实例化

类模板必须显式实例化(不能像函数模板那样隐式推导)。

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

cpp 复制代码
Stack<int> st1;      // 实例化出一个存储 int 的栈类
Stack<double> st2;   // 实例化出一个存储 double 的栈类
cpp 复制代码
程序代码段(编译后):
┌───────────────────────────────────────────────────────────┐
│ Stack<int> 类(编译器生成)                               │
│  - _array: int*                                           │
│  - Push(const int&)                                       │
├───────────────────────────────────────────────────────────┤
│ Stack<double> 类(编译器生成)                            │
│  - _array: double*                                        │
│  - Push(const double&)                                    │
└───────────────────────────────────────────────────────────┘
        ↑                           ↑
        │                           │
    使用 Stack<int> st1        使用 Stack<double> st2
相关推荐
nashane2 小时前
HarmonyOS 6学习:解决异步场景下Toast提示框无法弹出的UI上下文丢失问题
学习·ui·harmonyos·harmony app
历程里程碑2 小时前
2. Git版本回退全攻略:轻松掌握代码时光机
大数据·c++·git·elasticsearch·搜索引擎·github·全文检索
极客智造2 小时前
深度解析 C++ 类继承与多态:面向对象编程的核心
c++
码喽7号5 小时前
Vue学习七:MockJs前端数据模拟
前端·vue.js·学习
零号全栈寒江独钓5 小时前
基于c/c++实现linux/windows跨平台获取ntp网络时间戳
linux·c语言·c++·windows
CSCN新手听安5 小时前
【linux】高级IO,以ET模式运行的epoll版本的TCP服务器实现reactor反应堆
linux·运维·服务器·c++·高级io·epoll·reactor反应堆
三品吉他手会点灯5 小时前
STM32F103 学习笔记-21-串口通信(第4节)—串口发送和接收代码讲解(中)
笔记·stm32·单片机·嵌入式硬件·学习
松☆7 小时前
C++ 算法竞赛题解:P13569 [CCPC 2024 重庆站] osu!mania —— 浮点数精度陷阱与 `eps` 的深度解析
开发语言·c++·算法
(Charon)7 小时前
【C++/Qt】C++/Qt 实现 TCP Server:支持启动监听、消息收发、日志保存
c++·qt·tcp/ip