【C++】模板、宏、命名空间、

news/2024/10/4 19:14:29 标签: c++, 开发语言

十五、C++中的模板templates

本部分讨论C++中的模板templates
在别的语言中,比如java、c、c#等托管语言中,模板类似泛型的概念,但模板比泛型要强大得多。模板有点像宏,而泛型却非常受限于类型系统以及其他很多因素。同时模板也是一个巨大的、复杂的话题,本部分仅仅是浅浅的入门。

1、什么是模板
模板就是基于你给定的一套规则让编译器为你写代码。或者通俗的说就是,你写个模板,里面抠出一些空,这些空填上不同的东西,就是一个可用的对象。或者我举个例子,比如开发票,其实发票的格式都是一样的,只有抬头、金额、数量等几个要素不一样。你把空发票就可以看出模板,里面的抬头、金额、数量等几个地方是空的,你只要根据不同的客户填上不同的信息,每个客户的发票就开好了。通俗的理解,模板就是那个空发票。你把要填的信息填到对应的空里面,就你生成一张特定客户的发票了。
所以模板就是把代码的某些部分挖掉,然后传给编译器挖掉部分要填的内容,编译器就帮你完成这段代码了。所以说模板就是你给编译器一个套路,然后再给要填的空的答案,编译器就自动帮你完成了。

2、为什么要用模板

如上左图所示,我只是想写一个func函数,但是我得要允许func函数可以接受各种类型的参数,比如整型、字符串、浮点型等等,那此时我就得写好几个函数重载,如上图A处。而且这些重载函数除了参数类型不一样外,其他地方都一模一样。手动重复写A处这么多差别不大的函数太费劲。如果我写一个模板,其他都写全,就把参数类型空出来,然后我给编译器传入那个空的答案,编译器像填空一样帮我填上就行了。这样我就省力很多了。这就是template诞生的初衷。

说明:cout是可以接受任何基本类型的或者说C++内置类型,就是我们现在正在使用的这些类型。

3、template的语法
上右图B处是template的语法:template单词就表示你定义了一个模板;尖括号里面的typename是模板的参数;T是名字,你可以随便取。
T用在C处。也就是T可以在整个模板代码中使用,来代替任何出现类型的地方。比如如果代码中出现int value,我们就可以写成T value;再比如如果出现string value,我们也可以写成T value。

当你定义了一个模板后(B处),编译器就会在编译期评估这个模板。所以上右图B、C处的代码不是真正的代码,func函数也不是一个真正的函数,它只是模板的一部分。只有当我们实际调用它的时候(D处),func函数才被真正的创建,创建时也是根据我们传入的参数类型,T才被替换,func才被创建出来,并作为源代码的一部分进行编译的。所以,比如MSVC编译器就不会对你不使用的模板错误进行报错,但是比如Clang等一些编译器会报错。

4、模板的工作原理

当我们调用func("hello")时,模板的另一个版本的函数就会被编译器创造(把尖括号中的类型替换T),并复制到模板下面,然后才编译。
所以模板的工作原理就是,当你调用模板中的函数时,编译器就根据你给它的信息,把该填的空都对应填上,并将生成的代码复制到模板后面即可。

模板不仅可以让编译器帮我们写函数,还可以写类。事实上,大量的C++标准模板库同样完全使用了模板,下面我们再展示如何写类的模板:

上面右图我们创造数组的方式,和C++标准模板库创造array数组的方式一样。记不记得array数组也是让我们在尖括号里面提供两个参数,一个是数组的类型,一个是数组的长度。所以模板有点像C++的meta programming(元编程),就是编译器在编译时实际还进行了编程。

了解模板模板的工作原理后,我们自然可以判断,不是什么情况都适合使用模板的,有的个性化非常强的工作,你就还是别使用模板了。如果是一些重复性很强的工作,比如日志系统,比如要写很多类型的重载函数,这样的工作使用模板就是非常合适的。

十六、C++的宏(Macro)

C++的宏(Macro)是一种让你写代码时更方便、更简洁、更灵活的工具。

1、宏是什么?
当我们编译C++代码时,第一步就是预处理器会过一遍我们所能看到的源代码中的所有语句。预处理的工作原理基本就是不停的查找-替换、查找-替换...这样过一遍我们的源代码。当预处理器看到#时,它会把#后面的内容都替换成对应的代码。所以宏也是这个时候、这样被替换的。

当预处理器把全部源代码处理一遍后,才是编译器开始编译实际上要编译的代码,编译器把预处理器替换完毕的文本代码编译成二进制指令。所以预处理阶段基本上就是一个文本编辑阶段。也所以当我们了解了与编译器的工作原理后,我们是知道什么代码被喂入了编译器的。

所以,在C++中我们可以使用预处理器来"宏"化某些操作,这样我们就不用一遍又一遍的手动输入代码了,我们就可以利用预处理器,在某种程度上帮助我们实现自动化。预处理器就是一个替换的功能,所以重复的东西我们可以让预处理器帮我们在预编译阶段就替换一下。

2、宏定义的定义及类型
C++中的宏是一种预处理指令,用于在预编译时将代码中的标识符替换为指定的文本。
宏是用#define指令来定义的,用#undef指令来取消定义。
宏名是标识符,宏值可以是任何合法的C++表达式。在编译时,预处理器会将代码中所有出现的宏名替换为宏值。所以从表面上看,宏就是一种代码片段,可以被宏的值所替换。

宏有两种类型:类对象宏和类函数宏。
类对象宏就类似一个常量,是不需要分配内存空间的。因为在预编译阶段,它就被作为二进制可执行指令的一部分,随同指令一起被存放到代码区,在程序运行过程中是不可改变、不可寻址的。
类函数宏就像一个函数,只不过不需要调用和返回,因为是预处理器的操作,而预处理是在源代码预处理阶段上场的,所以压根就涉及不到函数的调用和返回这些事情,仅仅就是替换一段代码片段的操作而已。而函数调用和返回是编译器在编译阶段发生的。
因为是一段代码片段嘛,所以类函数宏在定义时是可以带参数,称为带参数的宏定义。语法格式为:#define 宏名(参数列表) 宏值。

下面给大家展示3个宏示例:

第1个宏,在预编译时,预处理器会将代码中的所有PI替换为3.1415926
第2个宏,在预编译时,预处理器会将代码中的MAX(3, 5)替换为宏值,替换后的代码就是:int max_num = ((3) > (5) ? (3) : (5));
第3个宏,在预编译时,预处理器把符号WAIT替换cin.get()字符串。

可见,宏只是预编译器的替换功能。预编译器是先把源代码从头到尾扫描一遍,把里面该替换的源码全部替换了,此时源代码还是字符形式的代码。预编译器替换完毕后,代码才进入编译阶段,此时是编译器上场对预编译完毕后的字符源码进行编译,编译完毕源代码已经变成二进制机器码了。然后再是链接器上场,把所有该链接的都链接上,才生成.exe可执行文件。所以本部分一定要结合【C++】编译原理_c++编译原理-CSDN博客 编译原理这部分一起理解,这篇博文里面有预处理完毕后的生成文件展示。

从上面3个示例中我们也可以得出结论:不要随便使用宏,比如上例3,别人看到你的代码中有一个WAIT,就很懵,你这是啥标识符???别人还得去查找,才知道WAIT是cin.get()。尤其是这个WAIT标识符还是一个全局变量,就是在别的文件中也是可以直接用WAIT的。如果此时读你代码的人在别的文件中看到WAIT这个标识符,他得查好几个文件才能找到你这个WAIT的意思,此时读你代码的人就可能要崩溃了。所以不要随便使用宏,你的代码不仅你自己读,也得要别人也易读。写代码不是炫技,是准确传递信息,那你就得按照规则传递,对方才不会误解。

3、宏的使用场景
当你需要在编译时替换一些常量或表达式,而不是在运行时计算它们。
当你需要定义一些简单的操作,而不想为它们创建函数或模板。
当你需要根据不同的编译选项生成不同的代码,比如调试信息或条件编译。
当你需要使用一些特殊的语法或符号,比如变长参数或字符串化。

这里展示一个非常有用的使用场景:就是我们在开发代码的时候,我们可能想用一些东西,但是当代码开发完毕,发行时,有些东西又不想让它运行,比如日志,客户是不关心的,所以发行版是不需要运行日志的,此时我们可以设置一些宏,使在开发模式下编译是有这些代码的,但发行模式下编译就没有了这些代码,也让发行版有更好的性能了:

此外,宏对于跟踪或者调试也是非常重要。在debug构建中,我们可能会想知道,比如main.cpp的第15行,多少个字节被分配了?这些被分配的内存是来自从哪个文件、哪行代码?宏就可以帮我们做这些事情。比如你把new关键字替换成一个自定义的单词,宏就会自动的跟踪是从哪个文件的哪一行进行分配的、分配了多少字节等等。 下面再写一个例子,来展示如何用宏来测试main函数。就是创建一个包含所有的main函数的宏。
宏要求只能写一行,那我们可以使用反斜杠来写多行的宏。反斜杠是换行字符的转义,就是Enter键的转义。下面例子的宏也是可以正常运行的:

4、使用宏的注意事项
但是宏也有很多局限和风险,所以应该谨慎使用,并尽量用C++提供的其他特性来代替它们
(1)C++提供了哪些特性可以代替宏呢?
C++提供了一些特性可以代替宏,比如:
用inline函数或模板来代替函数式宏,可以保证类型安全和编译优化。
用constexpr或const来代替常量宏,可以避免重复定义和命名冲突。
用using或typedef来代替类型别名宏,可以增加可读性和灵活性。
用enum class或namespace来代替枚举宏,可以提供作用域和强类型检查。
用if constexpr或模板特化来代替条件编译宏,可以实现编译时分支选择。
用字符串字面量或运算符重载来代替字符串化或连接宏,可以实现更自然的语法。

(2)哪些情况是必须使用宏的呢?
宏在C++中的必要性已经大大降低了,但是还有一些情况是无法用其他特性替代的,比如:
当你需要定义一些编译器或平台相关的常量或标志,比如__FILE__或_WIN32。
当你需要使用一些预处理指令,比如#include或#pragma。
当你需要实现一些复杂的元编程技巧,比如BOOST_PP库。

(3)宏和内联函数有以下几个区别:
宏是由预处理器展开的,而内联函数是由编译器复制到调用处的。
宏必须在程序开始处定义,而内联函数可以在程序任何地方声明。
宏不能访问类的数据成员,而内联函数可以。
宏没有类型检查和返回类型,而内联函数有。
宏可能会导致副作用和错误,而内联函数不会。

(4)宏和模板的区别:
宏和模板还是不一样的。宏发生在预编译阶段,模板则是发生在编译阶段。宏是在预编译阶段被预处理器替换的。模板则是在编译阶段,编译器发现有模板函数被调用了,才生成函数代码并把生成的代码复制到模板后面,此时的函数都已经是二进制的形式了,所以编译器复制的也是二进制。

待续。。。。


http://www.niftyadmin.cn/n/5690266.html

相关文章

【Spring Security】基于SpringBoot3.3.4版本②如何配置免鉴权Path

基于Spring Boot 3.3.4,详细说明Spring Security 6.3.3的使用 摘要本地开发环境说明SecurityFilterChain介绍application.ymlWen3SecurityProperties.java修改DemoWen3Security修改SecurityFilterChainIgnoredPathController.javaIgnoredPathController2.java启动工程测试测试…

01:(寄存器开发)点亮一个LED灯

寄存器开发 1、单片机的简介1.1、什么是单片机1.2、F1系列内核和芯片的系统架构1.3、存储器映像1.4、什么是寄存器 2、寄存器开发模板工程3、使用寄存器点亮一个LED4、代码改进15、代码改进2 本教程使用的是STM32F103C8T6最小系统板,教程来源B站up“嵌入式那些事”。…

02:(寄存器开发)流水灯/按键控制LED

寄存器开发 1、LED流水灯2、按键控制LED 1、LED流水灯 通过第一章的学习,我们已然知晓了LED的点亮和熄灭的方式,下面学习流水灯的制作流程。 流水灯呈现的样子:就是第一个LED灯点亮,延迟一段时间,第一个LED灯熄灭第二…

Study-Oracle-10-ORALCE19C-RAC集群维护

一路走来,所有遇到的人,帮助过我的、伤害过我的都是朋友,没有一个是敌人。 一、RAC的逻辑架构与进程 1、RAC 与单实例进程的对比 2、RAC相关进程功能 3、在主机查看RAC进程 其他的不列举了 4、RAC集群启停命令 检查集群状态 ORACLE 19C …

项目-坦克大战笔记-墙体销毁以及人机销毁

在子弹撞到墙或者人机身上时会将碰撞到的墙体或者人机销毁 我们需要做到几点 检测子弹碰撞到的墙体或者人机将物体获取到 每帧遍历墙体列表与人机列表,检测被碰撞的墙,创建一个方法返回值为对应类型将被碰撞的物体返回出来 public static gudin wallp…

Go基础学习11-测试工具gomock和monkey的使用

文章目录 基础回顾MockMock是什么安装gomockMock使用1. 创建user.go源文件2. 使用mockgen生成对应的Mock文件3. 使用mockgen命令生成后在对应包mock下可以查看生成的mock文件4. 编写测试代码5. 运行代码并查看输出 GomonkeyGomonkey优势安装使用对函数进行monkey对结构体中方法…

Pikachu-Sql Inject-搜索型注入

MySQL的搜索语句: select * from table where column like %text%; 如:使用引号闭合左边的引号, or 11 把所有数据查询出来; # 注释掉后面的 引号等; test or 11# 查询出结果: 注入的核心点…

【CKA】十二、持久化存储卷PersistentVolume

12、持久化存储卷PersistentVolume 1. 考题内容: 2. 答题思路: 按题目要求检查各个参数,我就是第一次没看清楚,把ReadWriteOnce写成ReadWriteMany了,幸亏做完检查了一遍 这个参数可有可无,加上也不影响 …