【C++初阶:模板初阶】函数模板 | 类模板

简介: 【C++初阶:模板初阶】函数模板 | 类模板

文章目录

【写在前面】

之前在学数据结构时就说过,C 语言没有实现数据结构的库,非常的麻烦,为什么 C 语言没有这样的库呢,因为它不支持泛型编程。所以在 C++ 中它支持了泛型编程,支持了模板 —— 函数模板、类模板。

这里只是模板的入门,只为了先入门 STL。后面 C++ 初阶会对模板进行进阶的学习。

一、泛型编程

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。

如何实现一个通用的交换函数 ❓

void Swap(int& left, int& right)
{
  int temp = left;
  left = right;
  right = temp;
}
void Swap(double& left, double& right)
{
  double temp = left;
  left = right;
  right = temp;
}
int main()
{
  int a = 0, b = 1;
  double c = 1.1, d = 2.2;
  Swap(a, b);
  Swap(c, d);
  return 0;
}

📝说明

虽然能够达到目的,但是也有缺陷:

  1. 重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数
  2. 代码的可维护性比较差,一个出错可能所有的重载都出错

能否告诉编译器一个模子,让编译器根据不同的类型利用该模子生成代码 ❓

在 C++ 中,也能够存在这样一个模具,通过给这个模具中填充不同材料 (类型),来获得不同材料的铸件 (生成具体类型的代码),那将会节省许多头发。巧的是前人早已将树栽好,我们只需在此乘凉。

二、函数模板

💦 函数模板的概念

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

💦 函数模板的格式

template<typename T1, typename T2,…,typename Tn>

返回值类型 函数名(参数列表){}

template<typename T>
template<class T>

📝说明

对于 T 我们可以任意起,如 L、K、Compare 等,这个名称是自己取的,但是一般要符合意境,前期一般喜欢用 T (Type 的意思)。

注意 typename 是用来定义模板参数的关键字,也可以使用 class (这里不能用 struct 代替 class)。typename 是新增的,好多地方都习惯用 class,包括后面学习的 STL (它的源码里都用的 class),现阶段可以认为 typename 和 class 没有区别,到后面会有一点区别,学到了再看。

函数模板实现交换 ❓

//如果写了对应的函数,那么它依然会去调用,但是没必要
/*void Swap(int& left, int& right)
{
  int temp = left;
  left = right;
  right = temp;
}*/
template<class T>
void Swap(T& x1, T& x2)
{
  T tmp = x1;
  x1 = x2;
  x2 = tmp;
}
int main()
{
  int a = 0, b = 1;
  double c = 1.1, d = 2.2;
  int* p1 = &a, *p2 = &b;
  Swap(a, b);
  Swap(c, d);
  Swap(p1, p2);
  return 0;
}

📝说明

这里就可以知道它可以针对所有类型完成交换工作。

那它一个函数能完成这里几种函数的功能吗 ???

💦 函数模板的原理

那么如何解决上面的问题呢?大家都知道,瓦特改良蒸汽机,人类开始了工业革命,解放了生产力。机器生产淘汰掉了很多手工产品。本质是什么,重复的工作交给了机器去完成。有人给出了论调:懒人创造世界。

这里的懒人指的是行动上变懒了 (只是不想做重复的事情),思想上并没有滑坡。

马云:世界是懒人创造的

如上代码一个函数能完成这里几种函数的功能吗 ❓

显然是不能的

它们在调用的时候都要执行 Swap 函数,如果它是一段指令,它是没法完成的。经调试每次调用都往 Swap 函数里走,实际上 VS 的编译器为了方便调试所以在调试器上做了手脚,所以实际还是调用了对应的函数,这个过程叫做模板的实例化。

可以看到汇编代码,它们依然去调用对应的函数:

编译器是怎么帮我们完成的呢 ❓

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器,可以看到模板就是让你写的时候省劲了,但实际调用还是无差别。

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用 double 类型使用函数模板时,编译器通过对实参类型的推演,将 T 确定为 double 类型,然后产生一份专门处理 double 类型的代码,对于其它类型也是如此。

对于类型的推演,跟 auto 有关系吗 ???

并没有关系。这里的场景是不一样的:这里是针对调用一个函数或是下面的类时,根据用的角度去指定参数,然后把参数替换生成对应的代码;auto 是不能做参数和返回值的,它是在定义变量的时候用 —— auto e = 3.14; 它是根据 3.14 的类型去推演 e 的类型。可以看到虽然它们使用场景不一样,但是原理还是很相似的。

库里的 swap ❓

其实 C++ 库里有给 swap 的实现 (也就是说不需要自己写了):

int main()
{
  int a = 3, b = 5;
  swap(3, 5);
  int i(1);
  int(10);//匿名
  return 0;
}

📝说明

到了这里可以认为内置类型是有构造函数的,当然这里只是用法上的一个构造 —— 初始化

比如后面要学的 STL ,不排除 T 就是 int :

💦 函数模板的实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。

  1. 隐式实例化:让编译器根据实参推演模板参数的实际类型
  2. 显式实例化:在函数名后的 <> 中指定模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
  return left + right;
}
int main()
{
  int a1 = 10, a2 = 20;
  double d1 = 10.1, d2 = 20.2;
  cout << Add(a1, a2) << endl;//ok
  cout << Add(d1, d2) << endl;//ok
  //cout << Add(a1, d2) << endl;//???
  //1、
  cout << Add(a1, (int)d2) << endl;
  cout << Add((double)a1, d2) << endl;
  //2、上面是实参去推演形参的类型,这里不需要推演,显示实例化指定T的类型
  cout << Add<int>(a1, d2) << endl;
  cout << Add<double>(a1, d2) << endl;
  return 0;
}

📝说明

Add(a1, d2):该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型,通过实参 a1 将 T 推演为 int,通过实参 d1 将 T 推演为 double,但模板参数列表中只有一个 T,所以编译器无法确定此处到底该将 T 确定为 int 还是 double 而报错。

怎么解决呢 ❓

假设我们不用模板,那么编译器就不会推演了,而这里能从 double 到 int 的原因是它们是相近类型,其中发生了隐式类型转换。

这里有两种处理方式:

  • 用户自己强转
  • 使用显式实例化

显式实例化的场景 ❓

class A
{
public:
   A(int x)
   {}
};
template<class T>
T func(int x)
{
  T a(x);
  return a;
}
int main()
{
  //func(1);//err
  func<A>(1);
  func<int>(1);
  return 0;
}

📝说明

有些函数模板里面参数中没用模板参数,函数体内才有用,也就意味着无法通过参数推演 T 的类型,只能显示实例化。

💦 函数模板的匹配规则

  1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
//专门处理int的加法函数q 
int Add(int left, int right)
{
  return left + right;
}
//通用加法函数
template<class T>
T Add(T left, T right)
{
  return left + right;
}
int main()
{
  //模板匹配原则:
  //1、有现成完全匹配的,就直接调用,没有现成调用的,实例化模板生成
  Add(1, 2);
  //2、有需要转换匹配的,那么它会优先选择去实例化模板生成
  Add(1.1, 2.2);
  return 0;
}

📝说明

Add(1, 2):

比如说,今天你回家了,着急吃饭,家里没有人,你吃饭有两种方式,用妈妈给你的钱点外卖、冰箱里有菜自己做,那肯定是点外卖。编译器也是一样,第一个是现成的,直接调用就行,第二个编译器还要根据实参推导形参生成。所以编译器它会去调用第一个函数。

Add(1.1, 2.2):

镇上没有外卖,只能自己做。所以它会去调用第二个函数。

如果你妈不想让你点外卖,你必须得自己做 ❓

显示实例化:Add<< int >(1, 2);

  1. 对于非模板函数和同名函数模板,如果其他条件都相同,在调用时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
//专门处理int的加法函数
int Add(int left, int right)
{
  return left + right;
}
//通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
  return left + right;
}
void Test()
{
  Add(1, 2); //与非函数模板类型完全匹配,不需要函数模板实例化
  Add(1, 2.0); //模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}
  1. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

小结:完全匹配 > 模板 > 转换匹配

三、类模板

💦 类模板的定义格式

template<class T1, class T2, ..., class Tn>
class 类模板名
{
 //类内成员定义
};

栈的泛型问题 ❓

typedef int STDataType;
class Stack
{
private:
  STDataType* _a;
  int _top;
  int _capacity;
};
int main()
{
  Stack st1;//int
  Stack st2;//double
  return 0;
}

📝说明

这里想让 st1 是 int,st2 是 double,显然这里做不到。C 语言中的 typedef 只是增强程序的可维护性,不能解决泛型的问题。

类模板解决栈泛型 ❓

struct TreeNode
{
};
template<class T>
class Stack
{
private:
  T* _a;
  int _top;
  int _capacity;
};
int main()
{
  Stack<TreeNode>st1;//TreeNode*
  Stack<int>st2;//int
  return 0;
}

📝说明

对于函数模板可以根据实参去推演形参的类型,但是类在用的时候,首先是定义对象,所以类模板的使用都是显示实例化。

Stack< TreeNode* >st1 和 Stack< int >st2 用的是一个类 ❓

它们的模板参数不同,用的不是同一个类。

调试后发现,st1 里的 _a 是 TreeNode* 类型, st2 里的 _a 是 int* 类型。

💦 类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。

//Stack类名,Stack<int>才是类型
Stack<int>s1;
Stack<double>s2;
struct TreeNode
{};
template<class T>
class Stack
{
public:
  Stack(int capacity = 4)
    : _a(new T[capacity])
    , _top(0)
    , _capacity(capacity)
  {}
  ~Stack()
  {
    delete[]_a;
    _a = nullptr;
    _top = _capacity = 0;
  }
  //类里面声明,类外面定义呢???
  void Push(const T& x);
private:
  T* _a;
  int _top;
  int _capacity;
};
template<class T>
void Stack<T>::Push(const T& x)//指定域,且需要声明模板
{}
int main()
{
  Stack<TreeNode*>st1;//TreeNode*
  Stack<int>st2;//int
  return 0;
}

📝说明

在类模板里声明,类模板外定义,与以前的不同。

普通类,类名就是类型

类模板,类名不是类型,类型是 Stack

函数/类模板不支持把声明写到 .h,定义写到 .cpp 的方式,会报链接错误,原因后面会详细讲。

.h里实例化了,Stack.cpp里没有实例化,test.cpp去找的时候,只有声明,没有定义。所以解决方法就是声明和定义不要分离,当然要分离也有方法,但是这种方法比较 low,在模板的进阶会详细学习。


相关文章
|
11天前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
51 18
|
11天前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
37 13
|
11天前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
37 5
|
11天前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
26 5
|
11天前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
32 4
|
11天前
|
设计模式 IDE 编译器
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
26 3
|
2月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
76 2
|
2月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
128 5
|
2月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
138 4
|
2月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
195 4