06 模板初阶

97 阅读13分钟

C++ 模板概述

C++模板详解 —— 函数模板与类模板 | CSDN

在 C++ 中,模板(Template)是一个非常强大的特性,它可以让我们编写与特定数据类型无关的代码,最终由编译器根据实际的类型生成特定的代码。模板主要分为两类:函数模板类模板。模板的引入大大增强了 C++ 语言的灵活性和代码复用性,减少了重复代码的编写。

1. 为什么需要模板?

假设你想编写一个通用的函数来交换两个变量的值。最初,你可能会想到通过函数重载来实现,例如:

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

虽然使用函数重载可以满足不同数据类型的需求,但这种方法存在一些问题:

  1. 代码重复:每增加一个新的类型(例如 floatlong),都需要为其编写一个新的 Swap 函数。这使得代码复用性差。
  2. 可维护性差:如果 Swap 函数的实现出现错误,可能需要修改每个重载版本,这会导致错误传播并增加维护的复杂度。
  3. 扩展性差:当需要处理的新类型增多时,维护这些重载函数的工作量也随之增加。

如何解决这些问题?

可以通过 函数模板 来解决这些问题。模板可以让你编写通用的函数或类,这些函数或类在编译时根据实际使用的类型生成特定的版本。

2. 函数模板

函数模板 允许你定义一个蓝图或框架,它并不依赖于某种特定的数据类型,而是通过在编译时根据传入的类型生成具体类型的函数。这样你就能够编写一个函数来处理所有类型的参数,而无需手动为每个类型编写不同的函数。

2.1 函数模板的定义

一个典型的函数模板定义如下:

template<typename T>
void Swap(T& left, T& right)
{
    T temp = left;
    left = right;
    right = temp;
}
  • template<typename T>:声明了一个模板,T 是一个类型参数,表示可以接受任何数据类型,T 是自己命名的,可以是 TYA 等等(同时也可以使用 template <class T>二者是等价的,这里先知道可以使用 class,至于为什么,以后再讨论)。
  • void Swap(T& left, T& right):这是模板函数的定义,T 会根据调用时传入的参数类型来具体化。

这段代码可以交换任意类型的两个变量,不需要为每种类型单独写一个函数。

2.2 模板的使用

当我们使用模板时,编译器会根据传递给模板的实际类型来生成相应的代码。

对于不同的类型(如 `int`、`double`),编译器会自动根据传入的类型生成不同版本的 `Swap` 函数,而不需要手动为每个类型编写不同的函数。
#include <iostream>
using namespace std;

// 泛型 Swap 函数,接受任意类型的引用参数
template <typename T>
void Swap(T& left, T& right)
{
    T temp = left;
    left = right;
    right = temp;
}

int main()
{
    int a = 5, b = 10;
    Swap(a, b);  // 使用 int 类型生成 Swap(int& left, int& right)
    cout << "a = " << a << ", b = " << b << endl;

    double x = 1.1, y = 2.2;
    Swap(x, y);  // 使用 double 类型生成 Swap(double& left, double& right)
    cout << "x = " << x << ", y = " << y << endl;

    return 0;
}

多个参数的模板函数(多类型支持): 模板函数可以支持多个模板参数类型,通过模板参数列表可以定义多个不同类型的参数。

#include <iostream>
using namespace std;

template<typename T1, typename T2>
void print(const T1& left, const T2& right)
{
    cout << left << " " << right << endl;
}

int main()
{
    print(1, 2);         // 整型参数
    print(1, 2.2);       // 整型和浮点型参数
    print("Hello", 2.2); // 字符串和浮点型参数

    return 0;
}

模板函数推导类型(自动推导):

#include <iostream>
using namespace std;

template<class T>
T Add(const T& left, const T& right)
{
    return left + right;
}

int main()
{
    int a1 = 10, a2 = 20;
    double d1 = 10.1, d2 = 20.2;

    // 编译器自动推导类型
    cout << Add(a1, a2) << endl;       // 使用 int 类型
    cout << Add(d1, d2) << endl;       // 使用 double 类型
    cout << Add(a1, (int)d1) << endl;  // 混合类型时自动推导为 int 类型
    cout << Add((double)a1, d1) << endl; // 混合类型时自动推导为 double 类型

    return 0;
}

显式指定模板类型: 有时需要显式指定模板类型,尤其是在类型推导无法正确推导时。

// 当参数类型不一致时,必须显式指定或强制类型转换
Add(10, 20.5);          // 错误:T 无法推导为两种不同类型
Add<int>(10, 20.5);     // 正确:T 显式指定为 int,第二个参数隐式转换为 int
Add<double>(10, 20.5);  // 正确:T 显式指定为 double,第一个参数隐式转换为 double
#include <iostream>
using namespace std;

template<class T>
T Add(const T& left, const T& right)
{
    return left + right;
}

int main()
{
    int a1 = 10;
    double d1 = 10.1;

    // 显式转换 int 类型进行计算
    cout << Add(a1, (int)d1) << endl;  		// 将 double 类型 d1 显式转换为 int 类型进行加法运算
    // 显式转换 double 类型进行计算
    cout << Add((double)a1, d1) << endl;    // 将 int 类型 a1 显式转换为 double 类型进行加法运算
    // 显式指定模板类型为 int,进行计算
    cout << Add<int>(a1, d1) << endl;    	// 显式指定 Add 模板为 int 类型,但会进行隐式转换,最终结果为 int 类型

    return 0;
}

模板函数返回指针(返回类型为指针): 模板函数不仅能返回基本类型的值,还可以返回指针类型(如动态分配内存时)。

#include <iostream>
using namespace std;

template<class T>
T* Alloc(int n)
{
    return new T[n];                        // 动态分配数组
}

int main()
{
    // 使用模板函数分配数组
    double* p1 = Alloc<double>(10);         // 分配 10 个 double 类型的元素

    // 可以在这里操作 p1 数组,记得使用完后 delete[] 释放内存
    delete[] p1;                            // 释放动态分配的内存

    return 0;
}

混合类型进行计算(加法操作等): 当我们进行加法操作时,如果参数类型不同,C++ 可以通过类型转换处理不同类型之间的计算。

#include <iostream>
using namespace std;

template<class T>
T Add(const T& left, const T& right)
{
    return left + right;
}

int main()
{
    int a1 = 10;
    double d1 = 10.1;

    // 混合类型计算
    cout << Add(a1, (int)d1) << endl; // 转换为 int 类型后进行加法
    cout << Add((double)a1, d1) << endl; // 转换为 double 类型后进行加法

    return 0;
}

2.3 隐式实例化与显式实例化

  • 隐式实例化:编译器根据你传入的实参类型自动推导出模板参数的具体类型。例如,Swap(a, b) 会自动推导出 Tint,生成一个 Swap(int& left, int& right) 的函数。
  • 显式实例化:你也可以显式地指定模板参数类型。例如:
// 显式实例化的典型场景(在源文件中使用)
template void Swap<int>(int&, int&);      			// 生成 int 版本
template void Swap<double>(double&, double&); 		// 生成 double 版本

这种方式可以避免编译器自动推导模板参数的类型,而是由你明确指定。

2.4 模板的匹配与优先级

C++ 在选择函数时有一套匹配规则:

函数模板与非模板函数共存:当你既有非模板函数又有模板函数时,C++ 编译器会根据传入参数的类型优先选择非模板函数。例如:

void Swap(int& left, int& right);  // 非模板函数
template<typename T>
void Swap(T& left, T& right);  // 模板函数

当你传入 int 类型时,编译器会优先选择 Swap(int& left, int& right)

模板函数匹配规则:如果模板能够提供更好的匹配,编译器会优先选择模板函数。模板函数通常会考虑类型转换,而非模板函数则不会进行自动类型转换。

3. 模板的优势

  • 提高代码复用性: 使用函数模板的最大好处是可以大大减少重复代码。例如,使用模板函数可以避免为每个数据类型编写多次相同的代码,只需要编写一次模板函数,编译器会根据不同的类型实例化出对应的代码。
  • 代码的可维护性: 当你需要修改 Swap 函数的实现时,只需要修改模板函数本身。所有使用了这个模板的地方,编译器会自动更新生成的代码,这大大提高了代码的可维护性。
  • 自动类型推导: 模板能够通过类型推导来决定类型,使得调用者不必显式指定类型。这不仅简化了代码,还减少了出错的可能性。
  • 避免错误: 模板的另一个好处是,可以避免因手动编写多个重载函数而导致的错误。例如,模板函数不会被错误地应用于不兼容的类型,而编译器会在编译时进行类型检查,确保类型安全。

4. 总结

C++中的模板是一种非常强大的特性,它能够实现与类型无关的通用代码,从而提高代码复用性、可维护性和扩展性。模板函数尤其适合用于处理不同类型但逻辑相同的代码,它允许编写一个蓝图或模具,编译器根据实际传入的类型生成具体的函数或类实例。通过模板,程序员可以避免冗余代码的编写,减少出错的可能,同时保持代码的简洁和高效。


类模板:提高代码复用性与灵活性

在 C++ 中,类模板是一个强大的工具,它允许我们编写通用的类,而不依赖于特定的类型。通过类模板,我们可以提高代码的复用性,减少冗余代码,并且使得代码更加灵活易于扩展。

代码重构:从重复代码到通用模板

在实际开发中,我们经常遇到需要为不同数据类型编写类似代码的情况。假设我们需要实现一个栈类,并且这个栈类要处理多种数据类型,比如 intdouble 类型。最初的做法可能是为每种数据类型写一个新的类:

// int 类型栈
class StackInt
{
public:
    StackInt(size_t capacity = 3) {
        _array = (int*)malloc(sizeof(int) * capacity);
        _capacity = capacity;
        _size = 0;
    }

    void Push(int data)
    {
        _array[_size] = data;
        _size++;
    }

    ~StackInt()
    {
        free(_array);
    }

private:
    int* _array;
    int _capacity;
    int _size;
};

// double 类型栈
class StackDouble
{
public:
    StackDouble(size_t capacity = 3)
    {
        _array = (double*)malloc(sizeof(double) * capacity);
        _capacity = capacity;
        _size = 0;
    }

    void Push(double data)
    {
        _array[_size] = data;
        _size++;
    }

    ~StackDouble()
    {
        free(_array);
    }

private:
    double* _array;
    int _capacity;
    int _size;
};

问题分析:

虽然 StackIntStackDouble 做的工作基本相同,唯一的区别是它们处理的类型不同(intdouble)。但是,这种做法导致了冗余的代码。每增加一个新的数据类型,我们都需要为其编写一个类似的类,这样代码就会变得越来越冗长,维护起来也变得更为困难。


类模板的引入:

为了避免这种冗余代码,并提高代码的复用性,我们可以使用 C++ 的类模板来解决这个问题。类模板允许我们为不同的数据类型生成相同功能的类,而无需重复编写相似的代码。

通过类模板,我们可以将 StackIntStackDouble 合并为一个通用的栈类 Stack<T>,其中 T 代表数据类型。通过实例化类模板,编译器会根据我们提供的类型自动生成不同版本的栈类。

下面是通过类模板重构后的栈类代码:

template<class T>  // 定义一个类模板,T 是类型参数
class Stack
{
public:
    Stack(size_t capacity = 3) : _capacity(capacity), _size(0)
    {
        _array = new T[capacity];   // 使用 T 类型动态分配内存
    }

    void Push(const T& data)
    {
        _array[_size++] = data;  	// 将数据压入栈
    }

    ~Stack()
    {
        delete[] _array;  			// 释放内存
    }

private:
    T* _array;  					// 存储数据的数组,类型由模板参数决定
    int _capacity;  				// 栈的最大容量
    int _size;  					// 栈当前的元素个数
};

通过类模板,我们可以在 `main` 函数中实例化栈类,指定具体的类型:
int main()
{
    Stack<int> s1;    // 创建一个存储 int 类型的栈
    Stack<double> s2; // 创建一个存储 double 类型的栈

    s1.Push(10);      // 向 int 类型栈中压入整数
    s2.Push(3.14);    // 向 double 类型栈中压入浮点数

    return 0;
}

代码解析:

  1. 模板定义template<class T> 定义了一个类模板,其中 T 是类型参数,表示栈中元素的类型。在实例化时,T 会被替换为具体的类型。
  2. 通用数据类型:类模板中的 _array 被定义为 T*,这使得我们可以使用任意类型的数据。无论是 intdouble 还是其他自定义类型,类模板都会根据我们指定的类型生成相应的栈类。
  3. 内存分配:在构造函数中,我们使用 new T[capacity] 来动态分配内存,T 会根据实际类型决定内存的大小。例如,当 Tint 时,会分配一个 int 类型的数组;当 Tdouble 时,会分配一个 double 类型的数组。
  4. 入栈操作Push 方法接受一个 const T& 类型的参数,允许我们将任何类型的数据压入栈中。
  5. 析构函数:使用 delete[] 来释放栈的内存,避免内存泄漏。

在上面的代码中,我们通过指定不同的模板参数(intdouble)来创建不同类型的栈实例。每个栈都具有相同的功能,但能够处理不同的数据类型。


类模板的优势

  • 提高代码复用性:类模板允许我们编写一次通用代码,而不需要为每种数据类型编写独立的类。只需定义一个模板,编译器会根据实际使用的类型自动生成不同类型的类。这使得代码更加简洁并减少了冗余。
  • 更加灵活:类模板的一个显著优点是它的灵活性。你可以根据需要创建不同类型的栈(或其他数据结构)。例如,可以创建 Stack<int> 来存储整数,创建 Stack<double> 来存储浮点数,甚至可以用自定义类作为模板参数来存储自定义对象。
  • 易于维护:使用类模板时,修改模板代码会自动影响所有实例化的类。这意味着当我们需要支持新的数据类型时,只需修改模板定义,而无需修改现有的类定义,从而减少了重复工作。
  • 避免冗余代码:每种数据类型都写一个独立的类会导致代码冗长,且容易出错。类模板提供了一个统一的代码框架,减少了冗余代码,提高了代码质量。

总结

类模板是 C++ 的一项强大特性,它允许你编写通用的类,并且能够处理不同类型的数据。通过类模板,我们可以避免冗余代码,提高代码复用性和可维护性,并使代码更加灵活。类模板在 C++ 标准库(如 STL)中广泛应用,特别是在实现容器类和算法时。掌握类模板的使用,能够帮助开发者编写更加简洁、灵活、易于扩展的代码。