当前位置: 首页 > news >正文

C++——模板初阶

目录

引言

泛型编程

函数模板

1.函数模板的基本用法

2.函数模板的原理

3.函数模板的实例化

3.1 隐式实例化

3.2 显式实例化

(1)先强制类型转换

(2)指定模板参数的实际类型

4.函数模板的匹配规则

类模板

1.类模板的基本用法

2.栈类模板的实现

3.类模板的实例化

4.类模板为何优于 typedef

非类型模板参数

1.概念与用法

2.使用示例

模板的特化

1.函数模板特化

2.类模板的特化

3.模板的特化方式

3.1 全特化

3.2 偏特化

结束语


引言

在学习完类与对象之后,我们接下来学习C++中一重要特性——模板

泛型编程

首先我们先来思考:如何实现一个通用的交换函数呢?

void swap(int& x, int& y)
{int tmp = x;x = y;y = tmp;
}

显然,这个代码有很大的弊端,每一种类型都写一个,同时每个函数名都不能相同,无疑是非常繁琐的。这一点我们可以利用C++中函数重载实现,支持函数可以同名。

像这样:

void swap(int& x, int& y)
{int tmp = x;x = y;y = tmp;
}
void swap(double& x, double& y)
{int tmp = x;x = y;y = tmp;
}

但是我们如果需要实现支持字符串,或者其他自定义类型,还需要继续添加一个swap函数。这样实在是太麻烦了,也显得代码十分冗长。

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

为了解决这个问题,C++引入了模版这个概念,并且依次延伸出了泛型编程的思想。

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

C++中的模板主要分为两种:函数模板类模板

函数模板

1.函数模板的基本用法

函数模板允许定义一个函数,其参数类型或返回类型在编译时才确定。这样,同一个函数模板可以用于多种不同的数据类型,而无需为每种类型都编写一个独立的函数。

具体语法如下:

template <typename T> // 或者使用 class 关键字代替 typename  
void func(const T& x)
{  // 函数体  
}

也可以实现两个类型及以上的模板:

template<typename T1, typename T2>
void func(const T1& x, const T2& y)
{// 函数体
}

我们可以根据函数模板写一段实现针对不同类型的交换函数,代码如下:

template<typename T>
void Swap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}int main()
{int a = 0;int b = 1;cout << "交换前:" << a << "," << b << endl;Swap(a, b);cout << "交换后:" << a << "," << b << endl;double c = 1.14, d = 5.14;cout << "交换前:" << c << "," << d << endl;Swap(c, d);//调用浮点型cout << "交换后:" << c << "," << d << endl;return 0;
}

注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用 struct 代替 class)

输出结果为:

2.函数模板的原理

我们思考一下:不同的类型调用的函数模版是否是同一个函数?

我们接下来通过反汇编来观察一下上面那段代码:

通过反汇编可以观察到:调用实参类型不同时,调用的函数不同。

那么函数模版到底是如何调用的呢?

实际上,函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。 所以其实模板就是将本来应该我们做的重复的事情交给了编译器

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

3.函数模板的实例化

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

3.1 隐式实例化

隐式实例化:让编译器根据实参推演模板参数的实际类型

template<class T>
T Add(const T& x, const T& y)
{return x + y;
}int main()
{// 编译器自动推导类型cout << (Add(3, 5)) << endl;return 0;
}
3.2 显式实例化

在某些特定的场景无法使用隐式实例化,如下所示:

template<class T>
T Add(const T& x, const T& y)
{return x + y;
}int main()
{int a = 1;double b = 3.14;cout << (Add(a, b)) << endl;return 0;
}

在这里编译器会报错:

这是因为编译器不知道把T推演成 int 还是 double 。为了解决这个问题,我们通常有以下两种方法:

(1)先强制类型转换
template<class T>
T Add(const T& x, const T& y)
{return x + y;
}int main()
{int a = 1;double b = 3.14;cout << (Add(a, (int)b)) << endl;return 0;
}
(2)指定模板参数的实际类型
template<class T>
T Add(const T& x, const T& y)
{return x + y;
}int main()
{int a = 1;double b = 3.14;cout << (Add<int>(a, b)) << endl;return 0;
}

如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。

4.函数模板的匹配规则

(1)一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

// 专门处理int的加法函数
int Add(int x, int y)
{cout << "int Add(int x, int y)" << endl;return x + y;
}// 通用加法函数
template<class T>
T Add(T x, T y)
{cout << "T Add(T x, T y)" << endl;return x + y;
}void Test()
{Add(1, 2);		// 与非模板函数匹配,编译器不需要特化Add<int>(1, 2); // 调用编译器特化的Add版本
}int main()
{Test();return 0;
}

输出结果为:

(2)对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而 不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板

// 专门处理int的加法函数
int Add(int x, int y)
{cout << "int Add(int left, int right)" << endl;return x + y;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 x, T2 y)
{cout << "T1 Add(T1 x, T2 y)" << endl;return x + y;
}
void Test()
{Add(1, 2);		// 与非函数模板类型完全匹配,不需要函数模板实例化Add(1, 2.0);	// 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函
}int main()
{Test();return 0;
}

输出结果为:

类模板

类模版与函数模版类似,只不过作用对象是类。

1.类模板的基本用法

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

来看个简单的例子:

template<class T>
class A
{
public:A(T x, T y):_x(x),_y(y){}T add(){return _x + _y;}
private:T _x;T _y;
};int main()
{A<int> a1(5, 3);cout << a.add() << endl; A<double> a2(1.14, 5.14);cout << b.add() << endl;return 0;
}

注意:类模板中函数放在类外进行定义时,需要加模板参数列表。

如下所示:

template<class T>
class A
{
public:A(T x, T y):_x(x),_y(y){}T add();
private:T _x;T _y;
};//类外实现
template<class T>
T A<T>::add()
{return _x + _y;
}

输出结果为:

2.栈类模板的实现

我们可以尝试写一个栈类的模板:

template<class T>
class Stack
{
public:// 构造函数  Stack(int n = 4): _array(new T[n]), _capacity(n), _size(0){// ...}// 析构函数  ~Stack(){delete[] _array;_array = nullptr;_capacity = _size = 0;}Stack(const Stack&) = delete;Stack& operator=(const Stack&) = delete;// 向栈中添加新元素x  void Push(const T& x){// 检查是否需要扩容  if (_size == _capacity){T* tmp = new T[2 * _capacity];memcpy(tmp, _array, sizeof(T) * _size);delete[] _array;_array = tmp;_capacity *= 2;}_array[_size++] = x;}// 弹出栈顶元素  void Pop(){assert(!IsEmpty()); // 确保栈不为空  --_size;}// 获取栈顶元素(不弹出)  T& Top(){assert(!IsEmpty()); // 确保栈不为空  return _array[_size - 1];}// 检查栈是否为空  bool IsEmpty() const{return _size == 0;}// 获取栈的大小  size_t Size() const{return _size;}private:T* _array;size_t _capacity;size_t _size;
};

顺便写个main函数测试一下:

int main()
{Stack<int> intStack;intStack.Push(1);intStack.Push(2);intStack.Push(3);intStack.Push(4);cout << "栈顶元素: " << intStack.Top() << endl; intStack.Pop();         // 弹出一个元素cout << "现在栈顶元素: " << intStack.Top() << endl; cout << "栈的大小: " << intStack.Size() << endl; // 测试IsEmpty(此时栈不应为空)  cout << "栈是否为空: " << (intStack.IsEmpty() ? "是" : "否") << endl; return 0;
}

输出结果为:

3.类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的 类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。所以利用类模版创建对象时必须要对类模版先显式实例化。

像这个:

int main()
{Stack<int> st1;st1.Push(1);Stack<double> st2;st2.Push(3.14);return 0;
}

4.类模板为何优于 typedef

类模板相比于typedef在C++编程中具有显著的优越性,这主要体现在它们各自的功能和应用场景上。typedef主要用于为现有的类型定义一个新的名称(别名),而类模板则提供了一种更灵活、更强大的方式来定义可以在多种数据类型上工作的类。

类模板:通过类模板,可以定义一个与具体数据类型无关的类,这个类可以在实例化时指定数据类型。这种特性使得类模板能够在多种数据类型上重用相同的代码,减少了代码冗余,提高了代码的复用性。同时,它也支持泛型编程,使得函数或类可以更加通用,不依赖于具体的数据类型。

typedef:typedef只是为现有的类型定义了一个新的名称,它本身并不支持泛型编程,也不具备在不同数据类型上重用代码的能力。

非类型模板参数

非类型模板参数(Non-type Template Parameters)是C++模板编程中的一个重要概念,它允许模板参数不仅仅是类型,还可以是常量值或表达式。

1.概念与用法

概念:非类型模板参数是指模板参数列表中不是类型参数的部分,它们可以是整型常量、枚举常量、指针或引用等,但不能是浮点数或类类型的对象。这些参数在模板实例化时被具体的值所替代。

用法:非类型模板参数常用于需要根据不同值生成不同代码的场景,如定义固定大小的数组、控制循环次数等

2.使用示例

// 定义一个模板类型的静态数组
template<class T, size_t n>
class array
{
public:T& operator[](size_t index){return _array[index];}const T& operator[](size_t index)const{return _array[index];}private:T _array[n];//定义数组
};

我们可以通过传参实现控制静态数组的大小,如下所示:

array<int, 10> a;		//开辟一个10个整型大小的数组
array<double, 20> b;	//开辟一个20个浮点型大小的数组
  • 非类型模板参数必须是常量表达式,且其值在编译时已知。
  • 浮点类型和非类类型对象不能作为非类型模板参数。

模板的特化

模板的特化,在C++编程中,是一种对模板的特定类型或类型组合提供专门实现的技术。模板特化有时也被称为模板的具体化,主要分为函数模板特化和类模板特化两种。

1.函数模板特化

函数模板特化是在一个统一的函数模板不能在所有类型实例下正常工作时,需要定义类型参数在实例化为特定类型时函数模板的特定实现版本。这通常是因为对于某些特定类型,通用的函数模板实现可能无法满足需求或者会产生不正确的结果。

函数模板特化需要做到:

(1)存在基础模板

(2)有关键字template与空尖括号

(3)指定特化类型

(4)保持参数一致性

// 模板类A,包含一个公共的setValue函数 
template<class T>
class A 
{
public:T value;A(T v) : value(v) {// ...}// 一个通用的setValue函数(不是特化的主体)  void setValue(T v) {value = v;cout << "泛化版本的setValue" << endl;}
};template<class T>
void Print(T v) 
{cout << "泛化版本的Print: " << v << endl;
}// 对Print函数的int类型特化  
template<>
void Print<int>(int v) 
{cout << "特化版本的Print: " << v << endl;
}int main() 
{A<double> aDouble(3.14);  // 使用泛化版本的A类  aDouble.setValue(2.71);   // 调用泛化版本的setValue  Print(3.14);         // 调用泛化版本的PrintPrint(42);           // 调用特化版本的Print  return 0;
}

 输出结果为:

2.类模板的特化

类模版的特化与函数模版特化类似。

举个简单的例子:

template<class T>
class A
{
public:// 泛化版本的构造函数,接受一个类型为T的参数v// 用于初始化成员变量_valueA(T v) : _value(v){cout << "泛化版本" << endl;}
private:T _value;
};template<>
class A<int>
{
public:// 特化版本的构造函数,接受一个int类型的参数v// 用于初始化成员变量_valueA(int v) : _value(v){cout << "特化版本" << endl;}
private:int _value;
};int main() 
{A<double> aDouble(3.14);  // 使用泛化版本  A<int> aInt(42);          // 使用特化版本  return 0;
}

输出结果为:

3.模板的特化方式

模板的特化方式主要分为两种:全特化和偏特化。这些特化方式适用于模板类和模板函数。

3.1 全特化

全特化是指为模板的所有类型参数提供一个完全不同于通用模板版本的特殊实现。在全特化中,模板的所有类型参数都被明确指定,并且为这些特定的类型参数组合提供了一个全新的实现。

// 泛化版本的类模板A  
template<class T1, class T2>
class A 
{
public:A(T1 v1, T2 v2) : _value1(v1), _value2(v2) {cout << "泛化版本" << endl;}private:T1 _value1;T2 _value2;
};// A的全特化版本,其中T1和T2都被特化为int  
template<>
class A<int, int> 
{
public:A(int v1, int v2) : _value1(v1), _value2(v2) {cout << "全特化版本" << endl;}private:int _value1;int _value2;
};int main() 
{A<double, int> aDoubleInt(3.14, 42);  // 使用泛化版本  A<int, int> aIntInt(1, 2);            // 使用全特化版本  return 0;
}
3.2 偏特化

偏特化是指当模板有多个类型参数时,只对其中一部分类型参数进行特化,而保留其他类型参数为通用模板中的形式。偏特化仅适用于类模板,不适用于函数模板

// 泛化版本的类模板A  
template<class T1, class T2>
class A 
{
public:// 泛化版本的构造函数  A(T1 v1, T2 v2) : _value1(v1), _value2(v2) {cout << "泛化版本" << endl;}private:T1 _value1;T2 _value2;
};// A的偏特化版本,其中T1被特化为int  
template<class T2>
class A<int, T2> 
{
public:// 偏特化版本的构造函数  A(int v1, T2 v2) : _value1(v1), _value2(v2) {cout << "偏特化版本" << endl;}private:int _value1;T2 _value2;
};int main() 
{A<double, int> aDoubleInt(3.14, 42);  // 使用泛化版本  A<int, int> aIntInt(1, 2);        // 使用偏特化版本  return 0;
}

结束语

哇最近有点摆烂,写的有一捏捏慢。

花了一段时间终于是把模板的一些内容简要的写了一下。

感谢各位大佬的支持!!!

求点赞收藏评论关注!!!


http://www.mrgr.cn/news/32935.html

相关文章:

  • 智慧校园建设解决方案建设系统简介
  • C Prime Plus 第6章习题
  • 索引的使用
  • Hadoop的安装
  • 【推广】图书|2024新书《大模型RAG实战:RAG原理、应用与系统构建》汪鹏、谷清水、卞龙鹏等,机械工业出版社
  • CDVAE项目环境配置
  • cv环境设置
  • expressjs 如何封装接口响应数据
  • 用 HTML + JavaScript DIY 一个渐进式延迟法定退休年龄测算器
  • Linux操作系统面试题记录
  • 行阶梯形矩阵的定义,通过正例和反例说明如何判断一个矩阵是不是行阶梯形矩阵
  • iTerm2下载并配置
  • nacos适配人大金仓的数据库
  • 【BetterBench博士】2024年中国研究生数学建模竞赛 E题:高速公路应急车道紧急启用模型 问题分析
  • 【我的 PWN 学习手札】House Of Karui —— tcache key 绕过手法
  • python多继承 - 子类指定父类
  • 基于SpringBoot+Vue的考研百科网站系统
  • 线程池实现的是什么接口
  • 如何安装部署kafka
  • 色彩管理中的Gamma值的理解