一 . 预定义符号
C语言设置了一些预定义符号 , 可以直接使用 , 预定义符号也是在预处理期间处理的。
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANI C ,其值为1,否则未定义
例如:可以在VS 写如下代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __TIME__);
printf("%s\n", __DATE__);
printf("%d\n", __STDC__);
return 0;
}
会发现:
二 . #define 定义常量
#define MAX 1000
//为register这个关键字,创建一个简短名字
#define reg register
//用形象的符号来替换一种实现
#define do_forever for(;;)
//在写case 语句的时候自动把break 写上
#define CASE break;case
//如果定义的stuff过长 , 可以分成几行写,除了最后一行外,每一行的后面都加
//一个反斜杠(续行符)
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
注意:换行符 \ 的后面 , 只能接换行 , 不能再加空格或者其他字符
举例一:
#define M 100
#define STR "hehe"
int main()
{
int a = M;
printf("%d\n", a);
printf("%d\n", M);
printf("%s\n", STR);
return 0;
}
举例二:
注意 : 在用 define 定义标识符的时候 , 后面的stuff 的最后 不要加 ;
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define M 100;
int main()
{
printf("%d\n", M);
return 0;
}
这个错误代码 出现了语法错误 --> 在预处理中 是这样的 -->printf("%d\n", M ; )
三 . #define 定义宏
#define 机制包括了一个规定 , 允许把参数替换到文本中 , 这种实现通常称为宏 (macro) 或者定义宏 ( define maroc )
注意 : 宏的替换参数 , 是不经过计算 , 直接替换!
宏的申明方式:
举例:
宏可以接受一个参数
#define SQUARE(N) N*N
int main()
{
int x = 5;
int ret = SQUARE(x);
printf("%d\n", ret);
return 0;
}
如果想计算 x+1
宏可以接受一个参数
#define SQUARE(N) N*N
int main()
{
int x = 5;
int ret = SQUARE(x+1);
printf("%d\n", ret);
return 0;
}
解决 x+1 的方法:
再来看一下代码 , 代码一 ,运行后得出 --> 12 ,且看代码二 :66?
#define DOUBLE(N) N+N
//代码一:
int main()
{
int x = 6;
int ret = DOUBLE(x);
printf("%d\n", ret);
return 0;
}
//代码二:
int main()
{
int x = 6;
int ret =10* DOUBLE(x);
printf("%d\n", ret);
return 0;
}
所以 用于对数值表达式进行求值的宏定义都应该用这样的方式加上括号 , 避免在使用宏时 , 由于参数中的操作符或临近操作符之间不可预料的相互作用 。
四 . 带有副作用的宏参数
当宏参数在宏定义中出现超过一次的时候 , 如果参数带有副作用 ,那么在使用这个宏的时候就可能出现危险 , 导致不可预料的后果 , 副作用就是表达式求值的时候出现的永久性效果。
所以一般来说:
x+1; //无副作用
x++; //带有副作用
例题:
#define MAX(X,Y) ((X)>(Y) ? (X):(Y))
//X Y 出现超过一次
int main()
{
int a = 7;
int b = 5;
int m = MAX(a++, b++);
printf("%d\n", m);
printf("a=%d ,b=%d\n",a,b );
return 0;
}
在这里 , 左看右看觉得 MAX ( ) , 这玩意很像函数 , 这里也看看使用函数会有什么不一样的结果 ,具体对比 ---> 且往下看
int Max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
int a = 7;
int b = 5;
//先计算 , 再把值传给函数
int m = Max(a++, b++);
printf("m=%d\n", m);//7
printf("a=%d\n", a);//8
printf("b=%d\n", b);//6
return 0;
}
五 . 宏替换的规则
1. 在调用宏时 , 首先对参数进行检查 ,看看是否包含任何有#define 定义的符号 。 如果是 , 他们首先被替换 。
2. 替换文本随后被插入到程序中原来文本的位置 。 对于宏 , 参数名被他们的值所替换 。
3. 随后 , 再次对结果文件进行扫描 , 看看它是否包含任何有 #define 定义的符号 , 如果是 , 就重复上述过程
注意 :
1. 宏参数和#define 定义中可以出现其他#define 定义的符号 , 但是对于宏 , 不能出现递归
2. 当预处理器搜索#define 定义的符号的时候 , 字符串常量的内容并不被搜索。
六 . 宏函数的对比
宏通常被应用于执行简单的运算 。
比如在两个数种找出最大的一个市 , 写成下面的宏 , 更有优势一些:
#define MAX(a,b) ((a) >(b) 函数? ( a) : (b) )
为何不用函数来完成这个任务?
- 用于调用函数和从函数返回的代码可能比实际执行这个效性计算工作所需要的时间更多 。 所以宏比函数在程序规模和速度方面更胜一筹
- 函数的参数必须声明为特定类型 , 所以函数只能在类型合适的表达式上使用 。 而宏的参数是与类型无关的!
和函数相比,宏也有劣势:
1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较段,否则可能大幅度增加程序的长度。
2.宏是没法调试的。
3.宏与类型无关,导致不够严谨
4.宏可能会带来运算符优先级的问题,会容易出错。
七 . # 和 ##
7.1 #
#运算符将宏的一个参数转化为字符串的字面量。它仅允许出现在带参数的宏的替换列表中。
#运算符所执行的操作可以理解为 " 字符串化 "
使用:
int main()
{
printf("helloworld\n");
printf("hello""world\n");
//会被当成一个整体的字符串来看待
return 0;
}
观察以下代码发现 ,每条语句功能相似 , 重复性高 , 思考能否使用宏来实现相应功能?
int main()
{
int a = 5;
//the value of a is 3
printf("the value of a is %d\n",a);
int b = 20;
printf("the value of b is %d\n", b);
float f = 3.14f;
printf("the value of f is %f\n", f);
return 0;
}
//宏其实就是参数的替换
#define PRINT(v,format) printf("the value of "#v" is "format"\n", v)
int main()
{
int a = 5;
PRINT(a, "%d");
int b = 20;
//printf("the value of b is %d\n", b);
PRINT(b, "%d");
float c = 3.14f;
//printf("the value of c is %f\n", c);
PRINT(c, "%f");
return 0;
}
7.2 ##
可以把位于它两边的符号合成一个符号,它允许宏定义从 分离的文本片段 创建 标识符 。##被称为记号粘合 。
用 ## 连接必须产生一个合法的标识符。否则结果就是未的定义。
#define CAT(x,y) x##y
int main()
{
int Class115 = 100;
printf("%d\n", CAT(Class, 115));
return 0;
}
思考:如果要写一个函数求两个数的较大值的时候,不同类型就得写不同的函数。
int int_max(int x, int y)
{
return x > y ? x : y;
}
float float_max(float x, float y)
{
return x > y ? x : y;
}
int main()
{
int a = int_max(5, 6);
printf("%d\n", a);
float b = float_max(3.14, 1.2);
printf("%f\n", b);
return 0;
}
以上代码太过于繁琐 , 可以使用宏和##操作符试试:
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
return x > y ? x : y;\
}
GENERIC_MAX(int)
GENERIC_MAX(float)
int main()
{
int a = int_max(5, 6);
printf("%d\n", a);
float b = float_max(3.14, 1.2);
printf("%f\n", b);
return 0;
}
其实在实际的开发过程中 ## 的使用很少 , 很难举出非常贴切的例子。
八 . 命名约定
一般来讲 , 函数与宏的使用语法是很相似 。 所以语法本身没办法帮助我们区分 。
所以一般来说 , 我们的习惯是:
把宏 ---> 全部 大写
函数名 ---> 部分大写
但是这个并不是铁律 , 这里只是建议把宏写成大写 , 便于后续区分
九 . #undef
这条指令用于移除一个宏定义
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的就名字首先就要被移除
#define M 100
int main()
{
printf("%d\n", M);
//想重新定义M , 让他存其他数字
#undef M
#define M 520
printf("%d\n", M);
return 0;
}
十 . 命令行定义
许多C编译器提供了一种能力 , 允许在命令行中定义符号 , 用于启动编译过程。
例 : 根据同一个源文件要编译出一个程序的不同版本的时候 , 这个特性有点用处 。 (假定某个程序中声明了一个某长度的数组 , 如果机器内存有限 ,需要一个很小的数组 , 但是另外一个机器内存大些 , 需要一个数组能大些 。)
#include <stdio.h>
int main()
{
int array [ARRAY_SIZE];
int i = 0;
for(i = 0; i< ARRAY_SIZE; i ++)
{
array[i] = i;
}
for(i = 0; i< ARRAY_SIZE; i ++)
{
printf("%d " ,array[i]);
}
printf("\n" );
return 0;
}
//linux 环境演⽰gcc -D ARRAY_SIZE=10 programe.c
十一 . 条件编译
在编译一个程序的时候 , 将一条语句(一组语句) 选择编译或者放弃是很方便的 。可以使用条件编译指令。
比如说:
调试性的代码 , 删除可惜 , 保留碍事 ---> 可以选择性编译!!!!
#define __DEBUG__
int main()
{
int arr[10] = { 0 };
for (int i = 0; i < 10; i++)
{
arr[i] = i + 1;
#ifdef __DEBUG__
//用来检查是否存放数组
printf("%d ", arr[i]);
#endif
}
printf("\n");
return 0;
}
常见的条件编译指令:
# if 常量表达式
//......
#endif
//常量表达式由预处理器求值
#define M 3
int main()
{
#if M==3
printf("hello world!\n");
#endif
return 0;
}
要注意的是 , 不能定义一个局部变量 , 然后用 #if 来做条件编译 ,因为在预处理阶段 , 局部变量还没有被创建出来。
多个分支的条件编译
# if 常量表达式
//.......
#elif 常量表达式
//......
#else
//......
#endif
#define M 1
int main()
{
#if M==1
printf("hello world!\n");
#elif M==2
printf("hello!\n");
#else
printf("world!\n");
#endif
return 0;
}
判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
不关心值 , 只关心是否被定义过
#define M 10
int main()
{
//这里就是判断M是否定义过,关于值是多少,不关心
#if defined(M)
printf("hehe\n");
#endif
#if !defined(M)
printf("good!\n");
#endif
#ifdef M
printf("hello world!\n");
#endif
#ifndef M
printf("perfet!\n");
#endif
return 0;
}
嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
十二 . 头文件的包含
12.1 头文件的包含的方式
本地文件包含:
#include "filename"
查找策略 : 先在源文件所在的目录下查找 , 如果该头文件未被找到 ,编译器就像查找库函数头文件一样在标准位置查找头文件。
如果找不到就提示编译错误!
Linux 环境的标准头文件的路径:
/ use / include
库文件包含 :
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
那是不是就可以说 , 对于库文件也可以使用 " " 的形式包含?
答案是肯定的 --> 可以 , 但是这样做查找的效率就会低一些,当然这样也不容易区分是库文件还是本地文件了。
12.2 嵌套文件包含
#include 指令可以使另一个文件被编译 。 就像它实际出现于 #include 指令的地方一样。
怎样被替换? ---> 预处理器先删除这条指令 , 并用包含文件的内容替换
一个文件被包含10 次 , 那就实际被编译10次 。 如果重复包含 , 对编译的压力就比较大。
如果这样写,test.c 文件中 test.h 会被包含多次 , 如果test.h 文件比较大,这样预处理后代码量会剧增 , 如果工程比较大 , 有公共使用的头文件 ,被大家都能使用 , 又不做任何处理 , 后果会不堪设想 。
如何解决 ? 条件编译
每个头文件的开头写:
#ifndef __TEST_H__#define __TEST_H__//头⽂件的内容#endif //__TEST_H__或者#pragma once
十三 . 其他预处理指令
#error
#pragma#line#pragma pack()这里不做过多介绍 , 可以参考《C语言深度解剖》
十四 . 小练
14.1 小练1
写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
考查:offsetof 宏的实现
#define MY_OFFSETOF(s,m) ((size_t)&(((s*)0)->m))
typedef struct
{
short a;
int b;
}StructType;
int main()
{
int ret = MY_OFFSETOF(StructType, b);
printf("%d\n", ret);
return 0;
}
14.2 小练2
写一个宏,可以将一个整数的二进制位的奇数位和偶数位交换
#define SWAP_BIT_INT(n) ((((n)&0x55555555)<<1) | (((n)&0xaaaaaaaa)>>1))
int main()
{
int n = 13;
int ret = SWAP_BIT_INT(n);
printf("%d\n", ret);
return 0;
}