C预处理器和C库(C语言学习笔记)

本章主要内容:

  • 预处理指令——#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指令本身。

  • 第二部分是选定的缩写,也称

    1. 类对象宏(object-like macro) -- 代表值

    2. 类函数宏(function-like macro)

    3. 宏的名称必须遵循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

上述写法是错误的!!


(未完,待更新)

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值