开远市新农村数字建设网站,深圳市外贸公司,wordpress playyo,做网站做国外广告文章目录 一、#和##1.#运算符2.##运算符 二、预处理指令#undef三、条件编译1.单分支条件编译2.多分支条件编译3.判断符号是否被定义4.判断符号是否没有被定义 四、头文件的包含1.库头文件的包含2.本地头文件的包含3.嵌套包含头文件的解决方法使用条件编译指令使用预处理指令#pr… 文章目录 一、#和##1.#运算符2.##运算符 二、预处理指令#undef三、条件编译1.单分支条件编译2.多分支条件编译3.判断符号是否被定义4.判断符号是否没有被定义 四、头文件的包含1.库头文件的包含2.本地头文件的包含3.嵌套包含头文件的解决方法使用条件编译指令使用预处理指令#pragma 五、其它预处理指令六、C语言更新结束感言 一、#和##
1.#运算符 这里的#不是#include和#define前面的那个#它是一个运算符这个运算符将宏的⼀个参数转换为字符串字⾯量它仅允许出现在带参数的宏的替换列表中#运算符所执⾏的操作可以理解为”字符串化“ 在讲解这个运算符之前我们先来看一段代码如下
#include stdio.hint main()
{printf(hello world!\n);printf(hello world!\n);return 0;
}这里的第二个printf中的字符串由两个字符串组成这样写和第一种printf的效果有什么不同呢我们来看看代码运行结果 可以看到两个printf打印出来的内容是一致的所以我们可以得出如果两个字符串挨着一起写会实现字符串的合并 为了讲清楚#运算符我们先来看看这样一个例子我们定义一个变量a 3然后我们想要在屏幕上打印 the value of a is 3该怎么做呢经过前面的学习我们已经可以直接写出来了如下
int a 3;
printf(the value of a is %d\n,a);这个时候我们随便更改a的值都可以使得这句话是正确的它会随着a的改变而改变然后我们这个时候说再创建几个变量也要以这种形式进行打印那么每打印一次我们都要写这么长的一串有没有什么办法简化一下呢 这个时候我们就可以定义一个宏来解决在上面那个字符串中有三个变化的地方一个就是of后面的a还有就是占位符%d然后就是最后面的a那么我们就可以定义一个宏PRINT它的参数就是我们要打印的那个值和占位符如下
#define PRINT(x , format) printf(the value of x is %d\n,x);首先我们写出来这个宏后可以发现最后面的那个x会随着我们提供的变量变化所以已经解决了还有两个地方要处理就是of后面的x和后面的占位符因为它们都在字符串中所以替换的时候不会替换它们那么我们怎么办呢 对于占位符的替换我们可以使用上面学习的两个字符串可以合并的思想如下
#define PRINT(x , format) printf(the value of x is format\n,x);这样前面的the value of x is 就变成一个字符串后面的\n也变成了一个字符串这个时候只需要我们传参的时候传上我们的占位符就可以了如下
int a 3;
PRINT(a, %d);经过传参后format会被替换成%d又是一个字符串前面一个中间一个后面一个字符串最后合并成了一个字符串 然后就是最后一个要解决的问题就是of后面的x我们怎么能够使得它随着我们的传参变化而变化呢这个时候就要请出我们的#运算符了它可以使得宏的参数字符串化只需要将x左右的字符串分开然后在它前面加上#即可如下
#define PRINT(x , format) printf(the value of #x is format\n,x)这里将原本的参数x字符串化了最后也变成了一个字符串和其它几个字符串合并在了一起但是这个时候x的内容就会随着我们的传参改变而改变了比如把变量a传给x那么of后面就是a把变量b传给x那么of后面就是b 然后我们来看看完整代码
#include stdio.h#define PRINT(x , format) printf(the value of #x is format\n,x)int main()
{int a 3;float b 5.1f;PRINT(a, %d);PRINT(b, %f);return 0;
}运行结果 这样我们就使用#运算符实现了参数的字符串化#运算符基本上用不到但是我们还是要了解一下毕竟它还是挺有趣的
2.##运算符 ##运算符又是一个完全不同的运算符了#运算符的作用是是参数字符串化##运算符则是可以把位于它两边的符号合成⼀个符号它允许宏定义从分离的⽂本⽚段创建标识符。##被称为记号粘合运算符 比如有一个变量class115我们就可以通过class和115的粘合来得到虽然看起来没有意义但是我们这里也只是举例后面我们会使用##运算符实现一个很有趣的功能 现在我们就来使用class115这个来举个例子我们定义一个宏它的作用就是帮我们粘合两个符号如下
#define CAT(x,y) x##yCAT这个宏就可以帮助我们粘合我们传过去的参数x和y比如我们传参class和115那么它就可以帮我们粘合成class115如下
CAT(class,115);
//经过预处理后变成
class115我们现在写一段代码测试一下看看它能否实现我们的要求
#include stdio.h#define CAT(x,y) x##yint main()
{int class115 5;printf(%d\n, CAT(class, 115));//这里CAT(class,115)相当于class115//因为CAT的作用就是粘合两个符号//这句话就变成了printf(%d\n,class115)//会直接打印5return 0;
}接下来我们来看看代码运行结果 可以看到这里确实实现了我们的想法将class和115两个符号粘合到了一起组合成了变量名class115 上面我们举的例子很简单甚至有点大聪明因为我们只是想要说明##运算符的作用就是起到两边符号的粘合作用接下来我们就来实现一些有趣的宏 我们就以求最大值这个函数为例当我们要找出两个整型数据的最大值时我们需要一个函数当我们要找出两个浮点型数据的最大值时又需要一个函数来实现但是其实这两个函数的实现内容是非常一致的如下
int int_max(int x, int y){return xy?x:y;}float float_max(float x, float y){return xyx:y;}现在我们就来写一个宏宏名为GENERATE_MAX这个宏的作用就是根据我们传过去的数据类型自动生成一个找两个数据中最大值的函数它们的形式类似于类型_max这个时候我们就可以用到我们的##运算符如下
#define GENERATE_MAX(type) \
type type##_max(type x, type y) \
{\return xy?x:y; \
}上面就是我们写出的创建函数的宏它可以根据参数type来确定函数名和函数类型其中的\是续航符可以使内容看起来隔开了实际上是连贯起来的 这里我们也使用到了##运算符如果我们单纯写一个type_max那么在处理时会把它当作一个整体每次我们创建的函数名就都是type_max了而不会根据type的变化生成不同的函数名 而如果我们使用了##运算符就可以使得type成为一个独立的参数而后面的_max就是粘合的符号这样type这个类型在变化我们创建的函数名也就跟着变化了 现在我们就使用这个宏来分别创建一个比较整型最大值和浮点型最大值的函数如下
GENERATE_MAX(int)
GENERATE_MAX(float)接着在VS2022中我们将鼠标指向这个宏随后我们就可以看到我们使用这个宏后会发生什么可以看出来这两条语句经过处理后会替换成两个函数如下图 如上图从扩展到后面的信息可以看出经过预处理后这个宏会被替换成两个名为int_max和float_max的函数实现求最大值的功能是不是特别神奇呢代码可以写的这么有趣 接着我们就赶紧使用一下这两个函数来验证一下是否正确如下
#include stdio.h#define GENERATE_MAX(type) \
type type##_max(type x, type y) \
{\return xy?x:y; \
}GENERATE_MAX(int)
GENERATE_MAX(float)int main()
{int a 4;int b 5;float c 5.3f;float d 6.2f;int ret1 int_max(a, b);float ret2 float_max(c, d);printf(%d %f\n, ret1, ret2);return 0;
}我们来看看运行结果 可以看到我们使用##运算符写的宏确实帮我们生成了两个不同类型的最大值函数并且函数名也随着类型进行改变是不是非常美非常神奇呢
二、预处理指令#undef #undef指令的作用是移除一个#define的定义它的使用格式如下
#undef NAME其中的NAME就是我们要移除的宏的名称当我们想要更换一下宏名的定义时就可以使用#undef指令先移除它原本的定义然后再重新定义它如下
#include stdio.h
#define N 100int main()
{ printf(%d\n, N);
#undef N
#define N helloprintf(%s\n, N);return 0;
}这里我们首先使用#define将N定义为了100将它打印后我们想要改变它的定义就使用#undef把它原本的定义移除然后将它重新定义成了一个字符串然后再重新打印我们来看看它的运行结果
三、条件编译 条件编译有点类似于我们的分支语句不过条件编译是在预处理阶段进行的它会根据我们的条件来决定是否编译某些语句接下来我们就来学习条件编译
1.单分支条件编译 单分支条件编译就是我们只有一条分支需要进行条件判断使用格式如下
#if 常量表达式
//如果条件为真那么就编译这里的语句
//如果条件为假就不会编译这里的语句
#endif
//结束的标志可以看到单分支条件编译从#if开始然后再#endif结束它和分支语句类似但是它有一个结束标志就是#endif这是分支语句中没有的 如果条件为真那么就会编译中间的语句也就是说最后会执行那些语句如果为假则不会执行但是这个条件需要一个常量表达式我们等下来解释为什么不能使用变量现在我们可以先来测试一下#if和#endif如下
#include stdio.h#define N 5int main()
{
#if N 6printf(hello\n);
#endifreturn 0;
}这段代码很简单最后运行它应该什么都不会打印因为我们定义的N是5这里当然不等于6所以不会编译语句printf(“hello\n”)也就不会执行它那么经过预处理后这条语句跑哪里去了呢 如果条件编译的结果为假那么条件编译中的语句经过预处理后会被直接删除就像我们的注释一样也是经过预处理后直接删除所以后面编译就不会带上条件编译中的语句最后运行生成的可执行程序也就不会执行这段语句 现在我们来看看这段代码的运行结果 现在我们再回到之前的那个问题为什么#if后面必须跟一个常量表达式不能是变量呢这是因为#if是在预处理阶段进行处理的预处理指令在预处理阶段还没有给变量分配空间也就是变量在这个阶段都不存在自然不能使用变量了只能使用常量
2.多分支条件编译 多分支条件编译也与分支语句中的多分支语句原理差不多我们来看看在多分支条件编译中需要用到哪些语句
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
//...这里我们列出的结构就是多分支条件编译的结构这样看有点陌生我们拿分支语句中的多分支语句跟它们进行一一对应来进行类比学习
#if -- if
#elif -- else if
#else -- else
#endif -- 分支语句中没有是条件编译结束的标志这样看是否就简单多了它们的用法都差不多只是条件编译在预处理阶段进行处理不能使用含变量的表达式我们现在就来看一个例子看看它的结果是什么如下
#include stdio.h#define N 10int main()
{
#if N 5printf(hehe\n);
#elif N 10printf(haha\n);
#elseprintf(hello\n);
#endifreturn 0;
}可以先自行思考这里我们直接给出答案最后程序只会打印haha首先第一个#if中判断N是不是5很明显不是所以hehe将不会被打印然后到了#elif判断N是否等于10条件成立最后就会打印10也就不会走到#else它的内容也就不会被打印 我们来看看代码运行结果
3.判断符号是否被定义 在编译⼀个程序的时候我们如果要将⼀条语句⼀组语句编译或者放弃编译就可以使用条件编译比如调试性的代码删掉很浪费保留又很碍事我们就可以使用条件编译在编译的时候不编译这些调试性的代码 在这里我们就可以使用一个技巧在最开头使用#define定义一个符号如果我们没有注释或者删除这个符号那么我们就可以编译里面的调试性代码进行正常调试如果我们注释或者删除这个符号那么我们就不编译里面的调试性代码不影响代码的正常运行 在实现这个功能之前我们先来学习如何判断一个符号是否被定义有两种方式
使用#if defined进行判断从字面意思来也很容易理解判断符号是否已经被定义它的使用格式如下
#if defined(符号)#if defined后面的小括号里面就要写上要判断是否被定义过的符号
使用#ifdef进行判断#ifdef实际上就是#if defined的缩写只是缩写后它的使用方法有点不同如下
#ifdef 符号在使用#ifdef就不再需要小括号了而是直接在后面写上我们要判断是否被定义过的符号 现在我们学习了如何判断一个符号是否被定义过现在就来实现一下上面的我们提出的功能首先我们定义一个符号DEBUG来表示调试当我们注释掉它的时候调试信息跟着一起不会执行了 我们现在就引入一个场景使用循环往数组里面存放信息为了保证我们往数组里存放数据成功了我们每存放一次数据就将它打印一次这个打印就是我们的调试信息为了检查我们是否成功往数组存放信息的调试性代码 现在我们就来看这样一个场景如何使用#ifdef或者是#if defined如下
#include stdio.h#define DEBUG
//debug的意思是调试int main()
{int arr[5] { 0 };for (int i 0; i 5; i){//往数组存放数据arr[i] i 1;
#ifdef DEBUG//等价于#if defined(DEBUG)//打印一下数据看看数据是否存放进去了printf(%d , arr[i]);
#endif}return 0;
}如果我们定义过DEBUG这个符号那么就会执行调试性语句printf(%d , arr[i])这里我们定义了DEBUG这个符号那么代码就会编译中间的调试性语句我们来看看代码运行结果是否是这样的 可以看到数据被打印出来了说明这个调试性的语句参与编译了现在我们把定义DEBUG这个符号的语句注释掉看看数组中的数据还会不会被打印 可以看到数组中的数据没有被打印了也就是中间调试性的语句没有被编译和执行于是我们就通过#ifdef或者#if defined实现根据符号是否被定义来确实是否编译代码了
4.判断符号是否没有被定义 这里我们就简单介绍一下判断符号是否没有被定义的两个方法不再举例了因为它和上面的判断符号是否被定义用法差不多
使用#if !defined这个条件编译语句就是在上面我们讲过的#if defined的defined前加上一个表示否定所以这个条件编译语句就是判断符号是否没有被定义格式还是和#if defined一致这里就不再赘述了使用#ifndef这个条件编译语句就是在上面我们讲过的#ifdef的def前面加上一个n表示no也是否定含义所以这个条件编译语句就是判断符号是否没有被定义格式还是和#ifdef一致这里就不再赘述了 这里我们就不再举例使用了因为我们后面讲到包含头文件还会用到它们在那里的使用就更加常见了我们耐心往下看
四、头文件的包含 头文件的包含的本质就是拷贝当我们包含一个头文件后会直接将头文件的内容拷贝过来接下来我们就来学习头文件的包含方式以及嵌套包含头文件时如何解决代码冗余的问题
1.库头文件的包含 库头文件里面包含了C语言帮我们实现的功能我们只需要包含库头文件就可以使用相应的功能和函数如标准输入输出头文件stdio.h我们在包含这种库头文件时一般会使用尖括号来进行包含如下
#include stdio.h当我们使用来进行包含头文件时程序会直接去我们IDE的标准路径下去查找如果找不到就提示编译错误
2.本地头文件的包含 本体头文件就是我们自己写的头文件比如add.h这种手动实现的头文件我们在包含这种本体头文件时常常使用双引号来进行包含如下
#include add.h那么程序就会先在源⽂件所在⽬录下查找如果该头⽂件未找到编译器就像查找库函数头⽂件⼀样在标准位置查找头⽂件 那么问题来了我们可不可以使用双引号“这种方式来包含库头文件答案是可以那么为什么我们还要使用来包含库头文件而不是统一使用双引号来包含头文件 这可能是我们平常写代码没有涉及到的原因我们平常写代码最多创建3到5个头文件所以使用双引号包含库头文件也没什么影响但是如果在一个大型工程中创建了3到5万个头文件呢 如果使用”“包含库头文件那么每次都要去当前文件夹里找这个头文件但是我们知道实际上不可能找得到然后再去标准路径找这个头文件我们平常使用的头文件较少可能影响不大 但是在大型工程中这多余的步骤将会造成不小的开销 所以我们还是要养成良好的习惯包含库头文件使用尖括号包含本地头文件使用双引号” 最后我们来总结一下使用尖括号包含头文件和使用双引号包含头文件的不同
尖括号包含头文件的查找策略直接去标准路径下去查找如果找不到就提⽰编译错误双引号包含头文件的查找策略 先在源⽂件所在⽬录下查找如果该头⽂件未找到编译器就像查找库函数头⽂件⼀样在标准位置查找头⽂件
3.嵌套包含头文件的解决方法 在最开始我们提到了我们包含头文件的本质就是进行代码的拷贝将头文件的所有内容拷贝进我们的源文件那么我们如果嵌套包含也就是可能多次包含了同一个头文件多次势必会造成代码的冗余 我们先来看看包含一个头文件多次可能的场景如图 在上图场景中我们每个功能实现都包含了头文件add.h最后进行汇总时势必会包含三次头文件add.h造成代码的冗余 可能有的同学就提出疑问了一般头文件也不会有太多内容呀就算多拷贝几次也还好啊其实不然我们一般会在本地头文件包含我们需要使用的库头文件就拿最常使用的头文件stdio.h来举例我们来看看这个库头文件有多少行代码 方法就是正常包含头文件stdio.h然后使用ctrl加单击的方法就可以点进这个头文件了我们拉到最后发现它居然有两千多行代码如下图 那么如果此时我们多次包含了这个头文件势必会造成代码冗余所以我们要想办法来解决这个问题
使用条件编译指令 我们可以使用刚刚学习的条件编译指令解决具体就是#ifndef或者#if !defined这两个指令这里我们就选择#ifndef比较好写 由于头文件的包含就是代码的拷贝所以我们可以根据这个特点来设计一个功能就是一旦包含头文件我们就判断是否定义了某个符号如果没有定义我们就定义一下它然后执行后面的头文件包含如果这个符号已经被定义了那么就跳过头文件的包含不执行如下 #ifndef __TEST_H__#define __TEST_H__//头⽂件的内容#endif //放到头文件最后这个思路是不是很妙呢这就是我们避免头文件嵌套包含的第一种方法
使用预处理指令#pragma 这种方法就更为简单了我们可以在VS2022上创建一个头文件我们发现头文件中自动包含了一条语句如下 我们可以看到一创建头文件这条语句就出现了这就是今天要介绍的第二个方法在头文件开头写下预处理指令#pragma once那么就可以解决头文件嵌套包含的问题是不是特别简单呢 当然在VS上帮我们自动写上这条指令了如果在其它编译器上创建头文件没有这条语句那么我们直接加上就可以了也十分简单
五、其它预处理指令 除了我们本文介绍的预处理指令其实还有非常多的预处理指令把它们讲完也不太现实所以我们就只讲了一些常用的预处理指令 如果还想要学习更多的预处理指定就需要大家自己去自行了解了这里我就推荐一本书想要深入学习预处理指令就可以参考一下这本书《C语⾔深度解剖》
六、C语言更新结束感言 那么我们C语言的学习就到这里就结束了一路走来也真是不容易不知道大家是否有收获呢如果有的话也不枉我这么努力的更新 从下一篇文章我们就开始学习数据结构了在里面我们会手动实现那些数据结构可以体会到二级指针和递归的暴力美学狠狠期待一下吧 那么今天就到这里感谢观看有疑问欢迎提出 bye~