类模板:实现通用数据结构的基础

类模板:实现通用数据结构的基础

在上一篇博客中,我们详细讲解了函数模板的定义、调用方式与实战场景,核心解决了"功能相同、类型不同"的函数复用问题,掌握了C++泛型编程的入门技巧。但在实际开发中,除了通用函数,我们更常遇到"数据结构逻辑相同,但存储的数据类型不同"的场景------比如栈、队列、链表、数组,既需要存储int类型数据,也需要存储double、string,甚至自定义结构体类型数据。

如果按照传统面向对象的方式,我们需要为每种数据类型单独定义一个类:比如IntStack(存储int的栈)、DoubleStack(存储double的栈)、StringStack(存储string的栈)。这种方式会导致和传统重载函数一样的问题:代码极度冗余、维护成本高、扩展性差,一旦需要修改数据结构的逻辑(如栈的出栈判断、链表的插入逻辑),所有对应类型的类都要逐一修改,极易出错。

而解决这一问题的核心,就是泛型编程的另一大核心技术------类模板(Class Template)。类模板允许我们脱离具体数据类型,定义通用的类框架,编译器会根据实际指定的类型,自动生成对应类型的类实例,实现"一份类模板,适配多种数据类型"的通用数据结构。

类模板是实现通用数据结构的基础,也是C++标准库(STL)的核心底层原理------STL中的vector、list、stack、queue等容器,本质上都是类模板的实现。本文将衔接上一篇函数模板的知识点,从"传统类型专属类的痛点"切入,详解类模板的定义语法、实例化方式、核心特性,结合"通用栈""通用数组"两个高频实战案例,规避初学者常见误区,帮你彻底掌握类模板,打通C++泛型编程与通用数据结构的学习链路。

核心前提回顾:1. 函数模板的定义、调用与底层原理(编译器自动生成具体类型代码);2. 面向对象的类、对象、成员函数、访问控制(public/private)等基础;3. 基本数据类型与自定义结构体的使用(实战案例必备)。

一、先看痛点:传统类型专属类的弊端(为什么需要类模板?)

在讲解类模板之前,我们先通过一个"栈数据结构"的实战场景,直观感受传统类型专属类的冗余与不便------这也是类模板诞生的核心原因,更是笔试中"类模板作用"的高频考点。

场景再现:实现多类型栈数据结构

需求:实现一个栈数据结构,支持push(入栈)、pop(出栈)、top(获取栈顶元素)、isEmpty(判断栈空)四个核心功能,分别适配int、double两种数据类型。按照传统方式,我们需要定义两个独立的类,代码如下:

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

// 1. 定义存储int类型的栈(IntStack)
class IntStack {
private:
    int* arr;       // 存储int类型数据的数组
    int topIndex;   // 栈顶索引
    int capacity;   // 栈的容量
public:
    // 构造函数:初始化栈
    IntStack(int cap = 10) : capacity(cap), topIndex(-1) {
        arr = new int[capacity]; // 动态分配int数组
    }

    // 析构函数:释放内存(避免内存泄漏)
    ~IntStack() {
        delete[] arr;
    }

    // 入栈:将int类型数据压入栈顶
    void push(int value) {
        if (topIndex >= capacity - 1) {
            cout << "栈满,无法入栈!" << endl;
            return;
        }
        arr[++topIndex] = value;
    }

    // 出栈:删除栈顶元素
    void pop() {
        if (isEmpty()) {
            cout << "栈空,无法出栈!" << endl;
            return;
        }
        topIndex--;
    }

    // 获取栈顶元素(int类型)
    int top() const {
        if (isEmpty()) {
            cout << "栈空,无栈顶元素!" << endl;
            return -1; // 临时返回无效值
        }
        return arr[topIndex];
    }

    // 判断栈是否为空
    bool isEmpty() const {
        return topIndex == -1;
    }
};

// 2. 定义存储double类型的栈(DoubleStack)
class DoubleStack {
private:
    double* arr;    // 存储double类型数据的数组(仅此处与IntStack不同)
    int topIndex;   // 栈顶索引(与IntStack完全一致)
    int capacity;   // 栈的容量(与IntStack完全一致)
public:
    // 构造函数:初始化栈(逻辑与IntStack完全一致,仅数组类型不同)
    DoubleStack(int cap = 10) : capacity(cap), topIndex(-1) {
        arr = new double[capacity]; // 动态分配double数组
    }

    // 析构函数:释放内存(与IntStack完全一致)
    ~DoubleStack() {
        delete[] arr;
    }

    // 入栈:将double类型数据压入栈顶(仅参数类型不同)
    void push(double value) {
        if (topIndex >= capacity - 1) {
            cout << "栈满,无法入栈!" << endl;
            return;
        }
        arr[++topIndex] = value;
    }

    // 出栈:删除栈顶元素(与IntStack完全一致)
    void pop() {
        if (isEmpty()) {
            cout << "栈空,无法出栈!" << endl;
            return;
        }
        topIndex--;
    }

    // 获取栈顶元素(double类型,仅返回值类型不同)
    double top() const {
        if (isEmpty()) {
            cout << "栈空,无栈顶元素!" << endl;
            return 0.0; // 临时返回无效值
        }
        return arr[topIndex];
    }

    // 判断栈是否为空(与IntStack完全一致)
    bool isEmpty() const {
        return topIndex == -1;
    }
};

int main() {
    // 使用IntStack存储int类型数据
    IntStack intStack;
    intStack.push(10);
    intStack.push(20);
    cout << "IntStack栈顶元素:" << intStack.top() << endl;
    intStack.pop();
    cout << "IntStack出栈后,栈顶元素:" << intStack.top() << endl;

    // 使用DoubleStack存储double类型数据
    DoubleStack doubleStack;
    doubleStack.push(3.14);
    doubleStack.push(1.59);
    cout << "DoubleStack栈顶元素:" << doubleStack.top() << endl;
    doubleStack.pop();
    cout << "DoubleStack出栈后,栈顶元素:" << doubleStack.top() << endl;

    return 0;
}

运行结果与问题分析

运行结果看似正常(能正确实现两种类型栈的功能),但代码存在与传统重载函数完全一致的致命弊端,且冗余程度更甚:

Plain 复制代码
IntStack栈顶元素:20
IntStack出栈后,栈顶元素:10
DoubleStack栈顶元素:1.59
DoubleStack出栈后,栈顶元素:3.14
  1. 代码冗余极致严重:IntStack和DoubleStack的逻辑完全一致,唯一的区别就是"存储数据的数组类型""push参数类型""top返回值类型"------重复编写了大量完全相同的代码(构造、析构、pop、isEmpty等),代码体积翻倍。

  2. 维护成本极高:如果需要修改栈的逻辑(如增加栈扩容功能、修改栈满/栈空的提示信息),需要同时修改所有类型专属类(IntStack、DoubleStack),一旦遗漏某个类,就会导致逻辑不一致,且修改成本随类型数量增加而倍增。

  3. 扩展性极差:如果后续需要支持string、自定义结构体(如坐标)类型的栈,必须再次编写对应的类(StringStack、PointStack),重复劳动,无法实现"一次编写,终身复用"。

补充说明:这种"数据结构逻辑相同、存储类型不同"的场景,在实际开发中无处不在------比如链表(存储用户信息、商品信息)、队列(存储任务、消息)、数组(存储日志、数据报表)等。传统类型专属类的弊端,会让代码变得臃肿、难以维护,而类模板正是解决这一问题的最优方案。

核心需求:通用的"类型无关"类

我们真正需要的是:一份类框架,能够适配多种数据类型,无需重复编写类代码,且修改时只需修改一处。类模板就像一个"类的模具",我们只需定义一次通用的类框架(指定数据类型占位符),编译器会根据实际指定的类型,自动生成对应类型的类实例,彻底解决类型专属类的冗余问题。

简单来说,类模板与函数模板的核心思想一致,都是"泛型复用"------函数模板复用函数逻辑,类模板复用类的结构与逻辑,前者是通用函数的基础,后者是通用数据结构的基础。

二、核心知识点:类模板的定义与语法

类模板的核心思想是"用占位符表示类中使用的数据类型",其语法与函数模板相似,但有两个关键区别:1. 模板声明需紧跟在类定义之前;2. 类模板实例化时,必须显式指定类型(无法像函数模板那样自动推导)。

类模板的语法分为两部分:模板声明(Template Declaration)类定义(Class Definition),语法简洁且固定,初学者只需牢记模板声明的位置和实例化的要求即可。

1. 类模板的完整语法格式

类模板需先声明模板,再定义类,核心是用"模板参数"作为类中数据类型的占位符,语法格式如下:

cpp 复制代码
// 1. 模板声明(核心:定义模板参数,即"类型占位符")
// template <typename T>  或  template <class T>,两者完全等价(推荐用typename)
template <typename T>  // T是模板参数(占位符),可自定义名称(如Type、Data等)
// 2. 类定义(用模板参数T代替具体数据类型)
class 类名 {
private:
    // 成员变量:用T表示数据类型
    T 成员变量名;  // 如T* arr;(通用数组)、T data;(单个通用数据)
public:
    // 成员函数:参数类型、返回值类型均可使用T
    类名(参数列表);  // 构造函数(可使用T作为参数类型)
    ~类名();         // 析构函数
    void 成员函数名(参数列表);  // 普通成员函数
    T 成员函数名(参数列表);     // 返回值类型为T的成员函数
};

2. 语法细节拆解(必记,区别于函数模板)

针对类模板的语法,拆解6个核心细节,重点区分函数模板,避免初学者踩坑:

  1. 模板声明关键字:与函数模板一致,必须以template开头,模板参数列表用尖括号<>包裹,支持typename Tclass T,推荐使用typename(更直观)。

  2. 模板声明与类的关联:模板声明template <typename T> 必须紧跟在类定义之前,中间不能有其他代码(与函数模板一致),否则编译器无法识别该类是类模板。

  3. 模板参数的使用范围:模板参数T(占位符)仅在当前类模板的范围内有效,包括类的成员变量、成员函数(参数、返回值、函数体内部),超出类模板范围则无效。

  4. 多个模板参数:如果类需要适配多种不同类型(如存储两种不同类型的数据),可声明多个模板参数,用逗号分隔,例如:
    // 两个模板参数T1、T2,分别表示两种数据类型 template <typename T1, typename T2> class Pair { private: T1 first; // 第一种类型的数据 T2 second; // 第二种类型的数据 public: Pair(T1 f, T2 s) : first(f), second(s) {} T1 getFirst() { return first; } T2 getSecond() { return second; } };

  5. 类模板的成员函数定义(两种方式):

    • 内联定义:直接在类模板内部定义成员函数(与普通类一致),语法简单,适合简短函数(如前面示例中的isEmpty函数)。

    • 外部定义:在类模板外部定义成员函数,需额外添加模板声明,且类名后需加上<T>(表示是类模板的成员函数),格式如下:

      `template // 额外添加模板声明

      class Stack {

      public:

      void push(T value); // 内联声明,外部定义

      };

    // 外部定义:类名后加,表示是Stack类模板的成员函数

    template

    void Stack::push(T value) {

    // 函数逻辑

    }`

  6. 类模板与函数模板的核心区别:类模板必须显式指定类型才能实例化(无法自动推导),而函数模板支持自动类型推导------这是类模板最关键的特性,也是初学者最容易遗忘的点。

3. 用类模板重构"通用栈"功能

结合上述语法,我们用类模板重构前面的"栈数据结构",彻底解决代码冗余问题,对比传统类型专属类的差异,感受类模板的优势:

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

// 1. 模板声明:定义模板参数T(类型占位符,代表栈存储的数据类型)
template <typename T>
// 2. 类模板定义:通用栈(适配所有可赋值的数据类型)
class Stack {
private:
    T* arr;       // 通用数组:存储T类型数据(占位符T代替具体类型)
    int topIndex; // 栈顶索引(与类型无关,仍为int)
    int capacity; // 栈的容量(与类型无关,仍为int)
public:
    // 构造函数:初始化通用栈(参数类型可使用T,此处容量仍为int)
    Stack(int cap = 10) : capacity(cap), topIndex(-1) {
        arr = new T[capacity]; // 动态分配T类型数组(编译器自动替换为具体类型)
    }

    // 析构函数:释放内存(避免内存泄漏)
    ~Stack() {
        delete[] arr;
    }

    // 入栈:参数为T类型(适配具体存储类型)
    void push(T value) {
        if (topIndex >= capacity - 1) {
            cout << "栈满,无法入栈!" << endl;
            return;
        }
        arr[++topIndex] = value;
    }

    // 出栈:与类型无关,逻辑不变
    void pop() {
        if (isEmpty()) {
            cout << "栈空,无法出栈!" << endl;
            return;
        }
        topIndex--;
    }

    // 获取栈顶元素:返回值为T类型(适配具体存储类型)
    T top() const {
        if (isEmpty()) {
            cout << "栈空,无栈顶元素!" << endl;
            // 返回默认值(T()表示T类型的默认构造值,如int为0,double为0.0)
            return T();
        }
        return arr[topIndex];
    }

    // 判断栈是否为空:与类型无关,逻辑不变
    bool isEmpty() const {
        return topIndex == -1;
    }
};

int main() {
    // 类模板实例化:必须显式指定类型(<int>、<double>)
    // 1. 实例化int类型的栈(编译器自动生成IntStack类的逻辑)
    Stack<int> intStack;
    intStack.push(10);
    intStack.push(20);
    cout << "IntStack栈顶元素:" << intStack.top() << endl;
    intStack.pop();
    cout << "IntStack出栈后,栈顶元素:" << intStack.top() << endl;

    // 2. 实例化double类型的栈(编译器自动生成DoubleStack类的逻辑)
    Stack<double> doubleStack;
    doubleStack.push(3.14);
    doubleStack.push(1.59);
    cout << "DoubleStack栈顶元素:" << doubleStack.top() << endl;
    doubleStack.pop();
    cout << "DoubleStack出栈后,栈顶元素:" << doubleStack.top() << endl;

    return 0;
}

运行结果与核心优势

运行结果与传统类型专属类完全一致,但代码的简洁度和可维护性实现了质的飞跃:

Plain 复制代码
IntStack栈顶元素:20
IntStack出栈后,栈顶元素:10
DoubleStack栈顶元素:1.59
DoubleStack出栈后,栈顶元素:3.14

类模板的三大核心优势(对比传统类型专属类):

  1. 代码无冗余:一份类模板,适配所有可赋值的数据类型(int、double、string、自定义结构体等),无需重复编写类代码,代码体积大幅减小。

  2. 维护成本极低:如果需要修改栈的逻辑(如增加扩容、修改提示信息),只需修改类模板中的一处逻辑,编译器会自动同步到所有实例化的具体类型栈中,避免遗漏。

  3. 扩展性极强:如果后续需要支持string、自定义结构体类型的栈,无需修改类模板,只需显式实例化对应类型即可(如Stack、Stack),实现"一次编写,终身复用"。

补充:类模板的底层实现与函数模板一致,都是"编译器自动生成具体类型的类"------我们编写的Stack是通用框架,编译器在编译阶段,会根据实例化时指定的类型(如int、double),自动生成对应的类(如Stack_int、Stack_double),运行时执行的是这些生成的具体类,因此类模板不影响运行效率。

三、关键操作:类模板的实例化与使用

类模板的使用核心是"实例化"------只有实例化后,编译器才会生成具体类型的类,才能创建对象、调用成员函数。与函数模板不同,类模板必须显式指定类型才能实例化,不存在自动推导的方式,这是类模板使用的关键要点。

1. 类模板的实例化格式(必记)

类模板的实例化格式固定,需在类名后加上尖括号,指定具体的数据类型,格式如下:

cpp 复制代码
// 格式:类模板名<具体类型> 对象名(构造函数参数);
// 1. 实例化基本类型(int、double、string等)
Stack<int> intStack;          // 实例化int类型栈,使用默认构造函数
Stack<double> doubleStack(20); // 实例化double类型栈,指定容量为20
Stack<string> stringStack;    // 实例化string类型栈

// 2. 实例化自定义结构体类型(需先定义结构体)
struct Point {
    int x;
    int y;
};
Stack<Point> pointStack;      // 实例化存储Point结构体的栈

关键注意点:

  • 必须显式指定类型:Stack intStack; 是错误的(未指定类型),编译器无法推导T的具体类型,会直接编译报错。

  • 类型必须支持类模板中的操作:实例化时指定的类型,必须支持类模板中使用的操作(如赋值操作、构造函数等)------比如数组类型无法实例化栈(数组不支持赋值操作)。

  • 不同实例化类型是独立的:Stack和Stack是两个完全独立的类,由编译器分别生成,彼此之间没有继承关系,不能互相赋值或转换。

2. 类模板成员函数的外部定义(重点)

当类模板的成员函数较长时,推荐在类模板外部定义(提高代码可读性),此时需注意两个关键细节:1. 外部定义前必须再次添加模板声明;2. 类名后必须加上<T>,表示是类模板的成员函数。

示例(通用栈的成员函数外部定义):

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

// 模板声明(类模板外部定义成员函数,需先声明模板)
template <typename T>
class Stack {
private:
    T* arr;
    int topIndex;
    int capacity;
public:
    // 成员函数内联声明(外部定义)
    Stack(int cap = 10);  // 构造函数
    ~Stack();             // 析构函数
    void push(T value);   // 入栈函数
    void pop();           // 出栈函数
    T top() const;        // 获取栈顶元素
    bool isEmpty() const; // 判断栈空
};

// 1. 构造函数外部定义:模板声明 + 类名<T>::函数名
template <typename T>
Stack<T>::Stack(int cap) : capacity(cap), topIndex(-1) {
    arr = new T[capacity];
}

// 2. 析构函数外部定义
template <typename T>
Stack<T>::~Stack() {
    delete[] arr;
}

// 3. 入栈函数外部定义
template <typename T>
void Stack<T>::push(T value) {
    if (topIndex >= capacity - 1) {
        cout << "栈满,无法入栈!" << endl;
        return;
    }
    arr[++topIndex] = value;
}

// 4. 出栈函数外部定义
template <typename T>
void Stack<T>::pop() {
    if (isEmpty()) {
        cout << "栈空,无法出栈!" << endl;
        return;
    }
    topIndex--;
}

// 5. 获取栈顶元素外部定义
template <typename T>
T Stack<T>::top() const {
    if (isEmpty()) {
        cout << "栈空,无栈顶元素!" << endl;
        return T();
    }
    return arr[topIndex];
}

// 6. 判断栈空外部定义
template <typename T>
bool Stack<T>::isEmpty() const {
    return topIndex == -1;
}

// 测试外部定义的类模板
int main() {
    Stack<int> intStack;
    intStack.push(100);
    cout << "栈顶元素:" << intStack.top() << endl;
    return 0;
}

关键提醒:类模板的成员函数外部定义时,template <typename T>Stack<T>:: 缺一不可------缺少模板声明,编译器无法识别T;缺少<T>,编译器无法识别该函数是类模板的成员函数,都会导致编译报错。

3. 类模板的默认模板参数(可选,简化实例化)

类模板支持设置默认模板参数------在模板声明时,为模板参数指定一个默认的具体类型,当实例化时未显式指定类型,编译器会使用默认类型实例化类模板,简化实例化操作。

示例(设置默认模板参数):

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

// 模板声明:设置默认模板参数为int(T = int)
template <typename T = int>
class Stack {
private:
    T* arr;
    int topIndex;
    int capacity;
public:
    Stack(int cap = 10) : capacity(cap), topIndex(-1) {
        arr = new T[capacity];
    }
    void push(T value) {
        if (topIndex >= capacity - 1) { cout << "栈满!" << endl; return; }
        arr[++topIndex] = value;
    }
    T top() const { return arr[topIndex]; }
};

int main() {
    // 1. 未显式指定类型:使用默认模板参数int
    Stack<> intStack; // 等价于 Stack<int> intStack;(注意尖括号不能省略)
    intStack.push(10);
    cout << "默认类型栈顶元素:" << intStack.top() << endl;

    // 2. 显式指定类型:覆盖默认模板参数
    Stack<double> doubleStack;
    doubleStack.push(3.14);
    cout << "显式指定类型栈顶元素:" << doubleStack.top() << endl;

    return 0;
}

关键注意点:设置默认模板参数后,实例化时未显式指定类型,必须加上空的尖括号<>(如Stack<>),否则编译器会将Stack识别为普通类,而非类模板,导致编译报错。

四、实战场景:类模板的高频使用案例(入门必练)

类模板的核心价值是"实现通用数据结构",实际开发中,以下3个场景最为高频,结合案例练习,快速掌握类模板的实例化、成员函数外部定义、多模板参数等核心用法,衔接前序知识点。

场景1:通用数组类(支持任意类型,实现增删查改)

需求:实现一个通用数组类,支持存储任意类型的数据,提供三个核心功能:添加元素(add)、获取指定索引元素(get)、修改指定索引元素(set),适配int、string、自定义结构体类型。

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

// 模板声明:通用数组类,默认模板参数为int
template <typename T = int>
class Array {
private:
    T* arr;       // 通用数组,存储T类型数据
    int size;     // 数组当前元素个数
    int capacity; // 数组容量
public:
    // 构造函数:初始化数组容量
    Array(int cap = 5);
    // 析构函数:释放内存
    ~Array();
    // 成员函数:添加元素
    void add(T value);
    // 成员函数:获取指定索引元素(返回T类型)
    T get(int index) const;
    // 成员函数:修改指定索引元素
    void set(int index, T value);
    // 成员函数:获取数组当前大小
    int getSize() const;
};

// 1. 构造函数外部定义
template <typename T>
Array<T>::Array(int cap) : capacity(cap), size(0) {
    arr = new T[capacity];
}

// 2. 析构函数外部定义
template <typename T>
Array<T>::~Array() {
    delete[] arr;
}

// 3. 添加元素外部定义(自动扩容逻辑简化)
template <typename T>
void Array<T>::add(T value) {
    if (size >= capacity) {
        cout << "数组已满,自动扩容(简化版)!" << endl;
        // 简化扩容:容量翻倍,拷贝原有元素(实际开发中需处理内存分配失败)
        T* newArr = new T[capacity * 2];
        for (int i = 0; i < size; i++) {
            newArr[i] = arr[i];
        }
        delete[] arr;
        arr = newArr;
        capacity *= 2;
    }
    arr[size++] = value;
}

// 4. 获取指定索引元素外部定义
template <typename T>
T Array<T>::get(int index) const {
    if (index < 0 || index >= size) {
        cout << "索引越界!" << endl;
        return T();
    }
    return arr[index];
}

// 5. 修改指定索引元素外部定义
template <typename T>
void Array<T>::set(int index, T value) {
    if (index < 0 || index >= size) {
        cout << "索引越界!" << endl;
        return;
    }
    arr[index] = value;
}

// 6. 获取数组当前大小外部定义
template <typename T>
int Array<T>::getSize() const {
    return size;
}

// 自定义结构体(测试结构体数组)
struct Student {
    string name;
    int age;
    // 重载<<运算符,方便打印结构体
    friend ostream& operator<<(ostream& os, const Student& s) {
        os << "姓名:" << s.name << ",年龄:" << s.age;
        return os;
    }
};

int main() {
    // 1. 实例化int类型数组(默认模板参数)
    Array<> intArr;
    intArr.add(10);
    intArr.add(20);
    intArr.add(30);
    cout << "int数组大小:" << intArr.getSize() << endl;
    cout << "int数组索引1的元素:" << intArr.get(1) << endl;
    intArr.set(1, 200);
    cout << "修改后,int数组索引1的元素:" << intArr.get(1) << endl;

    // 2. 实例化string类型数组
    Array<string> strArr;
    strArr.add("C++");
    strArr.add("类模板");
    cout << "\nstring数组大小:" << strArr.getSize() << endl;
    cout << "string数组索引0的元素:" << strArr.get(0) << endl;

    // 3. 实例化Student结构体类型数组
    Array<Student> studentArr;
    studentArr.add({"张三", 18});
    studentArr.add({"李四", 19});
    cout << "\nStudent数组大小:" << studentArr.getSize() << endl;
    cout << "Student数组索引1的元素:" << studentArr.get(1) << endl;

    return 0;
}

场景2:多模板参数类模板(Pair类,存储键值对)

需求:实现一个Pair类模板,支持存储一对不同类型的数据(键值对,如int-string、string-double),提供获取键(getKey)、获取值(getValue)两个功能,适配任意两种类型的组合。

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

// 模板声明:两个模板参数T1(键类型)、T2(值类型),无默认参数
template <typename T1, typename T2>
class Pair {
private:
    T1 key;   // 键(T1类型)
    T2 value; // 值(T2类型)
public:
    // 构造函数:初始化键值对
    Pair(T1 k, T2 v) : key(k), value(v) {}
    // 获取键
    T1 getKey() const { return key; }
    // 获取值
    T2 getValue() const { return value; }
    // 修改值
    void setValue(T2 v) { value = v; }
};

int main() {
    // 1. 实例化int-string类型的键值对(键int,值string)
    Pair<int, string> p1(1, "C++");
    cout << "键:" << p1.getKey() << ",值:" << p1.getValue() << endl;
    p1.setValue("C++ 类模板");
    cout << "修改后,键:" << p1.getKey() << ",值:" << p1.getValue() << endl;

    // 2. 实例化string-double类型的键值对(键string,值double)
    Pair<string, double> p2("PI", 3.14159);
    cout << "\n键:" << p2.getKey() << ",值:" << p2.getValue() << endl;

    // 3. 实例化int-int类型的键值对(键值类型相同)
    Pair<int, int> p3(100, 200);
    cout << "\n键:" << p3.getKey() << ",值:" << p3.getValue() << endl;

    return 0;
}

场景3:类模板与函数模板结合(通用打印函数适配类模板对象)

需求:结合上一篇博客的函数模板,实现一个通用打印函数,能够打印类模板实例化对象中的所有元素------比如打印通用数组(Array)、通用栈(Stack)中的元素,实现"通用函数适配通用类"。

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

// 1. 类模板:通用数组类(简化版,仅保留核心功能)
template <typename T>
class Array {
private:
    T* arr;
    int size;
public:
    Array(int cap = 5) : size(0) { arr = new T[cap]; }
    ~Array() { delete[] arr; }
    void add(T value) { arr[size++] = value; }
    T get(int index) const { return arr[index]; }
    int getSize() const { return size; }
};

// 2. 函数模板:通用打印函数(适配Array<T>类模板对象)
template <typename T>
void printArray(const Array<T>& arr) {
    cout &lt;&lt; "数组元素:";
    for (int i = 0; i < arr.getSize(); i++) {
        cout << arr.get(i) << " ";
    }
    cout << endl;
}

// 自定义结构体
struct Point {
    int x;
    int y;
    // 重载<<运算符,让通用打印函数能够打印Point类型
    friend ostream& operator<<(ostream& os, const Point& p) {
        os << "(" << p.x << "," << p.y << ")";
        return os;
    }
};

int main() {
    // 1. 实例化int类型数组,用通用打印函数打印
    Array<int> intArr;
    intArr.add(10);
    intArr.add(20);
    intArr.add(30);
    printArray(intArr); // 函数模板自动推导T=int

    // 2. 实例化string类型数组,用通用打印函数打印
    Array<string> strArr;
    strArr.add("hello");
    strArr.add("world");
    printArray(strArr); // 函数模板自动推导T=string

    // 3. 实例化Point结构体数组,用通用打印函数打印
    Array<Point> pointArr;
    pointArr.add({1,2});
    pointArr.add({3,4});
    printArray(pointArr); // 函数模板自动推导T=Point

    return 0;
}

核心逻辑:类模板实现"通用数据结构",函数模板实现"通用操作逻辑",两者结合可实现更灵活的泛型编程------这也是STL库的核心设计思想(如vector容器+sort排序函数)。

五、高频误区:类模板的常见坑(入门必避)

初学者使用类模板时,容易陷入以下6个高频误区,每个误区对应错误示例和正确写法,结合前面的知识点,帮你快速规避,避免编译报错或逻辑错误。

误区1:类模板实例化时,未显式指定类型(编译报错)

cpp 复制代码
template <typename T>
class Stack {
    // 类模板定义(省略)
};

int main() {
    // 错误:类模板必须显式指定类型,无法自动推导
    Stack intStack;  // 编译报错:missing template arguments before 'intStack'

    // 正确:显式指定类型(<int>不可省略)
    Stack<int> intStack;
    // 正确:有默认模板参数时,可写空尖括号
    // template <typename T = int> class Stack;
    // Stack<> intStack;
}

误区2:类模板成员函数外部定义时,遗漏模板声明或(编译报错)

cpp 复制代码
template <typename T>
class Stack {
public:
    void push(T value); // 外部定义
};

// 错误1:遗漏模板声明(template <typename T>)
void Stack<T>::push(T value) {
    // 编译报错:'T' has not been declared
}

// 错误2:遗漏类名后的<T>
template <typename T>
void Stack::push(T value) {
    // 编译报错:invalid use of template-name 'Stack' without an argument list
}

// 正确:模板声明 + 类名<T>::函数名
template <typename T>
void Stack<T>::push(T value) {
    // 正确逻辑
}

误区3:不同实例化类型的类模板对象互相赋值(编译报错)

cpp 复制代码
template <typename T>
class Stack {
    // 类模板定义(省略)
};

int main() {
    Stack<int> intStack;
    Stack<double> doubleStack;

    // 错误:Stack<int>和Stack<double>是两个独立的类,无继承关系,无法互相赋值
    intStack = doubleStack;  // 编译报错:no match for 'operator='

    // 正确:仅相同实例化类型的对象可赋值
    Stack<int> intStack2;
    intStack = intStack2; // 正确(同类型)
}

误区4:类模板中使用的类型,未支持必要的操作(编译报错)

cpp 复制代码
template <typename T>
class Stack {
private:
    T* arr;
    int topIndex;
public:
    Stack() { arr = new T[10]; }
    void push(T value) { arr[++topIndex] = value; } // 依赖赋值操作
};

struct Point {
    int x;
    int y;
    // 错误:未重载赋值运算符(默认赋值运算符可使用,但如果自定义了构造函数可能失效)
    // 此处简化示例:如果Point有自定义构造函数且未重载赋值,push会报错
};

int main() {
    Stack<Point> pointStack;
    pointStack.push({1,2}); // 若Point未支持赋值操作,编译报错

    // 错误:数组类型不支持赋值操作,无法实例化栈
    Stack<int[]> arrStack; // 编译报错:invalid type in declaration before ';'
}

关键提醒:类模板中使用的操作(如赋值、比较、构造),实例化时指定的类型必须支持------否则编译器无法生成有效的代码,导致编译报错。

误区5:默认模板参数实例化时,省略尖括号(编译报错)

cpp 复制代码
// 模板声明:设置默认模板参数为int
template <typename T = int>
class Stack {
    // 类模板定义(省略)
};

int main() {
    // 错误:默认模板参数实例化时,必须加上空尖括号<>
    Stack intStack;  // 编译报错:missing template arguments before 'intStack'

    // 正确:加上空尖括号,使用默认模板参数
    Stack<> intStack;
}

误区6:类模板的模板参数与成员变量名冲突(逻辑错误)

cpp 复制代码
// 错误:模板参数名T,与成员变量名T冲突(编译器可识别,但逻辑混乱,易出错)
template <typename T>
class Stack {
private:
    T T; // 错误:成员变量名与模板参数名相同,易混淆
public:
    void push(T value) {
        this->T = value; // 需用this区分,逻辑混乱
    }
};

// 正确:模板参数名与成员变量名区分开(如T为模板参数,arr为成员变量)
template <typename T>
class Stack {
private:
    T* arr; // 正确:名称区分,逻辑清晰
public:
    void push(T value) {
        arr[++topIndex] = value;
    }
};

六、总结:类模板的核心要点

类模板是C++泛型编程的核心,也是实现通用数据结构的基础,其核心价值是"脱离具体数据类型,实现类的结构与逻辑复用",解决传统类型专属类的代码冗余、维护成本高、扩展性差等痛点。结合上一篇函数模板的知识点,我们已经打通了C++泛型编程的入门链路。

相关推荐
bugcome_com1 小时前
# C# 变量作用域详解
开发语言·c#
阿里嘎多学长1 小时前
2026-02-13 GitHub 热点项目精选
开发语言·程序员·github·代码托管
EE工程师1 小时前
数据结构篇 - 顺序队列
数据结构·顺序队列
小宋10211 小时前
Java 数据库访问 vs Python 数据库访问:JDBC vs ORM
java·数据库·python
汽车软件工程师0011 小时前
vector autosar配置一个CAN接收报文,RTE层发现并未接收到信号,怎样查这个问题
开发语言·autosar
君爱学习2 小时前
MySQL 分布式锁实现方案
java
寻寻觅觅☆2 小时前
东华OJ-基础题-122-循环数(C++)-难度难
开发语言·c++
白中白121382 小时前
算法题-14
数据结构·算法·leetcode
努力学编程呀(๑•ี_เ•ี๑)2 小时前
【405】Not Allowed
java·vue.js·nginx·node.js