CPrimerPlus_chapter16_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. 翻译程序的第一步

在预处理之前,编译器必须对程序进行一些翻译处理。首先,翻译器把源代码中出现的字符映射到源字符集。该过程处理多字节字符和三字符序列。

第二,编译器定位每个反斜杠后面跟着换行符的实例,并删除它们。也就是说,把下面两个物理行转换成一个逻辑行:

printf("What a wond\
        erful world!");

将被转换成printf("What a wonderful world!");

第三,编译器把文本划分成预处理记号序列、空白序列和注释序列(记号是由空格、制表符或换行符分隔的项)。编译器将用一个空格字符替换每一条注释。因此下面的代码:

int /*一些注释*/ fox;

将变成:int fox;

最后,程序已经准备好进入预处理阶段,预处理器查找一行中以#号开始的预处理指令。

2. 明示常量:#define

2.1 记号

从技术角度看,可以把宏的替换体看作是记号(token)型字符串,而不是字符型字符串。C预处理器记号是宏定义的替换体中单独的“词”。用空白将这些词分开。例如:

#define SIX 2*3

该宏定义有一个记号:2*3序列。

但是下面的宏定义有三个记号: 2、*、3。

#define SIX 2 * 3

2.2 重定义常量

假设先把LIMIT定义为20,稍后在文件中又把它定义为25,这个过程称为重定义常量。ANSI规定,只有新定义和旧定义完全相同,才允许重定义。例如,下面两个定义相同:

#define SIX 2 * 3
#define SIX 2 * 3

但是和下面的定义不同,因为下面的宏定义只有一个记号,而上面的定义有三个记号。

#define SIX 2*3

如果确实需要重定义宏,使用#udef指令。

2.3 在#define中使用参数

在#define中使用参数可以创建外形和作用与函数类似的类函数宏。类函数宏定义的圆括号中可以有一个或多个参数,随后这些参数出现在替换体中,例如:

#define MEAN(X,Y) (((X)+(Y))/2)

又例如,下面是一个类函数宏的示例:

#define SQUARE(X) X*X

在程序中可以这样用:

z = SQUARE(2);

这很像函数调用,但是它的行为和函数调用完全不同。看下面的例子,有一些陷阱,请注意:

#include 
#define SQUARE(X)  X*X
#define PR(X)   printf("The result is %d\n", X)

int main(int argc, char * argv[])
{
    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;
}

在我的环境下,执行结果是:


max_arg.png

上述代码中,PR(SQUARE(x+2));在预处理阶段被预处理器替换为:

printf("The result is %d.\n", x+2*x+2); /*x的值是5*/

因此,这里打印17,不是你预期的49。注意:用圆括号把宏的参数和整个替换体括起来,可以避免这样的问题。

PR(SQUARE(++x));在预处理阶段被预处理器替换为:

printf("The result is %d.\n", ++x*++x);

在我们的编译器中,会将两个++x计算完了之后,再去做乘法,因此结果是7*7得49。但是有的编译器不是这样。避免使用++x作为宏参数。

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

C允许字符串中包含宏参数。在类函数宏的替换体中,#作为一个预处理运算符,可以把记号转换成字符串。例如,如果x是一个宏形参,那么#x就是转换为字符串"x"的形参名。这个过程称为“字符串化”。例如:

#include 
#define PSQR(x) printf("The sqaure of " #x " is %d.\n", ((x)*(x)))

int main()
{
    int y =5;
    PSQR(y);
    PSQR(2 + 4);
    return 0;
}

打印结果如下:

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

##运算符将两个记号组成一个记号。例如,可以这样做:

#define XNAME(n)    x ## n

然后,宏XNAME(4)将被展开成x4。下面是一个例子:

#include 
#define XNAME(n)    x ## n
#define PRINT_XN(n) printf("x" #n " = %d\n", x ## n)

int main(void)
{
    int XNAME(1) = 4; /*被展开成 int x1 =4;*/
    int XNAME(2) = 20; /*被展开成 int x2 = 20;*/
    int x3 = 30;
    
    PRINT_XN(1); /*被展开成printf("x1 = %d\n", x1);*/
    PRINT_XN(2); /*被展开成printf("x2 = %d\n", x2);*/
    PRINT_XN(3); /*被展开成printf("x3 = %d\n", x3);*/
    
    return 0;
}

2.3.3 变参宏: ...__VA_ARGS__

一些函数(如printf())接受数量可变的参数。stdvar.h头文件提供了工具,让用户可以自定义带可变参数的函数。通过把宏参数列表中最后的参数写成省略号(即,三个点...)来实现这一功能。

这样,预定义宏__VA_ARGS__可用在替换体中,表明省略号代表什么。例如,下面的定义:

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

假设,稍后调用该宏:

PR("Howdy");
PR("weight=%d, shipping = $%.2f\n", wt, sp);

上面的宏调用,将展开成:

printf("Howdy");
printf("weight=%d, shipping = $%.2f\n", wt, sp);

2.4 宏和函数的选择

宏和函数的选择其实是时间和空间的权衡。宏生成内联代码,即在程序中生成语句。如果调用20次宏,相当于在程序中插入20行代码。而如果调用函数20次,程序中只有一份函数语句的副本,所以节省了空间。然而另一方面,程序的控制必须跳转至函数内,随后再返回主调程序,这显然比内联代码花费更多时间。

宏的一个优点是,不必担心变量类型(因为宏处理的是字符串,而不是实际的值)。

C99提供了第3种可替换的方法——内联函数。

对于简单函数,程序员通常使用宏。

要记住以下几点:

  • 宏名中不允许有空格,但是在替换体中有空格。ANSI C允许在参数列表中使用空格;

  • 用用圆括号把宏的参数和整个替换体括起来;

  • 用大写字母表示宏函数或宏常量的名称,这是惯例;

  • 确认使用宏可以加快程序运行速度再使用宏。在嵌套循环中使用宏,更有助与提高效率。

3. 文件包含:#include

#include用来包含一个头文件,头文件中最常用的形式如下:

  • 明示常量—— 例如,stdio.h中定义的EOF、NULL和BUFSIZE(标准I/O缓冲区的大小)

  • 宏函数——例如,getchar()通常用getc(stdin)定义,而getc()经常用于定义较复杂的宏,头文件ctype.h通常包含ctype系列函数的宏定义。

  • 函数声明——例如,string.h头文件包含字符串函数系列的函数声明;

  • 结构模板定义——标准I/O函数使用FILE结构,该结构中包含了文件和文件缓冲区相关的信息。FILE结构模板在stdio.h头文件中;

  • 类型定义——标准I/O函数使用指向FILE的指针作为参数。通常stdio.h用#define和typedef把FILE定义为指向结构的指针。类似地,size_t和time_t类型也定义在头文件中。

另外, 还可以使用头文件引用式声明一个外部变量,以供其他文件共享。即,在一个源文件引用式声明一个外部变量,在头文件中用extern关键字对这个变量作引用式声明,凡是#include这个头文件的所有源文件,都可以共享这个外部变量。

此外,在头文件中声明一个具有文件作用域、内部链接和const限定符的变量或数组,const防止值被意外修改,static以为这每个包含该头文件的源代码文件都获得一份该变量或数组的副本。这样就不需要在一个文件中作定义式声明,在另一个文件中作引用式声明以引用它。

#include#define是最常用的两个C预处理器。接下来,介绍一些其它的指令。

4. 其它指令

程序员可能要为不同的工作环境准备C程序和C库包。不同的环境可能使用不同的代码类型。预处理器提供一些指令,程序员通过修改#define的值即可生成可移植的代码。#undef指令取消之前的#define定义。#if、#ifdef、#ifndef、#else、#elif和#endif指令用于指定什么情况下编写哪些代码。#line指令用于重置行和文件信息,#error指令用于给出错误消息,#pragma指令用于向编译器发出指令。

4.1 #undef指令

#undef指令用于“取消”已定义的#define指令。 也就是说,假设有如下定义:

#define LIMIT 400   

然后,下面的指令:

#undef  LIMIT

将移除上面的定义。现在就可以把LIMIT重新定义为一个新值了。即使原来没有定义LIMIT,取消LIMIT的定义依然有效。如果想使用一个名词,又不确定之前是否已经使用过,为了安全起见,可以用#undef 指令先取消该名字的定义,然后再去定义。

4.2 从C预处理器角度看已定义

当C预处理器在预处理指令中发现一个标识符时,它会把该标识符当做已定义的未定义的。这里的已定义的表示由预处理器定义。 如果标识符是同一个文件中由前面的#define指令创建的宏名,而且没有用#undef指令关闭,那么该标识符就是已定义的。如果标识符不是宏,假设是一个文件作用域的C变量,那么该标识符对预处理器而言就是未定义的。

已定义宏可以是对象宏,包括空宏或函数宏:

#define LIMIT 1000  //LIMIT是已定义的
#define GOOD    //已定义的
#define A(X)  ((-(X))*(X))  //A是已定义的
int q;  //q不是宏,因此是未定义的
#undef  GOOD    //GOOD取消定义,因此是未定义的

注意,#define宏的作用域从它在文件中的声明处开始,直到用#undef取消宏为止,或延伸至文件尾(以二者中先满足的条件作为宏作用域的结果)。另外还要注意,如果宏通过头文件引入,那么#define在文件中的位置取决于#include指令的位置。

4.3 条件编译

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

4.3.1 #ifdef、 #else 和 #endif 指令

#ifdef  MAVIS        /*如果MAVIS已定义*/  
#include "horse.h"   /*如果使用较新的编译器和ANSI标准,这里支持缩进*/
#define STABLE 5     /*如果使用较新的编译器和ANSI标准,这里支持缩进*/   
#else                /*否则*/
#include "cow.h"
#include STABLE 15   /*如果使用较新的编译器和ANSI标准,这里支持缩进*/
#endif

也可以 #ifdef、 #else和#endif 标记C语句块。

/*ifdef.c -- 使用条件编译*/
#include 
#define JUST_CHECKING
#define LIMIT 4

int main(void)
{
    int i;
    int total = 0;
    for (i = 0; i<= LIMIT; i++)
    {
        total += 2 * i* i +1;
#ifdef  JUST_CHECKING
        printf("i=%d, running total = %d\n", i , total);
#endif   
    }
    printf("Grand total = %d\n", total);
    
    return 0;
}

可以把这种方法用在调试程序,定义JUST_CHECKING并合理使用#ifdef, 编译器将执行用于调试的程序代码,打印中间值。调试结束后,可移除JUST_CHECKING的定义并重新编译。如果以后还需要这个定义,重新插入定义即可。

4.3.2 #ifndef 指令

#ifndef指令与#ifdef指令的用法相似,也可与#else和#endif一起使用,但是它们的逻辑相反。#ifndef指令判断后面的标识符是否是未定义的,常用于定义之前未定义的常量。如下所示:

/*arrays.h*/
#ifndef SIZE
#define SIZE 100   /*为了兼容旧的实现,这里就不缩进了*/
#endif

通常,在包含多个头文件时,其中的文件可能包含了相同的宏定义。#ifndef指令可以防止相同的宏被重复定义,在首次定义一个宏的头文件中用#ifndef指令激活定义,随后在其他头文件中的定义都被忽略。

#ifndef 通常用于防止多次包含一个文件。也就是说,应该像下面这样设置头文件:

/*things.h*/
#ifndef THINGS_H_
#define THINGS_H_

/*头文件的其他内容*/
#endif

如果不用#ifndef, C在多次包含同一文件时有可能会编译错误,比如说头文件中包含结构声明。C标准中保留使用下划线作为前缀,所以我们在代码中尽量将下划线作为后缀,防止和标准头文件中的宏发生冲突。

4.3.3 #if 和 #elif 指令

#if 后面跟整型常量表达式,如果表达式为非零,则表达式为真。可以在指令中使用C的关系运算符和逻辑运算符:

#if SYS == 1
#include "ibm.h"
#endif

可以按照 if else 的形式使用 #elif (早期的实现不支持 #elif )。例如, 可以这样写:

#if SYS == 1
    #include    "ibmpc.h"
#elif SYS == 2
    #include    "vax.h"
#elif SYS == 3
    #include    "mac.h"
#else   
    #include    "general.h"
#endif

较新的编译器提供另一种方法测试名称是否已定义,即用 #if defined (VAX) 来代替#ifdef VAX

这里,defined 是一个预处理运算符, 如果它的参数是用 #defined 定义过的,则返回1;否则返回0。这种新方法的优点是,它可以和 #elif 一起使用。 下面用这种形式重写前面的示例:

#if defined (IBMPC)
    #include "ibmpc.h"
#elif defined (VAX)
    #include    "vax.h"
#elif defined (MAC)
    #include    "mac.h"
#else   
    #include    "general.h"
#endif

如果在 VAX 机上运行这几行代码,那么应该在文件前用代码定义 VAX:

#define VAX

5. 预定义宏

C标准规定了一些预定义宏,如下表所示:

含义
__DATE__ 预处理的日期("Mmm dd yyyy"形式的字符串字面量, 如 Nov 23 2013)
__FILE__ 表示当前源代码文件名的字符串字面量
__LINE__ 表示当前源代码文件中行号的整型常量
__STDC__ 设置为1时,表明实现遵守C标准
__STDC_HOSTED__ 本机环境设置为1; 否则设置为0
__STDC_VERSION__ 支持C99标准,设置为 199901L; 支持C11标准,设置为201112L
__TIME__ 翻译代码的时间, 格式为 "hh:mm:ss"

C99标准提供一个名为 __func__的预定义标识符, 它展开为一个代表函数名的字符串(该函数包含该标识符)。那么, __func__必须具有函数作用域,而从本质上看,宏具有文件作用域。 因此, __func__ 是C语言的预定义标识符, 而不是预定义宏。

下面的程序示例使用了一些预定义宏和预定义标识符。注意,其中一些是C99新增的,所以不支持C99的编译器可能无法识别它们。如果使用GCC,那么必须设置 -std= c99 或 -std=c11。

#include 
#define SQUARE(X)  X*X
#define PR(X)   printf("The result is %d\n", X)
#define PSQR(x) printf("The sqaure of " #x " is %d.\n", ((x)*(x)))
#define XNAME(n)    x ## n
#define PRINT_XN(n) printf("x" #n " = %d\n", x ## n)
int main(int argc, char * argv[])
{
    int x =5;
    int z;

    puts("**************使用类函数宏**************");
    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);

    int y =5;
    PSQR(y);
    PSQR(2 + 4);


    int XNAME(1) = 4; /*被展开成 int x1 =4;*/
    int XNAME(2) = 20; /*被展开成 int x2 = 20;*/
    int x3 = 30;

    PRINT_XN(1); /*被展开成printf("x1 = %d\n", x1);*/
    PRINT_XN(2); /*被展开成printf("x2 = %d\n", x2);*/
    PRINT_XN(3); /*被展开成printf("x3 = %d\n", x3);*/

    puts("**************使用预定义宏和预定义标识符**************");
    printf("The file is %s.\n", __FILE__);
    printf("The date is %s.\n", __DATE__);
    printf("The time is %s.\n", __TIME__);
    printf("The version is %ld.\n", __STDC_VERSION__);
    printf("This is line %d.\n", __LINE__ );
    printf("This function is %s.\n", __func__);

    why_me();

    return 0;
}

void why_me(void)
{
    printf("This function is %s.\n", __func__);
    printf("This is line %d.\n", __LINE__);
}

编译和运行:

gcc -Wall -g -std=c11 -g -c E:\StudyCLang\cpreprocessor\cpreprocessor.c -o obj\Debug\cpreprocessor.o
gcc -o bin\Debug\cpreprocessor.exe obj\Debug\cpreprocessor.o

得到的结果如下:


cpreprocessor.png

6. #line 和 #error

#line指令重置 __LINE____FILE__宏报告的行号和文件名。可以这样使用 #line :

#line   1000        // 把当前行号重置为1000
#line   10  "cool.c" // 把行号重置为10, 把文件名重置为

#error指令让预处理器发出一条错误消息,该消息包含指令中的文本。如果可能的话,编译过程应该中断。可以这样使用 #error 指令:

#if __STDC_VERSION__ != 201112L
#error Not C11

#endif

编译以上代码生成后,输出如下:

$ gcc newish.c
newish.c:14:2: error #error Not C11
$ gcc -std=c11 newish.c
$

7. #pragma

在现在的编译器中,可以通过命令行参数或IDE菜单修改编译器的一些设置。 #pragma 把编译器指令放入源码中。例如,在开发C99时,标准被称为C9X,可以使用下面的编译指示让编译器支持C9X:

#pragama c9x on

C99还提供 _ Pragama 预处理器运算符,该运算符把字符串转换成普通的编译指示。例如:

_Pragma("nonstandardtreatmenttypeB on")

等价于下面的指令:

#pragma nonstandardtreatmenttypeB on

由于该运算符不使用 #符号,因此可以把它作为宏展开的一部分:

#define PRAGMA(X)  _Pragma(#X)
#define LIMRG(X)    PRAGMA(STDC CX_LIMITED_RANGE X)

然后,可以使用类似下面的代码:

LIMGR (ON)

_Pragma 运算符完成“解字符串”的工作,即把字符串中的转义序列转换成它所代表的字符。因此

_Pragma("use_bool \"true "\false")

变成了:

#pragma use_bool "true "false

8. 泛型选择(C11)

程序设计中,泛型编程(generic programming)是指那些没有特定类型,但是一旦指定一种类型,就可以转换成指定类型的代码。C++ 在模板中可以创建泛型算法,然后编译器根据指定的类型,自动使用实例化代码。C没有这种功能,但是C11新增了一种表达式,叫做泛型选择表达式(generic selection expression),可以根据表达式的类型(即表达式的类型是int、double还是其他类型)选择一个值。泛型选择表达式不是预处理器指令,但是在一些泛型编程中它常用作 #define 宏定义的一部分。

下面是一个泛型选择表达式的示例:

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

_Generic是 C11 的关键字。泛型选择表达式和 switch 语句类似,只是前者用表达式的类型匹配标签,后者用表达式的值匹配标签。

下面是一个把泛型表达式和宏定义组合的例子:

#define MYTYPE(X)   _Generic((X),\
    int: "int",\
    float: "float",\
    double: "double",\
    default: "other"\
)

宏必须定义为一条逻辑行,但是可以用 \ 把一条逻辑行分隔成多条物理行。

9. 内联函数(C99)

通常,函数调用伴随着一定的开销,因为函数的调用过程包括:建立调用、传递参数、跳转到函数代码并返回。使用宏使得代码内联,可以避免这样的开销。C99 还提供了另一种方法: 内联函数(inline function)。 把函数变成内联函数,编译器可能会用内联代码替换函数调用,并(或)执行一些其他的优化,但是也可能不起作用。

创建内联函数的定义有多种。标准规定,具有内部链接的函数可以成为内联函数,还规定了内联函数的定义和调用该函数的代码必须在同一个文件中。因此,最简单的方法是使用函数说明符inline和存储类别说明符 static。 通常, 内联函数应定义在首次使用它的文件中,所以内联函数也相当于函数原型。

如下所示:

#include 
inline static void eatline()   //内联函数定义/原型
{
    while (getchar() != '\n')
        continue;
}
int main()
{
    ...
    eatline();  //函数调用
    ...
}

编译器查看内联函数的定义(也是函数原型),可能会用函数体中的代码替换 eatline() 函数调用。 也就是说, 效果相当于在函数调用的位置输入函数体中的代码。

内联函数应该比较短小。

编译器优化内联函数必须知道该函数的内容。所以内联函数的定义与函数的调用必须在同一个文件。鉴于此,内联函数都具有内部链接。因此,如果程序中有多个文件都要使用某个内联函数,那么这些文件都必须包含该内联函数的定义。最简单的做法是,将内联函数放入头文件,并在使用该内联函数的文件中包含该头文件即可。

//eatline.h
#ifndef     EATLINE_H_
#define     EATLINE_H_
inline static void eatline()   //内联函数定义/原型
{
    while (getchar() != '\n')
        continue;
}
#endif

一般不在头文件中放置可执行代码,但内联函数是个特例。因为内联函数具有内部链接,所以在多个文件中定义同一个内联函数并不会产生什么问题。

10. _Noreturn函数(C11)

C99新增 inline 关键字时,它是唯一的函数说明符(关键字 extern 和 static 是存储类别说明符,可应用于数据对象和函数)。C11 新增了第 2 个函数说明符 _Noreturn, 表明调用完成后函数不返回主调函数。 exit() 函数是 _Noreturn 函数的一个示例。一旦调用 exit(), 它不会再返回主调函数。

11. C库

最初,并没有官方的C库。 后来,基于unix 的C实现成为了标准。 ANSI C 委员会主要以这个标准为基础,开发了一个官方的标准库。 在意识到 C语言的应用范围不断扩大后,该委员会重新定义了这个库, 使之可以应用于其他系统。

我们讨论过一些标准库中的 I/O 函数、 字符串函数和字符函数。本节将介绍更多函数,首先看看如何使用C库。

11.1 访问C库

我们可以通过包含头文件、在编译或链接程序的某些阶段指定库,来实现访问C库。

11.2 使用库描述

要学会阅读函数文档。阅读文档的关键是看懂函数头。不同的标准,库函数的描述可能发生变化 。

以fread()函数为例, ANSI C90标准提供如下的描述:

#include    
size_t  fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

首先,给出了应该包含的头文件stdio.h。其次,使用了size_t 类型, size_t 类型被定义为 sizeof 运算符的返回值类型 —— 无符号整数类型, 通常是 unsigned int 或 unsigned long。 stddef.h 文件中包含了 size_t 类型的 typedef 或 #define 定义。 其它文件(包括 stdio.h) 通过包含stddef.h 来包含这个定义。 许多函数(包括fread() 函数)的实际参数中都要使用sizeof 运算符, 形参的size_t 类型正好匹配。

C99/C11 标准在以上的描述中加入了新的关键字 restrict:

#include 
size_t fread(void* restrict ptr, size_t size, size_t nmemb, FILE * restrict stream);

restrict 是C99新增的类型限定符,它允许编译器优化某部分代码,以支持更好的计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始 的方式。

12. 数学库

数学库中包含许多有用的数学函数。math.h 头文件提供这些函数的原型。 下表列出了一些声明在 math.h 中的函数。 注意,函数中涉及的角度都以弧度为单位。

原型 描述
double acos (double x) 返回余弦值为x的弧度(0 ~ π弧度)
double asin (double x) 返回正弦值为x的弧度 (-π/2 ~ +π/2 弧度)
double atan (double x) 返回正切值为 x 的弧度 (-π/2 ~ +π/2 弧度)
double atan2 (double y, double x) 返回正切值为 y/x 的弧度 (-π/2 ~ +π/2 弧度)
double cos (double x) 返回 x 的余弦值, x的单位为弧度
double sin (double x) 返回 x 的正弦值, x的单位为弧度
double tan (double x) 返回 x 的正切值, x的单位为弧度
double exp (double x) 返回x 的指数函数的值 (e^x)
double log (double x) 返回x的自然对数值
double log10 (double x) 返回x的以10为底的对数值
double pow (double x, double y) 返回x的y次幂
double sqrt (double x) 返回 x的平方根
double cbrt (double x) 返回 x 的立方值
double ceil (double x) 返回不小于 x 的最小整数值
double fabs (double x) 返回 x的绝对值
double floor (double x) 返回不大于 x 的最大整数值

12.1 三角函数简单示例

// cpreprocessor.h
#ifndef CPREPROCESSOR_H_INCLUDED_
#define CPREPROCESSOR_H_INCLUDED_

#include 
#include 

#define RAD_TO_DEG (180 /(4 * atan(1)))

//定义了极坐标的类型
typedef struct polar_v
{
    double magnitute;
    double angle;
} Polar_V;

//定义了直角坐标的类型
typedef struct rect_v
{
    double x;
    double y;
} Rect_V;

Polar_V rect_to_polar(Rect_V);

#endif // CPREPROCESSOR_H_INCLUDED_

// rect_pol.c   将直角坐标转换成极坐标
#include "cpreprocessor.h"

void rect_pol(void)
{
    Rect_V  input;
    Polar_V result;

    puts("Enter x and y coordinates, enter q to quit");
    while(scanf("%lf %lf", &input.x, &input.y) == 2)
    {
        result = rect_to_polar(input);
        printf("magnitue = %.2f, angle = %.2f", result.magnitute, result.angle);
    }
    puts("Bye");

    return 0;
}


Polar_V rect_to_polar(Rect_V rv)
{
    Polar_V pv;

    pv.magnitute = sqrt(rv.x * rv.x + rv.y * rv.y);
    if (pv.magnitute == 0)
        pv.angle = 0.0;
    else
        pv.angle = RAD_TO_DEG * atan2(rv.y, rv.x);

    return pv;
}

如果编译器出现下面的消息:

Undefined: _sqrt
或
'sqrt': Unresolved external

或其他类似的消息,表明编译器链接器没有找到数学库。UNIX 系统会要求使用 -lm 标记 指示链接器搜索数学库:

cc rect_pol.c -lm

注意, -lm 标记在命令行的末尾。因为链接器在编译器编译 C 文件后才开始处理。 在 LINUX 中使用 GCC 编译器可能要这样写:

gcc rect_pol.c -lm

12.2 类型变体

利用C11新增的泛型表达式定义一个泛型宏,根据参数类型选择最合适的数学函数版本。例如:

//泛型平方根函数
#define SQRT(X)  _Generic((X),\
    long double: sqrtl, \
    default: sqrt, \
    float: sqrtf)(X)

//泛型正弦函数
#define SIN(X) _Generic((X),\
    long double: sinl((X)/RAD_TO_DEG),\
    default: sin((X)/RAD_TO_DEG),\
    float: sinf((X)/RAD_TO_DEG)\
)

对于SQRT(), 先对泛型选择表达式求值得一个指针,然后通过该指针调用它指向的函数。

对于SIN(), 函数的调用在泛型选择表达式的内部。

12.3 tgmath.h 库(C99)

C99标准提供的tgmath.h头文件中定义了泛型类型宏,其效果和12.2类似。如果在 math.h 中为一个函数定义了 3 中类型 (float 、 double 、long double) 的版本, 那么 tgmath.h 文件就创建了一个泛型类型宏。例如,根据提供的参数类型,定义 sqrt() 宏展开为 sqrtf() 、 sqrt() 或 sqrtl() 函数。例如:

#include 
...
    float  x = 44.0;
    double y;
    y = sqrt(x);   // 调用宏, 所以是 sqrtf(x)
    y = (sqrt)(x); // 调用函数 sqrt()

13. 通用工具库

通用工具库包含各种函数,包括随机数生成器、查找和排序函数、转换函数和内存管理函数。第 12 章介绍过 srand() 、 rand() 、 malloc() 和 free() 函数。 在 ANSI C 标准中, 这些函数的原型都在 stdlib.h 头文件中。

13.1 exit() 和 atexit() 函数

atexit() 函数接收一个函数指针作为参数,注册被调用的函数,当退出程序时调用该函数。最先被注册的函数,最后才被调用。通常,atexit() 函数注册的函数应该不带任何参数且返回类型是 void。通常这些函数会执行一些清理任务。

exit() 在执行完 atexit() 指定的函数后,会完成一系列清理工作: 刷新所有输出流、关闭所有打开的流和关闭由标准I/O函数 tmpfile() 创建的临时文件。 然后 exit() 将控制权返还主机环境。在ANSI C环境中,非递归的main() 中使用 exit() 函数相当于return。 在main() 函数以外的函数中使用 exit() 也会终止整个程序。

13.2 qsort() 函数

快排算法在 C 实现中的名称是qsort() 。 qsort() 函数排序数组的数据对象, 其原型如下:

void  sqrt(void *base, size_t nmemb, size_t size, 
           int (*compar)(const void *, const void *));

第一个参数 base 是指针,指向待排序的数组的首元素。可以是任意类型的数组。

第二个参数 nmemb 是待排序项的数量。前面介绍过,size_t 定义在标准头文件汇总,是sizeof 运算符返回的整数类型。

第三个参数: 由于qsort() 将第一个参数转换成void指针,所以qsort() 不知道数组中的每个元素的大小。为此,函数原型用第三个参数 size , 显示指明待排序数组中的每个元素的大小。例如,如果排序 double 类型的数组,那么第三个参数应该是 sizeof(double) 。

第四个参数:qsort() 函数还需一个指向函数的指针。这个被指针指向的比较函数用于确定排序的顺序。该函数应接收两个参数: 分别指向待比较的亮相的指针。如果第一项大于第 2项,比较函数返回正数; 如果两项相等,则返回 0 。 如果第1项的值小于第2项,则返回负数。 qsort() 函数根据给定的其他信息计算出两个指针的值,然后传递给比较函数。

qsort() 原型中的第四个参数确定了比较函数的形式如下:

int (*compar) (const void *, const void *)

这表明 qsort() 最后一个参数是一个函数指针, 其指向的函数返回 int 类型值,且接收两个指向 const void 的指针作为参数,这两个指针指向待比较项。

你可能感兴趣的:(CPrimerPlus_chapter16_C预处理器和C库)