C++ -- 模板

发布于:2025-09-10 ⋅ 阅读:(25) ⋅ 点赞:(0)

       //////     欢迎来到 aramae 的博客,愿 Bug 远离,好运常伴!  //////

博主的Gitee地址:阿拉美 (aramae) - Gitee.com

时代不会辜负长期主义者,愿每一个努力的人都能达到理想的彼岸。

  • 1. 非类型模板参数
  • 2. 类模板的特化
  • 3. 模板的分离编译
引言: 本章较前面的模板初阶有内容上的扩充和提升,对模板的理解进一步加深,但总的来说本专题就模板而言属于入门级别,即了解->理解->简单运用,而想对C++模板有更全面深入学习和使用推荐阅读书籍 --《C++ Templates》-- 透彻剖析C++模板特性的强大功能(不建议C++初学者阅读)

基本回顾:什么是模板?为什么需要模板?

模板,顾名思义就是给一个样板,从而实现规模化生产;那么这里也是如此,C++ 模板是一种强大的特性,它允许你编写通用的代码,可以适用于多种数据类型,而不必为每种类型重复编写相似的代码。

假设你需要一个函数来比较两个数的大小,但数据类型可能是 intfloatdouble 等。没有模板时,你不得不为每种类型重写一个函数:

int max(int a, int b) { return a > b ? a : b; }
double max(double a,double b){ return a > b ? a : b; }
float max(float a, float b) { return a > b ? a : b; }
//非常繁琐。。。

那么,这个时候C++模板就给我们提供了便利:(下面的代码我们就只需要写一遍就达到了上面的效果)

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

 模板概念:

C++模板主要分为函数模板类模板两大类,但除此之外还有几种相关的模板类型。(这里主要涉及到函数模板和类模板)

模板不是直接可执行的代码,而是代码生成的蓝图。编译器在遇到具体类型的使用时,会根据模板生成对应类型的具体代码(实例化)。

模板分为两类:

  • 函数模板:生成通用函数
  • 类模板:生成通用类
1. 函数模板  
// 关键字template 开始,后面跟着模板参数列表
template <typename T> // 也可以用 'class' 关键字:template <class T>
T myMax(T a, T b) {
    return (a > b) ? a : b;
}
  • template: 声明开始一个模板。

  • <typename T>模板参数列表typename 关键字表明 T 是一个类型参数。你可以使用任何标识符(如 MyType),但 T 是惯例。

  • T: 一个占位符类型,在函数被调用时会被真正的类型(如 intstring)替换。

使用函数模板,编译器会根据你调用函数时传入的参数类型来实例化(Instantiate)模板,即生成特定类型的函数。

int main() {
    // 调用方式 1:显式指定类型
    std::cout << myMax<int>(3, 7) << std::endl;   // T 被实例化为 int
    std::cout << myMax<double>(3.14, 2.99) << std::endl; // T 被实例化为 double

    // 调用方式 2:隐式推导(更常用)
    // 编译器根据传入的 3 和 7 推断出 T 是 int
    std::cout << myMax(3, 7) << std::endl;

    // 编译器根据传入的 'c' 和 'd' 推断出 T 是 char
    std::cout << myMax('c', 'd') << std::endl;

    std::string s1 = "hello", s2 = "world";
    // 编译器推断出 T 是 std::string
    std::cout << myMax(s1, s2) << std::endl; // 比较结果是 "world" (按字典序)

    return 0;
}

多类型参数模板可以接受多个类型参数。


template <typename T1 ,typename T2>
void func(const T1& a, const T2& b )
{ cout << a <<" "<< b << endl; }


int main()
{
     func(1,2); // 1 2
	 func(42, "Answer");// 42 Answer

	return 0;
}
2. 类模板  

类模板允许你定义能够处理不同数据类型的类,如标准库的 std::vectorstd::liststd::map 等都是类模板。 

// 一个简单的栈类模板
template <typename T>
class Stack {
private:
    std::vector<T> elems; // 使用 vector 作为底层容器

public:
    void push(const T& elem);
    void pop();
    const T& top() const;
    bool empty() const { return elems.empty(); }
};

// 在类外定义成员函数时,每个函数前都要加上模板声明
template <typename T>
void Stack<T>::push(const T& elem) {
    elems.push_back(elem);
}

template <typename T>
void Stack<T>::pop() {
    if (empty()) {
        throw std::out_of_range("Stack<>::pop(): empty stack");
    }
    elems.pop_back();
}

template <typename T>
const T& Stack<T>::top() const {
    if (empty()) {
        throw std::out_of_range("Stack<>::top(): empty stack");
    }
    return elems.back();
}

使用类模板时,必须显式指定模板参数,因为编译器无法像函数模板那样从构造函数参数中推导出类模板的类型。

int main() {
    Stack<int> intStack;       // 一个存储 int 的栈
    Stack<std::string> strStack; // 一个存储 string 的栈

    intStack.push(7);
    std::cout << intStack.top() << std::endl;

    strStack.push("hello");
    std::cout << strStack.top() << std::endl;
    strStack.pop();

    return 0;
}

非类型模板参数:

模板参数不仅可以是要推断的类型,也可以是具体的值(如整数、枚举、指针或引用)

注意:
  • 1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
  • 2. 非类型的模板参数必须在编译期就能确认结果。
即: 非类型参数必须是编译期常量。
// 功能:创建一个固定大小的数组
template <typename T, int Size>
class FixedArray {
public:
    T arr[Size];
    int getSize() const { return Size; }
};

int main() {
    FixedArray<int, 10> myArray; // Size 被实例化为 10
    std::cout << myArray.getSize(); // 输出 10

    // FixedArray<double, 20> anotherArray;
    return 0;
}

模板的特化:

有时,对于特定的类型,通用的模板实现可能不是最优的,甚至可能是错误的。这时你可以为特定类型提供一个特殊的实现,这就是特化

1. 函数模板特化(较少使用)
函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字 template 后面接一对空的尖括号 <>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
// 通用模板
template <typename T>
bool isEqual(T a, T b) {
    return a == b;
}

// 为 const char* 类型提供特化版本
// 比较字符串内容,而不是指针地址
template <>
bool isEqual<const char*>(const char* a, const char* b) {
    return strcmp(a, b) == 0;
}

int main() {
    // 使用通用版本,比较指针地址,很可能返回 false
    std::cout << isEqual("hello", "hello") << std::endl; // 输出 0 (false)!危险!

    // 使用特化版本,比较字符串内容,返回 true
    std::cout << isEqual<const char*>("hello", "hello") << std::endl; // 输出 1 (true)
}

注意:现代 C++ 更推荐使用函数重载来代替函数模板特化,因为重载的规则更直观。

2. 类模板特化(更常用)
  • 全特化 :全特化即是将模板参数列表中所有的参数都确定化。
  • 偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。

参数更进一步的限制: 偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。

#include <iostream>

// 主模板
template <typename T>
class TypeInfo {
public:
    static const char* name() { return "Unknown type"; }
};

// 完全特化
template <>
class TypeInfo<int> {
public:
    static const char* name() { return "int"; }
};

template <>
class TypeInfo<double> {
public:
    static const char* name() { return "double"; }
};

// 偏特化 - 针对指针类型
template <typename T>
class TypeInfo<T*> {
public:
    static const char* name() { 
        static std::string name = std::string("pointer to ") + TypeInfo<T>::name();
        return name.c_str();
    }
};

特化的优先级规则:

当存在多个特化版本时,编译器会选择 最匹配 的版本,优先级从高到低:

  1. 全特化版本(完全匹配所有参数)
  2. 部分特化版本(匹配部分参数,且匹配度更高)
  3. 通用模板版本(无特化版本匹配时)

模板的分离编译:

“分离编译” 是指将代码分为 声明(.h 头文件) 和 定义(.cpp 源文件),分别编译后链接。但模板的 “延迟实例化” 特性导致传统分离编译失效,需特殊处理。

1. 为什么模板不能直接分离编译?

C++ 模板的核心特性是 延迟实例化:编译器只有在 “看到模板的具体使用” 时(如 Stack<int> s),才会根据模板生成对应类型的代码(实例化)。

传统分离编译的问题:

  • 头文件(.h):只包含模板的声明(如 template <typename T> class Stack;)。
  • 源文件(.cpp):包含模板的定义,但编译器在编译 .cpp 时,没有 “具体的使用场景”,无法触发实例化,因此不会生成任何模板实例的代码。
  • 链接阶段:其他文件(如 main.cpp)使用 Stack<int> 时,编译器会触发实例化,但此时模板的定义在 .cpp 中(已编译为目标文件,但未生成 Stack<int> 的代码),导致 链接错误(undefined reference)

2. 解决方案:3 种可行的编译方式
方案 1:将模板定义放在头文件中(推荐)

最常用的方式:将模板的 声明和定义都放在头文件(.h 或 .hpp)中。当其他文件包含头文件时,编译器能同时看到声明和定义,在使用时直接实例化,避免链接错误。

// Stack.hpp(头文件,包含声明和定义)
#ifndef STACK_HPP
#define STACK_HPP

template <typename T>
class Stack {
private:
    T* m_data;
    int m_top;
    int m_capacity;
public:
    Stack(int capacity);
    ~Stack();
    void push(const T& value);
    T pop();
    bool isEmpty() const;
};

// 模板定义(直接放在头文件中)
template <typename T>
Stack<T>::Stack(int capacity) : m_capacity(capacity), m_top(-1) {
    m_data = new T[m_capacity];
}

template <typename T>
Stack<T>::~Stack() {
    delete[] m_data;
}

template <typename T>
void Stack<T>::push(const T& value) {
    if (m_top < m_capacity - 1) {
        m_data[++m_top] = value;
    }
}

template <typename T>
T Stack<T>::pop() {
    if (!isEmpty()) {
        return m_data[m_top--];
    }
    throw "Stack underflow";
}

template <typename T>
bool Stack<T>::isEmpty() const {
    return m_top == -1;
}

#endif // STACK_HPP

使用时直接包含头文件:

// main.cpp
#include "Stack.hpp"
int main() {
    Stack<int> s(5);
    s.push(10);
    return 0;
}
方案 2:显式实例化(Explicit Instantiation)

在模板定义所在的源文件中,显式指定需要实例化的类型,编译器会提前生成这些类型的代码,避免链接错误。

  1. 头文件(Stack.h):只包含模板声明

    // Stack.h
    #ifndef STACK_H
    #define STACK_H
    
    template <typename T>
    class Stack {
    private:
        T* m_data;
        int m_top;
        int m_capacity;
    public:
        Stack(int capacity);
        ~Stack();
        void push(const T& value);
        T pop();
        bool isEmpty() const;
    };
    
    #endif // STACK_H
    
  2. 源文件(Stack.cpp):包含模板定义 + 显式实例化

    // Stack.cpp
    #include "Stack.h"
    
    // 模板定义
    template <typename T>
    Stack<T>::Stack(int capacity) : m_capacity(capacity), m_top(-1) {
        m_data = new T[m_capacity];
    }
    
    template <typename T>
    Stack<T>::~Stack() {
        delete[] m_data;
    }
    
    // ... 其他成员函数定义 ...
    
    // 显式实例化:指定需要生成的类型(如 int、double)
    template class Stack<int>;    // 生成 Stack<int> 的所有代码
    template class Stack<double>; // 生成 Stack<double> 的所有代码
    

  3. 主文件(main.cpp):包含头文件并使用

    // main.cpp
    #include "Stack.h"
    int main() {
        Stack<int> s1(5);    // 已显式实例化,正常使用
        Stack<double> s2(5); // 已显式实例化,正常使用
        // Stack<std::string> s3(5); // 错误!未显式实例化,链接失败
        return 0;
    }
方案 3:使用 “模板实现文件”(.tpp)

将模板定义放在单独的 “模板实现文件”(如 .tpp)中,然后在头文件的末尾包含该 .tpp 文件,本质上是 “将定义拆分到单独文件,但仍通过头文件暴露给编译器”。

  1. 头文件(Stack.h):声明 + 包含 .tpp

    // Stack.h
    #ifndef STACK_H
    #define STACK_H
    
    template <typename T>
    class Stack {
    // ... 成员声明 ...
    };
    
    // 包含模板实现文件(必须在类声明之后)
    #include "Stack.tpp"
    
    #endif // STACK_H
    

  2. 模板实现文件(Stack.tpp):包含模板定义

    // Stack.tpp(注意:不包含头文件保护,因为只被 Stack.h 包含一次)
    template <typename T
【分离编译扩展阅读】 http://blog.csdn.net/pongba/article/details/19130

模板总结:

【优点】

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

【缺陷】

  • 1. 模板会导致代码膨胀问题,也会导致编译时间变长
  • 2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误

结语:感谢相遇

/// 高山仰止,景行行止。虽不能至,心向往之 ///


网站公告

今日签到

点亮在社区的每一天
去签到