从零开始C++----二.(下篇)模版进阶与编译全过程的复习

系列文章目录

提示:本篇将开始更深层次的学习关于模版的知识

如果模版的基础知识有遗忘的,可以看看下面这篇哦!

从零开始C++----二.模版


文章目录

目录

​编辑

系列文章目录

文章目录

前言

一、模版

1.非类型模版参数(支持缺省参数)

[2. 模板的特化](#2. 模板的特化)

[2.1 概念](#2.1 概念)

[2.2 函数与模版的执行顺序](#2.2 函数与模版的执行顺序)

1.三者同时存在

2.删去普通函数

3.类模板的特化

[3.1 全特化](#3.1 全特化)

[3.2 偏特化](#3.2 偏特化)

[3.3 特化的执行顺序](#3.3 特化的执行顺序)

二、模版的分离与编译

1.编译全过程的复习

[1.1 预处理阶段(最前期:文本替换)](#1.1 预处理阶段(最前期:文本替换))

[1.2 编译阶段(核心:把 C/C++ 变成汇编)](#1.2 编译阶段(核心:把 C/C++ 变成汇编))

[1.3 汇编阶段(把汇编 → 二进制机器码)](#1.3 汇编阶段(把汇编 → 二进制机器码))

[1.4 链接阶段(最后一步:合并所有 .o 文件)](#1.4 链接阶段(最后一步:合并所有 .o 文件))

2.什么是分离编译

2.1.模板的分离编译

三、模板总结


前言

提示:本章即将开启模版的升入学习,在学习后,一定要记得学了什么!

例如:1.什么是非类型模版参数

2.什么是模版的特化

3.类模板特化又分为哪几种情况?

4.编译的过程是怎样的?


提示:以下是本篇文章正文内容

一、模版

回顾:在上次讲解模版的时候我们简单提了一下非类型模版参数,接下来详细讲解一下

1.非类型模版参数(支持缺省参数)

模板参数分类类型形参与非类型形参
类型形参:出现在模板参数列表中,跟在 class 或者 typename 之类的参数类型名称
非类型形参,就是用一个常量作为类 ( 函数 ) 模板的一个参数,在类 ( 函数 ) 模板中可将该参数当成常 量来使用

cpp 复制代码
​
​
namespace CDSlice
{
// 定义一个模板类型的静态数组
    template<class T, size_t N = 10>
    class array
    {
    public:
    T& operator[](size_t index)
    {
        return _array[index];
    }
    const T& operator[](size_t index)const
    {
        return _array[index];
    }
    size_t size()const
    {
        return _size;
    }
    bool empty()const
    {
        return 0 == _size;
    }
    private:
    T _array[N];
    size_t _size;
    };
}

​

​

注意:

  1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的
  2. 非类型的模板参数必须在编译期就能确认结果
    举几个反例:
cpp 复制代码
// ❌ 非法:浮点数不能作为非类型模板参数
template<double PI>
struct Circle {};

// ❌ 非法:字符串字面量不能直接作为非类型模板参数
template<const char* str>
struct Msg {};
Msg<"hello"> m;
cpp 复制代码
✅ 合法:用常量表达式(如constexpr变量、字面量)作为参数
template<int N>
struct Array {
    int arr[N];
};
Array<10> a; // 编译期确定N=10,合法

❌ 非法:用运行时变量作为参数

int size = 10;
Array<size> a; // 错误:size不是编译期常量

2. 模板的特化

模板特化 = 给模板的「某些特定类型 / 参数」,单独写一套专属代码

  • 通用模板:给所有类型用的默认代码
  • 特化模板:给某一个 / 几个特定类型 用的定制代码

2.1 概念

通常情况下, 使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些
错误的结果 ,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板
函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇
怪的错误

cpp 复制代码
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl; // 可以比较,结果正确
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl; // 可以比较,结果正确
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl; // 可以比较,结果错误
return 0;
}

可以看到, Less 绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示
例中, p1 指向的 d1 显然小于 p2 指向的 d2 对象,但是 Less 内部并没有比较 p1 和 p2 指向的对象内
容,而比较的是 p1 和 p2 指针的地址,这就无法达到预期而错误。 此时,就需要对模板进行特化。
即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方 。模板特化中分为 函数模板特化
类模板特化
以下来具体演示一下:

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

// 日期类(方便比较)
struct Date
{
    int _year, _month, _day;

    Date(int y, int m, int d) : _year(y), _month(m), _day(d) {}

    // 重载 < 让日期能比较
    bool operator<(const Date& d) const
    {
        if (_year != d._year) return _year < d._year;
        if (_month != d._month) return _month < d._month;
        return _day < d._day;
    }
};

// 函数模板:通用比较
template<class T>
bool Less(T left, T right)
{
    cout << "使用【通用模板】比较:";
    return left < right;
}

 //特化:专门处理指针!!!(解决错误)
//template<>
//bool Less<Date*>(Date* left, Date* right)
//{
//    cout << "使用【指针特化】比较(解引用后比较):";
//    return *left < *right;
//}

int main()
{
    // 1. 普通 int
    cout << Less(1, 2) << endl;

    // 2. Date 对象:先定义大日期,再定义小日期
    Date d2(2022, 7, 8); // 大日期,先定义,地址低
    Date d1(2022, 7, 7); // 小日期,后定义,地址高

    cout << Less(d1, d2) << endl; // 对象比较,仍然是 1(正确)

    // 3. Date 指针
    Date* p1 = &d1; // 高地址
    Date* p2 = &d2; // 低地址
    cout << Less(p1, p2) << endl; // 地址比较:高地址 < 低地址 → 0(错误!)

    return 0;
}


可以看见运行后的结果为1,1,0,证明两者比较的是地址,而非对象内容,但是我一旦写了对应的模版特化

此时返回的是1,1,1,可见此时的比较是正确的
注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该
函数直接给出( 即不写模版,直接写对应函数 )

cpp 复制代码
bool Less(Date* left, Date* right)
{
return *left < *right;
}

2.2 函数与模版的执行顺序

结论:当三者同时存在时,按排序顺序决定调用的优先级

1. 普通函数(非模板)最高优先级!

2.模板特化

3.通用模板

以下以例子来证明:

1.三者同时存在
cpp 复制代码
#include <iostream>
using namespace std;

struct Date {
    int _year, _month, _day;
    Date(int y, int m, int d) : _year(y), _month(m), _day(d) {}
    bool operator<(const Date& d) const { return true; }
};

// 1. 【通用模板】
template<class T>
bool Less(T left, T right)
{
    cout << "通用模板\n";
    return left < right;
}

// 2. 【模板特化】
template<>
bool Less<Date*>(Date* left, Date* right)
{
    cout << "模板特化\n";
    return *left < *right;
}

// 3. 【普通函数】(非模板!)
bool Less(Date* left, Date* right)
{
    cout << "普通函数\n";
    return *left < *right;
}
int main()
{
    Date d1(2022, 7, 7), d2(2022, 7, 8);
    Less(&d1, &d2); 
}

当三者同时存在时,优先调用普通函数,因为这是现成的,编译器直接使用不香吗?

2.删去普通函数
cpp 复制代码
#include <iostream>
using namespace std;

struct Date {
    int _year, _month, _day;
    Date(int y, int m, int d) : _year(y), _month(m), _day(d) {}
    bool operator<(const Date& d) const { return true; }
};

// 1. 【通用模板】
template<class T>
bool Less(T left, T right)
{
    cout << "通用模板\n";
    return left < right;
}

// 2. 【模板特化】
template<>
bool Less<Date*>(Date* left, Date* right)
{
    cout << "模板特化\n";
    return *left < *right;
}

// 3. 【普通函数】(非模板!)
//bool Less(Date* left, Date* right)
//{
//    cout << "普通函数\n";
//    return *left < *right;
//}
int main()
{
    Date d1(2022, 7, 7), d2(2022, 7, 8);
    Less(&d1, &d2); 
}

可见,删去普通函数后,走的是模版特化

因此三者的优先级顺序一目了然


3.类模板的特化

3.1 全特化

全特化即是将模板参数列表中所有的参数都确定化

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:
    int _d1;
    char _d2;
};
void TestVector()
{
    Data<int, int> d1;
    Data<int, char> d2;
}

3.2 偏特化

偏特化:任何针对模版参数进一步进行条件限制设计的特化版本

比如对以下类模版:

cpp 复制代码
template<class T1, class T2>
class Data
{
    public:
    Data() {cout<<"Data<T1, T2>" <<endl;}
    private:
    T1 _d1;
    T2 _d2;
};

偏特化有以下两种表现方式:

1. 部分特化: 将模板参数类表中的一部分参数特化

cpp 复制代码
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
    public:
    Data() {cout<<"Data<T1, int>" <<endl;}
    private:
    T1 _d1;
    int _d2;
};

2.参数更进一步的限制:
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一
个特化版本

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

// 主模板(通用版本)
template <typename T1, typename T2>
class Data {
public:
    Data() {
        cout << "通用版本 Data<T1, T2>" << endl;
        cout << "T1: " << typeid(T1).name() << endl;
        cout << "T2: " << typeid(T2).name() << endl;
    }
};

// 偏特化版本:第一个参数是引用,第二个是指针
template <typename T1, typename T2>
class Data <T1&, T2*> {
public:
    Data() {
        cout << "特化版本 Data<T1&, T2*>" << endl;
        int a = 0;
        T1& x = a;   // 等价于 int& x = a;
        T2* y = &a;  // 等价于 int* y = &a;
        T1 z = a;    // 等价于 int z = a;

        cout << "x 的类型: " << typeid(x).name() << endl;
        cout << "y 的类型: " << typeid(y).name() << endl;
        cout << "z 的类型: " << typeid(z).name() << endl;
    }
};

int main() {
    // 匹配通用版本
    Data<int, double> d1;
    cout << "---------------------" << endl;
    // 匹配偏特化版本
    Data<int&, int*> d2;
    return 0;
}

运行结果可知:


3.3 特化的执行顺序

结论:

  1. 全特化(最优先)
  2. 偏特化(其次)
  3. 主模板(最后兜底)
cpp 复制代码
// 1. 主模板(兜底)
template <typename T>
class A { ... };

// 2. 偏特化(模式匹配)
template <typename T>
class A<T*> { ... };  // 匹配所有指针类型

// 3. 全特化(精确匹配,最高优先级)
template <>
class A<int> { ... }; // 只匹配 int

结果:

cpp 复制代码
A<int>    a;  // ✔ 全特化(最高优先级)
A<double> b;  // ✔ 主模板(无匹配)
A<int*>   c;  // ✔ 偏特化(无全特化,匹配模式)
A<float*> d;  // ✔ 偏特化

多个偏特化同时存在时怎么办?

规则:越精确、越特殊,优先级越高

cpp 复制代码
template <typename T>         class X {};        // 主模板
template <typename T>         class X<T*> {};    // 偏特化1(指针)
template <typename T>         class X<T&> {};    // 偏特化2(引用)
template <>                   class X<in

结果:

cpp 复制代码
X<int>    → 全特化
X<int*>   → 偏特化1
X<int&>   → 偏特化2
X<double> → 主模板

二、模版的分离与编译

1.编译全过程的复习

C/C++代码从写完到运行的完整编译过程共分为四个步骤:1.预处理 (Preprocessing)2.编译 (Compilation)3.汇编 (Assembly)4.链接(Linking)


1.1 预处理阶段(最前期:文本替换)

  • 展开头文件 #include <iostream>→ 直接把 iostream 文件内容复制粘贴到当前代码里

  • 宏替换 #define MAX 100→ 代码里所有 MAX 变成 100

  • 删除注释 所有 ///* */ 全部删掉

  • 处理条件编译 #if #ifdef #endif 这些

最终:将.cpp/.c文件 → .i文件


1.2 编译阶段(核心:把 C/C++ 变成汇编)

  • 语法检查(有没有少分号、括号不匹配、类型错误)
  • 语义分析(函数调用是否合法、类型是否匹配)
  • 模板实例化!!! 上面写的 Less<int>Less<Date*> 在这里生成真实函数
  • 生成汇编代码

最终:将.i文件 → .s文件(汇编代码文件)


1.3 汇编阶段(把汇编 → 二进制机器码)

汇编器把 .s 文件翻译成:

  • 二进制指令
  • 数据
  • 符号表(函数名、变量名、地址)

最终:将.s文件 → .o文件(目标文件,二进制)

注意:此时还不能运行,因为还没链接


1.4 链接阶段(最后一步:合并所有 .o 文件)

  • 找到你调用的函数的实现, 比如 coutstrcmpstd::string 这些都在标准库

  • **合并所有 .o 文件,**比如 a.cpp + b.cpp + main.cpp 会合成一个程序

  • 确定函数地址、变量地址把 "函数名" 变成真正的内存地址

最终: main.o + 库文件 → a.exe / a.out(可直接运行)


2.什么是分离编译

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式

2.1.模板的分离编译

假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:

cpp 复制代码
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}

分析:

原因:

解决方法

  1. 将声明和定义放到一个文件 "xxx.cpp" 里面或者 xxx.h 其实也是可以的 。推荐使用这种
  2. 模板定义的位置显式实例化 。这种方法不实用,不推荐使用

三、模板总结

【优点】

  1. 模板复用了代码,节省资源,更快的迭代开发, C++ 的标准模板库 (STL) 因此而产生
  2. 增强了代码的灵活性
    【缺陷】
  3. 模板会导致代码膨胀问题,也会导致编译时间变长
  4. 出现模板编译错误时,错误信息非常凌乱,不易定位错误
相关推荐
23471021272 小时前
4.17 学习笔记
开发语言·软件测试·笔记·python·学习
智者知已应修善业2 小时前
【51单片机按键控制流水灯+数码管显示按键次数】2023-6-15
c++·经验分享·笔记·算法·51单片机
汉克老师2 小时前
GESP2023年12月认证C++三级( 第三部分编程题(1、小猫分鱼))
c++·算法·模拟算法·枚举算法·gesp三级·gesp3级
不知名的老吴2 小时前
View的三大特性之一:迟绑定
开发语言·c++·算法
深邃-2 小时前
【Web安全】-基础环境安装:虚拟机安装,JDK环境安装(1)
java·开发语言·计算机网络·安全·web安全·网络安全·安全架构
前端老石人2 小时前
前端网站换肤功能的 3 种实现方案
开发语言·前端·css·html
Legendary_0082 小时前
LDR6500U PD取电芯片:赋能设备Type-C升级,解锁高效安全取电新体验
c语言·开发语言·安全
Rust研习社2 小时前
深入理解 Rust 裸指针:内存操作的双刃剑
开发语言·后端·rust
Huangjin007_2 小时前
【C++ STL篇(四)】一文拿捏vector常用接口!
开发语言·c++·学习