【C++篇】引领C++模板初体验:泛型编程的力量与妙用

文章目录

  • C++模板编程
    • 前言
      • 第一章: 初始模板与函数模版
        • 1.1 什么是泛型编程?
          • 1.1.1 为什么要有泛型编程?
          • 1.1.1 泛型编程的优势
        • 1.2 函数模板的基础
          • 1.2.1 什么是函数模板?
          • 1.2.2 函数模板的定义格式
          • 1.2.3 示例:通用的交换函数
            • 输出示例:
          • 1.2.4 模板中的`typename`与`class`
        • 1.3 函数模板的原理
          • 1.3.1 函数模板的实例化
          • 1.3.2 隐式实例化与显式实例化
      • 第二章: 类模板
        • 2.1 类模板概念
          • 2.1.1 类模板的定义格式
          • 2.1.2 示例:简单的类模板
            • 输出示例:
        • 2.2 类模板的实例化
        • 2.3 类模板中的成员函数定义
        • 2.4 为什么不建议类模板的定义和声明分离?
          • 2.4.1 模板的编译时行为
          • 2.4.2 链接器无法找到定义
          • 2.4.3 无法预编译模板
          • 2.4.4 解决方案:将声明和定义放在同一个头文件中
      • 第三章: 模板的匹配原则
        • 3.1 模板的匹配原则
  • 写在最后

C++模板编程

💬 欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力!

👍 点赞、收藏与分享:觉得这篇文章对你有帮助吗?别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力!
🚀 分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对C++感兴趣的朋友,让我们一起进步!

前言

C++作为一门强大的编程语言,以其丰富的功能和灵活的设计著称。模板编程是C++中非常重要的一个特性,通过模板可以实现泛型编程,编写与数据类型无关的代码,极大地提高了代码的复用性和可维护性。本文将从泛型编程、函数模板、类模板等几个方面详细讲解C++模板的使用,并结合实际的代码示例进行分析,帮助大家全面掌握模板编程的知识。

本篇文章将包含以下几个部分:

  1. 泛型编程的基本概念
  2. 函数模板的定义与使用
  3. 类模板的实现
  4. 模板的匹配原则

通过阅读本文,你将能够掌握C++模板编程的基础知识,理解其背后的工作原理,并学会如何在实际项目中应用这些技术。


第一章: 初始模板与函数模版

1.1 什么是泛型编程?

泛型编程(Generic Programming)是C++中的一种编程范式,旨在编写与数据类型无关的通用代码。这意味着你可以编写一次代码,并通过不同的数据类型进行复用。C++通过模板(Template)来实现泛型编程,模板是泛型编程的核心工具。

1.1.1 为什么要有泛型编程?
  • 问题提出:如何实现一个通用的交换函数呢?
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;
}
void Swap(char& left, char& right)
{char temp = left;left = right;right = temp;
}
//....等等

使用函数重载虽然可以实现,但是有一下几个不好的地方:

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

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

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

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

1.1.1 泛型编程的优势

泛型编程的主要优势包括:

  • 代码复用性强:通过模板,你可以避免为每个数据类型单独编写相同功能的代码。
  • 提高代码的可维护性:代码只需编写一次,减少了冗余代码,后续如果需要修改或修复,只需在一处进行。
  • 减少编写错误:重复编写代码时容易出错,而模板可以让编译器自动生成所需代码,减少人为失误。

在这里插入图片描述


1.2 函数模板的基础
1.2.1 什么是函数模板?

函数模板(Function Template)是一个与类型无关的函数“蓝图”。通过模板参数,编译器在编译期间会根据实际的数据类型生成相应的函数版本。

1.2.2 函数模板的定义格式

我们可以通过以下格式来定义一个函数模板:

template<typename T> 
返回类型 函数名(参数列表) {// 函数体
}
  • template:告诉编译器接下来的内容是模板。
  • typename T:定义一个模板参数T,可以用来表示任何类型。
  • 返回类型参数列表可以使用T作为数据类型。

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

1.2.3 示例:通用的交换函数

那我们就可以使用模板来编写交换函数,它可以交换任意类型的数据:

/*** @brief 通用的交换函数* @tparam T 通用的类型参数,由编译器根据实参推断* @param left 左侧变量* @param right 右侧变量*/
template<typename T>
void Swap(T& left, T& right) {T temp = left;left = right;right = temp;
}int main() {int a = 10, b = 20;double x = 1.1, y = 2.2;char c1 = 'A', c2 = 'B';// 使用模板函数进行交换Swap(a, b);    Swap(x, y);    Swap(c1, c2);  std::cout << "交换后的整数: " << a << " " << b << std::endl;std::cout << "交换后的浮点数: " << x << " " << y << std::endl;std::cout << "交换后的字符: " << c1 << " " << c2 << std::endl;return 0;
}
输出示例:
交换后的整数: 20 10
交换后的浮点数: 2.2 1.1
交换后的字符: B A
1.2.4 模板中的typenameclass

在定义模板时,typenameclass是可以互换的。你可以选择以下两种方式:

template<typename T>  // 使用 typename
template<class T>     // 使用 class

虽然两者功能相同,但推荐使用typename,因为它能够更好地表达该参数是一个类型参数,避免与类的定义产生混淆。


1.3 函数模板的原理

函数模板的核心在于它不是一个真正的函数,而是一个编译器用来生成特定类型函数的蓝图。编译器根据模板的使用情况推导出具体的类型,并生成相应的代码。这一过程称为模板的实例化

1.3.1 函数模板的实例化

当我们调用模板函数时,编译器会根据实际的参数类型生成对应的函数版本。比如:

template<class T>
T Add(const T& left, const T& right) {return left + right;
}int main() {int a = 10, b = 20;double x = 1.1, y = 2.2;Add(a, b);  // 生成 Add<int> 版本Add(x, y);  // 生成 Add<double> 版本return 0;
}

在这里插入图片描述

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

1.3.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.0, d2 = 20.0;Add(a1, a2);Add(d1, d2);Add(a1, d1);/*
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有
一个T,
编译器无法确定此处到底该将T确定为int 或者 double类型而报错
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要
背黑锅*/// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化Add(a1, (int)d1);//自己强制转换return 0;
}

模板实例化分为两种:

  1. 隐式实例化:编译器根据实参推导出模板参数,并自动生成函数。例如上面的Add函数就是隐式实例化。
  2. 显式实例化:如果想要强制指定模板参数,可以使用显式实例化:

就是第二种处理方式

int main(void)
{int a = 10;double b = 20.0;// 显式实例化Add<int>(a, b);return 0;
};

第二章: 类模板

2.1 类模板概念

类模板(Class Template)是用于定义与类型无关的类,它允许我们在类的定义中使用模板参数,编译时再根据实际类型进行类的实例化。类模板与函数模板类似,只不过它是用来生成类的。

2.1.1 类模板的定义格式

定义类模板的语法格式如下:

template<class T>
class 类名 {// 类的成员变量和方法
};
  • template<class T>:告诉编译器这是一个模板类,T是一个类型参数。
  • 类的成员和方法可以使用T作为数据类型,编译时由用户提供的类型来替代T
2.1.2 示例:简单的类模板

下面是一个简单的栈(Stack)类模板,用于存储任意类型的数据:

#include<iostream>using namespace std;/*** @brief 栈的类模板* @tparam T 通用的类型参数*/
template<class T>
class Stack {
public:Stack(size_t capacity = 4) {_array = new T[capacity];_capacity = capacity;_size = 0;}~Stack() {delete[] _array;}/// @brief 将元素压入栈中void Push(const T& data) {if (_size == _capacity) {Expand();  // 扩容}_array[_size++] = data;}/// @brief 弹出栈顶元素void Pop() {if (_size > 0) {--_size;}}/// @brief 返回栈顶元素T& Top() {if (_size > 0) {return _array[_size - 1];}throw out_of_range("栈为空!");}/// @brief 检查栈是否为空bool IsEmpty() const {return _size == 0;}/// @brief 获取栈中元素的数量size_t Size() const {return _size;}private:T* _array;size_t _capacity;size_t _size;/// @brief 扩展栈的容量void Expand() {size_t newCapacity = _capacity * 2;T* newArray = new T[newCapacity];for (size_t i = 0; i < _size; ++i) {newArray[i] = _array[i];}delete[] _array;_array = newArray;_capacity = newCapacity;}
};int main() {Stack<int> st;  // 创建存储整数的栈st.Push(10);st.Push(20);st.Push(30);cout << "栈顶元素: " << st.Top() << endl;st.Pop();cout << "弹出后栈顶元素: " << st.Top() << endl;return 0;
}
输出示例:
栈顶元素: 30
弹出后栈顶元素: 20

在这个类模板中,T是一个通用类型参数。Stack<int>Stack类模板的一个实例化,表示它是一个存储int类型数据的栈。编译器会根据实际使用的类型自动生成相应的类。

2.2 类模板的实例化

与函数模板不同,类模板在使用时必须显示地提供类型参数。实例化类模板时,必须在类名后面的尖括号<>中指定实际的类型参数。例如:

Stack<int> st1;    // 实例化为处理 int 类型的栈
Stack<double> st2; // 实例化为处理 double 类型的栈

这里的Stack<int>Stack<double>分别表示不同的类型,即不同的模板实例。编译器会根据模板参数生成相应的类代码。

2.3 类模板中的成员函数定义

对于类模板,成员函数可以在类定义内或定义外实现。类模板的成员函数定义外置时,需要在函数名之前加上模板声明和模板参数。例如:

template<class T>
void Stack<T>::Push(const T& data) {if (_size == _capacity) {Expand();  // 扩容}_array[_size++] = data;
}
2.4 为什么不建议类模板的定义和声明分离?

在C++中,类模板的实现与普通类有一个显著的区别:模板是在编译时根据实际类型实例化的,而不是像普通的类那样在编译期和链接期处理。这导致了一个很重要的问题:如果将类模板的声明和定义分离到不同的文件中,可能会导致链接错误。以下是详细原因:

2.4.1 模板的编译时行为

类模板的本质是一个“蓝图”,它并不是一个完整的类,而是一个在需要时根据实际类型生成代码的模式。因此,模板只有在实际使用(实例化)时,编译器才会生成对应的类型的代码。编译器无法预先知道你会使用哪些类型来实例化模板,因此它不会为模板生成实际的代码。

2.4.2 链接器无法找到定义

当你将类模板的声明放在头文件中,而把定义放在.cpp文件中时,模板实例化的过程可能发生在不同的编译单元中。因为模板只有在编译期被实例化,链接器在链接时无法看到模板的定义,除非在编译时所有模板的实例化代码都可见。如果定义在.cpp文件中,其他使用模板的编译单元无法找到这个定义,导致链接器报错

2.4.3 无法预编译模板

与普通类不同,类模板无法被预编译或只在一个编译单元中定义然后供其他单元使用。普通的类在编译过程中,编译器会生成目标代码并储存在.obj文件中,链接时其他编译单元可以引用这些已生成的代码。而类模板无法这样做,因为它需要知道使用时的类型才能生成实际的代码。

2.4.4 解决方案:将声明和定义放在同一个头文件中

为了避免上述问题,C++的惯用方法是将类模板的声明和定义都放在同一个头文件中。这使得每个使用模板的编译单元在实例化模板时,编译器能够访问到模板的定义,并根据需要生成实际的代码。这种方式确保了编译器能够在编译期处理模板的实例化,而不会在链接时出现找不到定义的问题。

错误用法:

// Stack.h
template<typename T>
class Stack {
public:void Push(const T& value);// 声明在头文件中
};// Stack.cpp
template<typename T>
void Stack<T>::Push(const T& value) {// 定义在.cpp文件中
}

在这种情况下,如果不同编译单元使用了Stack<int>Stack<double>,链接器可能会报错,因为它无法找到模板的定义。

正确用法:

// Stack.h
template<typename T>
class Stack {
public:void Push(const T& value);
};template<typename T>
void Stack<T>::Push(const T& value) {// 声明和定义都在头文件中
}

这种方法确保每个编译单元都能访问到模板的完整定义,避免链接时的错误。

总结:

类模板的代码只有在实例化时才生成,因此类模板的定义必须在每个使用它的编译单元中可见。将模板的声明和定义放在同一个头文件中,可以确保模板实例化时能够访问到其定义,避免链接错误。这也是为什么大多数C++开发者在编写模板时会将模板的实现放在头文件中的原因。

注意:

函数模板与类模板不同,当代大多数编译器支持函数模板的声明和定义分离,这是因为函数模板的实例化往往只涉及函数的具体调用,不像类模板这么复杂,具体之后的博客会更详细的讲解此处的内容,敬请期待哦💕


第三章: 模板的匹配原则

3.1 模板的匹配原则

C++编译器在调用模板时,会根据实参类型和函数参数类型进行匹配。模板的匹配规则如下:

  1. 优先调用非模板函数:如果存在一个与实参完全匹配的非模板函数,编译器将优先调用非模板函数,而不是通过模板生成一个实例。

    例如:

    int Add(int a, int b) {return a + b;
    }template<typename T>
    T Add(T a, T b) {return a + b;
    }int main() {int a = 10, b = 20;cout << Add(a, b) << endl;  // 调用非模板版本
    }
    
  2. 如果非模板函数没有匹配,则调用模板实例:如果模板函数比非模板函数更能匹配参数类型,编译器将生成模板实例。

    例如:

    double Add(double a, double b) {return a + b;
    }template<typename T>
    T Add(T a, T b) {return a + b;
    }int main() {int a = 10, b = 20;double x = 1.1, y = 2.2;cout << Add(a, b) << endl;  // 调用模板实例 Add<int>cout << Add(x, y) << endl;  // 调用非模板版本 Add(double, double)
    }
    

写在最后

本文基础的讲解了C++模板编程的基础知识,涵盖了泛型编程、函数模板、类模板、模板匹配原则等概念。通过这些模板功能,C++开发者可以编写更加灵活和可复用的代码,大幅提高编程效率。

模板编程虽然强大,但使用时也需要谨慎,尤其是在处理模板特化和匹配规则时。如果能够合理地使用模板技术,相信你的代码质量将会有显著提升。


以上就是关于【C++篇】引领C++模板初体验:泛型编程的力量与妙用的内容啦,在之后会有另一篇博客来讲解有关模板的更多进阶内容,敬请期待哦,然后各位大佬有什么问题欢迎在评论区指正,或者私信我也是可以的啦,您的支持是我创作的最大动力!❤️

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1542806.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【解密 Kotlin 扩展函数】自定义函数(十二)

导读大纲 1.1 在 Kotlin 中创建集合1.2 自定义 joinToString 函数来实现字符串打印 1.1 在 Kotlin 中创建集合 学习如何创建集合 使用setOf函数创建集合, 使用mapOf创建映射, 使用listOf创建列表<1> to 并不是一个特殊的结构体, 而是一个普通函数 infix修饰符表示这是一…

Spring Cloud Gateway 之动态uri 自定义过滤器

背景&#xff1a;第三方公司 请求本公司入参和出参一样的同一个接口&#xff0c;根据业务类型不一样需要不同业务微服务处理 &#xff0c;和第三方公司协商在请求头中加入业务类型方便我公司在网关成分发请求。 1&#xff1a;在spring cloud gateway yml 中加入路由 重点是 -…

人工智能领域-----机器学习和深度学习的区别

机器学习和深度学习都是人工智能领域中的重要概念&#xff0c;它们之间存在以下一些区别&#xff1a; 一、定义与概念 机器学习&#xff1a; 是一种让计算机自动学习和改进的方法&#xff0c;通过从数据中学习模式和规律&#xff0c;从而能够对新的数据进行预测或决策。涵盖了…

【C++笔试强训】如何成为算法糕手Day1

学习编程就得循环渐进&#xff0c;扎实基础&#xff0c;勿在浮沙筑高台 循环渐进Forward-CSDN博客 笔试强训第一天 目录 循环渐进Forward-CSDN博客 第一题&#xff1a;两个数组的交集 暴力循环法&#xff1a; 哈希法 &#xff1a; 数组下标法&#xff1a; 第二题&#x…

MySQL:事务的ACID特性隔离级别脏读/不可重复读/幻读/Next-Key锁——场景复现

目录 1、什么是事务 2、 事务的ACID特性 2.1 事务的隔离性 3、为什么要使用事务&#xff1f; 4、查看支持事务的存储引擎 5、使用事务 5.1 控制事务 5.1.1 开启事务 5.1.2 关闭事务 5.2 开始一个事务&#xff0c;执行修改后回滚 5.3 开始一个事务&#xff0c;执行修…

句子成分——每日一划(十)

目录 一、原句 二、主要句子成分 三、 分词短语部分 四、定语从句部分 五、结构总结 六、句子改良 一、原句 Z-Library has always been a part of my study, providing many books that would otherwise require a lot of time or money to find. 来源&#xff1a;写作…

【网络安全】身份认证+wan优化+终端控制

用户身份认证 在允许用户访问你的网络时对其进行验证是至关重要的。不幸的是很多情况下&#xff0c;简单的用户名与密码验证并不可靠。公司通常需要更强大的针对访问信息价值较高系统(例如网络管理员系统与财务系统)的用户群体的验证。 双因子身份验证是根据“你知道的”和“你…

查询一条 SQL 语句的流程

查询一条sql语句的流程 连接器:建立连接&#xff0c;管理连接、校验用户身份查询缓存:查询语句如果命中查询缓存则直接返回&#xff0c;否则继续往下执行&#xff08;MSQL8.0 已删除&#xff09;解析 SQL&#xff1a;通过解析器对 SQL 查询语句进行词法分析、语法分析&#xf…

用uniapp 及socket.io做一个简单聊天 升级 9

比这之前优化了以下功能 上线通知 群聊里适时显示在线人数 约请好友 通过好友通过socket 相应端自动变化 PC端可以拉取摄象头拍照 PC端可以录音发送 拉起摄象头发送录象 <template><view class""><scroll-view scroll-y"true" class&…

Java启动Tomcat: Can‘t load IA 32-bit .dll on a AMD 64-bit platform报错问题解决

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

树莓派pico上手

0 介绍 不同于作为单板计算机的树莓派5&#xff0c;树莓派 pico 是一款低成本、高性能的微控制器板&#xff0c;具有灵活的数字接口。主要功能包括&#xff1a; 英国树莓派公司设计的 RP2040 微控制器芯片双核 Arm Cortex M0 处理器&#xff0c;弹性的时钟频率高达 133 MHz26…

Tomcat 靶场攻略

CVE-2017-12615 步骤一&#xff1a;环境搭建 cd vulhub/tomcat/CVE-2017-12615 docker-compose up -d docker ps 步骤二&#xff1a;漏洞复现 http://192.168.10.190:8080/ 步骤二&#xff1a;首页进行抓包 Tomcat允许适⽤put⽅法上传任意⽂件类型&#xff0c;但不允许js…

小程序-基础知识1

Mustache语法 小程序和vue一样提供了插值语法 但是小程序不能调用方法{{xxxx()}} hidden属性 hidden是所有组件都默认拥有的属性&#xff0c; hidden与wx:if的区别&#xff1a; wx:if是控制组件是否渲染,hidden控制显示或隐藏是通过添加hidden属性。 wx:for 除了可以遍历…

HCIA--实验十九:配置接口DCHP

一、实验内容 1.需求/要求&#xff1a; 通过一台5700交换机和一台PC&#xff0c;通过在交换机的接口上配置接口DHCP来实现PC自动获取ip地址。 二、实验过程 1.拓扑图&#xff1a; 2.步骤&#xff1a; 1.给vlan10配置ip地址&#xff0c;进入vlan10开启接口的DHCP&#xff1…

Java数据库连接——JDBC

目录 1、JDBC简介 2、JDBC应用 2.1 建立数据库连接 2.1.1 DriverManager静态方法获取连接 2.1.2 DataSource对象获取 2.2 获取SQL执行对象 2.2.1 SQL注入 2.2.2 Statement(执行静态SQL) 2.2.3 PreparedStatement(预处理的SQL执行对象) 2.3 执行SQL并返回结果 2.4 关…

【笔记】材料分析测试:晶体学

晶体与晶体结构Crystal and Crystal Structure 1.晶体主要特征 固态物质可以分为晶态和非晶态两大类&#xff0c;分别称为晶体和非晶体。 晶体和非晶体在微观结构上的区别在于是否具有长程有序。 晶体&#xff08;长程有序&#xff09;非晶&#xff08;短程有序&#xff09…

机器人机构、制造

简单整理一下&#xff0c;在学习了一些运动学和动力学之类的东西&#xff0c;简单的整合了一些常用的机械结构和图片。 1.电机&#xff1a; 市面上的电机有&#xff1a;直流电机&#xff0c;交流电机&#xff0c;舵机&#xff0c;步进电机&#xff0c;电缸&#xff0c;无刷电…

李宏毅结构化学习 03

文章目录 一、Sequence Labeling 问题概述二、Hidden Markov Model(HMM)三、Conditional Random Field(CRF)四、Structured Perceptron/SVM五、Towards Deep Learning 一、Sequence Labeling 问题概述 二、Hidden Markov Model(HMM) 上图 training data 中的黑色字为x&#xff…

基于单片机的水位检测系统仿真

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于51单片机&#xff0c;DHT11温湿度检测&#xff0c;水位检测&#xff0c;通过LCD1602显示&#xff0c;超过阈值报警&#xff0c;继电器驱动电机转动。通过矩阵按键切换选择设置各项参数阈值。 …

【Linux】通过内核以太层可查看应用程序运行时访问外网情况

比如&#xff0c;SourceInsight3.exe从外网接收信息&#xff1a; 下边是运行firefox时内核打印的日志&#xff0c;可以看到浏览器运行时调用了很多的操作系统内核系统调用&#xff0c;比如&#xff1a;文件读写、网络数据包的收发等等&#xff0c;其实这些日志还并不全&#x…