乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • C++ 学习路径
  • 第1章 C++概述与开发环境

    • C++ 语言概述与编译模型
    • 第一个 C++ 程序与规范
    • 开发环境配置
    • 预处理指令详解
  • 第2章 基本语法与类型系统

    • 变量与基本类型
    • 类型转换
    • 枚举类型
    • 引用与指针
    • 数组与 stdarray
    • 字符串与原始字符串字面量
    • const 与 constexpr
    • nullptr 与空指针
    • 类型推导 auto 与 decltype
    • 基于范围的 for 循环
    • 值类别全面解析
    • char16_t 与 char32_t
    • static_assert 编译期断言
  • 第3章 函数与重载

    • 函数声明与定义
    • 函数重载
    • 默认参数与内联函数
    • Lambda 表达式
    • 函数对象与 stdfunction
    • 后置返回类型与 noexcept
  • 第4章 类与对象

    • 类的基本定义
    • 构造函数与析构函数
    • 拷贝控制
    • 移动构造函数与移动赋值
    • 列表初始化与类内初始化器
    • 静态成员与嵌套类
    • 友元
    • =default 与 =delete
  • 第5章 继承与多态

    • 继承基础
    • 虚函数与多态
    • 虚函数表与动态绑定原理
    • 虚析构函数
    • 抽象类与纯虚函数
    • 多重继承与虚继承
    • 继承构造函数
  • 第6章 运算符重载

    • 运算符重载基础
    • 算术与关系运算符重载
    • 赋值与移动运算符重载
    • 特殊运算符重载
  • 第7章 模板与泛型编程

    • 函数模板
    • 类模板
    • 模板特化与偏特化
    • 可变参数模板
    • 别名模板与模板模板参数
    • SFINAE 与类型萃取
    • 依赖名与 typename/template 关键字
  • 第8章 异常处理

    • 异常处理机制
    • noexcept 与异常安全
    • RAII 与异常安全实践
  • 第9章 内存管理与智能指针

    • 动态内存与内存分区
    • RAII 惯用法
    • unique_ptr
    • shared_ptr 与 weak_ptr
    • 内存管理最佳实践
  • 第10章 右值引用与移动语义

    • 右值引用与值类别深度解析
    • std::move 原理与使用
    • 完美转发与 std::forward
    • 移动语义性能对比与最佳实践
  • 第11章 STL容器

    • vector 深度剖析
    • deque 内部机制
    • list 与 forward_list
    • map 与 set 深度解析
    • unordered 容器与哈希原理
    • array 与 tuple
    • 容器适配器
    • 容器选择全景指南
  • 第12章 STL算法与迭代器

    • 迭代器体系全解
    • 非变异算法
    • 变异算法
    • 排序与二分算法
    • Lambda 与算法组合
    • std::random 随机数库
    • 自定义迭代器开发
  • 第13章 IO流与文件

    • 标准 IO 流
    • 格式化输出控制
    • 文件流操作
    • 字符串流
    • std::regex 正则表达式
  • 第14章 并发与多线程

    • thread 基础与线程管理
    • mutex 与 lock_guard
    • unique_lock 与 condition_variable
    • thread_local 线程局部存储
    • atomic 与内存序
    • future 与 async 异步编程
    • std::chrono 时间库
  • 第15章 现代C++新特性

    • 从 C++11 到 C++20 演进路线
    • C++14 关键新特性
    • C++17 关键新特性
    • C++20 核心特性速览
  • 第16章 面试考点与最佳实践

    • C++ 综合最佳实践清单
    • 高频面试题精讲
    • 多线程面试题与实战
    • 内存管理常见陷阱与排查
  • 附录

    • C++ 核心知识点
    • C++ 专业术语

类模板

定义与作用

类模板允许定义一个类的蓝图,其成员的类型由模板参数决定:

template <typename T>
class Stack {
    std::vector<T> data_;
public:
    void push(const T& value);
    T pop();
};

核心价值:集合类、容器类、包装类的泛型化——一套代码适配所有类型。

核心原理

类模板实例化

完整示例

示例一:泛型栈的实现

场景说明:从零实现一个功能完整的泛型栈,对比不同模板实参实例化出的独立类型。

#include <iostream>
#include <vector>
#include <stdexcept>
#include <initializer_list>
#include <utility>

template <typename T>
class Stack {
public:
    Stack() = default;

    // 列表初始化构造
    Stack(std::initializer_list<T> init) : data_(init) {}

    // 拷贝构造
    Stack(const Stack& other) = default;

    // 移动构造
    Stack(Stack&& other) noexcept = default;

    // 压栈(常量引用版)
    void push(const T& value) {
        std::cout << "  压栈(拷贝): " << value << std::endl;
        data_.push_back(value);
    }

    // 压栈(右值引用版——移动语义)
    void push(T&& value) {
        std::cout << "  压栈(移动): " << value << std::endl;
        data_.push_back(std::move(value));
    }

    // 出栈
    T pop() {
        if (empty()) throw std::underflow_error("栈空:不能 pop");
        T top = std::move(data_.back());
        data_.pop_back();
        return top;
    }

    // 查看栈顶
    T& top() {
        if (empty()) throw std::underflow_error("栈空");
        return data_.back();
    }

    const T& top() const {
        if (empty()) throw std::underflow_error("栈空");
        return data_.back();
    }

    bool empty() const { return data_.empty(); }
    size_t size() const { return data_.size(); }

    // 清空
    void clear() { data_.clear(); }

private:
    std::vector<T> data_;
};

int main() {
    std::cout << "===== Stack<int> =====" << std::endl;
    Stack<int> int_stack;
    int_stack.push(10);
    int_stack.push(20);
    int_stack.push(30);

    std::cout << "栈大小: " << int_stack.size() << std::endl;
    while (!int_stack.empty()) {
        std::cout << "弹出: " << int_stack.pop() << std::endl;
    }

    std::cout << "\n===== Stack<std::string> =====" << std::endl;
    Stack<std::string> str_stack = {"Alice", "Bob", "Charlie"};
    std::cout << "栈顶: " << str_stack.top() << std::endl;
    str_stack.push(std::string("Dave"));  // 右值——触发移动版本

    std::cout << "全部出栈:" << std::endl;
    while (!str_stack.empty()) {
        std::cout << "  " << str_stack.pop() << std::endl;
    }

    // Stack<int> 和 Stack<double> 是两个不同的类型
    std::cout << "\n===== 类型独立性 =====" << std::endl;
    Stack<int> si;
    si.push(42);
    Stack<double> sd;
    sd.push(3.14);

    // si = sd;  // 编译错误!Stack<int> 和 Stack<double> 不兼容
    std::cout << "Stack<int> 大小: " << si.size() << std::endl;
    std::cout << "Stack<double> 大小: " << sd.size() << std::endl;

    return 0;
}

预期输出:

===== Stack<int> =====
  压栈(拷贝): 10
  压栈(拷贝): 20
  压栈(拷贝): 30
栈大小: 3
弹出: 30
弹出: 20
弹出: 10

===== Stack<std::string> =====
栈顶: Charlie
  压栈(移动): Dave
全部出栈:
  Dave
  Charlie
  Bob
  Alice

===== 类型独立性 =====
  压栈(拷贝): 42
  压栈(拷贝): 3.14
Stack<int> 大小: 1
Stack<double> 大小: 1

示例二:Pair 的实现与 CTAD(C++17)

#include <iostream>
#include <string>
#include <utility>

// 手写 Pair 类模板
template <typename T1, typename T2>
class Pair {
public:
    Pair() : first_(), second_() {}

    Pair(const T1& first, const T2& second)
        : first_(first), second_(second) {}

    // 通用构造(支持移动)
    template <typename U1, typename U2>
    Pair(U1&& first, U2&& second)
        : first_(std::forward<U1>(first))
        , second_(std::forward<U2>(second)) {}

    T1 first_;
    T2 second_;
};

// C++17 CTAD 推导指引(手工版标准 Pair 不需要,编译器自带)
// 此处演示手工 Pair 的 CTAD
template <typename T1, typename T2>
Pair(T1, T2) -> Pair<T1, T2>;

// 打印函数模板
template <typename T1, typename T2>
std::ostream& operator<<(std::ostream& os, const Pair<T1, T2>& p) {
    return os << "{" << p.first_ << ", " << p.second_ << "}";
}

int main() {
    std::cout << "===== 手写 Pair =====" << std::endl;

    // 显式指定模板参数
    Pair<std::string, int> p1("张三", 95);
    std::cout << p1 << std::endl;

    // C++17 CTAD:编译器自动推导
    Pair p2("李四", 88.5);  // 推导为 Pair<const char*, double>
    std::cout << p2 << std::endl;

    Pair p3(std::string("王五"), 92);  // 推导为 Pair<std::string, int>
    std::cout << p3 << std::endl;

    // 嵌套
    Pair<Pair<std::string, int>, double> nested(Pair<std::string, int>("赵六", 85), 3.7);
    std::cout << "嵌套: " << nested << std::endl;

    return 0;
}

预期输出:

===== 手写 Pair =====
{张三, 95}
{李四, 88.5}
{王五, 92}
嵌套: {{赵六, 85}, 3.7}

逐段分析:

  • 类模板定义在类名后紧接着 <typename T>——每个成员函数定义也都需要这个前缀
  • Stack<int> 和 Stack<double> 是完全不同的类型——各自独立实例化模板成员函数
  • CTAD(C++17)允许省略模板参数,编译器从构造函数实参推导——Pair("李四", 88.5) 不需要写 <const char*, double>
  • 推导指引 Pair(T1, T2) -> Pair<T1, T2>; 告诉 CTAD 如何从构造函数参数推导模板参数

易错场景与面试考点

易错场景

场景错误表现正确做法
类模板的成员函数定义在 .cpp链接时找不到定义定义和声明都在头文件,或用显式实例化
混淆不同模板实参的类型尝试赋值不兼容的类型使用同一个模板实参,或写转换函数
忘记成员函数定义前的 template<typename T>编译错误每个类外定义都要重复模板声明
类模板中的 using 别名别名本身不需要 typename注意依赖名与非依赖名的区别
CTAD 推导意外推导为不希望的类型显式指定模板参数或使用推导指引修正

常见面试问题

  1. 类模板和函数模板的区别?——函数模板通过实参推导类型参数;类模板必须显式指定模板实参(C++17 前),各实例是独立类型。类模板可以偏特化,函数模板不可。

  2. 为什么类模板的完整定义必须在头文件?——编译器需要看到完整的类定义才能为每个实例化生成代码。每个翻译单元独立实例化,链接时去重。

  3. C++17 CTAD 是什么?——Class Template Argument Deduction,类模板参数推导。允许 std::pair p(1, 2.0) 省略模板参数,编译器从构造函数推导。

  4. 类模板的静态成员在不同实例间共享吗?——不共享。Stack<int>::static_var 和 Stack<double>::static_var 是不同的变量,各自独立。

  5. 模板类的友元如何处理?——友元函数模板需前置声明,类模板内的友元声明可能涉及模板参数。

小结

  • 类模板:用 <typename T> 参数化整个类的类型——每个实例是独立类型
  • 定义必须在头文件——除非显式实例化
  • 不同模板实参生成完全不同的类型——Stack<int> ≠ Stack<double>
  • CTAD(C++17):从构造函数参数推导类模板参数,省略显式指定
  • 成员函数定义需在类外时加 template<typename T> 前缀
上一页
函数模板
下一页
模板特化与偏特化