很多人觉得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
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值