C++入门自学Day17-- 模版进阶知识

前言

C++ 的两个强大且常被考查的主题就是 模板(templates)继承/多态(inheritance & polymorphism)。模板把"泛型"和"编译期多态"带入语言,允许你写出与类型无关但高效的代码;继承把"运行时多态"带入语言,方便用公共接口操纵不同实现。

本文按教学博客的风格来写:先给出模板函数的使用与进阶技巧 (含现代 C++ 的常见高级手段),再讲 C++ 的继承特性(含虚函数、对象切片、虚继承等),每部分配上示例、常见陷阱与最佳实践,最后做统一总结。示例尽量贴近工程实战,适合直接放在博客里。


主要内容简介

  • 1、模板函数基础:函数模板、类型推导、非类型模板参数、模板重载/特化

  • 2、模板进阶:完美转发、折叠表达式、变参模板、SFINAE/enable_if、std::void_t 检测、C++20 Concepts

  • 3、继承基础:单继承、派生类、访问控制、构造/析构顺序、切片问题

  • 4、运行时多态:虚函数、纯虚函数(抽象类)、虚析构函数、override/final、dynamic_cast

  • 5、多重继承与虚继承:菱形问题、虚基类、二义性解决

  • 6、模板与继承的交互:CRTP、用模板约束继承关系(is_base_of / concepts)

  • 7、常见坑 & 最佳实践总结


一、C++ 模板函数(从基础到进阶)

1.1 函数模板 --- 基本用法

cpp 复制代码
template<typename T>
T Add(T a, T b) {
    return a + b;
}

int x = Add<int>(1, 2); // 显示指定
auto y = Add(1, 2);     // 编译器推导为 int

要点:

  • 模板定义通常放在头文件(因为编译器需要在使用点见到定义进行实例化)。

  • 支持显式模板参数 或由编译器类型推导

  • 函数模板可以与普通函数重载共存,重载解析遵循普通函数重载规则 + 模板优先级。


1.2 非类型模板参数 & 模板重载/特化

cpp 复制代码
template<typename T, int N>
class Array{
private: 
    T _a[N];
};

int main(){
    Array<int, 1000> a;
}
  • 非类型模板参数(如 N)允许在编译期传入常量。

  • 函数模板 可以做完整特化 (rare),但不能做部分特化。部分特化只适用于类模板。

可以作为非类型模板参数的类型

  • 整数类型(int, char, bool, 枚举等)

  • 指针或引用(必须是指向具有静态存储期的对象或函数的常量指针/引用)

  • std::nullptr_t

  • 注意:

  • 浮点型及自定义类型无法作为非类型模版参数

模版的特化(针对某些类型进行特殊化处理):

在函数模板显式特化时,函数签名必须和主模板一致,只有模板参数的具体化不同,否则不是有效的特化。

cpp 复制代码
template<typename T>
bool IsEqual(const T& left , const T& right){
    return left == right;
}
// 模版的特化(特殊化处理)
template<>
bool IsEqual<char*>(char* const& left , char* const& right){ 
    return (strcmp(left, right) == 0);
}

int main(){
    int a = 0, b = 1;
    cout << IsEqual(a,b)<<endl;
    const char* p1 = "Hello";
    const char* p2 = "World";
    cout<< IsEqual(p1,p2) << endl;
}

输出描述:

0

0


特化的分类:1、全特化。2、偏特化

cpp 复制代码
template<class T1, class T2>
class Data{
    public:
        Data(){
            cout<< "Data<T1,T2>"<<endl;
        }
    private:
        T1 _d1;
        T2 _d2;
};
template<>
class Data<int, char>{
    public:
        Data(){
            cout<< "全特化 Data<int,char>"<<endl;
        }
    private:
        ;
};
template<class T2>
class Data<int,T2>{
    public:
        Data(){
            cout<<"偏特化 Data<int,T2>"<<endl;
        }
    private:
        ;
};
int main(){
    Data<short, short> d1;
    Data<int, char> d2;
    Data<int, double> d3;
}

输出描述:

Data<T1,T2>
全特化 Data<int,char>
偏特化 Data<int,T2>


1.3 变参模板(variadic templates)与折叠表达式(C++17)

cpp 复制代码
// 递归版本(老式)
template<typename T>
T Sum(T v){ return v; }

template<typename T, typename... Ts>
T Sum(T head, Ts... tail) {
    return head + Sum(tail...);
}

// C++17 折叠表达式,简洁高效
template<typename... Ts>
auto Sum2(Ts... args) {
    return (args + ... + 0); // fold expression
}

1.4 转发引用与完美转发(perfect forwarding)

用于实现通用工厂/封装函数,避免不必要拷贝:

cpp 复制代码
template<typename T, typename... Args>
std::unique_ptr<T> MakeUnique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

注意:

  • T&& 在模板上下文中可能是 转发引用(又称万能引用)。

  • 使用 std::forward 将参数按原始值类别(左值/右值)传递,保留移动语义。


二、C++ 模板的分离编译

🔹 1. 模板的基本编译机制

  • 普通函数 / 类:编译器在编译 .cpp 文件时就能生成确定的符号。

  • 模板函数 / 类:只有在**实例化(instantiation)**时(即编译器遇到具体类型实参)才会生成代码。

👉 因此模板代码必须在编译时可见 ,这就是为什么我们通常把模板的实现,函数,类的声明都写在 .h 文件里,而不是 .cpp。


🔹 2. 为什么模板不能直接"分离编译"

假设你写了两个文件:

cpp 复制代码
// MyTemplate.h
template<typename T>
T add(T a, T b);
cpp 复制代码
// MyTemplate.cpp
#include "MyTemplate.h"
template<typename T>
T add(T a, T b) {
    return a + b;
}

然后在 main.cpp 里:

cpp 复制代码
#include "MyTemplate.h"
int main() {
    int x = add(1, 2);  // 想用 int 版本
}

⚠️ 这会导致 链接错误 :因为 main.cpp 编译时看到 add 的声明,但 .o 文件里没生成 add<int> 的实例化。

因为mytemplate.cpp 中函数模版并不知道其对应类型。实例化是在main.cpp

编译器不会自动去 MyTemplate.cpp 里"搜模板定义"。


🔹 3. 解决方法

有三种常见方法:

✅ 方法一:全写在头文件里(最常见)
cpp 复制代码
// MyTemplate.h
template<typename T>
T add(T a, T b) {
    return a + b;
}

这样所有用到 add 的翻译单元都能看到实现,编译器就能实例化。


✅ 方法二:显式实例化(explicit instantiation)

你可以在 .cpp 里告诉编译器:

"我需要这些具体类型的实例化,帮我生成一次即可"。

cpp 复制代码
// MyTemplate.h
template<typename T>
T add(T a, T b);
cpp 复制代码
// MyTemplate.cpp
#include "MyTemplate.h"

template<typename T>
T add(T a, T b) {
    return a + b;
}

// 显式实例化
template int add<int>(int, int);
template double add<double>(double, double);

这样 main.cpp 里用 add<int> 或 add<double> 时,链接器能找到在 MyTemplate.o 里已经生成的符号。

👉 适合库开发,不想把实现暴露在头文件时。


✅ 方法三:把实现放在 .tpp 或 .inl 文件中

约定俗成的做法:

  • .h 里只放声明

  • .tpp(或 .inl)里放实现

  • 在 .h 文件末尾 #include "MyTemplate.tpp"


这样使用者只需要 #include "MyTemplate.h",实现还是"逻辑分离"的。

  • 模板必须在实例化时可见,所以不能像普通函数那样分离编译。

  • 常见解决办法:

    1. 全部写在 .h(最常见)。

    2. 在 .cpp 中显式实例化需要的类型。

    3. .h + .tpp 组合,让逻辑结构更清晰。


三、总结

  • 模板:提供"编译期泛型"与静态多态。掌握变参模板、完美转发、SFINAE(或 Concepts)、std::void_t 检测,是写出高质量模板库的关键。优先用 Concepts(C++20)以获得更可读的接口约束。

相关推荐
91刘仁德6 小时前
c++ 类和对象(上)
开发语言·c++·经验分享·笔记·算法
泽虞6 小时前
《LINUX系统编程》笔记p8
linux·运维·服务器·c语言·笔记·面试
阿捏利9 小时前
C++ Primer Plus 第六版 第二章 编程题
c++·编程题·c++ primer plus
海洋的渔夫9 小时前
1-ruby介绍、环境搭建、运行 hello world 程序
开发语言·后端·ruby
≮傷£≯√12 小时前
C语言线程之死锁
c语言·线程·系统编程
葵野寺12 小时前
【RelayMQ】基于 Java 实现轻量级消息队列(五)
java·开发语言·java-rabbitmq
007php00712 小时前
Go 面试题: new 和 make 是什么,差异在哪?
后端·算法·docker·容器·面试·职场和发展·golang
AI 嗯啦13 小时前
Python 爬虫案例:爬取豆瓣电影 Top250 数据
开发语言·爬虫·python
彭刷子13 小时前
[c语言]简单的进行多次计算的+-*/计算机
c语言·学习