C语言学习之预处理 ゝ一世哀愁。 2024-03-30 16:37 20阅读 0赞 **目录** 1.什么是预处理 2.头文件展开 3.去注释 4.宏替换 4.1 什么是宏 4.2 宏的作用范围 4.3 使用宏的小建议 4.4 \#和\#\# 4.5 宏替换vs去注释 5.条件编译 5.1 什么是条件编译 5.2 条件编译的使用 5.3 条件编译的作用 6.总结 -------------------- ### 1.什么是预处理 ### 我们用C语言直接写出来的代码是不能被计算机进行识别的,这其中需要进行一系列过程使源码转换成计算机所能识别的二进制语言,这一系列过程就叫做翻译。源码翻译过程主要有四步: > **预处理**:头文件展开,去注释,宏替换,条件编译等 > > **编译**:将C语言翻译成汇编语言 > > **汇编**:将汇编语言转化为可重定向目标文件(可被链接,已经是二进制,但不是可执行文件) > > **链接**:自身程序+库文件进行关联,形成可执行程序 > > **本篇文章我们就来谈谈预处理这个环节,并了解如何定义宏,如何进行条件编译。** ### 2.头文件展开 ### 我们知道,在编写C程序的时候,我们经常需要包含许多头文件,我们会使用C语言的预处理指令\#include来包含头文件。在程序进行预处理时,就会将头文件的内容复制到我们的代码文件中,以便我们进行函数,变量的调用。如: #include<stdio.h> #include<string.h> #include"game.h" > 对于\#include,有以上两种使用方法,区别如下 > > * 使用尖括号`< >`,编译器会到系统路径下查找头文件; > * 而使用双引号`" "`,编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。 > > 我们通常使用尖括号包含标准库中的头文件,因为标准头文件是在系统路径下的;而对于我们自己创建的头文件,由于其在当前目录下,因此需要使用双引号。 ### 3.去注释 ### 我们经常在写代码的时候添加一些注释,而这些注释会在预处理阶段进行替换,编译器会将注释全部替换为空格。在C语言中,注释有两种写法: //int a = 10; // C++风格注释,使用//,推荐 /* int a = 10; */ // C风格注释,使用/**/,不推荐 我们推荐C++风格的写法,原因是C风格的写法在进行嵌套注释的时候可能会出现问题: /* //位置1 int a = 10; while (a) { printf("%d", a); /* //位置2 a--; */ //位置3 } */ //位置4 > 由于/\*会与离其最近的\*/进行匹配,所以位置1和位置3的/\*和\*/会优先进行匹配,导致二者间的代码被替换为空格,后面的\*/便无法进行匹配,导致出错。而换成C++风格的注释就不会出现这种错误。 ### 4.宏替换 ### #### 4.1 什么是宏 #### 在C语言中,我们通常使用\#define命令来定义宏。该命令允许把一个名称指定成任何所需的文本,例如一个常量值或者一条语句。在定义了宏之后,无论宏名称出现在源代码的何处,预处理器都会把它用定义时指定的文本替换掉。如下: #include<stdio.h> #define MAX 1000 #define MUL(x,y) (x)*(y) int main() { printf("%d ", MAX); printf("%d", MUL(3, 2)); return 0; } 编译器进行宏替换后的结果如下: #include<stdio.h> #define MAX 1000 #define MUL(x,y) (x)*(y) int main() { printf("%d ", 1000); printf("%d", 3 * 2); return 0; } ![08cbffb624e947aa857df875957a671a.png][] -------------------- #### 4.2 宏的作用范围 #### 源文件的任何地方都可以定义宏,与是否在函数内外无关,我们习惯将其写在最上方。宏的作用范围是从定义处往后都是有效的,之前无效。\#undef可以提前结束宏的作用范围。例如: #include<stdio.h> #define MAX 1000 void fun() { #define RESET 0 } #define MUL(x,y) (x)*(y) int main() { #define MIN -1000 #undef MUL return 0; } > 由于宏的定义与是否在函数内外无关,所以对于MAX,RESET,MIN这三个宏的作用范围都是从定义处往后直到程序结束。而对于MUL宏,由于提前使用了\#undef取消定义,因此其作用范围为定义处到取消定义处。 下面我们通过一道小例题加深印象: #include<stdio.h> int main() { #define X 3 #define Y X*2 #undef X #define X 2 int z = Y; printf("%d\n", z); return 0; } > 由于Y宏的内容为X\*2,因此int z=Y的内容就会被替换为int z=X\*2。而对于X宏,由于此时X宏的内容为2,因此最后z变量存放的值就是2\*2=4。 ![bbb88c477efe429e88d3575a8fe194e1.png][] -------------------- #### 4.**3 使用宏的小建议** #### > **1.在定义宏的时候,如果具有参数,建议将参数用括号括起来,防止因为替换后优先级导致出错。** > > **2.宏定义如果需要定义多条语句,建议使用do\{\}while(0)进行封装,使得代码适用性强。** > > -------------------- > > 下面我们来逐条分析: **对于第1点具体做法如下:** #define MUL(x,y) x * y //不推荐 #define MUL(x,y) (x) * (y) //推荐 我们推荐使用第二种写法,原因是预处理期间进行宏替换时只是简单的文本替换,如果x或y是一个表达式时并不会先进行计算再传入,如下: #define MUL(x,y) x * y int main() { int m = MUL(2 + 3, 6); //相当于int m = 2 + 3 * 6; return 0; } > 由于只是简单的文本替换,并不会先将2和3相加后再与6相乘,而是根据替换后的符号优先级进行计算,最后m=20,与目标值不同。 #define MUL(x,y) (x) * (y) int main() { int m = MUL(2 + 3, 6); //相当于int m = (2 + 3) * (6); return 0; } > 而当加上括号时,由于括号的优先级高,无论传入的表达式如何,都会先计算括号内的内容,最终m=30,与目标值相同。 **对于第2点具体做法如下:** #define SET int a = 10; a--; //不推荐 #define SET do{ int a = 10; a--;}while(0) //推荐 我们推荐第二种做法,下面通过一个场景来说明为什么第二种做法更好: #define SET int a = 10; a--; int main() { int flag = 0; if (flag) SET; else flag = 0; return 0; } > 当我们使用第一种做法时,上述代码显然无法编译通过,原因时宏替换后if语句后就跟上了两条语句,不符合C语言的语法。如果未来有程序员使用我们定义的宏又没有在if语句后添加花括号,就会导致程序出错。而如果我们在宏定义处添加括号,宏替换后就会变成\{...\};else的形式,同样不符合语法,程序无法通过编译。 #define SET do{ int a = 10; a--;}while(0) //推荐 int main() { int flag = 0; if (flag) SET; else flag = 0; return 0; } > 而如果我们使用第二种做法,则宏替换后SET;就会变成一条do\{\}while(0);循环语句,此时if语句后就只跟一条循环语句,符合C语言语法。且这条语句只执行一次,程序正常运行。且未来程序员无论有没有在if语句后加上花括号都不会影响结果,代码适用性强。 -------------------- #### 4.4 \#和\#\# #### \#和\#\#是C语言中的预处理指令,它们只能在宏定义中使用。功能如下: > **\#:**当在宏定义中出现使用\#参数的形式,就是将参数的字面值转换为字符串。如\#define STR(s) \#s ,这个宏作用就是把s的字面值转为字符串常量。 > > -------------------- > > **\#\#:**如果出现aa\#\#bb\#\#cc这种使用双井号定义的宏,作用就是形成一个新的符号aabbcc,注意是符号而不是字符串。 //#和##样例 #define STR(s) #s #define TMP(x,y) x##y #define CONS(x,y) x##e##y int main() { int tmp12=100; printf("%s\n", STR(12345)); //将整形12345转换为字符串打印 printf("%d\n", TMP(tmp, 12)); //将tmp和12拼接为符号tmp12,这是一个整形变量,打印100 printf("%lf\n", CONS(2, 3)); //将2,e,3拼接为符号2e3,一种科学计数法,2X10^3 return 0; } > 上述代码宏替换后 STR(12345)变为“12345”;TMP(tmp,12)变为符号tmp12,是存储着100的变量;CONS(2,3)变为符号2e3,为2X10^3。 -------------------- #### 4.**5 宏替换vs去注释** #### 我们来看一段有意思的代码: #include<stdio.h> #define ABS // int main() { ABS printf("hello"); } 我们定义了一个ABS宏为//,并在主函数中使用了这个宏。我们不妨来猜测以下程序是否会输出hello字符串? * 如果宏替换先于去注释,则会先将ABS替换为//,然后将printf语句注释掉,最后将注释变为空格,程序不会输出。 * 如果去注释先于宏替换,则会先将//后本行的内容作为注释改为空格,最后ABS宏相当于一个空格,宏替换时将ABS宏文本替换为空格,程序输出字符串hello。 ![4157fc8127fe473d8e29fc4e2c8e012e.png][] > 运行程序,程序打印hello字符串。 > > 因此,**去注释先于宏替换进行**。 ### 5.条件编译 ### #### 5.1 什么是条件编译 #### 一般情况下,源程序中所有的非注释行都需要参加编译。但是有时希望对其中一部分内容只在满足一定条件下才进行编译,即对一部分内容指定编译条件,这就是“条件编译”。使用条件编译后,在预处理阶段,如果使用条件编译的代码块满足条件,则保留;如果不满足条件,则预处理器就会将代码裁剪,使其不会在后续阶段被编译。 > C语言中常见的条件编译指令有**\#ifdef,\#ifndef,\#if,\#else,\#elif,\#if defined(),\#endif** #### 5.2 条件编译的使用 #### * *\#ifdef和\#ifndef:* #include<stdio.h> //#define MONEY 0 int main() { #ifdef MONEY { printf("money"); //当定义MONEY打印 } #else { printf("free"); //当没有定义MONEY打印 } #endif return 0; } > **\#ifdef**:如果定义了指定的宏,就执行下面代码;如果没有定义,则执行\#else后的代码(如果没有,则不执行)。 > > **\#ifndef**:与\#ifndef相反,没有定义了指定宏就执行下面代码,定义了指定的宏就执行\#else的代码或不执行。 > > **\#endif**:结束条件编译,任何条件编译结束后都要使用\#endif。 > > -------------------- > > 因此,以上代码会打印出free,而\#ifdef与\#else间的代码会在预处理期间被裁剪。当我们定义MONEY宏时,则会打印free。需要注意的是,\#ifdef是判断是否定义宏,而不是判断真假,即定义了MONEY宏,无论MONEY为真为假都会输出money。(\#ifndef与\#ifdef功能相反,使用方法一致,这里就不再讨论) * *\#if和\#elif* #define NUM 0 int main() { #if NUM==1 { printf("1"); //为真打印1 } #elif NUM==2 { printf("2"); //为真打印2 } #else { printf("0"); //为真或没有定义打印0 } #endif return 0; } > **\#if:**表达式如果为真则执行下面代码,如果为假则裁剪代码不编译。 > > **\#elif和\#else:**与if()...else if()...else 语句类似,实现多分支选择功能,区别就在于\#elif和\#else可以不加花括号跟多条语句。 > > -------------------- > > 由于\#if并不是通过判断是否定义宏而是判断表达式真假来实现条件编译,则以上代码会打印0。当我们将NUM改为1时,则会打印1;当改为2时,则会打印2;当NUM未定义时,就会将NUM默认当作假(也就是0),打印0。 * *\#if definefd()和\#if !defined()* #define MONEY 0 int main() { #if defined(MONEY) { printf("money"); //定义了MONEY则打印 } #endif #if !defined(MONEY) { printf("free"); //没定义MONEY则打印 } #endif return 0; } > \#if definefd(),\#if !defined()其实和\#ifdef,\#ifndef()功能相同,对应关系如下: > > **\#if defined() <=> \#ifdef ; \#if !definef() <=> \#ifndef** > > -------------------- > > 当MONEY宏被定义,\#defined(MONEY)结果为真,\#if defined()成立,执行下面语句,打印money;当指定宏没有被定义,\#defined(MONEY)结果为假,\#if !defined()成立,执行下面语句,打印free。同样的,也可以使用\#else与\#elif实现多分支条件编译。 #### 5.3 条件编译的作用 #### > 1.我们可以使用条件编译来裁剪代码,用于快速实现某种目的,如版本维护(free,收费),功能裁剪以及代码的跨平台性。这样写出的软件一般只需维护一份代码,当制作者想要发行不同版本时,只需定义特定的宏就可以实现功能的改变。 > > -------------------- > > 2.一个C工程可能有多个.c文件,而.c文件可能又包含多个.h文件,难免会出现头文件重复包含的现象。这将会导致大量重复的代码被拷贝至我们的文件中,后期编译时会大大降低效率。因此我们可以使用条件编译来防止头文件重复包含,如下: > > //头文件fun.h > #ifndef _FUN_H_ > #define _FUN_H_ > > //内容 > > > #endif > > 当没有包含过fun.h时,\#ifndef \_FUN\_H\_成立,定义\_FUN\_H\_宏并保留后续内容。当未来有文件想再次包含时,由于\_FUN\_H\_宏被定义,\#ifndef \_FUN\_H\_不成立,预处理器就将代码裁剪掉,不会进行编译,实现了预防头文件重复包含的作用。 > > -------------------- > > **此外,还可以使用\#pragma once预处理指令防止头文件重复包含。** ### 6.总结 ### > **1.预处理主要分为头文件展开,去注释,宏替换,条件编译四个步骤** > > **2.C++风格的注释相较于C风格注释较优** > > **3.宏替换只是单纯的文本替换,使用时要考虑多种情况** > > **4.去注释要优先于宏替换** > > **5.条件编译可以用于版本维护和防止头文件重复包含等场景** -------------------- ***以上,就是本期的全部内容。*** ***制作不易,能否点个赞再走呢qwq*** [08cbffb624e947aa857df875957a671a.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/30/2d7166447f6c4df1851d04d489021f2b.png [bbb88c477efe429e88d3575a8fe194e1.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/30/c339ea508029426386f4756a8cc81b6e.png [4157fc8127fe473d8e29fc4e2c8e012e.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/30/84e0c012e38d47a8bad8267c446ae570.png
还没有评论,来说两句吧...