C++模板初阶:让代码“复制粘贴”自动化

在编程中,我们经常会遇到这样的情况:写的代码逻辑完全相同,只是处理的数据类型不同。比如,我们想要一个交换两个变量的函数,int类型写一遍,double类型写一遍,char类型再写一遍......这不仅枯燥,还容易出错。

C++ 提供了模板这一强大工具,让编译器帮我们"自动生成"这些重复的代码。今天我们就来聊聊模板的入门知识。

一、泛型编程是什么?

泛型编程,简单来说,就是编写与类型无关的通用代码。它是一种代码复用的手段,而模板正是实现泛型编程的基础。

你可以把模板想象成一个"模具",我们往里面填充不同的类型,它就能生成对应类型的代码。这样,我们只需要维护一份代码,编译器会根据我们使用时的类型,自动生成多个版本。

二、函数模板

1. 函数模板的概念

函数模板代表了一个函数家族,它本身不是函数,而是告诉编译器如何生成函数的一个"蓝图"。

2. 函数模板的格式

cpp 复制代码
template<typename T>
void Swap(T& left, T& right) {
    T temp = left;
    left = right;
    right = temp;
}
  • template 是定义模板的关键字。

  • typename T 表示定义一个类型参数 T,你也可以用 class T,但不能用 struct

  • T 可以任意命名,通常习惯用大写字母。

3. 函数模板的原理

编译器在编译阶段,会根据函数模板的使用情况,推演出具体的类型,并生成对应的函数代码。

比如你调用 Swap(a, b),如果 ab 都是 int 类型,编译器就会生成一个 int 版本的 Swap 函数。如果是 double,就生成 double 版本。这一切都在编译期完成,不会影响运行效率。

4. 函数模板的实例化

使用函数模板时,编译器会帮我们生成具体类型的函数,这个过程叫实例化。它分为两种:

隐式实例化

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

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.0, d2 = 20.0;
    Add(a1, a2);  // T 被推导为 int
    Add(d1, d2);  // T 被推导为 double
    // Add(a1, d1); // 错误!T 无法同时推导为 int 和 double
    return 0;
}

当模板参数只有一个 T,但传入不同类型时,编译器无法确定 T 应该是什么类型,会报错。解决方法有两种:

  • 强制类型转换:Add(a1, (int)d1);

  • 显式实例化

显式实例化

在函数名后加上 <类型>,显式指定模板参数的类型:

cpp 复制代码
int main() {
    int a = 10;
    double b = 20.0;
    Add<int>(a, b);  // 强制使用 int 版本,b 会被隐式转换为 int
    return 0;
}

5. 模板参数的匹配原则

  • 如果有普通函数函数模板同时存在,且普通函数完全匹配,编译器会优先调用普通函数。

  • 如果模板能生成一个更匹配的函数,编译器会选择模板。

cpp 复制代码
#include <iostream>
using namespace std;
// 专门处理int的加法函数
int Add(int left, int right) {
    return left + right;
}

// 通用加法函数(两个模板参数)
template<class T1, class T2>
T1 Add(T1 left, T2 right) {
    return left + right;
}

void Test() {
    cout << Add(1, 2)<<endl;     // 调用普通函数(精确匹配)
   cout << Add(1.5, 3);   // 调用模板,生成 Add(double,int) 版本,更匹配
}
int main() {
    Test();

    return 0;
}

ADD(1.5,3)返回值类型是 double的唯一原因:第一个参数 1int 类型。,

这就是模板类型推导的核心规则:函数调用时的实参类型,决定了模板参数的类型,进而决定了返回值类型。

  • 普通函数支持自动类型转换,而模板函数一般不会进行隐式类型转换(除非显式实例化或强制转换)。

三、类模板

1. 类模板的定义格式

类模板的定义和函数模板类似,使用 template<class T> 放在类定义前。

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

    void Push(const T& data);

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

2. 类模板的成员函数定义

如果成员函数在类外定义,需要加上模板声明,并指定类模板的实例化类型:

cpp 复制代码
template<class T>
void Stack<T>::Push(const T& data) {
    // ======================
    // 扩容逻辑(直接写在这里)
    // ======================
    if (_size == _capacity) {
        // 1. 计算新容量(2倍扩容)
        size_t newCapacity = _capacity * 2;

        // 2. 开辟新空间
        T* temp = new T[newCapacity];

        // 3. 拷贝旧数据
        for (size_t i = 0; i < _size; ++i) {
            temp[i] = _array[i];
        }

        // 4. 释放旧空间
        delete[] _array;

        // 5. 指向新空间 + 更新容量
        _array = temp;
        _capacity = newCapacity;
    }

注意:模板的声明和定义通常不建议分离到 .h.cpp 文件中,否则容易出现链接错误。一般将模板的实现也放在头文件中。

3. 类模板的实例化

类模板的实例化必须显式指定类型,与函数模板不同:

cpp 复制代码
#include <iostream>
using namespace std;

template<typename T>
class Stack {
public:
    Stack(size_t capacity = 4) {
        _array = new T[capacity];
        _capacity = capacity;
        _size = 0;
    }

    // 析构函数:释放内存
    ~Stack() {
        delete[] _array;
        _array = nullptr;
    }

    // 入栈 + 扩容逻辑 完全写在一起
    void Push(const T& data);

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

// 入栈 + 扩容 写在一起,不拆分函数
template<class T>
void Stack<T>::Push(const T& data) {
    // ======================
    // 扩容逻辑(直接写在这里)
    // ======================
    if (_size == _capacity) {
        // 1. 计算新容量(2倍扩容)
        size_t newCapacity = _capacity * 2;

        // 2. 开辟新空间
        T* temp = new T[newCapacity];

        // 3. 拷贝旧数据
        for (size_t i = 0; i < _size; ++i) {
            temp[i] = _array[i];
        }

        // 4. 释放旧空间
        delete[] _array;

        // 5. 指向新空间 + 更新容量
        _array = temp;
        _capacity = newCapacity;
    }

    // 正常入栈
    _array[_size] = data;
    ++_size;
}

// 测试
int main() {
    Stack<int> st;//int 类型的栈
    for (int i = 1; i <= 10; ++i) {
        st.Push(i);
        cout << "入栈:" << i << endl;
    }
    Stack<double>* pst = new Stack<double>;//double类型的栈
    pst->Push(3.14);
    cout << "入栈:" << 3.14 << endl;
    return 0;
}

4.Stack<int> st;Stack<double>* pst = new Stack<double>不同:

1. 先看两个写法

写法 1:栈上对象(局部变量)

Stack<int> st;

写法 2:堆上对象(动态分配)

Stack<double>* pst = new Stack<double>;

2. 最核心区别(必背)

Stack<int> st;

  • 在栈内存上创建对象
  • 生命周期自动管理 :函数结束,对象自动销毁,析构函数自动调用
  • 不需要手动 delete
  • 变量名 st 就是对象本身

Stack<double>* pst = new Stack<double>;

  • 在堆内存上创建对象
  • 生命周期手动管理必须自己 delete,否则内存泄漏
  • pst指针,指向堆上的对象
  • 访问成员要用 ->,不能用 .
3.内存泄露不同
1. 先看 Stack<int> st;(栈对象)
  • 对象在栈上
  • 函数结束 → 自动调用析构函数~Stack ()

那如果析构函数里没写 delete [] 会怎样?

结果:

  • st 这个栈对象本身确实会自动销毁
  • 但是!_array 指向的堆数组,永远留在堆里没人管内存泄漏
2. 再看 new Stack<double>(堆对象)

Stack<double>* pst = new Stack<double>;

  • 对象本体在堆上
  • 指针 pst 在栈上
  • 只有你写 delete pst; 才会触发:
    1. 调用析构函数~Stack ()在析构函数里执行:delete[] _array;
    2. 释放对象本身的堆内存 释放 new 出来的 Stack 本体

如果你不写 delete pst

  • 析构函数永远不会执行
  • 里面的 delete[] _array 根本跑不到
  • 双重内存泄漏:
    1. _array 指向的数组泄漏
    2. Stack 对象本身也泄漏
4.最终结论(必须记住)
  • 栈对象:析构函数自动调用
  • 堆对象:析构函数只有 delete 时才调用
  • 不管栈还是堆,_array 都是你自己 new 的,必须靠析构里的 delete [] 释放
  • 堆对象你不 delete → 析构不跑 → 内部数组直接泄漏
相关推荐
sonnet-102912 分钟前
函数式接口和方法引用
java·开发语言·笔记
Bat U16 分钟前
JavaEE|多线程(二)
java·开发语言
电化学仪器白超22 分钟前
小乌龟Git全程图形化操作指南:嵌入式本地版本管理与Gitee私有云备份实战
git·python·单片机·嵌入式硬件·物联网·gitee·自动化
没有口袋啦35 分钟前
《基于 GitOps 理念的企业级自动化 CI/CD 流水线》
阿里云·ci/cd·云原生·自动化·k8s
YIN_尹43 分钟前
【Linux系统编程】进程地址空间
linux·c++
烤麻辣烫1 小时前
JS基础
开发语言·前端·javascript·学习
EverestVIP1 小时前
C++中空类通常大小为1的原理
c++
froginwe111 小时前
C++ 文件和流
开发语言
Dxy12393102161 小时前
Python在图片上画矩形:从简单边框到复杂标注的全攻略
开发语言·python
独自破碎E1 小时前
面试官:你有用过Java的流式吗?比如说一个列表.stream这种,然后以流式去处理数据。
java·开发语言