1 预定义符号
C语言设置了一些预定义符号,可以直接使用,预定义符号也是在预处理阶段处理的。
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
#include<stdio.h>
int main()
{
printf("file = %s\n", __FILE__);
printf("line = %d\n", __LINE__);
printf("date = %s\n", __DATE__);
printf("time = %s\n", __TIME__);
return 0;
}
2 define定义常量
基本语法:
// 名字 内容
#define name stuff
#define MAX 100
#define reg register//为register这个关键字创建一个简短的名字
#define do_forever for(;;)//用更形象的符号来替换一种实现
#define CASE break;case //写case语句的时候自动把break加上
//如果定义的stuff过长,可以分成几行写,除了最后一行外,每行的后面都加上一个反斜杠(\续行符)
#define DEBUG_PRINT printf("file:%s\tline:%d\t\
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ ,\
__DATE__,__TIME__ )
注意:在#define
定义标识符的时候,不要在最后加上 ;,这样会引发一些问题。
#include<stdio.h>
#define MAX 1000;
int main()
{
int max = 0;
if (1)
max = MAX;
else
max = 0;
return 0;
}
这里之所以会出现错误,就是因为#define
在定义MAX的时候,后面加上了分号,而if语句后面没有使用大括号,默认后边只有一条语句
,会出现语法错误。
3 define定义宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的声明方式:
//parament_list代表参数
#define name(parament_list) stuff
注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解析为stuff的一部分
。
#include<stdio.h>
#define SQUARE(X) X*X
int main()
{
int ret = SQUARE(5);
//ok
printf("%d\n", ret);//25
//5+1*5+1
ret = SQUARE(5 + 1);
//why?不应该是36吗
printf("%d\n", ret);//11
return 0;
}
这是为什么呢?我们一起来看看原因是什么呢?这里的X=5+1,那么SQUARE(X)=5+1*5+1,就自然等于11了。现在应该明白了吧。那么如何解决这个问题呢?
//这样就可以解决刚才的问题了
#define SQUARE(X) (X)*(X)
但是这样还是不够完美。有可能会引发新的错误。请看接下来的示例。
#include<stdio.h>
#define DOUBLE(X) (X)+(X)
int main()
{
int ret = 10 * DOUBLE(5);
//这里不应该是100吗?为什么是55呢
printf("%d\n", ret);//55
return 0;
}
10*DOUBLE(5)=10*(5)+(5)=55
现在应该明白了吧。那要想产生100的效果,我们就要对宏进行修改。
//这样就完美了,不易出错
#define DOUBLE(X) ((X)+(X))
总结:所以对于数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用
。
4 带有副作用的宏参数
当宏参数在宏定义中出现超过一次的时候,如果宏参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
#include<stdio.h>
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
int x = 5;
int y = 8;
//z=( (x++)> (y++) ? (x++) : (y++) )
// 使用前 5 8
//后置++,先使用再++, z=( (x++)> (y++) ? (x++) : (y++) )
// 6 9 10
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//x=6,y=10,z=9
return 0;
}
这个示例就可以证明副作用的宏参数所带来的问题。
5 宏替换的规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值所替换。
3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:1.宏参数和#define定义中可以出现其它#define定义的符号。但是对于宏,不能出现递归。
2.当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
6 宏函数的对比
7 #和##运算符
7.1 #运算符
#运算符是将宏的一个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。
#运算符执行的操作可以理解成“字符串化”。
#include<stdio.h>
#define PRINT(x,format) printf("the value of "#x" is "format"\n",x)
int main()
{
int a = 10;
PRINT(a,"%d");
return 0;
}
7.2 ##运算符
##运算符可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。##被称为记号粘合。
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
#include<stdio.h>
#define GET_MAX(type) \
type type##_max(type x,type y) \
{ \
return (x>y?x:y); \
}
//相当于int int_max(int x,int y)
//{
// return (x>y?x:y);
//}
GET_MAX(int);
int main()
{
int ret = int_max(3, 5);
printf("%d\n", ret);
return 0;
}
8 命名约定
函数和宏的使用语法很相似。所以语言本身没办法帮我们区分二者。平时的习惯是:宏名全部大写,函数名不要全部大写。
9 #undef
这条指令用于移除一个宏定义。
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
#undef NAME
#include<stdio.h>
#define PRINT(x,format) printf("the value of "#x" is "format"\n",x)
int main()
{
int a = 10;
PRINT(a, "%d");
//撤销宏定义
#undef PRINT
PRINT('b', "%d");//err
return 0;
}
10 条件编译
在编译一个程序的时候,我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
常见的条件编译指令:
1.
#if 常量表达式
//...
#endif
2.//多个分支的条件编译指令
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
10.1 单个分支条件编译指令
#include<stdio.h>
#define M 2
int main()
{
//如果常量表达式为真,则执行语句
#if M
printf("haha\n");
#endif
return 0;
}
10.2 多个分支条件编译指令
#include<stdio.h>
#define M 3
int main()
{
#if M==1
printf("haha\n");
#elif M==2
printf("heihei\n");
#else
printf("hehe\n");
#endif
return 0;
}
#include<stdio.h>
#define __DEBUG__
int main()
{
//判断__DEBUG__是否被定义的两种方式,如果定义了,就执行语句
#if defined __DEBUG__
printf("haha\n");
#endif
#ifdef __DEBUG__
printf("haha\n");
#endif
return 0;
}
#include<stdio.h>
#define __DEBUG__
int main()
{
//__DEBUG__没有被定义,则执行语句
#if !defined __DEBUG__
printf("haha\n");
#endif
#ifndef __DEBUG__
printf("haha\n");
#endif
return 0;
}
11 头文件的包含
11.1 本地文件包含
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。
11.2 库文件包含
#include<filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是就可以说,对于库文件也可以使用" "
的形式包含。
答案是可以的。但是这样查找的效率就低些,也不容易区分是库文件还是本地文件了。
11.3 嵌套文件包含
#include指令可以使另外一个文件被编译。就像它实际出现于#include指令的地方一样。预处理器先删除这条指令,并用包含文件的内容替换。
一个头文件被包含10次,那就实际被编译10次,如果重复包含,对编译的压力就比较大。
如何解决头文件被重复包含的问题呢?
条件编译或者
#pragma once
这样就可以避免头文件被重复引入。
12 其它预处理指令
#error
#pragma
#line
....
#pragma pack()
完结:C语言到这里就结束了。感兴趣的小伙伴点点关注和点赞吧。感谢各位。