C++ 学习笔记(五):模板基础——函数模板与类模板
模板是 C++ 的编译期多态机制。写一份代码,编译器根据实际使用的类型自动生成对应版本,运行时没有额外开销。
一、函数模板
写一个交换两个变量的函数,C 里要为每种类型单独写一个:
1 | void swap_int(int *a, int *b) { int t = *a; *a = *b; *b = t; } |
C++ 里用函数模板:
1 | template<typename T> |
typename T 是类型参数,可以写成 class T,含义相同,typename 更直观。
显式指定类型
大多数情况下编译器能推导出 T,但也可以手动指定:
1 | swap<int>(x, y); // 显式指定为 int |
多个类型参数
1 | template<typename T, typename U> |
二、类模板
函数模板解决单个函数的复用,类模板解决整个数据结构的复用。
一个固定容量的环形缓冲区,嵌入式里非常常用:
1 | template<typename T, size_t N> |
使用:
1 | RingBuffer<uint8_t, 64> uart_rx; // 存 uint8_t,容量 64 |
N 是非类型模板参数,编译期确定大小,buf_ 直接在对象内部,不需要动态分配——这是嵌入式里用类模板的最大好处。
三、模板特化
有时候某个特定类型需要不同的实现,可以针对这个类型写特化版本。
1 | // 通用版本 |
调用时编译器会优先选择特化版本:
1 | abs_val(-3); // 用通用版本,T=int |
偏特化
类模板还支持偏特化——只固定部分类型参数:
1 | // 通用版本 |
函数模板不支持偏特化,只能用重载代替。
四、模板的编译期计算
模板不只能参数化类型,也能做编译期计算:
1 | // 编译期计算 2 的 N 次方 |
这种技术叫模板元编程,可以把部分计算从运行时移到编译期,嵌入式里用来生成查找表、CRC 表、三角函数表:
1 | // 编译期生成 CRC32 查找表(实际项目里的用法) |
五、typename 和 class 的区别
在模板参数里两者完全等价:
1 | template<typename T> void func(T x); |
但在模板内部访问嵌套类型时,必须用 typename 消歧义:
1 | template<typename T> |
这是编译器的限制:它不知道 T::iterator 是类型还是变量,typename 明确告诉它是类型。
六、模板在嵌入式里的取舍
模板的好处很明显:零运行时开销,类型安全,代码复用。但也有代价:
代码膨胀:每种类型实例化一份代码。RingBuffer<uint8_t, 64> 和 RingBuffer<uint16_t, 64> 是完全独立的两份代码,Flash 占用翻倍。
编译时间增长:模板在头文件里展开,每个包含头文件的编译单元都要处理。
调试困难:模板报错信息出了名的难看。
实际使用建议:
- 数据结构类(环形缓冲区、队列、栈)非常适合模板——同一份代码服务不同类型,比虚函数版本少一个指针开销
- 驱动抽象层酌情使用,如果类型组合不多,直接写具体类更清晰
- 避免为了”通用”而过度模板化——三个用到的类型,写三个具体类反而更好维护
总结
- 函数模板:同一逻辑适配不同类型,编译器自动推导,也可以显式指定
- 类模板:参数化整个数据结构,非类型参数(如
size_t N)可以编译期确定大小 - 模板特化:针对特定类型提供定制实现
- 模板元编程:把计算从运行时移到编译期
- 代价:代码膨胀、编译时间、报错难读——按需使用,不要过度设计
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Wayson's Blog!
评论




