C语言之预处理处罚详情

打印 上一主题 下一主题

主题 658|帖子 658|积分 1974

前言

我们了解预处理处罚有利于理解步伐运行,自行查找题目以及代码修复。
1.预界说符号

C语言设置了一些预界说符号,可以直接使用,会在预处理处罚期间处理处罚。
  1. __FILE__  //进行编译的源文件
  2. __LINE__  //文件当前的行号
  3. __DATE__  //文件被编译的日期
  4. __TIME__  //文件被编译的时间
  5. __STDC__  //如果编译器遵循ANSI C,其值为1,否则未定义
复制代码
测试代码如下:
  1. #include <stdio.h>
  2. int main()
  3. {
  4.         printf(" __FILE__: %s\n", __FILE__);
  5.         printf(" __LINE__: %d\n", __LINE__);
  6.         printf(" __DATE__: %s\n", __DATE__);
  7.         printf(" __TIME__: %s\n", __TIME__);
  8.         //printf(" __STDC__: %D\n", __STDC__);
  9.         return 0;
  10. }
复制代码
VS2022 X64输出结果如下

VS未界说__STDC__ .


2.#define界说常量

根本语法:
  1. #define name stuff
复制代码
测试代码如下
  1. #include <stdio.h>
  2. #define MAX 100//定义常量
  3. #define reg register//为关键字register创建一个简短的名字
  4. #define do_forever for(;;)//死循环,用更形象的符号来替换一种实现
  5. #define CASE break;case//在写case语句的时候自动加上break
  6. //定义的stuff过长,可以分成几行写,除最后一行,每行后面加续行符'\'
  7. #define DEBUG_PRINT printf("file:%s\nline:%d\n\
  8.                                                         date:%s\ntime:%s\n",\
  9.                                                         __FILE__,__LINE__,\
  10.                                                         __DATE__,__TIME__)
  11. int main()
  12. {
  13.         //CASE测试
  14.         int input = 1;
  15.         switch(input)
  16.         {
  17.         case 0:
  18.                 //
  19.         CASE 1:
  20.                 //
  21.                 break;
  22. }
  23.         //测试
  24.         DEBUG_PRINT;
  25.         return 0;
  26. }
复制代码
VS2022 X64输出结果如下

注:#define界说标识符最好不要在末端加";",因为#define name stuff
就是将name换成stuff,如果是stuff;,本来的name就会被更换成stuff;
举个例子
  1. #include <stdio.h>
  2. #define M 100;
  3. int main()
  4. {
  5.         int a = M;
  6.         printf("%d\n", M);
  7.         return 0;
  8. }
复制代码
上面的代码等价于下面的代码
  1. #include <stdio.h>
  2. int main()
  3. {
  4.         int a = 100;;//这个地方没问题,因为;等价于空语句
  5.         printf("%d\n", 100;);//但是这个地方影响打印,会报错
  6.         if(1)
  7.                 a = M;//这个地方也有问题,因为if后没有{},只能跟一条语句,现在是两条语句
  8.         else
  9.                 a = 0;
  10.         return 0;
  11. }
复制代码
VS2022 X64系统如下


3.#define界说宏

#define机制包括了一个规定,答应把参数更换到文本中,这种实现通常称为宏(macro)或界说宏(define macro)。
下面是宏的申明方式:
  1. #define name(parament-list) stuff
复制代码
此中的parament-list是一个由逗号隔开的符号表,它们大概出现在stuff中。
留意:
参数列表的左括号必须与name相邻,如果两者之间有任何空白存在,参数列表就会被表明为stuff的一部分。
举个例子:
  1. #define SQUARE(x) x*x
复制代码
该宏吸取参数x,用x*x的值代替SQUARE(x).
测试代码如下:
  1. #include <stdio.h>#define SQUARE(x) x*x
  2. int main(){        printf("%d\n", SQUARE(5));        printf("%d\n", SQUARE(5+1));        return 0;}
复制代码
VS2022 X64输出结果如下
  1. 25
  2. 11
复制代码
其实宏更换的时候是直接更换,不会对传入的表达式进行计算,所以SQUARE(5+1)等价于5+1*5+1,因此第二行的结果是11.
所以我们要考虑在参数的表面带上括号,防止由于相邻利用符优先级的关系导致结果有偏差。
对宏界说修改后,下面代码的输出结果是36.
  1. #include <stdio.h>
  2. #define SQUARE(x) (x)*(x)
  3. int main()
  4. {
  5.         printf("%d\n", SQUARE(5+1));
  6.         return 0;
  7. }
复制代码
最安全的方式其实是在末了结果也加上括号,如下代码所示。
  1. #define SQUARE(x) (x)*(x)
复制代码
比如下面这段代码输出结果是66,是10*(5+1)+(5+1),但我们想要的是10ADD(6),也就是1012=120.
  1. #include <stdio.h>
  2. #define ADD(x) (x)+(x)
  3. int main()
  4. {
  5.         printf("%d\n", 10*ADD(5 + 1));
  6.         return 0;
  7. }
复制代码
修改后的代码如下所示,输出结果是120.
  1. #include <stdio.h>
  2. #define ADD(x) ((x)+(x))
  3. int main()
  4. {
  5.         printf("%d\n", 10*ADD(5 + 1));
  6.         return 0;
  7. }
复制代码
综上所述,宏界说要留意在各个参数以及终极结果加上括号。

4.带有副作用的宏参数

比如前置、后置++(–)会对参数本身进行修改,所以我们在界说宏的时候,如果传入的参数是a++等内容,特殊是多次更换的情况下,由于参数本身被修改,结果将会是不可猜测的。
举个例子:
  1. #include <stdio.h>
  2. #define MAX(a,b) ((a)>(b)?(a):(b))
  3. int main()
  4. {
  5.         int x = 5;
  6.         int y = 8;
  7.         int z = MAX(x++, y++);
  8.         printf("%d %d %d\n", x, y, z);
  9.         return 0;
  10. }
复制代码
代码int z = MAX(x++, y++);等价于int z = ((x++)>(y++)?(x++)y++));后置++是先使用后++,首先辈行x与y的比较,x<y,且此时x酿成6,y酿成9,执行z = y++,z酿成9,y+1酿成10.
VS2022 X64输出结果如下
  1. 6 10 9
复制代码
但是我们想要的是x和y进行比较,将较大的值赋给z.
当然这个例子感觉有些“为赋新词强说愁”的意味,但是有时候确实会存在参数自身发生变革导致宏结果的不确定性。所以在使用宏的时候只管不要去传带有++、- -这些运算符的参数,很轻易出现意想不到的结果。
5.宏更换的规则

在步伐中扩展#define界说符号和宏时,需要涉及几个步骤。

  • 在调用宏时,首先对参数进行检查,看是否包含任何由#define界说的符号。如果是,首先更换这些符号。
  • 更换文本随后被插入到步伐中原来文本的位置,对于宏,参数名被其更换。
  • 末了再次对结果文件进行扫描,看看它是否包含任何由#define界说的符号。如果是,重复上述处理处罚过程。
留意:

  • 宏参数和#define界说中可以出现其它#define界说的符号。但是宏,不能递归!
  • 当预处理处罚器搜索#define界说的符号的时候,字符串常量的内容并不被搜索。
举例说明第二点如下所示:
  1. #include <stdio.h>
  2. #define MAX(a,b) ((a)>(b)?(a):(b))
  3. int main()
  4. {
  5.         int a = MAX(3, 4);
  6.         printf("MAX(3,4)=%d\n",a);//字符串中的宏不会被替换
  7.         return 0;
  8. }
复制代码
VS2022 X64输出结果如下
  1. MAX(3,4)=4
复制代码
6.宏和函数的对比

宏通常被应用于执行简单的运算,比如在两个数字=中找出较大的一个时,写成下面的宏,更有优势一些。
  1. #define MAX(a,b) ((a)>(b)?(a):(b))
复制代码
为什么不用函数来完成这个任务呢?
缘故原由有二:

  • 用于调用函数和从函数返回的代码大概比实际执行这个小型计算工作所需要的时间更多,所以宏比函数在步伐的规模和速度方面更胜一筹
  • 更为告急的是,函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用,反之,这个宏可以适用于整型、长整型、浮点型等可以用>比较的类型,宏的参数与类型无关
和函数相比宏的劣势:

  • 每次使用宏的时候,一份宏界说的代码将插入到步伐中。除非宏比较短,否则大概大幅度增长步伐的长度。
  • 宏是没法调试的。
  • 宏由于类型无关,也就不够严谨。
  • 宏大概带来运算符优先级的题目,导致结果大概不确定。
宏有时候可以做函数做不到的事变,比如,宏的参数可以出现类型,但是函数不可以。
举例代码如下:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #define MALLOC(num,type)\
  4.         (type*)malloc(num*sizeof(type))
  5. int main()
  6. {
  7.         int* p= MALLOC(10, int);
  8.         if (p = NULL)
  9.         {
  10.                 perror("MALLOC");
  11.                 return 1;
  12.         }
  13.         for (int i = 0; i < 10; i++)
  14.                 p[i] = i + 1;
  15.         for (int i = 0; i < 10; i++)
  16.                 printf("%d ",p[i]);
  17.         return 0;
  18. }
复制代码
综上,宏和函数的比较如下:

7.#和##运算符

7.1 #运算符

#运算符将宏的一个参数转换为字符串字面量,仅出现在带参数的宏更换列表中。
对于变量a,其值为10,我们想打印:the value of a is 10.
可以用以下宏界说:
  1. #define PRINT(n) printf("the value of " #n " is %d\n",n)
  2. //同一行多个字符串打印的时候会自动拼接,#n会自动转化为字符串
复制代码
举例如下:
  1. #include <stdio.h>
  2. #define PRINT(n) printf("the value of " #n " is %d\n",n)
  3. int main()
  4. {
  5.         int a=10,b=11,c=12;
  6.         PRINT(a);
  7.         PRINT(b);
  8.         PRINT(c);
  9.         return 0;
  10. }
复制代码
VS2022 X64输出结果如下所示

7.2 ##运算符

##运算符可以把位于其两侧的符号合成一个符号,答应宏界说从分离的文本片断创建标识符,##被称为记号粘合。
如许的连接必须产生一个合法的标识符,否则其结果就是未界说。
举个例子,写一个函数求两个数较大值,不同的数据类型就要写不同的函数。
  1. int int_max(int x, int y)
  2. {
  3.         return x > y ? x : y;
  4. }
  5. float float_max(float x, float y)
  6. {
  7.         return x > y ? x : y;
  8. }
复制代码
如果我们用下面这种方式,
  1. #define GENERIC_MAX(type)\
  2. type type##_max(type x,type y)\
  3. {\
  4.         return x > y ? x : y;\
  5. }
  6. GENERIC_MAX(int)
  7. GENERIC_MAX(float)
  8. int main()
  9. {
  10.         int a = int_max(2, 3);
  11.         printf("%d\n", a);
  12.         float b = float_max(2, 3);
  13.         printf("%f\n", b);
  14.         return 0;
  15. }
复制代码
VS2022 X64输出结果如下所示
  1. 3
  2. 3.000000
复制代码
在实际开发中##使用较少,很难举出非常贴切的例子。
8.命名约定

一般来讲,函数和宏的使用语法很相似,
命名习惯为:宏界说一般都会将name的所有字母设置为大写,函数名一般只有开头字母大写。
9.undef

该指令用于移除一个宏界说。
  1. #define M 10
  2. #undef M//首先移除旧的宏定义名,才能重新定义
  3. #define M 10
复制代码
10.命令行指令

许多C的编译器提供了一种本事,答应在命令行中界说符号,用于启动编译过程。
例如:可以用于同一个源文件编译一个步伐的不同版本(假定某个步伐中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大一些,我们需要一个大一些的数组)。
  1. #include <stdio.h>
  2. int main()
  3. {
  4. int array [ARRAY_SIZE];
  5. int i = 0;
  6. for(i = 0; i< ARRAY_SIZE; i ++)
  7. {
  8. array[i] = i;
  9. }
  10. for(i = 0; i< ARRAY_SIZE; i ++)
  11. {
  12. printf("%d " ,array[i]);
  13. }
  14. printf("\n" );
  15. return 0;
  16. }
复制代码
编译指令:
  1. //linux环境
  2. gcc -D ARRAY_SIZE=10 programe.c
复制代码

11.条件编译

在编译一个步伐的时候,可以使用条件语句选择性编译语句。
比如:调试性的代码。
  1. #include <stdio.h>
  2. #define __DEBUG__
  3. int main()
  4. {
  5. int i = 0;
  6. int arr[10] = {0};
  7. for(i=0; i<10; i++)
  8. {
  9. arr[i] = i;
  10. #ifdef __DEBUG__
  11. printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
  12. #endif //__DEBUG__
  13. }
  14. return 0;
  15. }
复制代码
常见的条件编译指令:
1.
  1. #if 常量表达式
  2.                 //...
  3. #endif
  4. //常量表达式由预处理器求值
复制代码
如:
  1. #define __DEBUG__ 1
  2. #if __DEBUG__
  3.         printf("haha\n");
  4. #endif
复制代码
2.多个分支的条件编译
  1. #if 常量表达式
  2.                 //...
  3. #elif 常量表达式
  4.                 //...
  5. #else
  6.                 //...
  7. #endif
复制代码
举例代码如下:
  1. #include <stdio.h>
  2. #define __DEBUG__ 2
  3. #if __DEBUG__==0
  4.         printf("hehe\n");
  5. #elif __DEBUG__==1
  6.         printf("haha\n");
  7. #else
  8.         printf("heihei\n");
  9. #endif
复制代码
3.判断是否被界说
  1. #if defined(symbol)
  2. #ifdef symbol
  3. #if !defined(symbol)
  4. #ifndef symbol
复制代码
4.嵌套指令
  1. #if defined(OS_UNIX)
  2.                 #ifdef OPTION1
  3.                                 unix_version_option1();
  4.                 #endif
  5.                 #ifdef OPTION2
  6.                         unix_version_option2();
  7.                 #endif
  8. #elif defined(OS_MSDOS)
  9.                 #ifdef OPTION2
  10.                                 msdos_version_option2();
  11.                 #endif
  12. #endif
复制代码

12.头文件的包含

12.1 头文件包含方式

12.1.1 当地头文件包含

  1. #include "filename"
复制代码
查找计谋:先在源文件地点目次下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在尺度位置查找文件。
Linux情况的尺度头文件路径:
  1. /usr/include
复制代码
VS情况的尺度头文件路径:
  1. C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
  2. //这是VS2013的默认路径
复制代码
根据安装路径去找。
12.1.2 库文件包含

  1. #include <filename.h>
复制代码
一般尺度头文件格式为#include <stdio.h>,即使用<>,而不是"";使用""的头文件查找计谋如上所述,使用<>的头文件会直接在尺度位置查找。所以,如果对库文件使用"",会先在目次文件下探求,但这是浪费时间的,效率较低,并且也不轻易区分到底是库文件还是当地文件
12.2 嵌套文件包含

#include指令可以使一个文件被编译,预处理处罚会将这条指令的内容更换为该文件包含的内容。
但是,如果多次引用头文件,头文件内容就会被多次包含,对编译的压力比较大。
test.h
  1. void test();
  2. struct Stu
  3. {
  4.         int id;
  5.         char name[20];
  6. };
复制代码
test.c
  1. #include "test.h"
  2. #include "test.h"
  3. #include "test.h"
  4. #include "test.h"
  5. #include "test.h"
  6. int main()
  7. {
  8.         return 0;
  9. }
复制代码
如果是如许的话,test.c文件将5次包含test.h的内容,如果tets.h内容比较多,预处理处罚代码剧增。如果工程较大,公共使用的头文件被多次引用,不做任何处理处罚,导致预处理处罚后的代码冗余。
每个头文件的开头写:
  1. #ifndef __TEST_H__
  2. #define __TEST_H__
  3. //头文件的内容
  4. #endif  //__TEST_H__
复制代码
或者
  1. #pragma once
复制代码
可以制止头文件的重复引入
注:推荐《⾼质量C/C++编程指南》中附录的考试试卷(很告急)。
笔试题:

  • 头⽂件中的 ifndef/define/endif是⼲什么用的?
  • #include <filename.h>
    和 #include “filename.h” 有什么区别?

13.其他预处理处罚指令

  1. #error
  2. #pragma
  3. #line
  4. ...//大家有兴趣自行了解
  5. #pragma pack()//用于改变编译器结构体的默认对齐数
复制代码
参考《C语言深度解剖》学习。

总结

“路漫漫其修远兮,吾将上下而求索。”是C语言末了一篇啦,下次就是数据结构啦!头文件、宏界说等内容比较多,记得消化哟~

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

曂沅仴駦

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表