很多人觉得C++模板很难学习和适应,不值得浪费时间,今天它的白痴指南来了(第一部分)

文章背景

大多数C ++程序员由于其困惑的性质而远离C ++模板。 反对模板的借口:

  • 很难学习和适应。
  • 编译器错误是模糊的,而且很长。
  • 不值得的努力。

承认模板很难学习,理解和适应。 然而,我们从使用模板中获得的好处将超过负面影响。 有 比可以围绕模板包装的泛型函数或类要多得多。 我会说明他们。

从技术上讲,C ++模板和STL(标准模板库)是同级的。 在本文中,我只会介绍核心级别的模板。 本系列的下一部分将围绕模板介绍更高级和有趣的内容,以及有关STL的一些专门知识。

目录

语法句

  • 功能模板
  • 带有模板的指针,引用和数组
  • 带有功能模板的多种类型
  • 功能模板-模板功能
  • 显式模板参数规范
  • 功能模板的默认参数

类模板

  • 具有类模板的多种类型
  • 非类型模板参数
  • 模板类作为类模板的参数
  • 带类模板的默认模板参数
  • 类的方法作为功能模板

文末杂谈

【零声学院官方许可】2小时精通掌握《STL模板库》技术


语法句

您可能知道,模板很大程度上使用尖括号:小于( < )和大于( > )运算符。 对于模板,它们总是以这种形式一起使用:

< Content >

哪里可以用Content

  1. class T / typename T
  2. 数据类型,映射到 T
  3. 整体规格
  4. 映射到上述规范的整数常量/指针/参考。

对于点1和2,符号 T不过是某种数据类型,它可以是任何数据类型-基本数据类型( intdouble等)或UDT。

让我们跳到一个例子。 假设您编写了一个输出数字两倍(两倍)的函数:

void PrintTwice(int data)
{
    cout << "Twice is: " << data * 2 << endl;         
}

可以称为传递一个 int

PrintTwice(120); // 240

现在,如果要打印a的两倍 double,则可以将此函数重载为:

void PrintTwice(double data)
{
    cout << "Twice is: " << data * 2 << endl;         
}

有趣的是,类型 ostream (该 cout对象)具有用于多个重载 operator << -适用于所有基本数据类型。 因此,相同/相似的代码对 int和都适用 double,并且我们的 不需要更改 PrintTwice重载 -是的,我们只是 复制粘贴了 它。 如果我们使用 printf-functions之一,则这两个重载看起来像:

void PrintTwice(int data)
{
    printf("Twice is: %d", data * 2 );
}

void PrintTwice(double data)
{
    printf("Twice is: %lf", data * 2 );
}

这里的关键是不是 cout还是 print要在控制台上显示,但有关代码-这是 绝对相同的 。 这是 之一 我们可以利用C ++语言提供的常规功能的众多情况 :模板!

模板有两种类型:

  • 功能模板
  • 类模板

C ++模板是一种编程模型,它允许 将 插入 任何数据类型 到代码(模板代码)中。 没有模板,您将需要为所有必需的数据类型一次又一次地复制相同的代码。 显然,如前所述,它需要代码维护。

无论如何,这是 的 简化版 PrintTwice使用模板 :

void PrintTwice(TYPE data)
{
    cout<<"Twice: " << data * 2 << endl;
}

在此,实际 类型 TYPE将被推断通过根据传递给函数的参数的编译器(确定)。 如果 PrintTwice被称为 PrintTwice(144);这将是一个 int,如果你通过 3.14这个功能, TYPE就可以推断为 double类型。

您可能会感到困惑 TYPE,即编译器将如何确定这是一个函数模板。 是否在 TYPE使用 定义了类型 typedef某处 关键字 ?

不,我的孩子! 在这里,我们使用关键字 template让编译器知道我们正在定义函数模板。

功能模板

这是 模板 函数 PrintTwice

template<class TYPE>
void PrintTwice(TYPE data)
{
    cout<<"Twice: " << data * 2 << endl;
}

第一行代码:

template<class TYPE>

告诉编译器这是一个 功能模板。 的实际含义 TYPE将由编译器根据传递给此函数的参数推导出。 这里的名称 TYPE称为 模板类型形参

例如,如果我们将该函数称为:

PrintTwice(124);

TYPE将被编译器替换为 int,并且编译器 实例 将该模板函数 化为:

void PrintTwice(int data)
{
    cout<<"Twice: " << data * 2 << endl;
}

并且,如果我们将此函数称为:

PrintTwice(4.5547);

它将另一个实例化为:

void PrintTwice(double data)
{
    cout<<"Twice: " << data * 2 << endl;
}

这意味着,在您的程序中,如果 调用 ,则 PrintTwice使用 函数 intdouble参数类型 两个 编译器将生成此函数的 实例:

void PrintTwice(int data) { ... }
void PrintTwice(double data) { ... }

是的,代码是重复的。 但是这两个重载是由编译器而不是程序员实例化的。 真正的好处是您不必 也不必 也不必 复制粘贴 相同的代码, 为不同的数据类型手动维护代码, 为稍后出现的新数据类型编写新的重载。 您只需要提供 的 模板 函数 ,其余的将由编译器管理。

由于现在有两个函数定义,因此代码大小也会增加。 代码大小(在二进制/汇编级别)将几乎相同。 实际上,对于 N 个数据类型, N 将创建 个相同函数(即重载函数)的实例。 如果实例化的函数相同,或者函数主体的某些部分相同,则存在高级的编译器/链接器级别优化,可以在某种程度上减小代码大小。 我现在不讨论它。

但是,积极的一面是,当您手动定义 N个 不同的重载(例如 N=10)时, 这 N个 无论如何都将对 不同的重载进行编译,链接和打包为二进制文件(可执行文件)。 但是,使用模板, 只有 所需的函数实例化才能进入最终可执行文件。 使用模板,函数的重载副本可能少于N,并且可能超过N-但恰好是所需副本的数量-不少!

另外,对于非模板实现,编译器必须编译所有这N个副本-因为它们在您的源代码中! 当您 附加 模板 使用通用函数 时,编译器将仅针对所需的数据类型集进行编译。 这基本上意味着,如果不同数据类型的数量小于 则编译会更快 N,

这将是一个完全有效的论据,即编译器/链接器可能会进行所有可能的优化,以从最终映像中删除未使用的非模板函数的实现。 但是,再次,请理解编译器必须 编译 所有这些重载(用于语法检查等)。 使用模板,仅针对所需的数据类型进行编译-您可以将其称为“ 按需编译 ”。

现在只有纯文字内容! 您可以返回并再次阅读。 让我们继续前进。

现在,让我们编写另一个函数模板,该模板将返回给定数字的两倍:

template<typename TYPE>
TYPE Twice(TYPE data)
{
   return data * 2;
}

您应该已经注意到,我使用的是typeName,而不是class。不需要,如果函数返回某些内容,则不需要使用typeName关键字。对于模板编程,这两个关键字非常相似。有两个关键字用于同一目的是有历史原因的,我讨厌历史。

但是,在某些情况下,您只能使用较新的关键字-TypeName。(当特定类型在另一个类型中定义,并且依赖于某个模板参数时-让我们将此讨论推迟到另一个部分)。

继续前进。当我们将此函数调用为:

cout << Twice(10);
cout << Twice(3.14);
cout << Twice( Twice(55) );

将生成以下函数集:

int     Twice(int data) {..}
double  Twice(double data) {..}

在上面截取的第三行代码中,调用了两次-第一次调用的返回值/类型将是第二次调用的参数/类型。因此,这两个调用都是int类型(因为参数类型和返回类型是相同的)。
如果模板函数是针对特定数据类型实例化的,则编译器将重用相同函数的实例-如果针对相同数据类型再次调用该函数。这意味着,无论在代码中的何处,您都可以使用相同类型的函数模板来调用函数模板-在相同的函数中,在不同的函数中,或者在另一个源文件(相同的项目/构建)中的任何位置。

让我们编写一个返回两个数字相加的函数模板:

template<class T>
T Add(T n1, T n2)
{
    return n1 + n2;
}

首先,我只是将模板类型参数的name-type替换为符号T。在模板编程中,您通常会使用T-但这是个人选择。最好使用反映类型参数含义的名称,这样可以提高代码的可读性。此符号可以是遵循C++语言中变量命名规则的任何名称。

其次,我为两个参数(n1和n2)重用了模板参数T-。

让我们稍微修改一下Add函数,该函数将把加法结果存储在局部变量中,然后返回计算值。

template<class T>
T Add(T n1, T n2)
{
    T result;
    result = n1 + n2;
    
    return result;
}

很容易解释,我在函数体中使用了类型参数T。您可能会问(您应该):“当编译器试图编译/解析函数add时,它如何知道结果的类型?”

那么,当查看函数模板体(Add)时,编译器不会看到T(模板类型参数)是否正确。它只需检查基本语法(如分号、关键字的正确使用、匹配的大括号等),并报告这些基本检查的错误。同样,它依赖于编译器来编译它如何处理模板代码-但是它不会报告任何由于模板类型参数而导致的错误。

为了完整起见,我要重申,编译器不会检查(目前仅与函数添加相关):

  • T具有默认构造函数(因此 T result;有效)
  • T支持使用 operator + (这样才 <code>n1+n2有效)
  • T具有 可访问的 副本/移动构造函数(因此该 return语句成功)

本质上,编译器必须分两个阶段编译模板代码:一次进行基本语法检查; 稍后对 每个实例化 函数模板的 -它将对模板数据类型执行实际的代码编译。

如果您不完全理解这两个阶段的编译过程,那完全可以。 阅读本教程时,您将获得坚定的理解,然后稍后再阅读这些理论课程!

也许干巴巴的文字看起来有些枯燥,如果单看文字不是很容易消化的话,可以进群973961276来跟大家一起交流学习,群里也有许多视频资料和技术大牛,配合文章一起理解应该会让你有不错的收获。

推荐一个不错的c/c++ 初学者课程,这个跟以往所见到的只会空谈理论的有所不同,这个课程是从六个可以写在简历上的企业级项目入手带领大家学习c/c++,正在学习的朋友可以了解一下。

带有模板的指针,引用和数组

首先是一个代码示例(不用担心-这是简单的代码段!):

template<class T>
double GetAverage(T tArray[], int nElements)
{
    T tSum = T(); // tSum = 0

    for (int nIndex = 0; nIndex < nElements; ++nIndex)
    {
        tSum += tArray[nIndex];
    }

    // Whatever type of T is, convert to double
    return double(tSum) / nElements;
}
  

int main()
{
    int  IntArray[5] = {100, 200, 400, 500, 1000};
    float FloatArray[3] = { 1.55f, 5.44f, 12.36f};

    cout << GetAverage(IntArray, 5);
    cout << GetAverage(FloatArray, 3);
}

对于第一个电话 GetAverage,在那里 IntArray通过,编译器将实例化这个功能:

double GetAverage(int tArray[], int nElements);

和类似的 float。 类型,因此保留返回 double由于数字的平均值在逻辑上适合 double数据 类型。 请注意,这仅是本示例-所包含的实际数据类型 T可能是一个类,可能无法转换为 double

您应该注意,函数模板可能具有模板类型参数以及非模板类型参数。 它不需要具有功能模板的所有参数即可从模板类型到达。 int nElements是这样的函数参数。

显然,注意和理解,模板类型参数只是 T,而不是 T*T[] -编译器是足够聪明来推断类型 intint[](或 int*)。 在上面给出的示例中,我已将其用作 T tArray[]函数模板的参数,并且 实际数据类型 T可以从中智能地确定的 。

通常,您会碰到过,并且还需要使用初始化,例如:

T tSum = T();

首先,这不是模板特定的代码-它属于C ++语言本身。 从本质上讲,这意味着:调用 的 默认构造函数 此数据类型 。 对于 int,它将是:

int tSum = int();

有效地使用初始化变量 0。 同样,对于 float,它将将此变量设置为 0.0f。 尽管尚未涵盖,但是如果用户定义的类类型来自 T,它将调用该类的默认构造函数(如果可调用,否则相关的错误)。 如您所知,它 T可能是任何数据类型,我们不能 初始化 tSum简单地使用整数零( 0)进行 。 实际上,它可能是某个字符串类,它使用空字符串( 对其进行初始化 "") 。

由于模板类型 T可以是任何类型,因此它也必须 += operator可用。 正如我们所知,它是可用于所有的基本数据类型( intfloatchar等)。 如果实际类型(用于 Tÿ

  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
是的,C++模板的定义和实现可以写在同一个文件中。事实上,C++编译器在编译过程中需要看到模板的定义和实现,所以将它们放在同一个文件中是常见且合理的做法。 C++模板通常包含两部分:头文件(.h或.hpp)和源文件(.cpp)。头文件中包含模板的声明和定义,而源文件中包含模板的实现。在使用模板的地方,编译器会根据需要将模板实例化为具体型的代码。 将模板定义和实现放在同一个文件中有以下几个优点: 1. 可读性和维护性:将模板定义和实现放在同一个文件中,可以更方便地查看和理解模板的完整实现。这样也更便于进行代码的维护和修改。 2. 编译效率:将模板定义和实现放在同一个文件中可以避免分离编译带来的额外开销。编译器在编译时可以直接看到完整的模板定义,从而生成正确的实例化代码,提高编译效率。 3. 依赖管理:将模板定义和实现放在同一个文件中可以简化依赖管理。当其他源文件需要使用该模板时,只需包含头文件即可,不需要额外处理源文件的依赖关系。 需要注意的是,模板的定义和实现通常都应该放在头文件中,并通过`#include`语句引入到其他源文件中。这样可以确保模板的定义在使用之前可见,以便编译器进行模板实例化。 总而言之,C++模板的定义和实现可以写在同一个文件中,这样有助于代码的可读性、维护性和编译效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值