C++ 模板初阶

在 C++ 编程中,代码复用是提高开发效率的关键。泛型编程作为实现代码复用的重要手段,让我们能够编写与类型无关的通用代码,而模板正是泛型编程的基础。本文将带你深入了解 C++ 中的模板,包括函数模板和类模板的概念、用法及原理。

一、泛型编程与模板

泛型编程的核心思想是编写与类型无关的通用代码,通过这种方式实现代码的复用。而模板则是泛型编程的技术基础,它允许我们定义一种通用的结构,在使用时再指定具体的类型。

模板主要分为两类:

  • 函数模板:用于生成通用的函数
  • 类模板:用于生成通用的类

二、函数模板

1. 函数模板概念

函数模板代表了一个函数家族,它与具体类型无关,在使用时会根据实参类型被参数化,从而生成该函数的特定类型版本。

2. 函数模板格式

函数模板的定义以template关键字开头,后跟模板参数列表,格式如下:

cpp 复制代码
template<typename T1, typename T2, ..., typename Tn>
// 函数定义

其中,typename是定义模板参数的关键字,也可以使用class(注意:不能用struct代替class)。

示例:

cpp 复制代码
template<class T>
void Swap(T& left, T& right)
{
    T temp = left;
    left = right;
    right = temp;
}

template<typename T1, typename T2>
void func(const T1& x, const T2& y)
{
    // 函数实现
}

main函数中使用这些模板函数:

cpp 复制代码
int main()
{
    int i = 1, j = 2;
    double m = 1.1, n = 2.2;
    Swap(i, j);  // 实例化为int类型的Swap函数
    Swap(m, n);  // 实例化为double类型的Swap函数
    
    func(i, m);  // 实例化为T1=int, T2=double的func函数
    func(i, j);  // 实例化为T1=int, T2=int的func函数
    return 0;
}

3. 标准库中的 swap 函数

C++ 标准库中已经实现了swap函数,定义在<utility>头文件中(通常会被其他头文件间接包含),使用时直接调用即可,要求传入的两个参数类型相同:

cpp 复制代码
int main()
{
    int i = 1, j = 2;
    double m = 1.1, n = 2.2;
    
    swap(i, j);  // 使用标准库的swap函数
    return 0;
}

4. 函数模板的原理

函数模板本身并不是一个实际的函数,而是一个蓝图或模具。编译器会根据我们使用函数模板时提供的实参类型,自动生成对应类型的具体函数。这相当于将原本需要我们手动编写多个类型版本函数的工作,交给了编译器来完成。

5. 函数模板的实例化

当我们用不同类型的参数使用函数模板时,就称为函数模板的实例化。模板参数实例化分为两种:

(1)隐式实例化

编译器根据实参的类型自动推导出模板参数的类型:

cpp 复制代码
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.0;
    Add(a1, a2);  // 隐式实例化为int类型的Add函数
    Add(d1, d2);  // 隐式实例化为double类型的Add函数
    
    // 当实参类型不同时,需要进行强制类型转换
    cout << Add(a1, (int)d1) << endl;  // 将d1强制转换为int
    cout << Add((double)a1, d1) << endl;  // 将a1强制转换为double
    return 0;
}

(2)显式实例化

我们直接指定模板参数的类型,格式为:函数名<类型>(实参)

cpp 复制代码
int main()
{
    int a1 = 10;
    double d1 = 10.1;
    cout << Add<int>(a1, d1) << endl;  // 显式指定为int类型,d1会被隐式转换为int
    return 0;
}

必须显式实例化的情况:当模板参数无法通过函数实参推导时,必须进行显式实例化。

cpp 复制代码
template<class T>
T* func1(int n)
{
    return new T[n];
}

int main()
{
    // 无法从实参int推导出T的类型,必须显式实例化
    double* p1 = func1<double>(2);
    return 0;
}

6. 模板参数的匹配原则

  • 一个非模板函数可以和一个同名的函数模板同时存在,并且函数模板可以被实例化为这个非模板函数。
  • 在调用时,若非模板函数和模板实例化后的函数都能匹配,会优先调用非模板函数。但如果模板能产生一个更匹配的函数,则会选择模板。
cpp 复制代码
template<class T>
T Add(const T& left, const T& right)
{
    return left + right;
}

// 非模板函数,与上面的函数模板同名
int Add(const int& x, const int& y)
{
    return (x + y) * 10;
}

int main()
{
    int a1 = 10, a2 = 20;
    cout << Add(a1, a2) << endl;  // 优先调用非模板函数,输出300
    return 0;
}
  • 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。

三、类模板

1. 类模板的定义格式

类模板的定义格式如下:

cpp 复制代码
template <class T1, class T2, ..., class Tn>
class 类模板名
{
    // 类内成员定义
};

2. 类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟上<>,并将实例化的类型放在<>中。类模板名字本身不是真正的类,实例化的结果才是真正的类。

cpp 复制代码
// Stack是类模板名,Stack<int>、Stack<double>才是真正的类
Stack<int> st1;      // 实例化为int类型的Stack类
Stack<double> st2;   // 实例化为double类型的Stack类

3. 类模板示例

下面是一个栈(Stack)的类模板示例:

cpp 复制代码
template<typename T>
class Stack
{
public:
    Stack(int n = 4)
        : _array(new T[n])
        , _size(0)
        , _capacity(n)
    {}

    ~Stack()
    {
        delete[] _array;
        _array = nullptr;
        _size = _capacity = 0;
    }

    void Push(const T& data)
    {
        // 判断是否需要扩容
        if (_size == _capacity)
        {
            T* tmp = new T[_capacity * 2];  // 扩容为原来的2倍
            memcpy(tmp, _array, sizeof(T) * _size);  // 拷贝数据
            delete[] _array;  // 释放旧空间

            _array = tmp;  // 指向新空间
            _capacity *= 2;
        }
        _array[_size++] = data;
    }

private:
    T* _array;      // 存储数据的数组
    size_t _size;   // 当前元素个数
    size_t _capacity;  // 容量
};

int main()
{
    // 类模板必须显式实例化
    Stack<int> st1;  // int类型的栈
    st1.Push(1);
    st1.Push(2);
    st1.Push(3);

    Stack<double> st2;  // double类型的栈
    st2.Push(1.1);
    st2.Push(2.2);
    st2.Push(3.3);

    return 0;
}

4. 类模板的声明和定义分离

类模板的声明和定义有一些特殊要求:

  • 类模板的声明和定义一般不建议分离到两个文件中(.h 和.cpp),通常放在同一个头文件中。
  • 声明和定义处都要写完整的模板参数列表(template<typename T>)。
  • 定义成员函数时,需要通过**类名<T>::**指明所属的模板类实例。

示例:

cpp 复制代码
// 类模板声明
template<typename T>
class Stack
{
public:
    Stack(int n = 4)
        : _array(new T[n])
        , _size(0)
        , _capacity(n)
    {}

    ~Stack()
    {
        delete[] _array;
        _array = nullptr;
        _size = _capacity = 0;
    }

    // 成员函数声明
    void Push(const T& data);

private:
    T* _array;
    size_t _size;
    size_t _capacity;
};

// 成员函数定义
template<typename T>
void Stack<T>::Push(const T& data)
{
    if (_size == _capacity)
    {
        T* tmp = new T[_capacity * 2];
        memcpy(tmp, _array, sizeof(T) * _size);
        delete[] _array;

        _array = tmp;
        _capacity *= 2;
    }
    _array[_size++] = data;
}
相关推荐
zore_c1 小时前
【数据结构】队列——超详解!!!(包含队列的实现)
c语言·网络·数据结构·c++·笔记·算法·链表
小杰帅气2 小时前
智能指针喵喵喵
开发语言·c++·算法
代码or搬砖2 小时前
悲观锁讲解
开发语言·数据库
特立独行的猫a2 小时前
cpp-linenoise介绍——让命令行拥有猫一般的敏捷
c++·ui·命令行·cpp-linenoise
hudawei9962 小时前
对比kotlin和flutter中的异步编程
开发语言·flutter·kotlin·异步·
南棱笑笑生2 小时前
20251219给飞凌OK3588-C开发板适配Rockchip原厂的Buildroot【linux-5.10】后解决启动不了报atf-2的问题
linux·c语言·开发语言·rockchip
deephub2 小时前
ONNX Runtime Python 推理性能优化:8 个低延迟工程实践
开发语言·人工智能·python·神经网络·性能优化·onnx
蕨蕨学AI2 小时前
【Wolfram语言】22 机器学习
开发语言·wolfram
百***78752 小时前
LLaMA 4 API国内稳定接入指南:中转服务全链路实操与优化方案
开发语言·php·llama