目录
1.预定义符号
预定义符号是可以直接使用,在预处理期间会被处理
__FILE__ //进⾏编译的源⽂件 __LINE__ //⽂件当前的⾏号 __DATE__ //⽂件被编译的⽇期 __TIME__ //⽂件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
printf("file:%s line:%d\n", __FILE__, __LINE__);
2.#define定义常量
#define name stuff #define MAX 1000 #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__ ) 定义时不要加;,因为替换后可能会造成语句提前中断,造成语法错误
3.#define定义宏
允许将参数替换到文本中,这种实现称为宏或定义宏
#define name(parament-list) stuff 比如#define square(x) x*x 凡是出现square(参数部分随机),都会被替换成参数部分*参数部分 但如果参数部分是个表达式,那么为了防止出现运算优先级导致的错误, 我们可以这样#define square(x) (x)*(x) 但还是会出现一个问题,比如 #define square(x) (x)+(x) 10*square(x) 这里看似是10*(x+x),但宏定义时外面没有大括号 所以最终语句是被解释成10*(x)+(x) 因此,在定义时,可以加一个大括号#define square(x) ((x)+(x)) 对于数值表达式进行求值的宏定义,都应该用这种方式加上括号,避免在 使用宏时由于参数中的操作符或临近操作符之间不可预料的相互作⽤。
4.带有副作用的宏参数
宏参数在宏的定义中超过一次时,如果参数有副作用,会出现不可预测的结果。副作用就是表达式求值时出现的永久性效果
x+1;//这是个表达式,不改变x本身的值 x++;//这个操作会改变x本身的值,因此有副作用
#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) ... x = 5; y = 8; z = MAX(x++, y++); printf("x=%d y=%d z=%d\n", x, y, z); 因为x++,y++会造成副作用,导致宏定义展开后,a和b的值在第二次出现时都发生了变化
5.宏替换的规则
展开宏和常量时
调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
替换文本随后被插入程序中原来文本的位置,对于宏,参数名被他们的值所替换
最后再次对结果文件扫描,看看是否包含任何由#define定义的符号,如果有,重复之前动作
宏参数和#define定义中可以出现其他#define定义的符号。但对于宏,不能出现递归
当预处理器搜索#define定义的符号时,字符串常量的内容并不被搜索
6.宏函数的对比
在两个数中找较大的一个时,可以写成下面的宏。
#define MAX(a, b) ((a)>(b)?(a):(b)) 这个宏定义的意思是,如果a>b,则整体值就是a 如果a<b则整体值就是b 这样写有2个优势 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多, 所以宏比函数在程序的规模和速度上更胜一筹。 函数的参数必须声明为特定类型。所以函数只能在类型合适的表达式上使用, 反之,这个宏可以使用于整型、长整型、浮点型等可以用>来比较的类型, 宏是类型无关的。 劣势: 每次使用宏时,一份宏定义的代码插入到程序中,除非宏比较短,否则可能大幅度增加 程序的长度。 宏是无法调试的。 宏由于类型无关,不够严谨 宏可能会带来运算符优先级的问题 宏参数可以出现类型 宏不能递归 #define MALLOC(num, type)\ (type )malloc(num sizeof(type)) ... //使⽤ MALLOC(10, int);//类型作为参数 //预处理器替换之后: (int )malloc(10 sizeof(int));
计算逻辑如果比较简单,可以使用宏。如果逻辑比较复杂,建议使用函数
7.#和##
7.1 #运算符
#运算符将宏的一个参数转换为字符串字面量。仅允许出现在带参数的宏的替换列表中
#运算符所执行的操作可以理解为字符串化
#define PRINT(n) printf("the value of "#n " is %d", n); 假如int a=10; PRINTF(a); 那么a替换进去后,就被变成printf("the value of ""a" " is %d", a) 运行后出现 the value of a is 10
7.2##运算符
##运算符可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符,##被称为记号粘合
合成的必须是一个合法的标识符,否则结果是未定义
int int_max(int x, int y) { return x>y?x:y; } float float_max(float x, float y) { return x>yx:y; } 这样写比较繁琐 我们可以这样: #define GENERIC_MAX(type) \ type type##_max(type x, type y)\ { \ return (x>y?x:y); \ } 这样的话,我们就可以用2条语句,创建2个函数 GENERIC_MAX(int) //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名 GENERIC_MAX(float)//替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名 使用时: int main() { //调⽤函数 int m = int_max(2, 3); printf("%d\n", m); float fm = float_max(3.5f, 4.5f); printf("%f\n", fm); return 0; }
8.命名约定
一般来讲,函数的宏使用语法很相似,语言本身无法区别二者,所以
宏名用大写,函数名不要全大写
9.#undef
用于移除一个宏定义
#undef NAME //如果现存的一个宏名字需要被重新定义 ,那么它的旧名字首先要被移除
10.命令行定义
很多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
11.条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或放弃是很方便的。因为有条件编译语句,满足编译,不满足不编译
调试形的代码,删除可惜,保留碍事,因此选择性的编译
#include <stdio.h> #define __DEBUG__ int main() { int i = 0; int arr[10] = {0}; for(i=0; i<10; i++) { arr[i] = i; #ifdef __DEBUG__ printf("%d\n", arr[i]);//为了观察数组是否赋值成功。 #endif //__DEBUG__ } return 0; }
1. #if 常量表达式 //... #endif //常量表达式由预处理器求值。 如: #define __DEBUG__ 1 #if __DEBUG__ //.. #endif 2.多个分⽀的条件编译 #if 常量表达式 //... #elif 常量表达式 //... #else //... #endif 3.判断是否被定义 #if defined(symbol) #ifdef symbol #if !defined(symbol) #ifndef symbol 4.嵌套指令 #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.头文件的包含
12.1头文件被包含的方式:
12.1.1本地文件包含
#include "filename"
查找策略 :先在源文件所在目录下查找,如果没找到头文件,编译器就像查找库函数头文件一样在标准位置查找头文件,如果找不到就提示编译错误
linux环境标准头文件路径:
/usr/include
vs环境标准头文件路径(vs2013):
C:\Program Files (x86)\Microsof Visual Studio 12.0\VC\include
12.1.2库文件包含
#include <filename.h>
查找头文件直接去标准路径下查找,没找到就提示编译错误
库文件也可以用双引号形式,但效率低,也不容易区分库文件还是本地头文件
12.2嵌套文件包含
我们已经知道,#include 指令可以使另外一个文件被编译,就像它实际出现于#include 指令的地方一样。
替换方式:预处理器先删除这条命令,再把包含文件的内容填入这个位置
一个头文件被包含是10次,那实际就被编译10次,如果重复包含,对编译的压力会比较大
#include "test.h" #include "test.h" #include "test.h" #include "test.h" #include "test.h" 如果直接这样写,文件会被包含5次,被拷贝5份放在.c文件中 如果.h文件比较大,那么预处理后代码量会剧增。 如果工程比较大,有公共使用的头文件,被大家都能使用,又不做任何处理, 后果不堪设想 为此 我们可以用条件编译。 头文件的开头我们可以这样写: #ifndef __TEST_H__ #define __TEST_H__ //头⽂件的内容 #endif //__TEST_H__ 或者: #pragma once