文章目录
- C预处理器和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库概述和一些特殊用途的方便函数。
-
C预处理器在程序执行之前查看程序(故称之为预处理器)。根据程序中的预处理器指令,预处理器把符号缩写替换成其表示的内容。预处理器可以包含程序所需的其他文件,可以选择让编译器查看哪些代码。预处理器并不知道C。基本上它的工作是把一些文本转换成另外一些文本。
翻译程序的第一步
-
在预处理之前,编译器必须对该程序进行一些翻译处理。首先,编译器把源代码中出现的字符映射到源字符集。该过程处理多字节字符和三字符序列——字符扩展让C更加国际化(详见附录B“参考资料VII,扩展字符支持”)。
-
第二,编译器定位每个反斜杠后面跟着换行符的实例,并删除它们。也就是说,把下面两个物理行(physical line):
printf("That’s wond\
erful!\n");
转换成一个逻辑行:
printf(“That’s wonderful\n!”); -
注意,在这种场合中,“换行符”的意思是通过按下Enter键在源代码文件中换行所生成的字符,而不是指符号表征\n。
-
第三,编译器把文本划分成预处理记号序列、空白序列和注释序列(记号是由空格、制表符或换行符分隔的项,详见16.2.1节)。这里要注意的是,编译器将用一个空格字符替换每一条注释
明示常量:#define
- 我们大量使用#define指令来定义明示常量(manifest 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;
}
-
预处理器指令从#开始运行,到后面的第1个换行符为止。也就是说,指令的长度仅限于一行。然而,前面提到过,在预处理开始前,编译器会把多行物理行处理为一行逻辑行。
-
每行#define(逻辑行)都由3部分组成。第1部分是#define指令本身。第2部分是选定的缩写,也称为宏。有些宏代表值(如本例),这些宏被称为类对象宏(object-like macro)。C语言还有类函数宏
-
第3部分(指令行的其余部分)称为替换列表或替换体(见图16.1)。一旦预处理器在程序中找到宏的示实例后,就会用替换体代替该宏(也有例外,稍后解释)。从宏变成最终替换文本的过程称为宏展开(macro expansion)。
-
预处理器不会进行实际的乘法运算,这一过程在编译时进行。预处理器不做计算,不对表达式求值,它只进行替换。
-
一般而言,预处理器发现程序中的宏后,会用宏等价的替换文本进行替换。 如果替换的字符串中还包含宏,则继续替换这些宏。唯一例外的是双引号中的宏。
-
那么,何时使用字符常量?对于绝大部分数字常量,应该使用字符常量。如果在算式中用字符常量代替数字,常量名能更清楚地表达该数字的含义。如果是表示数组大小的数字,用符号常量后更容易改变数组的大小和循环次数。如果数字是系统代码(如,EOF),用符号常量表示的代码更容易移植(只需改变EOF的定义)。助记、易更改、可移植,这些都是符号常量很有价值的特性。
-
另一方面,宏常量可用于指定标准数组的大小和const变量的初始值。
#define LIMIT 20
const int LIM = 50;
static int data1[LIMIT]; // 有效
static int data2[LIM]; // 无效
const int LIM2 = 2 * LIMIT // 有效
const int LIM3 = 2 * LIM; // 无效
-
在C中,非自动数组的大小应该是整型常量表达式,这意味着表示数组大小的必须是整型常量的组合(如5)、枚举常量和sizeof表达式,不包括const声明的值(这也是C++和C的区别之一,在C++中可以把const值作为常量表达式的一部分)。
记号
-
从技术角度来看,可以把宏的替换体看作是记号(token)型字符串,而不是字符型字符串。C预处理器记号是宏定义的替换体中单独的“词”。用空白把这些词分开。例如:
define FOUR 2*2
-
该宏定义有一个记号:2*2序列。但是,下面的宏定义中:
#define SIX 2 * 3
-
有3个记号:2、*、3。
-
替换体中有多个空格时,字符型字符串和记号型字符串的处理方式不同。
#define EIGHT 4 * 8
-
如果预处理器把该替换体解释为字符型字符串,将用4 * 8替换EIGHT。即,额外的空格是替换体的一部分。如果预处理器把该替换体解释为记号型字符串,则用3个的记号4 * 8(分别由单个空格分隔)来替换EIGHT。换而言之,解释为字符型字符串,把空格视为替换体的一部分;解释为记号型字符串,把空格视为替换体中各记号的分隔符。
-
*C编译器处理记号的方式比预处理器复杂。由于编译器理解C语言的规则,所以不要求代码中用空格来分隔记号。例如,C编译器可以把2*2直接视为3个记号,因为它可以识别2是常量,是运算符。
重定义常量
-
假设先把LIMIT定义为20,稍后在该文件中又把它定义为25。这个过程称为重定义常量。不同的实现采用不同的重定义方案。除非新定义与旧定义相同,否则有些实现会将其视为错误。另外一些实现允许重定义,但会给出警告。
-
具有相同的定义意味着替换体中的记号必须相同,且顺序也相同。因此,下面两个定义相同:
#define SIX 2 * 3
#define SIX 2 * 3
-
这两条定义都有3个相同的记号,额外的空格不算替换体的一部分。而下面的定义则与上面两条宏定义不同:
#define SIX 2*3
-
这条宏定义中只有一个记号,因此与前两条定义不同。
-
如果确实需要重定义常量,使用const关键字和作用域规则更容易些。
在#define中使用参数
-
在#define中使用参数可以创建外形和作用与函数类似的类函数宏。带有参数的宏看上去很像函数,因为这样的宏也使用圆括号。类函数宏定义的圆括号中可以有一个或多个参数,随后这些参数出现在替换体中
-
下面是一个类函数宏的示例:
#define SQUARE(X) X*X
-
在程序中可以这样用:
z = SQUARE(2);
/* 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;
}
x = 5
Evaluating SQUARE(x): The result is 25.
Evaluating SQUARE(2): The result is 4.
Evaluating SQUARE(x+2): The result is 17.
Evaluating 100/SQUARE(2): The result is 100.
x is 5.
Evaluating SQUARE(++x): The result is 42.
After incrementing, x is 7.
-
输出的结果是17,这不是一个平方值!导致这样结果的原因是,我们前面提到过,预处理器不做计算、不求值,只替换字符序列。预处理器把出现x的地方都替换成x+2。因此,x*x变成了x+2*x+2。如果x为5,那么该表达式的值为:
5+2*5+2 = 5 + 10 + 2 = 17 -
#define SQUARE(x) (x)*(x)
现在SQUARE(x+2)变成了(x+2)*(x+2),在替换字符串中使用圆括号就得到符合预期的乘法运算。
但是,这并未解决所有的问题。下面的输出行:100/SQUARE(2)
将变成:
100/2*2
根据优先级规则,从左往右对表达式求值:(100/2)*2,即50*2,得100。把SQUARE(x)定义为下面的形式可以解决这种混乱:
#define SQUARE(x) (x*x)
用宏参数创建字符串:#运算符
-
我下面是一个类函数宏:
#define PSQR(X) printf(“The square of X is %d.\n”, ((X)*(X)));
-
注意双引号字符串中的X被视为普通文本,而不是一个可被替换的记号。
-
C允许在字符串中包含宏参数。在类函数宏的替换体中,#号作为一个预处理运算符,可以把记号转换成字符串。例如,如果x是一个宏形参,那么#x就是转换为字符串"x"的形参名。这个过程称为字符串化(stringizing)。
/* subst.c -- 在字符串中替换 */
#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;
}
该程序的输出如下:
The square of y is 25.
The square of 2 + 4 is 36.
预处理器黏合剂:##运算符
-
与#运算符类似,##运算符可用于类函数宏的替换部分。而且,##还可用于对象宏的替换部分。##运算符把两个记号组合成一个记号。 例如,可以这样做:
#define XNAME(n) x ## n
然后,宏XNAME(4)将展开为x4。
// glue.c -- 使用##运算符
#include <stdio.h>
#define XNAME(n) x ## n
#define PRINT_XN(n) printf("x" #n " = %d\n", x ## n);
int main(void)
{
int XNAME(1) = 14; // 变成 int x1 = 14;
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;
}
该程序的输出如下:
x1 = 14
x2 = 20
x3 = 30
变参宏:…和_ _VA_ARGS_ _
-
通过把宏参数列表中最后的参数写成省略号(即,3个点…)来实现这一功能。这样,预定义宏
_ _VA_ARGS__可用在替换部分中,表明省略号代表什么。例如,下面的定义:#define PR(…) printf(_ _VA_ARGS_ _)
假设稍后调用该宏:
PR(“Howdy”);PR(“weight = %d, shipping = $%.2f\n”, wt, sp);
// variadic.c -- 变参宏
#include <stdio.h>
#include <math.h>
#define PR(X, ...) printf("Message " #X ": " _ _VA_ARGS_ _)
int main(void)
{
double x = 48;
double y;
y = sqrt(x);
PR(1, "x = %g\n", x);
PR(2, "x = %.2f, y = %.4f\n", x, y);
return 0;
}
第1个宏调用,X的值是1,所以#X变成"1"。展开后成为:
printf("Message " "1" ": " "x = %g\n", x);
宏和函数的选择
-
使用宏比使用普通函数复杂一些,稍有不慎会产生奇怪的副作用。一些编译器规定宏只能定义成一行。不过,即使编译器没有这个限制,也应该这样做。
-
宏和函数的选择实际上是时间和空间的权衡。宏生成内联代码,即在程序中生成语句。如果调用20次宏,即在程序中插入20行代码。如果调用函数20次,程序中只有一份函数语句的副本,所以节省了空间。然而另一方面,程序的控制必须跳转至函数内,随后再返回主调程序,这显然比内联代码花费更多的时间。
-
宏的一个优点是,不用担心变量类型(这是因为宏处理的是字符串,而不是实际的值)。因此,只要能用int或float类型都可以使用SQUARE(x)宏。
-
对于简单的函数,程序员通常使用宏,如下所示:
#define MAX(X,Y) ((X) > (Y) ? (X) : (Y))
#define ABS(X) ((X) < 0 ? -(X) : (X))
#define ISSIGN(X) ((X) == ‘+’ || (X) == ‘-’ ? 1 : 0)
-
·记住宏名中不允许有空格,但是在替换字符串中可以有空格。ANSI C允许在参数列表中使用空格。
-
·用圆括号把宏的参数和整个替换体括起来。这样能确保被括起来的部分在下面这样的表达式中正确地展开:
forks = 2 * MAX(guests + 3, last); -
·用大写字母表示宏函数的名称。该惯例不如用大写字母表示宏常量应用广泛。但是,大写字母可以提醒程序员注意,宏可能产生的副作用。
-
·如果打算使用宏来加快程序的运行速度,那么首先要确定使用宏和使用函数是否会导致较大差异。在程序中只使用一次的宏无法明显减少程序的运行时间。在嵌套循环中使用宏更有助于提高效率。许多系统提供程序分析器以帮助程序员压缩程序中最耗时的部分。
文件包含:#include
-
当预处理器发现#include指令时,会查看后面的文件名并把文件的内容包含到当前文件中,即替换源文件中的#include指令。这相当于把被包含文件的全部内容输入到源文件#include指令所在的位置。#include指令有两种形式:
#include <stdio.h> ←文件名在尖括号中
#include “mystuff.h” ←文件名在双引号
-
在UNIX系统中,尖括号告诉预处理器在标准系统目录中查找该文件。双引号告诉预处理器首先在当前目录中(或文件名中指定的其他目录)查找该文件,如果未找到再查找标准系统目录:
#include <stdio.h> ←查找系统目录
#include “hot.h” ←查找当前工作目录
#include “/usr/biff/p.h” ←查找/usr/biff目录
-
为什么要包含文件?因为编译器需要这些文件中的信息。例如,stdio.h文件中通常包含EOF、NULL、getchar()和putchar()的定义。getchar()和putchar()被定义为宏函数。此外,该文件中还包含C的其他I/O函数。
-
C语言习惯用.h后缀表示头文件,这些文件包含需要放在程序顶部的信息。头文件经常包含一些预处理器指令。有些头文件(如stdio.h)由系统提供,当然你也可以创建自己的头文件。
头文件示例
// names_st.h -- names_st 结构的头文件
// 常量
#include <string.h>
#define SLEN 32
// 结构声明
struct names_st
{
char first[SLEN];
char last[SLEN];
};
// 类型定义
typedef struct names_st names;
// 函数原型
void get_names(names *);
void show_names(const names *);
char * s_gets(char * st, int n);
// names_st.c -- 定义 names_st.h中的函数
#include <stdio.h>
#include "names_st.h" // 包含头文件
// 函数定义
void get_names(names * pn)
{
printf("Please enter your first name: ");
s_gets(pn->first, SLEN);
printf("Please enter your last name: ");
s_gets(pn->last, SLEN);
}
void show_names(const names * pn)
{
printf("%s %s", pn->first, pn->last);
}
char * s_gets(char * st, int n)
{
char * ret_val;
char * find;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
find = strchr(st, '\n'); // 查找换行符
if (find) // 如果地址不是NULL,
*find = '\0'; // 在此处放置一个空字符
else
while (getchar() != '\n')
continue; // 处理输入行中的剩余字符
}
return ret_val;
}
// useheader.c -- 使用 names_st 结构
#include <stdio.h>
#include "names_st.h"
// 记住要链接 names_st.c
int main(void)
{
names candidate;
get_names(&candidate);
printf("Let's welcome ");
show_names(&candidate);
printf(" to this program!\n");
return 0;
}
- ·两个源代码文件都使用names_st类型结构,所以它们都必须包含names_st.h头文件。
- ·必须编译和链接names_st.c和useheader.c源代码文件。
- ·声明和指令放在nems_st.h头文件中,函数定义放在names_st.c源代码文件中。
使用头文件
-
头文件中最常用的形式如下。
·明示常量——例如,stdio.h中定义的EOF、NULL和BUFSIZE(标准I/O缓冲区大小)。
·宏函数——例如,getchar()通常用getc(stdin)定义,而getc()经常用于定义较复杂的宏,头文件ctype.h通常包含ctype系列函数的宏定义。
·函数声明——例如,string.h头文件(一些旧的系统中是strings.h)包含字符串函数系列的函数声明。在ANSI C和后面的标准中,函数声明都是函数原型形式。
·结构模版定义——标准I/O函数使用FILE结构,该结构中包含了文件和与文件缓冲区相关的信息。FILE结构在头文件stdio.h中。
·类型定义——标准I/O函数使用指向FILE的指针作为参数。通常,stdio.h用#define或typedef把FILE定义为指向结构的指针。类似地,size_t和time_t类型也定义在头文件中。 -
许多程序员都在程序中使用自己开发的标准头文件。如果开发一系列相关的函数或结构,那么这种方法特别有价值。
-
另外,还可以使用头文件声明外部变量供其他文件共享。例如,如果已经开发了共享某个变量的一系列函数,该变量报告某种状况(如,错误情况),这种方法就很有效。这种情况下,可以在包含这些函数声明的源代码文件定义一个文件作用域的外部链接变量:
int status = 0; // 该变量具有文件作用域,在源代码文件
-
然后,可以在与源代码文件相关联的头文件中进行引用式声明:
extern int status; // 在头文件中
-
这行代码会出现在包含了该头文件的文件中,这样使用该系列函数的文件都能使用这个变量。虽然源代码文件中包含该头文件后也包含了该声明,但是只要声明的类型一致,在一个文件中同时使用定义式声明和引用式声明没问题。
-
需要包含头文件的另一种情况是,使用具有文件作用域、内部链接和const限定符的变量或数组。const防止值被意外修改,static意味着每个包含该头文件的文件都获得一份副本。因此,不需要在一个文件中进行定义式声明,在其他文件中进行引用式声明。
其他指令
#undef指令
-
#undef指令用于“取消”已定义的#define指令。 也就是说,假设有如下定义:
#define LIMIT 400
-
然后,下面的指令:
#undef LIMIT
-
将移除上面的定义。现在就可以把LIMIT重新定义为一个新值。即使原来没有定义LIMIT,取消LIMIT的定义仍然有效。
从C预处理器角度看已定义
- 预处理器在预处理器指令中发现一个标识符时,它会把该标识符当作已定义的或未定义的。这里的已定义表示由预处理器定义。如果标识符是同一个文件中由前面的#define指令创建的宏名,而且没有用#undef指令关闭,那么该标识符是已定义的。如果标识符不是宏,假设是一个文件作用域的C变量,那么该标识符对预处理器而言就是未定义的。
#define LIMIT 1000 // LIMIT是已定义的
#define GOOD // GOOD 是已定义的
#define A(X) ((-(X))*(X)) // A 是已定义的
int q; // q 不是宏,因此是未定义的
#undef GOOD // GOOD 取消定义,是未定义的
条件编译
- #ifdef、#else和#endif指令
#ifdef MAVIS
#include "horse.h" // 如果已经用#define定义了 MAVIS,则执行下面的指令
#define STABLES 5
#else
#include "cow.h" //如果没有用#define定义 MAVIS,则执行下面的指令
#define STABLES 15
#endif
/* ifdef.c -- 使用条件编译 */
#include <stdio.h>
#define JUST_CHECKING
#define LIMIT 4
int main(void)
{
int i;
int total = 0;
for (i = 1; 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;
}
编译并运行该程序后,输出如下:
i=1, running total = 3
i=2, running total = 12
i=3, running total = 31
i=4, running total = 64
Grand total = 64
- 2.#ifndef指令
- #ifndef指令判断后面的标识符是否是未定义的,常用于定义之前未定义的常量。
- 通常,包含多个头文件时,其中的文件可能包含了相同宏定义。#ifndef指令可以防止相同的宏被重复定义。在首次定义一个宏的头文件中用#ifndef指令激活定义,随后在其他头文件中的定义都被忽略。
// names.h --修订后的 names_st 头文件,避免重复包含
#ifndef NAMES_H_
#define NAMES_H_
// 明示常量
#define SLEN 32
// 结构声明
struct names_st
{
char first[SLEN];
char last[SLEN];
};
// 类型定义
typedef struct names_st names;
// 函数原型
void get_names(names *);
void show_names(const names *);
char * s_gets(char * st, int n);
#endif
- 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
预定义宏
// predef.c -- 预定义宏和预定义标识符
#include <stdio.h>
void why_me();
int main()
{
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()
{
printf("This function is %s\n", _ _func_ _);
printf("This is line %d.\n", _ _LINE_ _);
}
下面是该程序的输出:
The file is predef.c.
The date is Sep 23 2013.
The time is 22:01:09.
The version is 201112.
This is line 11.
This function is main
This function is why_me
This is line 21.
#line和#error
#line指令重置_ _LINE_ _和_ _FILE_ _宏报告的行号和文件名。可以这样使用#line:
#line 1000 // 把当前行号重置为1000
#line 10 “cool.c” // 把行号重置为10,把文件名重置为cool.c
-
#error指令让预处理器发出一条错误消息,该消息包含指令中的文本。如果可能的话,编译过程应该中断。可以这样使用#error指令:
#if _ _STDC_VERSION_ _ != 201112L
#error Not C11
#endif
编译以上代码生成后,输出如下:
$ gcc newish.cnewish.c:14:2: error: #error Not C11
$ gcc -std=c11 newish.c
$
#pragma
-
#pragma把编译器指令放入源代码中。例如,在开发C99时,标准被称为C9X,可以使用下面的编译指示(pragma)让编译器支持C9X:
#pragma c9x on
-
C99还提供_Pragma预处理器运算符,该运算符把字符串转换成普通的编译指示。例如:
_Pragma(“nonstandardtreatmenttypeB on”)
等价于下面的指令:
#pragma nonstandardtreatmenttypeB on
泛型选择(C11)
-
C11新增了一种表达式,叫作泛型选择表达式(generic selection expression),可根据表达式的类型(即表达式的类型是int、double还是其他类型)选择一个值。泛型选择表达式不是预处理器指令,但是在一些泛型编程中它常用作#define宏定义的一部分。
-
下面是一个泛型选择表达式的示例:
_Generic(x, int: 0, float: 1, double: 2, default: 3)
-
_Generic是C11的关键字。_Generic后面的圆括号中包含多个用逗号分隔的项。第1个项是一个表达式,后面的每个项都由一个类型、一个冒号和一个值组成,如float: 1。第1个项的类型匹配哪个标签,整个表达式的值是该标签后面的值。例如,假设上面表达式中x是int类型的变量,x的类型匹配int:标签,那么整个表达式的值就是0。如果没有与类型匹配的标签,表达式的值就是default:标签后面的值。泛型选择语句与switch语句类似,只是前者用表达式的类型匹配标签,而后者用表达式的值匹配标签。
-
下面是一个把泛型选择语句和宏定义组合的例子:
#define MYTYPE(X) _Generic((X),\
int: “int”,\
float : “float”,\
double: “double”,\
default: “other”\
)
宏必须定义为一条逻辑行,但是可以用\把一条逻辑行分隔成多条物理行。在这种情况下,对泛型选择表达式求值得字符串。例如,对MYTYPE(5)求值得"int",因为值5的类型与int:标签匹配。
// mytype.c
#include <stdio.h>
#define MYTYPE(X) _Generic((X),\
int: "int",\
float : "float",\
double: "double",\
default: "other"\
)
int main(void)
{
int d = 5;
printf("%s\n", MYTYPE(d)); // d 是int类型
printf("%s\n", MYTYPE(2.0*d)); // 2.0 * d 是double类型
printf("%s\n", MYTYPE(3L)); // 3L 是long类型
printf("%s\n", MYTYPE(&d)); // &d 的类型是 int *
return 0;
}
下面是该程序的输出:
int
double
other
other
内联函数(C99)
- 其实C99和C11标准中叙述的是:“把函数变成内联函数意味着尽可能快地调用该函数,其具体效果由实现定义”。因此,把函数变成内联函数,编译器可能会用内联代码替换函数调用,并(或)执行一些其他的优化,但是也可能不起作用。
- 标准规定具有内部链接的函数可以成为内联函数,还规定了内联函数的定义与调用该函数的代码必须在同一个文件中。
- 简单的方法是使用函数说明符inline和存储类别说明符static。通常,内联函数应定义在首次使用它的文件中,所以内联函数也相当于函数原型。如下所示:
#include <stdio.h>
inline static void eatline() // 内联函数定义/原型
{
while (getchar() != '\n')
continue;
}
int main()
{
...
eatline(); // 函数调用
...
}
- 由于并未给内联函数预留单独的代码块,所以无法获得内联函数的地址(实际上可以获得地址,不过这样做之后,编译器会生成一个非内联函数)。另外,内联函数无法在调试器中显示。
- 编译器优化内联函数必须知道该函数定义的内容。这意味着内联函数定义与函数调用必须在同一个文件中。鉴于此,一般情况下内联函数都具有内部链接。因此,如果程序有多个文件都要使用某个内联函数,那么这些文件中都必须包含该内联函数的定义。最简单的做法是,把内联函数定义放入头文件,并在使用该内联函数的文件中包含该头文件即可。
// eatline.h
#ifndef EATLINE_H_
#define EATLINE_H_
inline static void eatline()
{
while (getchar() != '\n')
continue;
}
#endif
- 一般都不在头文件中放置可执行代码,内联函数是个特例。因为内联函数具有内部链接,所以在多个文件中定义同一个内联函数不会产生什么问题。
//file1.c
...
inline static double square(double);
double square(double x) { return x * x; }
int main()
{
double q = square(1.3);
...
//file2.c
...
double square(double x) { return (int) (x*x); }
void spam(double v)
{
double kv = square(v);
...
//file3.c
...
inline double square(double x) { return (int) (x * x + 0.5); }
void masp(double w)
{
double kw = square(w);
...
- 如上述代码所示,3个文件中都定义了square()函数。file1.c文件中是inline static定义;file2.c文件中是普通的函数定义(因此具有外部链接);file3.c文件中是inline定义,省略了static。
- file1.c文件中的main()使用square()的局部static定义。由于该定义也是inline定义,所以编译器有可能优化代码,也许会内联该函数。file2.c文件中,spam()函数使用该文件中square()函数的定义,该定义具有外部链接,其他文件也可见。file3.c文件中,编译器既可以使用该文件中square()函数的内联定义,也可以使用file2.c文件中的外部链接定义。如果像file3.c那样,省略file1.c文件inline定义中的static,那么该inline定义被视为可替换的外部定义。
_Noreturn函数(C11)
- C99新增inline关键字时,它是唯一的函数说明符(关键字extern和static是存储类别说明符,可应用于数据对象和函数)。C11新增了第2个函数说明符_Noreturn,表明调用完成后函数不返回主调函数。
- _exit()函数是_Noreturn函数的一个示例,一旦调用exit(),它不会再返回主调函数。注意,这与void返回类型不同。void类型的函数在执行完毕后返回主调函数,只是它不提供返回值。
- _Noreturn的目的是告诉用户和编译器,这个特殊的函数不会把控制返回主调程序。告诉用户以免滥用该函数,通知编译器可优化一些代码。
C库
访问C库
- 不同的系统搜索这些函数的方法不同。下面介绍3种可能的方法。
- 1.自动访问
-
在一些系统中,只需编译程序,就可使用一些常用的库函数。
记住,在使用函数之前必须先声明函数的类型,通过包含合适的头文件即可完成。 - 2.文件包含
- 如果函数被定义为宏,那么可以通过#include指令包含定义宏函数的文件。通常,类似的宏都放在合适名称的头文件中。
- 3.库包含
- 在编译或链接程序的某些阶段,可能需要指定库选项。即使在自动检查标准库的系统中,也会有不常用的函数库。必须通过编译时选项显式指定这些库。注意,这个过程与包含头文件不同。头文件提供函数声明或原型,而库选项告诉系统到哪里查找函数代码。
使用库描述
-
ANSI C90标准提供了下面的描述:
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
-
首先,使用了新的函数原型格式。其次,改变了一些类型。size_t类型被定义为sizeof运算符的返回值类型——无符号整数类型,通常是unsigned int或unsigned long。stddef.h文件中包含了size_t类型的typedef或#define定义。其他文件(包括stdio.h)通过包含stddef.h来包含这个定义。许多函数(包括fread())的实际参数中都要使用sizeof运算符,形式参数的size_t类型中正好匹配这种常见的情况。另外,ANSI C把指向void的指针作为一种通用指针,用于指针指向不同类型的情况。
数学库
三角问题
- atan()函数接受一个double类型的参数(即正切值),并返回一个角度(该角度的正切值就是参数值)。但是,当线的x值和y值均为-5时,atan()函数产生混乱。因为(-5)/(-5)得1,所以atan()返回45°,该值与x和y均为5时的返回值相同。也就是说,atan()无法区分角度相同但反向相反的线(实际上,atan()返回值的单位是弧度而不是度,稍后介绍两者的转换)。
- 当然,C库还提供了atan2()函数。它接受两个参数:x的值和y的值。这样,通过检查x和y的正负号就可以得出正确的角度值。atan2()和atan()均返回弧度值。把弧度转换为度,只需将弧度值乘以180,再除以pi即可。
/* rect_pol.c -- 把直角坐标转换为极坐标 */
#include <stdio.h>
#include <math.h>
#define RAD_TO_DEG (180/(4 * atan(1)))
typedef struct polar_v {
double magnitude;
double angle;
} Polar_V;
typedef struct rect_v {
double x;
double y;
} Rect_V;
Polar_V rect_to_polar(Rect_V);
int main(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("magnitude = %0.2f, angle = %0.2f\n",
result.magnitude, result.angle);
}
puts("Bye.");
return 0;
}
Polar_V rect_to_polar(Rect_V rv)
{
Polar_V pv;
pv.magnitude = sqrt(rv.x * rv.x + rv.y * rv.y);
if (pv.magnitude == 0)
pv.angle = 0.0;
else
pv.angle = RAD_TO_DEG * atan2(rv.y, rv.x);
return pv;
}
下面是运行该程序后的一个输出示例:
Enter x and y coordinates; enter q to quit:10 10
magnitude = 14.14, angle = 45.00-12 -5
magnitude = 13.00, angle = -157.38
q
Bye.
类型变体
- C标准专门为float类型和long double类型提供了标准函数,即在原函数名后加上f或l后缀。因此,sqrtf()是sqrt()的float版本,sqrtl()是sqrt()的long double版本。
// generic.c -- 定义泛型宏
#include <stdio.h>
#include <math.h>
#define RAD_TO_DEG (180/(4 * atanl(1)))
// 泛型平方根函数
#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)\
)
int main(void)
{
float x = 45.0f;
double xx = 45.0;
long double xxx = 45.0L;
long double y = SQRT(x);
long double yy = SQRT(xx);
long double yyy = SQRT(xxx);
printf("%.17Lf\n", y); // 匹配 float
printf("%.17Lf\n", yy); // 匹配 default
printf("%.17Lf\n", yyy); // 匹配 long double
int i = 45;
yy = SQRT(i); // 匹配 default
printf("%.17Lf\n", yy);
yyy = SIN(xxx); // 匹配 long double
printf("%.17Lf\n", yyy);
return 0;
}
下面是该程序的输出:
6.70820379257202148
6.70820393249936942
6.70820393249936909
6.70820393249936942
0.70710678118654752
- SQRT()的定义也许更简洁。_Generic表达式的值就是函数名,如sinf。函数的地址可以代替该函数名,所以_Generic表达式的值是一个指向函数的指针。然而,紧随整个_Generic表达式之后的是(X),函数指针(参数)表示函数指针。因此,这是一个带指定的参数的函数指针。
- 简而言之,对于SIN(),函数调用在泛型选择表达式内部;而对于SQRT(),先对泛型选择表达式求值得一个指针,然后通过该指针调用它所指向的函数。
tgmath.h库(C99)
- C99标准提供的tgmath.h头文件中定义了泛型类型宏,其效果与程序清单16.15类似。如果在math.h中为一个函数定义了3种类型(float、double和long double)的版本,那么tgmath.h文件就创建一个泛型类型宏,与原来double版本的函数名同名。例如,根据提供的参数类型,定义sqrt()宏展开为sqrtf()、sqrt()或sqrtl()函数。换言之,sqrt()宏的行为和程序清单16.15中的SQRT()宏类似。
// 如果包含了tgmath.h,要调用sqrt()函数而不是sqrt()宏,可以用圆括号把被调用的函数名括起来:
#include <tgmath.h>
...
float x = 44.0;
double y;
y = sqrt(x); // 调用宏,所以是 sqrtf(x)
y = (sqrt)(x); // 调用函数 sqrt()
通用工具库
- 通用工具库包含各种函数,包括随机数生成器、查找和排序函数、转换函数和内存管理函数。第12章介绍过rand()、srand()、malloc()和free()函数。在ANSI C标准中,这些函数的原型都在stdlib.h头文件中。
exit()和atexit()函数
- ANSI标准还新增了一些不错的功能,其中最重要的是可以指定在执行exit()时调用的特定函数。atexit()通过注册要在退出时调用的函数来提供这一特性,atexit()函数接受一个函数指针作为参数。
/* byebye.c -- atexit()示例 */
#include <stdio.h>
#include <stdlib.h>
void sign_off(void);
void too_bad(void);
int main(void)
{
int n;
atexit(sign_off); /* 注册 sign_off()函数 */
puts("Enter an integer:");
if (scanf("%d", &n) != 1)
{
puts("That's no integer!");
atexit(too_bad); /* 注册 too_bad()函数 */
exit(EXIT_FAILURE);
}
printf("%d is %s.\n", n, (n % 2 == 0) ? "even" : "odd");
return 0;
}
void sign_off(void)
{
puts("Thus terminates another magnificent program from");
puts("SeeSaw Software!");
}
void too_bad(void)
{
puts("SeeSaw Software extends its heartfelt condolences");
puts("to you upon the failure of your program.");
}
下面是该程序的一个运行示例:
Enter an integer:212
212 is even.
Thus terminates another magnificent program from
SeeSaw Software!
- 1.atexit()函数的用法
- 要使用atexit()函数,只需把退出时要调用的函数地址传递给atexit()即可。函数名作为函数参数时相当于该函数的地址,所以该程序中把sign_off或too_bad作为参数。然后,atexit()注册函数列表中的函数,当调用exit()时就会执行这些函数。ANSI保证,在这个列表中至少可以放32个函数。最后调用exit()函数时,exit()会执行这些函数(执行顺序与列表中的函数顺序相反,即最后添加的函数最先执行)。
- atexit()注册的函数(如sign_off()和too_bad())应该不带任何参数且返回类型为void。通常,这些函数会执行一些清理任务,例如更新监视程序的文件或重置环境变量。
- 注意,即使没有显式调用exit(),还是会调用sign_off(),因为main()结束时会隐式调用exit()。
- 2.exit()函数的用法
- exit()执行完atexit()指定的函数后,会完成一些清理工作:刷新所有输出流、关闭所有打开的流和关闭由标准I/O函数tmpfile()创建的临时文件。然后exit()把控制权返回主机环境,如果可能的话,向主机环境报告终止状态。
qsort()函数
-
快速排序算法在C实现中的名称是qsort()。qsort()函数排序数组的数据对象,其原型如下:
void qsort(void *base, size_t nmemb, size_t size,*
int (*compar)(const void *, const void *));
-
第1个参数是指针,指向待排序数组的首元素。ANSI C允许把指向任何数据类型的指针强制转换成指向void的指针,因此,qsort()的第1个实际参数可以引用任何类型的数组。
-
第2个参数是待排序项的数量。函数原型把该值转换为size_t类型。前面提到过,size_t定义在标准头文件中,是sizeof运算符返回的整数类型。
-
由于qsort()把第1个参数转换为void指针,所以qsort()不知道数组中每个元素的大小。为此,函数原型用第3个参数补偿这一信息,显式指明待排序数组中每个元素的大小。例如,如果排序double类型的数组,那么第3个参数应该是sizeof(double)。
-
最后,qsort()还需要一个指向函数的指针,这个被指针指向的比较函数用于确定排序的顺序。该函数应接受两个参数:分别指向待比较两项的指针。如果第1项的值大于第2项,比较函数则返回正数;如果两项相同,则返回0;如果第1项的值小于第2项,则返回负数。qsort()根据给定的其他信息计算出两个指针的值,然后把它们传递给比较函数。
-
qsort()原型中的第4个参数确定了比较函数的形式:
int (*compar)(const void *, const void *)
-
这表明qsort()最后一个参数是一个指向函数的指针,该函数返回int类型的值且接受两个指向const void的指针作为参数,这两个指针指向待比较项。
/* qsorter.c -- 用 qsort()排序一组数字 */
#include <stdio.h>
#include <stdlib.h>
#define NUM 40
void fillarray(double ar [], int n);
void showarray(const double ar [], int n);
int mycomp(const void * p1, const void * p2);
int main(void)
{
double vals[NUM];
fillarray(vals, NUM);
puts("Random list:");
showarray(vals, NUM);
qsort(vals, NUM, sizeof(double), mycomp);
puts("\nSorted list:");
showarray(vals, NUM);
return 0;
}
void fillarray(double ar [], int n)
{
int index;
for (index = 0; index < n; index++)
ar[index] = (double) rand() / ((double) rand() + 0.1);
}
void showarray(const double ar [], int n)
{
int index;
for (index = 0; index < n; index++)
{
printf("%9.4f ", ar[index]);
if (index % 6 == 5)
putchar('\n');
}
if (index % 6 != 0)
putchar('\n');
}
/* 按从小到大的顺序排序 */
int mycomp(const void * p1, const void * p2)
{
/* 要使用指向double的指针来访问这两个值 */
const double * a1 = (const double *) p1;
const double * a2 = (const double *) p2;
if (*a1 < *a2)
return -1;
else if (*a1 == *a2)
return 0;
else
return 1;
}
下面是该程序的运行示例:
Random list:
0.0001 1.6475 2.4332 0.0693 0.7268 0.7383
24.0357 0.1009 87.1828 5.7361 0.6079 0.6330
1.6058 0.1406 0.5933 1.1943 5.5295 2.2426
0.8364 2.7127 0.2514 0.9593 8.9635 0.7139
0.6249 1.6044 0.8649 2.1577 0.5420 15.0123
1.7931 1.6183 1.9973 2.9333 12.8512 1.3034
0.3032 1.1406 18.7880 0.9887
Sorted list:
0.0001 0.0693 0.1009 0.1406 0.2514 0.3032
0.5420 0.5933 0.6079 0.6249 0.6330 0.7139
0.7268 0.7383 0.8364 0.8649 0.9593 0.9887
1.1406 1.1943 1.3034 1.6044 1.6058 1.6183
1.6475 1.7931 1.9973 2.1577 2.2426 2.4332
2.7127 2.9333 5.5295 5.7361 8.9635 12.8512
15.0123 18.7880 24.0357 87.1828
断言库
- assert.h头文件支持的断言库是一个用于辅助调试程序的小型库。它由assert()宏组成,接受一个整型表达式作为参数。如果表达式求值为假(非零),assert()宏就在标准错误流(stderr)中写入一条错误信息,并调用abort()函数终止程序(abort()函数的原型在stdlib.h头文件中)。assert()宏是为了标识出程序中某些条件为真的关键位置,如果其中的一个具体条件为假,就用assert()语句终止程序。通常,assert()的参数是一个条件表达式或逻辑表达式。如果assert()中止了程序,它首先会显示失败的测试、包含测试的文件名和行号。
assert的用法
/* assert.c -- 使用 assert() */
#include <stdio.h>
#include <math.h>
#include <assert.h>
int main()
{
double x, y, z;
puts("Enter a pair of numbers (0 0 to quit): ");
while (scanf("%lf%lf", &x, &y) == 2
&& (x != 0 || y != 0))
{
z = x * x - y * y; /* 应该用 + */
assert(z >= 0);
printf("answer is %f\n", sqrt(z));
puts("Next pair of numbers: ");
}
puts("Done");
return 0;
}
下面是该程序的运行示例:
Enter a pair of numbers (0 0 to quit):4 3
answer is 2.645751
Next pair of numbers:5 3
answer is 4.000000
Next pair of numbers:3 5
Assertion failed: (z >= 0), function main, file /Users/assert.c, line 14.
-
用if语句也能完成类似的任务:
if (z < 0)
{
puts(“z less than 0”);
abort();
}
-
但是,使用assert()有几个好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制。如果认为已经排除了程序的bug,就可以把下面的宏定义写在包含assert.h的位置前面:
#define NDEBUG
并重新编译程序,这样编译器就会禁用文件中的所有assert()语句。如果程序又出现问题,可以移除这条#define指令(或者把它注释掉),然后重新编译程序,这样就重新启用了assert()语句。
_Static_assert(C11)
- _Static_assert声明,可以在编译时检查assert()表达式。因此,assert()会导致正在运行的程序中止,而_Static_assert()会导致程序无法通过编译。_Static_assert()接受两个参数。第1个参数是整型常量表达式,第2个参数是一个字符串。如果第1个表达式求值为0(或_False),编译器会显示字符串,而且不编译该程序。
// statasrt.c
#include <stdio.h>
#include <limits.h>
_Static_assert(CHAR_BIT == 16, "16-bit char falsely assumed");
int main(void)
{
puts("char is 16 bits.");
return 0;
}
下面是在命令行编译的示例:
$ clang statasrt.c
statasrt.c:4:1: error: static_assert failed "16-bit char falsely assumed"
_Static_assert(CHAR_BIT == 16, "16-bit char falsely assumed");
^ ~~~~~~~~~~~~~~
1 error generated.
$
- _Static_assert要求它的第1个参数是整型常量表达式,这保证了能在编译期求值(sizeof表达式被视为整型常量)。不能用程序清单16.18中的assert代替_Static_assert,因为assert中作为测试表达式的z > 0不是常量表达式,要到程序运行时才求值。当然,可以在程序清单16.19的main()函数中使用assert(CHAR_BIT == 16),但这会在编译和运行程序后才生成一条错误信息,很没效率。
string.h库中的memcpy()和memmove()
void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
void *memmove(void *s1, const void *s2, size_t n);
- 这两个函数都从s2指向的位置拷贝n字节到s1指向的位置,而且都返回s1的值。所不同的是,memcpy()的参数带关键字restrict,即memcpy()假设两个内存区域之间没有重叠;而memmove()不作这样的假设,所以拷贝过程类似于先把所有字节拷贝到一个临时缓冲区,然后再拷贝到最终目的地。
// mems.c -- 使用 memcpy() 和 memmove()
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define SIZE 10
void show_array(const int ar [], int n);
// 如果编译器不支持C11的_Static_assert,可以注释掉下面这行
_Static_assert(sizeof(double) == 2 * sizeof(int), "double not twice int size");
int main()
{
int values[SIZE] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int target[SIZE];
double curious[SIZE / 2] = { 2.0, 2.0e5, 2.0e10, 2.0e20, 5.0e30 };
puts("memcpy() used:");
puts("values (original data): ");
show_array(values, SIZE);
memcpy(target, values, SIZE * sizeof(int));
puts("target (copy of values):");
show_array(target, SIZE);
puts("\nUsing memmove() with overlapping ranges:");
memmove(values + 2, values, 5 * sizeof(int));
puts("values -- elements 0-4 copied to 2-6:");
show_array(values, SIZE);
puts("\nUsing memcpy() to copy double to int:");
memcpy(target, curious, (SIZE / 2) * sizeof(double));
puts("target -- 5 doubles into 10 int positions:");
show_array(target, SIZE / 2);
show_array(target + 5, SIZE / 2);
return 0;
}
void show_array(const int ar [], int n)
{
int i;
for (i = 0; i < n; i++)
printf("%d ", ar[i]);
putchar('\n');
}
下面是该程序的输出:
memcpy() used:
values (original data):
1 2 3 4 5 6 7 8 9 10
target (copy of values):
1 2 3 4 5 6 7 8 9 10
Using memmove() with overlapping ranges:
values -- elements 0-4 copied to 2-6:
1 2 1 2 3 4 5 8 9 10
Using memcpy() to copy double to int:
target -- 5 doubles into 10 int positions:
0 1073741824 0 1091070464 536870912
1108516959 2025163840 1143320349 -2012696540 1179618799
可变参数:stdarg.h
-
stdarg.h头文件为函数提供了一个类似的功能,但是用法比较复杂。必须按如下步骤进行:
1.提供一个使用省略号的函数原型;
2.在函数定义中创建一个va_list类型的变量;
3.用宏把该变量初始化为一个参数列表;
4.用宏访问参数列表;
5.用宏完成清理工作。void f1(int n, …); // 有效
int f2(const char * s, int k, …); // 有效
char f3(char c1, …, char c2); // 无效,省略号不在最后
double f3(…); // 无效,没有形参
-
最右边的形参(即省略号的前一个形参)起着特殊的作用,标准中用parmN这个术语来描述该形参。在上面的例子中,第1行f1()中parmN为n,第2行f2()中parmN为k。传递给该形参的实际参数是省略号部分代表的参数数量。例如,可以这样使用前面声明的f1()函数:
f1(2, 200, 400); // 2个额外的参数
f1(4, 13, 117, 18, 23); // 4个额外的参数
-
声明在stdarg.h中的va_list类型代表一种用于存储形参对应的形参列表中省略号部分的数据对象。
double sum(int lim,…)
{
va_list ap; //声明一个存储参数的对象
-
然后,该函数将使用定义在stdarg.h中的va_start()宏,把参数列表拷贝到va_list类型的变量中。该宏有两个参数:va_list类型的变量和parmN形参。
va_start(ap, lim); // 把ap初始化为参数列表
-
下一步是访问参数列表的内容,这涉及使用另一个宏va_arg()。该宏接受两个参数:一个va_list类型的变量和一个类型名。第1次调用va_arg()时,它返回参数列表的第1项;第2次调用时返回第2项,以此类推。表示类型的参数指定了返回值的类型。例如,如果参数列表中的第1个参数是double类型,第2个参数是int类型,可以这样做:
double* tic;
int* toc;
…
tic = va_arg(ap, double); // 检索第1个参数
toc = va_arg(ap, int); //检索第2个参数
-
注意,传入的参数类型必须与宏参数的类型相匹配。如果第1个参数是10.0,上面tic那行代码可以正常工作。但是如果参数是10,这行代码可能会出错。这里不会像赋值那样把double类型自动转换成int类型。
-
最后,要使用va_end()宏完成清理工作。例如,释放动态分配用于存储参数的内存。该宏接受一个va_list类型的变量:
va_end(ap); // 清理工作
-
调用va_end(ap)后,只有用va_start重新初始化ap后,才能使用变量ap。
-
因为va_arg()不提供退回之前参数的方法,所以有必要保存va_list类型变量的副本。
va_copy()。该宏接受两个va_list类型的变量作为参数,它把第2个参数拷贝给第1个参数:
va_list ap;
va_list apcopy;
double tic;
int toc;
...
va_start(ap, lim); // 把ap初始化为一个参数列表
va_copy(apcopy, ap); // 把apcopy作为ap的副本
tic = va_arg(ap, double); // 检索第1个参数
toc = va_arg(ap, int); // 检索第2个参数
//varargs.c -- use variable number of arguments
#include <stdio.h>
#include <stdarg.h>
double sum(int, ...);
int main(void)
{
double s, t;
s = sum(3, 1.1, 2.5, 13.3);
t = sum(6, 1.1, 2.1, 13.1, 4.1, 5.1, 6.1);
printf("return value for "
"sum(3, 1.1, 2.5, 13.3): %g\n", s);
printf("return value for "
"sum(6, 1.1, 2.1, 13.1, 4.1, 5.1, 6.1): %g\n", t);
return 0;
}
double sum(int lim, ...)
{
va_list ap; // 声明一个对象存储参数
double tot = 0;
int i;
va_start(ap, lim); // 把ap初始化为参数列表
for (i = 0; i < lim; i++)
tot += va_arg(ap, double); // 访问参数列表中的每一项
va_end(ap); // 清理工作
return tot;
}
下面是该程序的输出:
return value for sum(3, 1.1, 2.5, 13.3): 16.9
return value for sum(6, 1.1, 2.1, 13.1, 4.1, 5.1, 6.1): 31.6
复习题
- 下面的几组代码由一个或多个宏组成,其后是使用宏的源代码。在每种情况下代码的结果是什么?这些代码是否是有效代码?(假设其中的变量已声明)
a.
#define FPM 5280 /每英里的英尺数/
dist = FPM * miles;
b.
#define FEET 4
#define POD FEET + FEET
plort = FEET * POD;
c.
#define SIX = 6;
nex = SIX;
d.
#define NEW(X) X + 5
y = NEW(y);
berg = NEW(berg) * lob;
est = NEW(berg) / NEW(y);
nilp = lob * NEW(-berg); - 修改复习题1中d部分的定义,使其更可靠。
- 定义一个宏函数,返回两值中的较小值。
- 定义EVEN_GT(X, Y)宏,如果X为偶数且大于Y,该宏返回1。
- 定义一个宏函数,打印两个表达式及其值。例如,若参数为3+4和4*12,则打印:
3+4 is 7 and 4*12 is 48 - 创建#define指令完成下面的任务。
a.创建一个值为25的命名常量。
b.SPACE表示空格字符。
c.PS()代表打印空格字符。
d.BIG(X)代表X的值加3。
e.SUMSQ(X, Y)代表X和Y的平方和。 - 定义一个宏,以下面的格式打印名称、值和int类型变量的地址:
name: fop; value: 23; address: ff464016 - 假设在测试程序时要暂时跳过一块代码,如何在不移除这块代码的前提下完成这项任务?
- 编写一段代码,如果定义了PR_DATE宏,则打印预处理的日期。
- 内联函数部分讨论了3种不同版本的square()函数。从行为方面看,这3种版本的函数有何不同?
- 创建一个使用泛型选择表达式的宏,如果宏参数是_Bool类型,对"boolean"求值,否则对"not boolean"求值。
- 下面的程序有什么错误?
#include <stdio.h>
int main(int argc, char argv[])
{
printf(“The square root of %f is %f\n”, argv[1],sqrt(argv[1]) );
} - 假设scores是内含1000个int类型元素的数组,要按降序排序该数组中的值。假设你使用qsort()和comp()比较函数。
a.如何正确调用qsort()?
b.如何正确定义comp()? - 假设data1是内含100个double类型元素的数组,data2是内含300个double类型元素的数组。
a.编写memcpy()的函数调用,把data2中的前100个元素拷贝到data1中。
b.编写memcpy()的函数调用,把data2中的后100个元素拷贝到data1中。
-
a.dist = 5280 * miles;有效。
b.plort = 4 * 4+4;有效。但是如果用户需要的是4 * (4+4),则应该使用#define POD (FEET+FEET)。
c.nex = = 6;;无效(如果两个等号之间没有空格,则有效,但是没有意义)。显然,用户忘记了在编写预处理器代码时不用加=。
d.y = y+5;有效。berg = berg+5 * lob;有效,但是可能得不到想要的结果。est = berg+5/y+5;有效,但是可能得不到想要的结果。 -
#define NEW(X) ((X) + 5)
-
#define MIN(X,Y) ( (X) < (Y) ? (X) : (Y) )
-
#define EVEN_GT(X,Y) ( (X) > (Y) && (X) % 2 == 0 ? 1 : 0 )
-
#define PR(X,Y) printf(#X " is %d and " #Y " is %d\n", X,Y)
(因为该宏中没有运算符(如,乘法)作用于X和Y,所以不需要使用圆括号。) -
a.#define QUARTERCENTURY 25
b.#define SPACE ’ ’
c.#define PS() putchar(’ ')或#define PS() putchar(SPACE)
d.#define BIG(X) ((X) + 3)
e.#define SUMSQ(X,Y) ((X)(X) + (Y)(Y)) -
试试这样:#define P(X) printf(“name: “#X”; value: %d; address: %p\n”, X, &X)
(如果你的实现无法识别地址专用的%p转换说明,可以用%u或%lu代替。) -
使用条件编译指令。一种方法是使用#ifndef:
#define _SKIP_ /* 如果不需要跳过代码,则删除这条指令 */#ifndef _SKIP_
/* 需要跳过的代码 */
#endif* -
#ifdef PR_DATE
printf(“Date = %s\n”, _ DATE _);
#endif -
第1个版本返回x*x,这只是返回了square()的double类型值。例如,square(1.3)会返回1.69。第2个版本返回(int)(x*x),计算结果被截断后返回。但是,由于该函数的返回类型是double,int类型的值将被升级为double类型的值,所以1.69将先被转换成1,然后被转换成1.00。第3个版本返回(int)(x*x+0.5)。加上0.5可以让函数把结果四舍五入至与原值最接近的值,而不是简单地截断。所以,1.69+0.5得2.19,然后被截断为2,然后被转换成2.00;而1.44+0.5得1.94,被截断为1,然后被转换成1.00。
-
这是一种方案: #define BOOL(X) _Generic((X), _Bool : “boolean”, default : “not boolean”)
-
应该把argv参数声明为char *argv[]类型。命令行参数被存储为字符串,所以该程序应该先把argv[1]中的字符串转换成double类型的值。例如,用stdlib.h库中的atof()函数。程序中使用了sqrt()函数,所以应包含math.h头文件。程序在求平方根之前应排除参数为负的情况(检查参数是否大于或等于0)。
-
a.qsort( (void )scores, (size_t) 1000, sizeof (double), comp);
b.下面是一个比较使用的比较函数:
int comp(const void * p1, const void * p2)
{
/ 要用指向int的指针来访问值 */
/* 在C中是否进行强制类型转换都可以,在C++中必须进行强制类型转换 */
const int * a1 = (const int *) p1; const int * a2 = (const int *)
p2;
if (*a1 > *a2)
return -1;
else if (*a1 == *a2)
return 0;
else
return 1;
} -
a.函数调用应该类似:memcpy(data1, data2, 100 * sizeof(double));
b.函数调用应该类似:memcpy(data1, data2+200 , 100 * sizeof(double));
编程练习
- 开发一个包含你需要的预处理器定义的头文件。
- 两数的调和平均数这样计算:先得到两数的倒数,然后计算两个倒数的平均值,最后取计算结果的倒数。使用#define指令定义一个宏“函数”,执行该运算。编写一个简单的程序测试该宏。
- 极坐标用向量的模(即向量的长度)和向量相对x轴逆时针旋转的角度来描述该向量。直角坐标用向量的x轴和y轴的坐标来描述该向量(见图16.3)。编写一个程序,读取向量的模和角度(单位:度),然后显示x轴和y轴的坐标。相关方程如下:
x = r*cos A y = r*sin A
需要一个函数来完成转换,该函数接受一个包含极坐标的结构,并返回一个包含直角坐标的结构(或返回指向该结构的指针)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3E7TRiiP-1617684629543)(file:///C:\Users\Administrator\Documents\Tencent Files\2429894028\Image\C2C\06FBD1F671345D80D978CD71207C587A.png)]
图16.3 直角坐标和极坐标 - ANSI库这样描述clock()函数的特性:
#include <time.h>
clock_t clock (void);
这里,clock_t是定义在time.h中的类型。该函数返回处理器时间,其单位取决于实现(如果处理器时间不可用或无法表示,该函数将返回-1)。然而,CLOCKS_PER_SEC(也定义在time.h中)是每秒处理器时间单位的数量。因此,两个clock()返回值的差值除以CLOCKS_PER_SEC得到两次调用之间经过的秒数。在进行除法运算之前,把值的类型强制转换成double类型,可以将时间精确到小数点以后。编写一个函数,接受一个double类型的参数表示时间延迟数,然后在这段时间运行一个循环。编写一个简单的程序测试该函数。 - 编写一个函数接受这些参数:内含int类型元素的数组名、数组的大小和一个代表选取次数的值。该函数从数组中随机选择指定数量的元素,并打印它们。每个元素只能选择一次(模拟抽奖数字或挑选陪审团成员)。另外,如果你的实现有time()(第12章讨论过)或类似的函数,可在srand()中使用这个函数的输出来初始化随机数生成器rand()。编写一个简单的程序测试该函数。
- 修改程序清单16.17,使用struct names元素(在程序清单16.17后面的讨论中定义过),而不是double类型的数组。使用较少的元素,并用选定的名字显式初始化数组。
- 下面是使用变参函数的一个程序段:
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
void show_array(const double ar[], int n);
double * new_d_array(int n, …);
int main()
{
double * p1;
double * p2;
p1 = new_d_array(5, 1.2, 2.3, 3.4, 4.5, 5.6);
p2 = new_d_array(4, 100.0, 20.00, 8.08, -1890.0);
show_array(p1, 5);
show_array(p2, 4);
free(p1);
free(p2);
return 0;
}
new_d_array()函数接受一个int类型的参数和double类型的参数。该函数返回一个指针,指向由malloc()分配的内存块。int类型的参数指定了动态数组中的元素个数,double类型的值用于初始化元素(第1个值赋给第1个元素,以此类推)。编写show_array()和new_d_array()函数的代码,完成这个程序。
-
/* * @Description: * @version: * @Author: * @Date: 2021-04-05 23:10:27 * @LastEditors: Please set LastEditors * @LastEditTime: 2021-04-05 23:36:44 */ /* TODO:2. 两数的调和平均数这样计算:先得到两数的倒数,然后计算两个倒数的平均值,最后取计算结果的倒数。使用#define指令定义一个宏“函数”,执行该运算。编写一个简单的程序测试该宏。 */ #include <stdio.h> #include <stdlib.h> #define AVE(X, Y) (1.0 / (((1.0 / X) + (1.0 / Y)) / 2.0)) int main(void) { printf("%g\n", AVE(2, 1)); system("pause"); return 0; }
-
/* * @Description: * @version: * @Author: * @Date: 2021-04-05 23:37:50 * @LastEditors: Please set LastEditors * @LastEditTime: 2021-04-06 11:53:28 */ /* TODO:3. 极坐标用向量的模(即向量的长度)和向量相对x轴逆时针旋转的角度来描述该向量。直角坐标用向量的x轴和y轴的坐标来描述该向量(见图16.3)。编写一个程序,读取向量的模和角度(单位:度),然后显示x轴和y轴的坐标。相关方程如下: x = r*cos A y = r*sin A 需要一个函数来完成转换,该函数接受一个包含极坐标的结构,并返回一个包含直角坐标的结构(或返回指向该结构的指针)。 */ #include <tgmath.h> #include <stdio.h> #include <stdlib.h> struct stru1 { double r; double A; }; struct stru2 { double x; double y; }; struct stru2 tranverse(struct stru1 a) { struct stru2 temp; temp.x = a.r * cos(a.A); temp.y = a.r * sin(a.A); return temp; } int main(void) { double r, a; scanf("%lf %lf", &r, &a); struct stru1 A = {.r = r, .A = a}; struct stru2 t = tranverse(A); printf("%g %g\n", t.x, t.y); system("pause"); return 0; }
-
/* * @Description: * @version: * @Author: * @Date: 2021-04-06 11:56:26 * @LastEditors: Please set LastEditors * @LastEditTime: 2021-04-06 12:07:34 */ /* TODO:4. ANSI库这样描述clock()函数的特性: #include <time.h> clock_t clock (void); 这里,clock_t是定义在time.h中的类型。该函数返回处理器时间,其单位取决于实现(如果处理器时间不可用或无法表示,该函数将返回-1)。然而,CLOCKS_PER_SEC(也定义在time.h中)是每秒处理器时间单位的数量。因此,两个clock()返回值的差值除以CLOCKS_PER_SEC得到两次调用之间经过的秒数。在进行除法运算之前,把值的类型强制转换成double类型,可以将时间精确到小数点以后。编写一个函数,接受一个double类型的参数表示时间延迟数,然后在这段时间运行一个循环。编写一个简单的程序测试该函数。 */ #include <stdio.h> #include <time.h> #include <stdbool.h> #include <stdlib.h> void delay(double sec) { time_t start = clock(); while (true) { time_t current = clock(); if (((double)current - (double)start) / CLOCKS_PER_SEC >= sec) break; printf("hhh\n"); } } int main(void) { delay(.1); system("pause"); return 0; }
-
/* * @Description: * @version: * @Author: * @Date: 2021-04-06 12:07:56 * @LastEditors: Please set LastEditors * @LastEditTime: 2021-04-06 12:21:37 */ /* TODO:5. 编写一个函数接受这些参数:内含int类型元素的数组名、数组的大小和一个代表选取次数的值。该函数从数组中随机选择指定数量的元素,并打印它们。每个元素只能选择一次(模拟抽奖数字或挑选陪审团成员)。另外,如果你的实现有time()(第12章讨论过)或类似的函数,可在srand()中使用这个函数的输出来初始化随机数生成器rand()。编写一个简单的程序测试该函数。 */ #include <stdio.h> #include <stdlib.h> #include <time.h> #include <stdbool.h> void fun(int arr[], int size, int n) { srand((unsigned int)time(0)); // 初始化种子 bool *flag = (bool *)malloc(sizeof(bool) * size); for (int i = 0; i < size; i++) flag[i] = false; for (int i = 0; i < n; i++) { int index = rand() % size; if (!flag[index]) { printf("%d\n", arr[index]); flag[index] = true; } else i--; } } int main(void) { int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; fun(arr, 10, 10); system("pause"); return 0; }
-
/* * @Description: * @version: * @Author: * @Date: 2021-04-06 12:22:03 * @LastEditors: Please set LastEditors * @LastEditTime: 2021-04-06 12:37:00 */ /* TODO: 6. 修改程序清单16.17,使用struct names元素(在程序清单16.17后面的讨论中定义过),而不是double类型的数组。使用较少的元素,并用选定的名字显式初始化数组。 */ #include <stdio.h> #include <stdlib.h> #include <string.h> #define NUM 3 struct names { char *last; char *first; }; void fillarray(struct names ar[], int n); void showarray(const struct names ar[], int n); int mycomp(const void *p1, const void *p2); int main(void) { struct names vals[NUM]; fillarray(vals, NUM); puts("Random list:"); showarray(vals, NUM); qsort(vals, NUM, sizeof(struct names), mycomp); puts("\nSorted list:"); showarray(vals, NUM); system("pause"); return 0; } void fillarray(struct names ar[], int n) { int index; ar[0].first = "Li"; ar[0].last = "Ming"; ar[1].first = "Dunol"; ar[1].last = "Trump"; ar[2].first = "Bie"; ar[2].last = "Deng"; } void showarray(const struct names ar[], int n) { int index; for (index = 0; index < n; index++) printf("%s %s\n", ar[index].first, ar[index].last); } /* 按从小到大的顺序排序 */ int mycomp(const void *p1, const void *p2) { /* 要使用指向double的指针来访问这两个值 */ const struct names *a1 = (const struct names *)p1; const struct names *a2 = (const struct names *)p2; int res; res = strcmp(a1->first, a2->first); if (res != 0) return res; else return strcmp(a1->last, a2->last); }
-
/* * @Description: * @version: * @Author: * @Date: 2021-04-06 12:37:23 * @LastEditors: Please set LastEditors * @LastEditTime: 2021-04-06 12:44:55 */ /* TODO:7. new_d_array()函数接受一个int类型的参数和double类型的参数。该函数返回一个指针,指向由malloc()分配的内存块。int类型的参数指定了动态数组中的元素个数,double类型的值用于初始化元素(第1个值赋给第1个元素,以此类推)。编写show_array()和new_d_array()函数的代码,完成这个程序。 */ #include <stdio.h> #include <stdlib.h> #include <stdarg.h> void show_array(const double ar[], int n); double *new_d_array(int n, ...); int main() { double *p1; double *p2; p1 = new_d_array(5, 1.2, 2.3, 3.4, 4.5, 5.6); p2 = new_d_array(4, 100.0, 20.00, 8.08, -1890.0); show_array(p1, 5); show_array(p2, 4); free(p1); free(p2); system("pause"); return 0; } void show_array(const double ar[], int n) { for (int i = 0; i < n; i++) printf("%g\n", ar[i]); } double *new_d_array(int n, ...) { va_list ap; va_start(ap, n); double *res = (double *)malloc(sizeof(double) * n); for (int i = 0; i < n; i++) { res[i] = va_arg(ap, double); } va_end(ap); return res; }