这个例子就能很好的说明宏是替换不是传参,所以在使用时一定要明确使用的目的
(6.)所以我们在使用宏的时候就一定要记得要加上括号,
这样进行了改进就不会出现问题了,所以一定要注意括号的使用
(7.)这边再进行一个举例:
此时的这个输出结果是55而不是100,所以这个就是因为替换完参数之后(表达式为 105+5),乘号的优先级高,所以先执行后执行+号,所以这个就不符合我们的意思,所以要加括号来进行改进
所以只有进行这样的改进,才有可能达到我的期望
(3.)#define 进行替换的规则
上述讲完了#define定义标识符和#define定义宏的概念之后,这边我们来讲一讲什么是#define的替换规则:
1.在调用宏的时候,首先对宏的参数进行检查,查看这个参数是否包含任何由#define定义的符号,如果有,在预编译的过程中,它们首先被替换
意思就是:
我们应该先看一下宏的参数是否是#define定义的表示符,如果是,首先替换这个参数的值(也就是我们第一步就是应该把图中宏的参数MAX给替换换成100,然后再调用宏)
2.替换文本随后就被插入到程序中原来文件的位置,对于宏,参数名就会被它们所对应的替换
3.最后就是检查,再次对结果文件进行扫描,看是否还包含#define定义的符号,如果有,就重复上述步骤
4.并且当我们在使用宏这个概念的时候有两个注意点
(1.)宏参数和#define定义中可以出现其它#define定义的变量,但是对于宏是没有递归这个概念的
(2.)当预处理器搜索#define定义的符号时,字符串常量是不在搜索范围内的
这个什么意思呢?
显而易见此时在双引号中的就是我的字符串常量,所以在预编译的过程中,这个字符串常量是不会被替换的,只有在双引号外的那个MAX此时才会被替换
(4.)如何将参数插入到字符串中(宏的方法使用)
1.首先是问题的引出(此时我想要的是我传参时传进去的是什么字符,此时它打印的就是什么字符,可是,此时却不能达到我想像中的景象 )
上面的输出并不是我想要的,我想要的是:
the a number is 10
the b number is 20
所以为了解决这个问题这边就涉及到了 # 和 ## 的使用
2.预处理操作符 # 和 ## 的介绍
(1.)# 的介绍
例: #X表达的就是X表达的内容的那个字符串(也就是表达的就是替换时的那个替换的数据)
如下图所示:
这样使用宏的定义,我就可以顺利的使用 # 的使用方法来解决上述的按个问题了
所以 # 真正的作用就是(把宏的参数直接转换为一个字符串,插入到我的代码中)
(2.)## 的介绍
1.此时进行对 ## 的讲解之前,我们先了解一下打印字符串时的小细节,如图:
显而易见上述的打印效果是相同的,所以此时就可以涉及到 ## 的作用了
这个就是##的作用(如上图),可以显而易见的知道,我的##的作用就是将##两端的内容连成一个符号,所以此时 a##b##c的意思就是 abc 的意思
2.所以##的作用就是把位于它两边的符号合成一个符号,它允许宏定义中从分离的文本片段创建出一个新的标识符
(5.) 宏和函数的比较
1.所以这边我们讲一下使用宏和使用函数的优点和缺点(对比)
(1.)首先我们这边引入一个新的概念:叫带副作用的参数 (叫参数会起作用,但是会遗留下来一些别的属性)
有副作用的意思就是此时的b虽然达到了我的目的,但是此时的a也发生了改变,所以这个就是参数a的副作用
( 2.)所以接下来我们再介绍一下什么是宏参数的副作用
(3.)所以当我们的宏参数具有副作用了,此时的输出的值就与我们想象中的完全不同了,接下来我们看一下为什么当宏参数具有副作用时,输出的是这些值;
(4.)首先当我的a++,b++开始替换到我的宏当中时(注意是替换不是传参),此时宏中的stuff(内容)就会变成 :
((a++) > (b++) ? (a++) : (b++))
此时当我进行判断的时候因为(后置加加是先使用后加加),所以(a++)>(b++)这个表达式判断完之后,a就变成了11,b就变成了12,并且这个表达式判断完成了,此时因为b是大于a的所以此时就会执行(a++) : (b++)) 中后面这步(b++),然后因为(后置加加是先使用后加加)所以这边是先把b=12返回给了MAX,所以MAX此时就是12,然后b自己在使用完之后实现后置加加,所以b此时就变成了13;
所以以上就是对宏参数副作用的一些简单的题目的讲解
2.这边我们将上述的比较两个值的大小的题目用函数和宏方法再实现一遍,然后区分出它们的区别,从而得出我们的主要目的(比较宏和函数的优缺点)
如上图所示,我们比较两个数的大小不仅可以用宏的方法也可以使用函数的方法,所以此时我们就可以很好的区别一下这两种方法的区别(文章底部有一个投票哦!参与参与)
(1.)首先第一点可以证明此时这个案例是我的宏的方法好,假如此时我传给函数的参数是别的类型的数据(例:浮点型,字符类型,长整型),但是我的函数却只可以接收 int 类型,所以此时就不能对我的别的类型进行有效的比较了,而此时我的宏的方法并没有限制它的类型,所以此时不管是什么类型的参数(只有可以进行替换)就都可以进行比较,所以就充分体现出了宏的优点和函数的不足;
(2.)第二点证明这个案例宏的使用比函数的使用更好的是(在编译和链接的阶段)(自己如果感兴趣的话自己去调试一下)(通过看汇编代码就可以很好的看出),当我们在使用函数的方法时,如果想要真正的运行起来,在编译过程中,此时是需要大量的调用各种相关程序的汇编代码,才可以真正 的轮到我关键语句(比大小语句)的调用,并且在这句关键语句执行完成之后,并不是直接就结束了,而是还要在后面执行大量的程序的汇编代码,才算是真正的结束,所以在函数的执行过程中,函数的前后是具有大量的汇编代码需要执行(函数的调用和返回的开销)(这样效率就显得很慢了)
,然而当我们使用宏的方法时,就没有这么的麻烦,汇编代码大大降低(因为宏在预处理阶段时,就完成了符号的替换,不仅不需要传参,而且不需要返回参数),所以此时宏在运行的时候就是非常快的(相比于函数的使用),所以这就可以充分的表明出宏的优点和函数的缺点;
(3.)第三点我们讲一下宏相对无函数的缺点
1.每次使用宏的时候,一份宏定义的代码插入到我的程序中时,除非此时这个宏比较短,否则就会大幅度的增加了程序的长度
2.宏是没法调式的
3.宏由于与类型无关,所以此时宏也是不严谨的
4.宏可能会带来运算符优先级的问题,导致程序容易出错
(4.)这边我们为了加深对宏的使用的理解,这边我们再进行两个例子的解释:
1.这边有一个函数不可能完成的操作,就是进行类型的传参,而使用我的宏,就可以有效的解决这个问题
2.为了加深宏的理解,再看一个代码
3.所以总的来说当我们在使用#define定义宏的时候,此时这个被定义的宏的使用就是将这个宏的名字和替换的参数放在我应该使用的地方,此时在预编译的过程中,这个宏就会自动的被替换为我定义宏时的那个宏中的stuff(内容),此时这个内容也就是我想要的代码,这样就可以使我的程序执行起来
(5.)所以以上就是宏和函数的各种优缺点和宏的使用
(6.)但是在我们以后学到C++的时候我们就会学一个叫 inline(内联函数)这个内联函数就能很好的解决我们的函数和宏的问题,内联函数不仅具有函数的优点,也具有宏的优点,这个就是宏和函数的集合
(7.)并且注意当我们在定义宏的时候(习惯上是用大写字母来定义 宏的名称),参数可用小写,并且在声明写函数的时候,函数名最好是不要全部大写,避免弄混
(6.)命令定义
1. #undef 的使用
如图:
此时我定义了一个MAX为100,然后我可以进行第一步的打印,然后因为我使用 #undef 此时这个定义就被取消掉了,所以在后面第二步我就无法再一次打印这个MAX的值了,因为此时已经取消了定义
2.什么是命令行定义:
这边讲一个新的知识点,什么是命令行定义,在许多的C语言编译器会提供一种能力,就是允许在命令行中定义符号,用于启动编译过程。假如我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,此时这个命令行定义就可以很好的提供作用(比如此时我想打印1到10的数,我就可以把此时的这个命令行定义在小于10 ,如果我想要打印1到100的数,此时我就可以把这个命令行定义在小于100的,所以这个就是命令行的大致使用)
(7.) 条件编译
1.什么是条件编译:在编译一个程序的时候我们如果将一条语句(一组语句)编译或者放弃是很容易的,因为我们有条件编译指令,有了条件编译我就可以使用选择性的编译,如:#ifdef、#if 、#endif
2.如图就可以充分的说明什么是条件编译的使用
3.此时的这个# ifdef就是条件编译(意思为假如我的DEBUG如果有被定义,此时就会执行# if中的语句,否则就不会执行)
4.所以当我的DEBUG此时被定义了之后,如图:
5.我# ifdef中的语句就会被执行
6.并且在条件编译中也是有多分支条件编译的,如图:
7.所以通过这写条件编译(#if #ifdef #endif),我就可以选择我要的语句和我不要的语句(在预处理阶段执行)
8.并且#ifdef DEBUG 和 #if define (DEBUG)这两句的意思是相同的,都是说明如果有定义,而此时如果都没定义的话这两句的意思 #ifndef DEBUG 和 #if !define (DEBUG)就是相同的,都是没有定义的意思
9.所以条件编译讲的就是我的这个代码有没有机会被执行到的意思,符合意思就执行,不符合意思,就跳过(都是在预编译过程中完成的)
10.这边还有一个条件编译的嵌套使用(头文件中使用异常的频繁),例:
可以清晰的看出此时我的stdio.h这个头文件中,大量使用到了条件编译的使用和嵌套
(8.) 预处理指令 #include
1.首先就是包括头文件(但是此时有两种头文件的包含)一种是库函数的包含,一种是本地文件的包含,例:#include<stdio.h> 和 #include“add.h”
2.但是此时使用这两种是有区别的,主要区别就在于查找策略的不同
(1.)当我们使用的是库函数头文件时,此时的查找策略就是:直接去头文件的标准路径下去查找,如果找不到就提示编译错误
(2.)当我们使用的是本地文件时,此时的查找策略就是:先在源文件所在的目录下查找,如果未找到该头文件,编译器就会像查找库函数头文件一样在标准位置查找头文件,如果还是找不到,就提示编译错误
3.所以按照以上的说法,此时其实当我在引用库函数头文件的时候(就也可以使用双引号来引用)例:include“stdio.h”,但是如果这样的话,会导致一定的问题(查找效率低的问题)并且这样写,也就不方便我们自己去区分库文件和本地文件了
4.并且这边有一个新的知识点,就是如何防止我的头文件被重复的包含
(1.)因为当我的头文件数量多时,并且此时这些头文件中也都各自包含了<stdio.h>这个头文件,就会导致,我在一个文件中包含这些头文件时,这个文件此时就会包含多个<stdio.h>头文件,这样就会导致出问题了
(2.)所以此时在每一个头文件的前面都会出现一个条件编译的判断代码,来防止这个头文件被同一个文件给重复调用
具体使用如图:
(3.)上图中的两种方法就可以防止我的一个头文件被重复调用
(4.)意思就是当我第一次想要调用这个头文件的时候,#ifndef __TEST__ 此时的这个__TEST__确实是没有被定义的,所以判断条件为真,所以此时走下一步#define __TEST__,所以此时这步走完(__TEST__就被定义了),然后走我需要调用的那个头文件中的对应的内容,所以此时当这个文件还想要调用我的这个头文件时,就会导致此时的 __ TEST __已经是被定义过了的,所以此时的第一个判断条件就直接为假,所以此时我的头文件中的内容就不能被调用,这样就非常好的避免了我的一个头文件被同一个文件调用多次的结果
(5.)还有另一种方法就是#pragma once的方法,这个方法也可以很好的避免重复调用的问题
(6.)在C语言中的预处理指令还有很多,所以如果感兴趣的话,就自己再去研究研究,这边我就讲这么多了,谁让我明天又要6点起来晨跑呢?(高级学校,真的会谢)
三、总结:以上就是C语言内容中的最后一个内容,叫我们如何认识C语言程序的编译、链接和执行
所以总的来说就是要知道一个代码跑起来的所以然。