C预处理器和C库
C 预处理器和 C 库
1 翻译程序工具 —-> 编译器
在预处理之前,编译器会做一些翻译处理工作。
- 把源代码中出现的
字符
映射到源字符集
。 - 定位每个
反斜杠
后面跟着换行符的实例
,并直接删除。将物理行转换成逻辑行。
printf(“That's wond\
erful!\n");
转成
printf(“That's wonderful!\n");
- 编译器把
文本
划分成预处理记号序列
、空白序列
和注释序列
(记号是由空格、制表符或换行符分隔的项)。
⚠️ 注意:编译器会用一个空格字符
替换每一条注释
。
int /*这看起来不像是一个空格*/ fox;
将变成
int fox;
1.1 明示常量:define
#define预处理器
指令 和 其他预处理器
指令一样,以 #
作为一行的开始。
ANSI 和后来标准都允许
#
前面有空格
或制表符
,也允许在#
和指令的其余部分之间有空格。
⚠️ 注意:指令长度仅限于一行(从#开始运行,到后面的第一个换行符为止)。
1.1.1 每行 define(逻辑行)
的 3 部分
-
#define
指令本身 -
选定的缩写,也称为
宏
。宏代表值称为
类对象宏
。宏的名称中不允许有空格。遵循 C 变量的命名规则。
-
替换列表或替换体。
从宏替换文本的过程称为
宏展开
。
⚠️ 注意:可在#define
行使用标准 C 注释。
宏可以表示任何字符串,也可表示整个 C 表达式。
预处理器不做计算,不对表达式求值 ,只替换字符序列。
1.2 在define
中使用参数
用#define 创建类似函数的类函数宏,~不是函数。
类函数宏定义的圆括号可以有一个或多个参数,随后参数出现在替换体中。
MEAN 是宏标识符
函数调用和宏调用的区别
- 函数调用:在
程序运行时
把参数的值传递给函数。 - 宏调用
在编译之前
把参数记号传递给程序。
⚠️ 注意:必要时使用足够多的圆括号来确保运算结合的正确顺序。
不要在宏中使用递增或递减运算。
1.2.1 用宏参数创建字符串:运算符
例:
# define PSQR(X) printf("The square of X id %d .\n",((X)*(X)));
双引号字符串中的 X 视为普通文本,而不是可被替换的记号。
C 允许在字符串中包含宏参数。在类宏函数的替换体中,#号
作为一个预处理运算符
,可以把记号转化为字符串
,这样的过程叫作字符串化(stringizing)
。例如,双引号中想表示宏形参,则使用 #X
即可。
2 预处理器粘合剂:运算符
可用于类函数宏
和对象宏
的替换部分。把两个记号组合成一个新的标识符。
2.1 变参宏:...
和 __VA_ARGS__
通过把宏参数列表
中最后的参数写成省略号(即3个点...)
来实现这一功能。预定义宏__VA_ARGS__
可用于替换部分中,表明省略号代表什么。
格式:
# define PR(...) printf(__VA_ARGS__)
C99/C11 对宏提供让用户自定义带可变参数的函数。由 stdvar.h 头文件
提供。
⚠️ 记住:省略号只能代替最后的宏参数。
3 宏和函数的选择
3.1 如何选择?
- 使用宏比普通函数更复杂一些,稍有不慎会有副作用。(一些编译器规定宏只能定义成一行)
- 需要注意时间和空间的制衡。
- 宏生成内联代码,在程序中生成语句。函数调用无论多少次,程序中只有一份函数语句的副本,节省了空间。
- 程序的控制必须跳转到函数内,随后再返回主调函数,这比内联代码花费更多时间。
- 对于简单的函数,程序员通常使用宏
3.1.1 宏的注意点
- 宏名不允许有空格,但替换字符中可以有空格。
- 圆括号把宏的参数和整个替换体括起来,能确保被括起来的部分在表达式正确展开。
大写字母表示宏函数的名称
(大写字母可以提醒程序员可能产生副作用)。例如:MAX(X,Y)
- 如果打算使用宏加快程序运行速度,首先要确定使用宏和使用参数是否会导致较大的差异。
在程序中使用一次的宏无法明显减少程序的运行时间,在嵌套循环中使用宏更有助于提高效率。
3.2 文件包含:include
预处理器在发现 #include指令
时,会查看后面的文件名并把文件内容包含到当前文件中(替换源文件
中的#include指令
),相当于被包含文件的全部内容输入到源文件#include 指令所在的位置。
# include<stdio.h> // 尖括号是标准系统文件中
# include "mystuff.h" // 双引号是自定义的头文件(一般是优先查找当前工作目录)
ANSI C 不为文件提供统一的目录模型,不同计算机所用的系统不同。
C 语言习惯用 .h
后缀表示头文件,一般是包含需要放在程序顶部的信息。
头文件经常包含一些预处理器指令。
#define指令、结构声明、typedef和函数原型
是编译器在创建可执行代码
时所需的信息,而不是可执行代码。
用 #ifndef
和 #define
防止多重包含头文件。可执行代码
通常是源代码文件
中,而不是头文件
中。
4 使用头文件
- 明示常量:
stdio.h
中定义的EOF、NULL和BUFSIZE
(标准 I/O 缓冲区大小) - 宏函数:
getc(stdin)
通常用getchar()
定义,而getc()
经常用于定义较复杂的宏,头文件 ctype.h
通常包含ctype
系列函数的宏定义。 - 函数声明:
string.h 头文件
包含字符串函数系列的函数声明
(函数声明都是函数原型形式)。 - 结构模版定义:标准 I/O 函数使用
FILE结构
(结构包含了文件和与文件缓冲区相关的信息)。FILE结构
在头文件stdio.h
中 - 类型定义:标准 I/O 函数使用
指向FILE的指针
作为参数。stdio.h 用
#define
或typedef
把FILE
定义为指向结构的指针
。
#include
和 #define 指令
是最常用的两个 C 预处理器特性。
4.1 其他指令
修改#define
的值即可 生成 可移植性的代码。
#undef指令
取消 之前的#define
定义。
#if、#ifdef、ifndef、#else、#elif 和 endif 指令
用于指定什么情况下编写哪些代码。
#line
指令用于重置行和文件信息。
#error
指令用于给出错误信息。
#pragma
指令用于向编译器发出指令。
4.1.1 undef
指令
#undef
指令用于 “取消” 已定义的 #define
指令。
处理器在识别标识符时,遵循与 C 相同的规则:标识符可以由 大写字母、小写字母、数字 和 下划线字符
组成,且首字符不能是数字。
#define宏
的作用域从它的文件中的声明处开始
,直到用 #undef 指令取消宏
为止,延伸至文件尾(以二者中先满足的条件作为宏作用域的结束)。
⚠️ 注意:如果宏通过头文件
引入,则#define
在文件中的位置取决于#include指令的位置
。
4.1.2 条件编译
使用指令告诉编译器根据编译时的条件执行或忽略信息(或代码)块。
#ifdef、#else 和 #endif指令
- ifdef 指令:预处理器已定义后面的标识符,则执行
#else
或endif指令
之前的所有指令并编译所有 C 代码。 - 如果未定义标识符,且有
#else指令
,则执行#else
和#endif指令
直接的所有代码。
- ifdef 指令:预处理器已定义后面的标识符,则执行
ifdef、#else
与 C 的if else
的区别:
预处理器不识别用于标记块的花括号(
{}
),因此使用#else(如果需要)
和#endif(必须存在)
来标记指令块。且这些指令结构可以嵌套
或标记C语句块
。
ifndef
指令 与 ifdef 类似。只是逻辑相反。
#ifndef指令
判断后面的标识符是否是未定义
的,常用于定义之前未定义
的常量。
#ifndef指令
可以防止相同的宏被重复定义。
# ifndef SIZE
# define SIZE = 77
# endif
#ifndef
指令通常用于防止多次包含一个文件。
# ifndef THINGS_H_
# define THINGS_H_
/*此处省略了头文件中的其他内容*/
# endef
#if
和elif
指令#if
指令类似 C 中的 if。
#if
后面跟整型常量表达式
,如果表达式为非零
,则表达式为真
。
条件编译的好处:使得程序移植性强。
5 泛型选择
泛型编程:指没有特定类型,但指定一种类型,则可以转换成指定类型的代码。
泛型选择表达式:根据表达式的类型选择一个值。不是预处理器指令。
_Gerneric(x,int :0,float:1,double:2,default:3)
_Gerneric
是 C11 关键字。后面的圆括号内
包含有多个逗号分隔
的项。 与 switch 语句类似。
对于泛型选择表达式求值时,程序不会先对第一个项求值,只确定类型。
只有匹配标签的类型后才会对表达式求值。
5.1 内联函数(C99)
和 _Noreturn
函数(C11)
5.1.1 内联函数
函数调用会有一定的开销,原因:函数的调用过程包括建立调用、传递参数、跳转到函数代码并返回
。
解决办法
- 使用宏使代码内联,可避免开销。
- C99 中方法:内联函数。
内部链接的函数可以成为内联函数。
内联函数的定义与调用该函数的代码必须在同一个文件中。
创建内联函数的方法:使用函数说明符inline
和存储类别说明符static
。
内联函数无法在调试器中显示
如果是多个文件使用某个内联函数,则将内联函数定义放在头文件中,并在使用的文件中引入头文件即可。
一般情况下,
不在头文件中放置可执行代码,内联函数特例。
5.1.2 _Noreturn
函数
C11 中新增函数说明符_Noreturn
,表明调用后函数不返回主调函数。
exit()
函数是 _Noreturn
函数的特例。。exit()
不会返回主调函数
⚠️ 注意:与void类型
不同,void函数
的类型在执行完毕后返回主调函数,但它不提供返回值
。
5.2 C 库
5.2.1 访问 C 库
-
自动访问
在使用函数之前必须先声明函数的类型,通过包含合适的头文件即可。
-
文件包含
通过#include 来引入。
-
库包含
通过编译时选项显式指定某些库。
与包含头文件不同,头文件提供函数声明或原型。
库选项告知系统到哪里查找函数代码。
5.2.2 数学库
5.2.3 通用工具库
通用工具库包含各种函数,包括随机数生成器、查找和排序函数、转换函数和内存管理函数。
这些函数均在 stdlib.h 头文件中
exit()
和atexit()
函数main()函数
返回系统时将自动调用exit()
函数。
atexit()
函数的用法
使用atexit()
函数,只需把退出时要调用的函数地址
传递给atexit()
即可。
exit()
函数的用法
exit()
执行完 atexit()
指定的函数后,会完成一些清理工作:刷新所有输出流、关闭所有打开的流和关闭由标准I/O函数 tmpfile()
创建的临时文件。
qsort()
函数
快速排序算法(qsort()函数
):排序数组的数据对象。原型如下:
void qsort(void *base,size_t nmemb,size_t size,
int (*compare)(const void *,const void *));
第 1 个参数:值指向待排序数组首元素的指针。可引用任何类型的数组。
第 2 个参数:待排序项的数量。
第 3 个参数:数组中每个元素占用的空间大小。
第 4 个参数:一个指向函数的指针(返回 int 类型的值且接受两个指向 const void 的指针作为参数)。
5.2.4 断言库
assert.h 头文件
支持的断言库:用于辅助调试程序
的小型库。由 assert()宏
组成,接受一个整型表达式作为参数。
assert()
的参数是一个条件表达式
或 逻辑表达式
。
如果
assert()
中止程序,则首先会显示失败的测试、包含测试的文件名和行号
。
C11 中新增 _Static_assert()
:
- 第 1 个参数:整型常量表达式
- 第 2 个参数:一个字符串
与 assert() 的区别
assert()
会导致 正在运行的程序中止。_Static_assert()
可导致 程序无法编译通过。