本章主要内容:
预处理指令——#define、#include、#ifdef、#else、#endif、#ifndef、#if、#elif、#line、#error、#pragma;
关键字—— _Generic、 _Noreturn、 _Static _assert;
函数/宏——sqrt()、atan()、atan2()、exit()、atexit()、assert()、memcpy()、memmove()、va_start()、va_arg()、va_copy()、va_end();
C预处理器的其他功能;
通用选择表达式;
内联函数;
C库概述和一些特殊用途的方便函数。
1.翻译程序的第一步
在处理程序之前,编译器必须对该程序进行一些翻译处理。
a.首先,编译器把源代码中出现的字符映射到源字符集。
该过程处理多个字节字符和三字序列——字符扩展让C更加国际化。
b.第二,编译器定位每个反斜杠后面跟着换行符的实例,并删除他们。
//例如把下面两个物理行(physical line)转换成一个逻辑行(logical line);
printf("hello\
world!\n");
printf("hello world!\n");
注意:在这种场合下,“换行符”的意思是通过按下Enter键在源代码文件中换行所生成的字符,而不是指符号表征\n。
由于预处理表达式的长度必须是一个逻辑单元,所以这一步为预处理做好了准备工作。一个逻辑行可以是多个物理行。
c.第三,编译器把文本划分成预处理记号序列、空白序列和注释序列(记号是由空格、制表符或换行符分隔的项,详见2.1节)。
注意:编译器将用一个空格字符替换每一条注释,而且实现可以用一个空格替换所有的空白字符序列(不包括换行符)。
d.最后,程序已经准备好进入预处理阶段,预处理查找一行中以#号开始的预处理指令。
2.明示常量:#define
指令可以出现在源文件的任何地方,其定义从指令出现的地方到该文件末尾有效。
#define指令常被用来定义明示常量(mainfest constant)(也叫符号常量),但是该指令还有许多其它用途。
/* preproc.c -- 简单的预处理示例 */
#include "stdio.h"
#define TWO 2 //可以使用注释
#define OW "Consistency is the last refuge of the unimagina\
tive. - Oscar Wilde" /* 反斜杠把定义延续到下一行 */
#define FOUR TWO*TWO
#define PX printf("x is %d.\n", x);
#define FMT "X is %d.\n"
int main(void)
{
int x = TWO;
PX;
x = FOUR;
printf(FMT, x);
printf("%s\n", OW);
printf("TWO: OW\n");
return 0;
}
预处理器指令从#开始运行,到后面的第一个换行符为止,指令的长度仅限于一个逻辑行。
每行#define(逻辑行)都由3部分组成。
第一部分是#define指令本身。
第二部分是选定的缩写,也称宏。
类对象宏(object-like macro) -- 代表值
类函数宏(function-like macro)
宏的名称必须遵循C变量的命名规则
第三部分称为替换列表或替换体
宏展开(macro expansion)
预处理器在程序中找到宏的实例后,就会用替换体代替该宏;从宏变成最终替换文本的过程称为宏展开。
唯一例外是双引号中的宏不进行宏展开。
2.1记号
从技术角度来看,可以把宏的替换体看作是记号(token)型字符串,而不是字符型字符串。C预处理器记号是宏定义的替换体中单独的“词”。
用空白把这些词分开。
#define FOUR 2*2该宏定义有1个记号: 2*2序列。
#define SIX 2 * 3该宏定义有3个记号: 2、*、3。
替换体中有多个空格时,字符型字符串和记号型字符串的处理方式不同。
#define EIGHT 4 * 8
如果预处理器把该替换体解释为字符型字符串,将用4 * 8替换EIGHT。即,额外的空格是替换体的一部分。如果预处理器把该替换体解释为记号型字符串,则用三个的记号4 * 8(分别由单个空格分隔)来替换EIGHT
C编译器处理记号的方式比预处理器复杂。由于编译器理解C语言的规则,所以不要求代码中的空格来分隔记号。例如, C编译器可以把2*2直接视为3个记号,因为它可以识别2是常量, *是运算符。
2.2重定义常量
假设先把LIMIT定义为20,稍后在该文件中又把它定义为25,这个过程称为重定义常量。不同的实现采用不同的重定义方案。除非新定义与旧定义相同,否则有些实现会将视其错误。另外一些实现允许重定义,但会给出警告。ANSI标准采用第一种方案,只有新定义和旧定义完全相同才允许重定义。
具有相同的定义意味着替换体的记号必须相同,且顺序也相同。
#define SIX 2 * 3
#define SIX 2 * 3
//这两个定义相同
#define SIX 2*3
//这条宏定义只有一个记号,因此与前两条定义不同。
如果需要重定义宏,使用#undef指令(稍后讨论)。如果需要讨论重定义常量,使用const关键字和作用域规则更容易。
3.在#define中使用参数
在#define中使用参数可以创建可以外形和作用与函数类似的类似的类函数宏。带有参数的宏看上去很像函数,因为这样的宏也使用圆括号。类函数宏定义的圆括中可以有一个或多个参数,随后这些参数出现在替换体中。
/* mac_arg.c -- 带参数的宏 */
#include "stdio.h"
#define SQUARE(X) X*X
#define PR(X) printf("The result is %d.\n", X)
int main(void)
{
int x = 5;
int z;
printf("X = %d\n", x);
z = SQUARE(x);
printf("Evaluating SQUARE(x): ");
PR(z);
z = SQUARE(2);
printf("Evaluating SQUARE(2): ");
PR(z);
printf("Evaluating SQUARE(X+2): ");
PR(SQUARE(x + 2));
printf("Evaluating 100/SQUARE(2): ");
PR(100 / SQUARE(2));
printf("x is %d.\n", x);
printf("Evaluating SQUARE(++x): ");
PR(SQUARE(++x));
printf("After incrementing, x is %x.\n", x);
return 0;
}
程序结果与预期不符,因为预处理不做计算、不求值,只替换字符序列。因此
SQUARE(x+2) = 5+2*5+2
//结果为17
函数调用和宏调用的区别:
函数调用在程序运行时把参数的值传给函数;
宏调用在编译之前把参数记号传递给程序。
#define SQUARE(X) (X)*(X)
//在替换字符串中使用圆括号就得到符合预期的乘法运算。
//但对于100/SQUARE(2)
#define SQUARE(X) (X*X)
//要同时解决前面的两种情况
#define SQUARE(X) ((X)*(X))
因此,宏调用必要时使用足够多的圆括号以确保运算和结合的正确顺序。但是这仍然无法使用于所有情况。SQUARE(++A)变成了++x*++x,在乘法运算前递增一次x,乘法运算后递增一次。C标准并未对这类运算规定顺序,对该表达式求值的这种情况称为未定义行为。无论哪种情况,x的开始值都是5,虽然代码上看只递增了一次,但是x的最终值为7。所以在编程中避免使用递增或递减运算符。但是,++x可作为函数参数,因为编译器会对++x求值得5后,再把5传给函数。
3.1用宏参数创建字符串:#运算符
#include "stdio.h"
#define PSQR(X) printf("The square of " #X " is %d.\n", ((X)*(X)))
int main(void)
{
int y = 5;
PSQR(y);
PSQR(2 + 4);
return 0;
}
C允许在字符串中包含宏参数。在类函数宏的替换体中,#号作为一个预处理符,可以把记号转换成字符串。例如,如果X是一个宏形参,那么#X就是转换为字符串“X"的形参名。这个过程称为字符串化。
3.2预处理器黏合剂:##运算符
与#运算符类似,##运算符可用于类函数宏的替换部分。而且,##还可用于对象宏的替换部分。##运算符把两个记号组合成一个记号。
#include "stdio.h"
#define XNAME(n) x ## n
#define PRINT_XN(n) printf("x" #n " = %d\n", x ## n) //PRINT_XN(n)宏用#运算符组合字符串,##运算符把记号组合为一个新的标识符。
int main(void)
{
int XNAME(1) = 14;
int XNAME(2) = 20;
int x3 = 30;
PRINT_XN(1);
PRINT_XN(2);
PRINT_XN(3);
return 0;
}
3.3变参宏:...和_ _ VA_ARGS_ _
一些函数(如printf())接受数量可变的参数。stdvar.h头文件(本章后面介绍)提供了工具,让用户自定义带可变参数的函数。C99/C11也对宏提供了这样的工具。
通过把宏参数列表中最后的参数写成省略号(...)来实现这一功能。这样,预定义宏_ _ VA_ARGS_ _可用在替换部分中,表明省略号代表什么。
#include "stdio.h"
#include "math.h"
#define PR(X,...) printf("Massage " #X ":" __VA_ARGS__)
int main(void)
{
double x = 48;
double y;
y = sqrt(x);
PR(1,"x = %g\n",x);
// printf("Massage " "1" ":" "x = %g\n", x);
PR(2, "x = %.2f, y = %.4f\n", x, y);
// printf("Massage " "2" ":" "x = %.2f, y = %.4f\n", x, y);
return 0;
}
注意:
省略号只能代替最后的宏参数:
#define WRONG(X, ... , Y) #X, ... , #Y
上述写法是错误的!!
(未完,待更新)