一、预定义符号
C语言设置了一些预定义符号 ,可以直接使用 ,预定义符号是在预处理期间处理的。
FILE //进行编译的源文件
LINE //文件当前的行号
DATE //文件被编译的日期
TIME //文件被编译的时间
STDC //如果编译器遵循ANSI C,其值为1,否则未定义
注意打印格式。
二、#define定义常量
基本语法:#define name stuff
举例:
#define MAX 1000 //用MAX表示1000
#define REG register //为 register这个关键字,创建一个简短的名字REG
#define do_forever for(;;) //用更形象的符号来替换一种实现(死循环)
#define CASE break;case //在写case语句的时候自动把 break写上
#define DEBUG_PRINT printf("file:%s\nline:%d\n \
date:%s\ntime:%s\n" ,\
FILE,LINE , \
DATE,TIME )
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
三、#define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro) 或定义宏(define macro)。
申明方式:#define name( parament-list ) stuff
其中的 paramet - list 是一个由逗号隔开的符号表,它们可能出现在 stuff 中。
注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
举例对数值表达式进行求值的宏定义:
#define SQRT(x) ( ( x ) + ( x ) )
对数值表达式进行求值的宏定义应该加上括号,避免在使用宏时由于参数中的操作符或相邻操作符之间不可预料的相互作用。
四、带有副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用 ,那么你在使用这个宏的时候就可能会出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
如:
y = x + 1;
y = x++;
两句代码虽然都实现了将x+1的值赋给y但是,第二句改变了x本身的值,这就是带有副作用的。
对于传参的函数,还有有参数的宏需要注意的是:函数的传参是参数部分计算出结果后,将结果传给形参;而宏的参数是替换进去的,不会发生计算,所以前后可能会导致变化。
五、宏替换的规则
(1)在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果有,它们就会首先被替换。
(2)替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
(3)再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果有,就重复上述处理过程。
注意:
(1)宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归。
(2)当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索。
六、宏和函数的对比
属性 | #define 定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序的序列中。除了非常小的宏之外,程序的长度会因为宏代码的插入而大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则相邻操作符的优先级可能会产生不可预料的后果 | 函数参数只在函数调用的时候求值一次,求值的结果值传递给函数。表达式的求值结果容易预测 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预料的结果 | 函数参数只在传参的时候求值一次,结果容易控制 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型 | 函数的参数是与类型有关的,如果参数类型不同,就需要不同的函数,即使他们执行的任务是相同的 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
七、#和##
1、#
运算符将宏的一个参数转换为字符串字面量 。它仅允许出现在带参数的宏的替换列表中。
运算符所执行的操作可以理解为" 字符串化 "。
举例:
需要注意的是:C语言支持多个字符串拼接打印。
2、##
可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。## 被称为记号粘合。
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
举例:
八、命名约定
命名习惯:
(1)把宏名全部大写
(2)函数名不要全部大写
九、#undef
这条指令用于移除一个宏定义。
#undef NAME //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
十、命令行定义
许多C的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
十一、条件编译
如果我们要将一条语句(一组语句)编译或者放弃可以通过条件编译指令
常见的条件编译指令:
1.
#if 常量表达式
//...
#endif //常量表达式由预处理器求值。
如: #define DEBUG 1
#if DEBUG
//..
#endif
2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3.判断是否被定义
#if defined(symbol)
#if !defined(symbol) //上一条的否定
#ifdef 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
十二、头文件的包含
1、本地文件包含
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件⼀样在标准位置查找头文件。 如果找不到就提示编译错误。
2、库文件包含
#include <filename>
查找策略:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
需要注意的是:理论上对于库文件也是可以用 " " 的形式包含,但是这样做查找的效率就会降低,并且这样也不容易区分是本地文件还是库文件。
3、嵌套文件包含
可以用条件编译的方法来解决头文件被重复引入的问题:
#ifndef TEST_H
#define TEST_H
//头文件的内容
#endif //
第一次进入之前 TEST_H 还未被定义,所以可以引入头文件,但是引入头文件的同时定义了__TEST_H__ 之后就不能再次引入头文件了。
十三、其他预处理指令
如offsetof
在 C 语言中, offsetof 是一个宏,用于获取结构体成员相对于结构体起始地址的偏移量。它在 <stddef.h> 头文件中定义。
(1)(*type)0首先将整数0强制类型转化为结构体指针类型,用来代表起始地址为0,是一个空指针。
(2)((type*)0)->member 通过创建的空指针访问结构体type中的成员member,编译器会根据成员member在结构体中的位置来计算地址。
(3)&((type*)0)->member 由于起始地址为0,所以取出的地址就是该成员member的偏移量。
推荐自主学习,培养自主学习的能力。
#error
#pragma
#line
......