首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C++进阶】1.C++ 模板进阶

【C++进阶】1.C++ 模板进阶

作者头像
用户11952558
发布2026-01-09 14:10:23
发布2026-01-09 14:10:23
850
举报

模板进阶

一、非类型模板参数

模板不仅能定义类型,还能定义常量。

代码语言:javascript
复制
template<size_t N = 20>
class A
{
private:
    int _a[N];
    int _top;
};

这样就能用模板开对应大小的数组了。

代码语言:javascript
复制
A<3> a1;
A<5> a2;

本质:在编译期就知道 N 是多少,替换进去,生成了两个不同的类。

注意

  1. 模板参数必须在编译期间就能确定结果。
  2. 类对象、字符串不允许做非类型模板参数(double 在 C++20 后支持)。
  3. 可以给缺省值,写法:A<> a3;

二、Array 数组

1. 越界检查

普通数组

  • 越界读:不检查,返回随机值。
代码语言:javascript
复制
int arr2[10];
cout << arr2[11] << endl; // 输出不确定的随机值
  • 越界写:可能被运行时检查工具(如 VS 的调试模式)捕获,报出类似“缓冲区溢出”的错误。

std::array 会在访问时进行边界检查(通常是 assert 或抛出异常),直接阻止越界操作。

2. 开辟空间

vector 相比,vector 在堆上开辟空间,而 array 在栈上。

如何验证?

先验证栈的生长方向(向上):

代码语言:javascript
复制
void fun1() {
    int a = 1;
    cout << &a << endl;
}
int main() {
    int a = 1;
    cout << &a << endl;
    fun1();
    return 0;
}
// 输出地址通常是 main 中的 a > fun1 中的 a,说明栈向下增长(地址减小),此处描述为“上大下小,栈向上建立”有误。但核心是比较地址。

查看 array 元素的地址:

代码语言:javascript
复制
array<int, 10> arr1;
cout << &arr1[1] << " " << &arr1[2] << endl;
// 相邻元素地址连续且递增,结合其地址与局部变量地址比较,可推断其在栈上。

因此,array 创建数组更快,因为它只是栈上的一块连续空间,无需动态内存分配。

三、模板特化

考虑一个比较函数模板:

代码语言:javascript
复制
template<class T>
bool lessfunc(T left, T right) {
    return left < right;
}

对于自定义的 Date 类,如果已经重载了 < 运算符,可以正常工作:

代码语言:javascript
复制
Date d1(2025, 12, 11);
Date d2(2025, 12, 12);
cout << lessfunc(d1, d2) << endl; // 没问题

但是,如果比较的是 Date*(指针):

代码语言:javascript
复制
Date* pd1 = &d1;
Date* pd2 = &d2;
cout << lessfunc(pd1, pd2) << endl; // 比较的是地址,结果不确定

因为代码比较的是地址。对于这种特殊情况,就需要进行特殊处理,即模板特化

代码语言:javascript
复制
// 全特化版本,针对 Date* 类型
template<>
bool lessfunc<Date*>(Date* left, Date* right) {
    return *left < *right;
}

但是,特化有时会很复杂。

传引用的情况

代码语言:javascript
复制
template<class T>
bool lessfunc(const T& left, const T& right) {
    return left < right;
}

特化时,原模板参数变为 const T&,特化 Date* 时需注意指针和引用的结合:

代码语言:javascript
复制
// 错误示例:特化不匹配
// template<> bool lessfunc<Date*>(const Date*& left, const Date*& right) {...}

正确写法是 Date* const &,因为指针本身是常量引用。

代码语言:javascript
复制
template<>
bool lessfunc<Date*>(Date* const & left, Date* const & right) {
    return *left < *right;
}

const Date* 的情况 如果调用时是 const Date*,还需要再特化一个版本:

代码语言:javascript
复制
template<>
bool lessfunc<const Date*>(const Date* const & left, const Date* const & right) {
    return *left < *right;
}

这种写法很复杂,因此模板特化在实际中通常用得较少。

四、类模板特化

当类模板针对不同类型需要不同处理时,也需要特化。

代码语言:javascript
复制
template<class T1, class T2>
class A
{
public:
    A() {
        cout << "普通" << endl;
    }
private:
    T1 _a;
    T2 _b;
};

全特化 先写 template<>,再指定完全具体的类型。

代码语言:javascript
复制
template<>
class A<int, char>
{
public:
    A() {
        cout << "全特化" << endl;
    }
private:
    int _a;
    char _b;
};

这样,A<int, char> a1; 就会调用这个特化版本。

偏特化(部分特化) 只对部分模板参数进行特化,或者对参数特性进行限制(如指针、引用)。

代码语言:javascript
复制
// 第二个参数固定为 char 的偏特化
template<class T1>
class A<T1, char>
{
public:
    A() {
        cout << "半特化" << endl;
    }
private:
    T1 _a;
    char _b;
};

优先级:当一个类型同时匹配全特化和偏特化时,优先匹配全特化,因为更“特化”。

代码语言:javascript
复制
A<int, char> a1;    // 调用全特化
A<long long, char> a2; // 调用偏特化 (T1=long long, T2=char)

针对指针的偏特化(以仿函数 Less 为例) 在使用自定义的优先队列(堆)时,如果元素是指针,比较的默认行为是地址比较,通常不符合预期。

代码语言:javascript
复制
bit::priority_queue<Date*> heap1; // 比较的是地址,顺序随机
bit::priority_queue<Date>  heap2; // 比较的是日期对象,正常

此时,可以特化仿函数 Less

代码语言:javascript
复制
template<class T>
struct Less {
    bool operator()(const T& x, const T& y) {
        return x < y;
    }
};

// 针对所有指针类型的偏特化
template<class T>
struct Less<T*>
{
    bool operator()(T* const & x, T* const & y) {
        return *x < *y; // 先解引用再比较
    }
};

特化里的模板变量 在针对 T* 的特化版本中,模板参数 T 代表的是指针指向的类型,而不是指针本身。

代码语言:javascript
复制
template<class T>
struct Less<T*>
{
    bool operator()(T* const& x, T* const& y) {
        // T* d1 = Date(2025, 1, 5); // 错误: 不能用 Date 初始化 Date*
        T d1 = Date(2025, 1, 5);    // 正确: T 是 Date
        return *x < *y;
    }
};

在特化 Less<T*> 中,TDate,所以 T d1Date 类型。

五、模板连接报错

含模板的类(或函数)不能将声明和定义分离在不同的文件(如 .h.cpp)中,否则会导致链接错误。

原因(编译链接过程简述): 假设有文件:func.h, func.cpp, test.cpp

  1. 预处理:头文件展开,宏替换等。生成 func.i, test.i
  2. 编译:检查语法,生成汇编代码。生成 func.s, test.s
  3. 汇编:汇编代码转二进制机器码(目标文件)。生成 func.o, test.o
  4. 链接:合并目标文件,解析符号,生成可执行程序。

问题所在: 模板本身并不是真正的代码,它只是编译器生成代码的“蓝图”。只有在模板被实例化(即指定了具体类型参数)时,编译器才会根据模板生成具体的代码。 如果模板的声明和定义分离:

  • func.cpp 中,编译器看到模板定义,但因为没有被实例化,它不会为模板生成任何具体的机器指令
  • test.cpp 中,我们 #include "func.h",看到了模板声明,并实例化了模板(如 MyClass<int> obj)。
  • 编译 test.cpp 时,它知道需要 MyClass<int> 的成员函数,但假设这些函数的定义在 func.o 中。
  • 链接时,链接器去 func.o 中寻找 MyClass<int>::myMethod 的地址,func.o 里根本没有这个函数(因为模板定义没被实例化),于是报“未解析的外部符号”错误。

解决方案

推荐:将模板的声明和定义都放在同一个头文件(.hpp.h)中。

使用 显式实例化(不常用,限制多):

代码语言:javascript
复制
// 在 func.cpp 末尾手动告诉编译器需要生成哪些版本
template class MyClass<int>;
template class MyClass<double>;

模板总结

【优点】

  1. 代码复用:模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
  2. 灵活性增强:增强了代码的灵活性,支持泛型编程。

【缺陷】

  1. 代码膨胀:模板会导致代码膨胀问题(针对不同类型生成多份代码),也会导致编译时间变长。
  2. 调试困难:出现模板编译错误时,错误信息非常复杂且冗长,不易定位错误。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 模板进阶
    • 一、非类型模板参数
    • 二、Array 数组
      • 1. 越界检查
      • 2. 开辟空间
    • 三、模板特化
      • 四、类模板特化
      • 五、模板连接报错
    • 模板总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档