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 → 析构不跑 → 内部数组直接泄漏
相关推荐
客卿1232 小时前
用两个栈实现队列
android·java·开发语言
java1234_小锋2 小时前
Java高频面试题:谈谈你对SpringBoot的理解?
java·开发语言·spring boot
2301_816651222 小时前
C++模块化设计原则
开发语言·c++·算法
Ulyanov2 小时前
Python GUI工程化实战:从tkinter/ttk到可复用的现代化组件架构
开发语言·python·架构·gui·tkinter
Bruce20489982 小时前
OpenClaw 5大高频自动化场景落地(附代码/配置)
运维·自动化
tobebetter95272 小时前
Debian / Ubuntu openclaw 浏览器自动化
ubuntu·自动化·debian
tobebetter95272 小时前
WSL2 + Windows + remote Chrome CDP openclaw 浏览器自动化
chrome·windows·自动化
chaofan9802 小时前
深度实战:2026年大模型应用如何解决“接口抖动”?五大主流平台横向测评
人工智能·自动化·api·claude opus
competes2 小时前
深圳程序员职业生涯
java·大数据·开发语言·人工智能·java-ee