文章目录

C++ 模板:函数模板与类模板的使用

发布于 2026-04-16 18:20:10 · 浏览 13 次 · 评论 0 条

C++ 模板是泛型编程的核心,允许你编写与数据类型无关的代码。这意味着你可以定义一套逻辑,让它同时适用于整数、浮点数甚至自定义对象,而无需重复编写多份相似的代码。本文将带你掌握函数模板与类模板的核心用法。


一、 函数模板:自动适应不同类型的函数

函数模板用于定义一个通用的函数,该函数可以接受多种类型的数据。

1. 定义函数模板

定义一个函数模板,使用 template 关键字,后跟尖括号 <> 包裹的类型参数。

编写以下语法结构:

template <typename T>
T getMax(T a, T b) {
    return (a > b) ? a : b;
}

在这个例子中,T 是一个占位符,代表任何数据类型。typename 关键字也可以用 class 替代,两者在模板定义中含义相同。

2. 隐式实例化:让编译器自动推断类型

当你调用这个模板函数时,编译器会根据传入的实参自动推导 T 的类型。

执行以下调用代码:

int intA = 5, intB = 10;
double doubleA = 5.5, doubleB = 10.5;

// 推导 T 为 int
int result1 = getMax(intA, intB);

// 推导 T 为 double
double result2 = getMax(doubleA, doubleB);

编译器看到 getMax(intA, intB) 时,会自动生成一个处理 int 类型的函数版本;看到 getMax(doubleA, doubleB) 时,则生成处理 double 类型的版本。

3. 显式实例化:手动指定类型

当参数类型复杂或编译器无法自动推断时,你需要指定类型。

使用尖括号在函数名后显式声明类型:

// 强制指定 T 为 double,即便传入的是 int
double result3 = getMax<double>(intA, doubleB);

这种做法会导致 intA 被隐式转换为 double 再进行比较。


二、 类模板:通用类型的容器

类模板允许你定义一个可以存储任何类型数据的类结构,类似于标准库中的 vectorlist

1. 定义类模板

创建一个简单的存储类 Box,它可以存储任意类型的数据。

输入以下代码:

template <class T>
class Box {
private:
    T item;
public:
    // 构造函数
    Box(T t) : item(t) {}

    // 设置值
    void setItem(T t) {
        item = t;
    }

    // 获取值
    T getItem() {
        return item;
    }
};

这里使用了 class T,效果与 typename T 完全一致。

2. 实例化类模板

与函数模板不同,类模板在使用时必须显式指定类型。编译器无法通过构造函数的参数完全推导出类模板的类型参数。

声明对象时指定类型:

// 实例化一个存储 int 的 Box
Box<int> intBox(100);

// 实例化一个存储 std::string 的 Box
Box<std::string> stringBox("Hello Templates");

3. 类模板成员函数的类外定义

如果在类外部定义成员函数,语法会比普通类稍微复杂一些。你需要重复模板声明,并指定作用域。

实现类外的成员函数:

// 必须重新声明 template <class T>
template <class T>
void Box::setItem(T t) {
    item = t;
}

template <class T>
T Box::getItem() {
    return item;
}

注意 Box 后面必须紧跟 <T>,告诉编译器这是类模板 Box 的成员函数,而不是普通类的成员函数。


三、 模板的类型推导与匹配逻辑

理解编译器如何选择模板函数有助于编写更健壮的代码。以下流程描述了当你调用一个重载函数(包含模板和非模板)时,编译器的决策过程。

graph TD A["用户发起函数调用"] --> B{是否存在完全匹配的\n非模板函数?} B -- "是" --> C["调用该非模板函数"] B -- "否" --> D{是否存在可实例化的\n模板函数?} D -- "是" --> E["实例化并调用模板函数"] D -- "否" --> F{是否存在允许\n类型转换的重载函数?} F -- "是" --> G["调用该重载函数"] F -- "否" --> H["编译错误: 无匹配函数"]

如果存在多个匹配的模板,编译器会根据“特化程度”选择最具体的一个。如果优先级相同,编译器将报错“二义性”。


四、 函数模板与类模板的对比

为了加深理解,我们将这两种模板的核心区别进行对比。

特性 函数模板 类模板
类型推导 编译器通常能自动推导参数类型 编译器无法自动推导,必须显式指定 <Type>
实例化关键词 调用时隐式生成或显式指定 func<int>() 声明对象时必须指定 ClassName<int> obj
主要用途 适用于算法逻辑相同,参数类型不同的场景 适用于数据结构相同,存储元素类型不同的场景
成员定义 通常在头文件中直接定义 类内定义可直接写,类外定义需带 template <T>

五、 实战案例:通用的栈

结合上述知识,我们构建一个简单的栈类。它支持压入和弹出任意类型的数据。

编写完整代码:

#include <iostream>
#include <vector>
#include <stdexcept>

template <typename T>
class Stack {
private:
    std::vector elements;
public:
    void push(const T& element) {
        elements.push_back(element);
    }

    T pop() {
        if (elements.empty()) {
            throw std::out_of_range("Stack<>::pop(): empty stack");
        }
        T elem = elements.back();
        elements.pop_back();
        return elem;
    }

    bool isEmpty() const {
        return elements.empty();
    }
};

int main() {
    try {
        // 实例化 int 栈
        Stack<int> intStack;
        intStack.push(10);
        std::cout << "Int Stack Pop: " << intStack.pop() << std::endl;

        // 实例化 string 栈
        Stack<std::string> stringStack;
        stringStack.push("Hello");
        stringStack.push("World");
        std::cout << "String Stack Pop: " << stringStack.pop() << std::endl;
    } catch (const std::exception& ex) {
        std::cerr << "Exception: " << ex.what() << std::endl;
    }
    return 0;
}

编译并运行这段代码。你会看到同一个类结构 Stack 成功处理了 intstd::string 两种完全不同的数据类型。如果在未来需要处理 double 类型,只需修改实例化代码,无需改动类定义。

确保在使用模板时,将声明和定义都放在头文件(.h.hpp)中,或者确保编译器在实例化模板时能看到完整的定义,否则可能会遇到“未定义引用”的链接错误。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文