跳转至

第 16 章 C预处理器和C库

第十六章 C预处理器和C库

👉【复习题】【编程练习题

1. 翻译程序工具 ----> 编译器

在预处理之前,编译器会做一些翻译处理工作。

  • 把源代码中出现的字符映射到源字符集
  • 定位每个反斜杠后面跟着换行符的实例,并直接删除。

    将物理行转换成逻辑行。

printf(That's wond\
        erful!\n"); 

转成

printf(That's wonderful!\n");
  • 编译器把文本划分成预处理记号序列空白序列注释序列(记号是由空格、制表符或换行符分隔的项)。

⚠️注意:编译器会用一个空格字符替换每一条注释

int /*这看起来不像是一个空格*/ fox;

将变成

int fox;

2. 明示常量:#define

#define预处理器 指令 和 其他预处理器指令一样,以 #作为一行的开始。

ANSI 和后来标准都允许 # 前面有空格制表符,也允许在 # 和指令的其余部分之间有空格。

⚠️注意:指令长度仅限于一行(从#开始运行,到后面的第一个换行符为止)。

2.1 每行 #define(逻辑行)的3部分

  • #define指令本身
  • 选定的缩写,也称为

    宏代表值称为类对象宏

    宏的名称中不允许有空格。遵循C变量的命名规则。

  • 替换列表或替换体。

    从宏替换文本的过程称为宏展开

⚠️注意:可在#define行使用标准C注释。

宏可以表示任何字符串,也可表示整个C表达式。

预处理器不做计算,不对表达式求值 ,只替换字符序列。

3. 在#define中使用参数

用#define创建类似函数的类函数宏,~不是函数~~。

类函数宏定义的圆括号可以有一个或多个参数,随后参数出现在替换体中。

MEAN 是宏标识符

函数调用和宏调用的区别

  • 函数调用:在程序运行时把参数的值传递给函数。
  • 宏调用在编译之前把参数记号传递给程序。

⚠️注意:必要时使用足够多的圆括号来确保运算结合的正确顺序。

不要在宏中使用递增或递减运算。

3.1 用宏参数创建字符串:#运算符

例:

#define PSQR(X) printf("The square of X id %d .\n",((X)*(X)));

双引号字符串中的X视为普通文本,而不是可被替换的记号。

C允许在字符串中包含宏参数。在类宏函数的替换体中,#号作为一个预处理运算符,可以把记号转化为字符串,这样的过程叫作字符串化(stringizing)。例如,双引号中想表示宏形参,则使用 #X 即可。

3.2 预处理器粘合剂:##运算符

可用于类函数宏对象宏的替换部分。把两个记号组合成一个新的标识符。

3.3 变参宏:...__VA_ARGS__

通过把宏参数列表中最后的参数写成省略号(即3个点...)来实现这一功能。预定义宏__VA_ARGS__可用于替换部分中,表明省略号代表什么。

格式:

#define PR(...) printf(__VA_ARGS__)

C99/C11对宏提供让用户自定义带可变参数的函数。由 stdvar.h 头文件提供。

⚠️记住:省略号只能代替最后的宏参数。

4. 宏和函数的选择

4.1 如何选择?

  • 使用宏比普通函数更复杂一些,稍有不慎会有副作用。(一些编译器规定宏只能定义成一行)
  • 需要注意时间和空间的制衡。

    • 宏生成内联代码,在程序中生成语句。函数调用无论多少次,程序中只有一份函数语句的副本,节省了空间。
    • 程序的控制必须跳转到函数内,随后再返回主调函数,这比内联代码花费更多时间。
  • 对于简单的函数,程序员通常使用宏

4.2 宏的注意点

  • 宏名不允许有空格,但替换字符中可以有空格。
  • 圆括号把宏的参数和整个替换体括起来,能确保被括起来的部分在表达式正确展开。
  • 大写字母表示宏函数的名称(大写字母可以提醒程序员可能产生副作用)。例如:MAX(X,Y)
  • 如果打算使用宏加快程序运行速度,首先要确定使用宏和使用参数是否会导致较大的差异。

    在程序中使用一次的宏无法明显减少程序的运行时间,在嵌套循环中使用宏更有助于提高效率。

5. 文件包含:#include

预处理器在发现 #include指令时,会查看后面的文件名并把文件内容包含到当前文件中(替换源文件中的#include指令),相当于被包含文件的全部内容输入到源文件#include指令所在的位置。

#include<stdio.h>     // 尖括号是标准系统文件中
#include "mystuff.h"  // 双引号是自定义的头文件(一般是优先查找当前工作目录)
ANSI C不为文件提供统一的目录模型,不同计算机所用的系统不同。

C语言习惯用 .h 后缀表示头文件,一般是包含需要放在程序顶部的信息。

头文件经常包含一些预处理器指令。

#define指令、结构声明、typedef和函数原型 是编译器在创建可执行代码时所需的信息,而~不是可执行代码~。

#ifndef#define 防止多重包含头文件。可执行代码通常是源代码文件中,而不是头文件中。

5.1 使用头文件

  • 明示常量: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 用 #definetypedefFILE定义为指向结构的指针

#include#define 指令 是最常用的两个C预处理器特性。

6. 其他指令

修改#define的值即可 生成 可移植性的代码。

#undef指令 取消 之前的#define定义。

#if、#ifdef、ifndef、#else、#elif 和 endif 指令 用于指定什么情况下编写哪些代码。

#line 指令用于重置行和文件信息。

#error 指令用于给出错误信息。

#pragma 指令用于向编译器发出指令。

6.1 #undef指令

#undef指令用于 “取消” 已定义的 #define 指令。

处理器在识别标识符时,遵循与C相同的规则:标识符可以由 大写字母、小写字母、数字 和 下划线字符组成,且~首字符不能是数字~。

#define宏的作用域从它的文件中的声明处开始,直到用 #undef 指令取消宏为止,延伸至文件尾(以二者中先满足的条件作为宏作用域的结束)。

⚠️注意:如果宏通过头文件引入,则#define在文件中的位置取决于#include指令的位置

6.2 条件编译

使用指令告诉编译器根据编译时的条件执行或忽略信息(或代码)块。

  • #ifdef、#else 和 #endif指令

    • ifdef 指令:预处理器已定义后面的标识符,则执行#elseendif指令之前的所有指令并编译所有C代码。
    • 如果未定义标识符,且有#else指令,则执行#else#endif指令直接的所有代码。

    ifdef、#else 与C的if else的区别:

    预处理器不识别用于标记块的花括号({}),因此使用 #else(如果需要)#endif(必须存在) 来标记指令块。且这些指令结构可以嵌套标记C语句块

  • ifndef指令

    与ifdef类似。只是逻辑相反。

    #ifndef指令判断后面的标识符是否是未定义的,常用于定义之前未定义的常量。

    #ifndef指令可以防止相同的宏被重复定义。

    1
    2
    3
    #ifndef SIZE
    #define SIZE = 77
    #endif
    

    #ifndef指令通常用于防止多次包含一个文件。

    1
    2
    3
    4
    #ifndef THINGS_H_
        #define THINGS_H_
        /*此处省略了头文件中的其他内容*/
    #endef
    
  • #ifelif指令

    #if指令类似C中的if。

    #if后面跟整型常量表达式,如果表达式为非零,则表达式为

条件编译的好处:使得程序移植性强。

6.3 泛型选择

泛型编程:指没有特定类型,但指定一种类型,则可以转换成指定类型的代码。

泛型选择表达式:根据表达式的类型选择一个值。不是预处理器指令

_Gerneric(x,int :0,float:1,double:2,default:3)

_Gerneric 是C11关键字。后面的圆括号内包含有多个逗号分隔的项。 与 switch语句类似。

对于泛型选择表达式求值时,程序不会先对第一个项求值,只确定类型。

只有匹配标签的类型后才会对表达式求值。

7. 内联函数(C99)_Noreturn函数(C11)

7.1 内联函数

函数调用会有一定的开销,原因:函数的调用过程包括建立调用、传递参数、跳转到函数代码并返回

解决办法

  1. 使用宏使代码内联,可避免开销。
  2. C99中方法:内联函数。

内部链接的函数可以成为内联函数。

内联函数的定义与调用该函数的代码必须在同一个文件中。

创建内联函数的方法:使用函数说明符inline 和存储类别说明符static

内联函数无法在调试器中显示

如果是多个文件使用某个内联函数,则将内联函数定义放在头文件中,并在使用的文件中引入头文件即可。

一般情况下,不在头文件中放置可执行代码,内联函数特例。

7.2 _Noreturn函数

C11中新增函数说明符_Noreturn ,表明调用后函数不返回主调函数。

exit() 函数是 _Noreturn 函数的特例。exit() 不会返回主调函数

⚠️注意:与void类型不同,void函数的类型在执行完毕后返回主调函数,但它不提供返回值

8. C库

8.1 访问C库

  • 自动访问

    在使用函数之前必须先声明函数的类型,通过包含合适的头文件即可。

  • 文件包含

    通过#include来引入。

  • 库包含

    通过编译时选项显式指定某些库。

    与包含头文件不同,头文件提供函数声明或原型。

    库选项告知系统到哪里查找函数代码。

8.2 数学库

8.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 的指针作为参数)。

8.4 断言库

assert.h 头文件 支持的断言库:用于辅助调试程序的小型库。由 assert()宏组成,接受一个整型表达式作为参数。

assert() 的参数是一个条件表达式逻辑表达式

如果assert()中止程序,则首先会显示失败的测试、包含测试的文件名和行号

C11 中新增 _Static_assert()

  • 第1个参数:整型常量表达式
  • 第2个参数:一个字符串

与assert() 的区别

  • assert() 会导致 正在运行的程序中止
  • _Static_assert() 可导致 程序无法编译通过