文章背景
大多数C ++程序员由于其困惑的性质而远离C ++模板。 反对模板的借口:
- 很难学习和适应。
- 编译器错误是模糊的,而且很长。
- 不值得的努力。
承认模板很难学习,理解和适应。 然而,我们从使用模板中获得的好处将超过负面影响。 有 比可以围绕模板包装的泛型函数或类要多得多。 我会说明他们。
从技术上讲,C ++模板和STL(标准模板库)是同级的。 在本文中,我只会介绍核心级别的模板。 本系列的下一部分将围绕模板介绍更高级和有趣的内容,以及有关STL的一些专门知识。
目录
语法句
- 功能模板
- 带有模板的指针,引用和数组
- 带有功能模板的多种类型
- 功能模板-模板功能
- 显式模板参数规范
- 功能模板的默认参数
类模板
- 具有类模板的多种类型
- 非类型模板参数
- 模板类作为类模板的参数
- 带类模板的默认模板参数
- 类的方法作为功能模板
文末杂谈
【零声学院官方许可】2小时精通掌握《STL模板库》技术
语法句
您可能知道,模板很大程度上使用尖括号:小于( <
)和大于( >
)运算符。 对于模板,它们总是以这种形式一起使用:
< Content >
哪里可以用Content
:
class T
/typename T
- 数据类型,映射到
T
- 整体规格
- 映射到上述规范的整数常量/指针/参考。
对于点1和2,符号 T
不过是某种数据类型,它可以是任何数据类型-基本数据类型( int
, double
等)或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
使用 函数 int
和 double
参数类型 两个 编译器将生成此函数的 实例:
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[]
-编译器是足够聪明来推断类型 int
从 int[]
(或 int*
)。 在上面给出的示例中,我已将其用作 T tArray[]
函数模板的参数,并且 实际数据类型 T
可以从中智能地确定的 。
通常,您会碰到过,并且还需要使用初始化,例如:
T tSum = T();
首先,这不是模板特定的代码-它属于C ++语言本身。 从本质上讲,这意味着:调用 的 默认构造函数 此数据类型 。 对于 int
,它将是:
int tSum = int();
有效地使用初始化变量 0
。 同样,对于 float
,它将将此变量设置为 0.0f
。 尽管尚未涵盖,但是如果用户定义的类类型来自 T
,它将调用该类的默认构造函数(如果可调用,否则相关的错误)。 如您所知,它 T
可能是任何数据类型,我们不能 初始化 tSum
简单地使用整数零( 0
)进行 。 实际上,它可能是某个字符串类,它使用空字符串( 对其进行初始化 ""
) 。
由于模板类型 T
可以是任何类型,因此它也必须 += operator
可用。 正如我们所知,它是可用于所有的基本数据类型( int
, float
, char
等)。 如果实际类型(用于 T
ÿ