C语言-第16章 C预处理器和C库

第16章 C预处理器和C库

16.1 翻译程序的第一步

在预处理之前,编译器必须对该程序进行一些翻译处理。首先,编译器把源代码中出现的字符映射到源字符集。该过程处理多字节字符和三字符序列——字符扩展让C更加国际化。 第二,编译器定位每个反斜杠后面跟着换行符的实例,并删除它们。也就是说,把下面两个物理行(physical line):

printf("That's wond\
erful!\n");
//转换成一个逻辑行(logical line):
printf("That's wonderful\n!");

注意,在这种场合中,“换行符”的意思是通过按下Enter键在源代码文件中换行所生成的字符,而不是指符号表征\n。 由于预处理表达式的长度必须是一个逻辑行,所以这一步为预处理器做好了准备工作。一个逻辑行可以是多个物理行第三,编译器把文本划分成预处理记号序列、空白序列和注释序列(记号是由空格、制表符或换行符分隔的项)。这里要注意的是, 编译器将用一个空格字符替换每一条注释。因此,下面的代码:

int/* 这看起来并不像一个空格*/fox;
//将变成:
int fox; 

而且,实现可以用一个空格替换所有的空白字符序列(不包括换行符)。最后,程序已经准备好进入预处理阶段,预处理器查找一行中以#号 开始的预处理指令。

16.2 明示常量:#define

预处理器指令#define和其他预处理器指令一样,以#号作为一行的开始。ANSI和后来的标准都允许#号前面有空格或制表符,而且还允许在#和指令的其余部分之间有空格。但是旧版本的C要求指令从一行最左边开始, 而且#和指令其余部分之间不能有空格。指令可以出现在源文件的任何地方,其定义从指令出现的地方到该文件末尾有效。我们大量使用#define指令来定义明示常量(manifest constant)(也叫做符号常量),但是该指令还有许多其他用途。

预处理器指令从#开始运行,到后面的第1个换行符为止。也就是说,指令的长度仅限于一行。然而,前面提到过,在预处理开始前,编译器会把多行物理行处理为一行逻辑行。

简单的预处理示例:

一般而言,预处理器发现程序中的宏后,会用宏等价的替换文本进行替 换。如果替换的字符串中还包含宏,则继续替换这些宏。唯一例外的是双引号中的宏。

#define _CRT_SECURE_NO_WARNINGS 1
/* preproc.c -- 简单的预处理示例 */
#include <stdio.h>
#define TWO 2 /* 可以使用注释 */
#define OW "始终如一是缺乏想象力者的最后避难所。\
-奥斯卡·王尔德" /* 反斜杠把该定义延续到下一行,编译器翻译时会将反斜杠删除并将其后的一个物理行转换成一个逻辑行 */
#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; //printf("X is %d.\n", x);
	x = FOUR;
	printf(FMT, x); //printf("X is %d.\n", x);
	printf("%s\n", OW); //printf("%s\n", "始终如一是缺乏想象力者的最后避难所。-奥斯卡·王尔德");
	printf("TWO: OW\n"); //输出:TWO: OW\n  【注意:这里的TWO不会被当作宏处理,而是以一个普通的字符串打印输出】
    printf("%d: %s\n", TWO, OW); //要打印宏TWO, OW,应该使用这种方式
	return 0;
}

每行#define(逻辑行)都由3部分组成。

  • 第1部分是#define指令本身

  • 第2部分是选定的缩写,也称为。有些宏代表值(如本例),这些宏被称为类对象宏(object-like macro)。C 语言还有类函数宏(function-like macro),稍后讨论。宏的名称中不允许有空格,而且必须遵循C变量的命名规则:只能使用字符、数字和下划线(_)字符,而且首字符不能是数字。

  • 第3部分(指令行的其余部分)称为替换列表或替换体。 一旦预处理器在程序中找到宏的示实例后,就会用替换体代替该宏(也有例外,稍后解释)。从宏变成最终替换文本的过程称为宏展开(macro expansion)。注意,可以在#define行使用标准C注释。

    由于编译器在编译期对所有的常量表达式(只包含常量的表达式)求值,所以预处理器不会进行实际的乘法运算,这一过程在编译时进行。预处理器不做计算,不对表达式求值,它只进行替换 注意,宏定义还可以包含其他宏(一些编译器不支持这种嵌套功能)。

那么,何时使用字符常量?对于绝大部分数字常量,应该使用字符常量。如果在算式中用字符常量代替数字,常量名能更清楚地表达该数字的含义。如果是表示数组大小的数字,用符号常量后更容易改变数组的大小和循环次数。如果数字是系统代码(如,EOF),用符号常量表示的代码更容易移植(只需改变EOF的定义)。助记、易更改、可移植,这些都是符号常量很有价值的特性。

C语言现在也支持const关键字,提供了更灵活的方法。用const可以创建在程序运行过程中不能改变的变量,可具有文件作用域或块作用域。另一方面,宏常量可用于指定标准数组的大小和const变量的初始值。

16.2.1 记号

从技术角度来看,可以把宏的替换体看作是记号(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语言的规则,所以不要求代码中用空格来分隔记号。例如,C编译器可以把2**2直接视为3个记号,因为它可以识别2是常量,* *是运算符。

16.2.2 重定义常量

假设先把LIMIT定义为20,稍后在该文件中又把它定义为25。这个过程称为重定义常量。不同的实现采用不同的重定义方案。除非新定义与旧定义相同,否则有些实现会将其视为错误。另外一些实现允许重定义,但会给出警告。ANSI标准采用第1种方案,只有新定义和旧定义完全相同才允许重定义。具有相同的定义意味着替换体中的记号必须相同,且顺序也相同。因此,下面两个定义相同:

如果需要重定义 宏,使用#undef 指令

#define SIX 2 * 3  //该宏有3个记号:2、*、3。
#define SIX 2 * 3 //重定义:宏和替换体都完全相同
#define SIX 2*3  //这条宏定义中只有一个记号(即2*3序列),因此与前两条定义不同。

16.3 在#define中使用参数⭐

在#define中使用参数可以创建外形和作用与函数类似的类函数宏。带有参数的宏看上去很像函数,因为这样的宏也使用圆括号。类函数宏定义的圆括号中可以有一个或多个参数,随后这些参数出现在替换体中,如图所示。

//类函数宏定义示例:
#define SQUARE(X) X*X
//在程序中可以这样用:
z = SQUARE(2);

预处理器不做计算、不求值,只替换字符序列。【在向带参数的宏传入计算表达式时,预处理器会将表达式直接替换替换体中的参数,并不会计算后再替换】

#define _CRT_SECURE_NO_WARNINGS 1
/* 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); //z = X*X
	printf("Evaluating SQUARE(x): ");
	PR(z);
	z = SQUARE(2);
	printf("Evaluating SQUARE(2): ");
	PR(z);
	printf("Evaluating SQUARE(x+2): ");
	/* 因为预处理器不做计算制作替换处理,计算是交给编译器执行;
	因此向宏SQURE传入x+2,预处理器把出现x的地方都替换成x+2,所以最终结果为:x+2*x+2=17 (x=5) */
	PR(SQUARE(x + 2)); //输出:Evaluating SQUARE(x+2): The result is 17.
	printf("Evaluating 100/SQUARE(2): ");
	/* 同理此处向宏PR传入的也是:100/2*2,所以最终结果为:printf("The result is %d.\n", 100/2*2) */
	PR(100 / SQUARE(2));
	printf("x is %d.\n", x);
	printf("Evaluating SQUARE(++x): ");
	/* 此处向宏PR传入的是:++x*++x,(x=5)所以最终结果为:printf("The result is %d.\n", ++x*++x ),
	因为这里x前缀自增了两次,所以不确定++x*++x = 6*7 = 42或者是7*7=49(标准并未对这类运算规定顺序,具体结果要看编译器) */
	PR(SQUARE(++x));
	printf("After incrementing, x is %x.\n", x); //x=7
	return 0;
}

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

下面是一个类函数宏:

#define PSQR(X) printf("The square of X is %d.\n", ((X)*(X))); /* 注意双引号字符串中的X被视为普通文本,而不是一个可被替换的记号。 */
//假设这样使用宏:
PSQR(8);
//输出为:
The square of X is 64.

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

#define _CRT_SECURE_NO_WARNINGS 1
/* 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); //用"y"替换#x。打印结果:The square of y is 25.
	PSQR(2 + 4); //,用"2 + 4"替换#x。打印结果:The square of 2 + 4 is 36.
	return 0;
}

16.3.2 预处理器黏合剂:##运算符

与#运算符类似,##运算符可用于类函数宏的替换部分。而且,##还可用于对象宏的替换部分##运算符把两个记号组合成一个记号。例如,可以 这样做:

#define XNAME(n) x ## n //使用该宏的效果是:xn 其中n将被替换成使用该宏时传入的参数

##作为记号粘合剂的用法:

#define _CRT_SECURE_NO_WARNINGS 1
// 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) = 10; // 变成 int x1 = 14; *****
	int XNAME(2) = 20; // 变成 int x2 = 20; *****
	int x3 = 30;
	PRINT_XN(1); // 变成 printf("x1 = %d\n", x1); x1 = 10
	PRINT_XN(2); // 变成 printf("x2 = %d\n", x2); x2 = 20 
	PRINT_XN(3); // 变成 printf("x3 = %d\n", x3); x3 = 30
	return 0;
}

16.3.3 变参宏:…和_ _VA_ARGS _ _

一些函数接受数量可变的参数。stdvar.h 头文件提供了工具,让用户自定义带可变参数的函数。C99/C11也对宏提供了这样的工具。虽然标准中未使用“可变”(variadic)这个词,但是它已成为描述这种工具的通用词。 **通过把宏参数列表中最后的参数写成省略号(即,3个点…)**来实现这一功能。这样,预定义宏image-20221215154615256可用在替换部分中,表明省略号代表什么。例如,下面的定义:

#define PR(...) printf(_ _VA_ARGS_ _)

变参宏使用示例:

#define _CRT_SECURE_NO_WARNINGS 1
// variadic.c -- 变参宏
#include <stdio.h>
#include <math.h> /* 包含sqrt()函数原型 */
#define PR(X, ...) printf("Message " #X ": " __VA_ARGS__)
int main(void)
{
    
    
	double x = 48;
	double y;
	y = sqrt(x); //对x开平方
	PR(1, "x = %g\n", x); //printf("Message 1: x = %g\n", x);
	PR(2, "x = %.2f, y = %.4f\n", x, y); //prinf("Message 2: x = %.2f, y = %.4f\n", x, y);
	return 0;
}

16.4 宏和函数的选择

使用宏比使用普通函数复杂一些,稍有不慎会产生奇怪的副作用。一些编译器规定宏只能定义成一行。 宏和函数的选择实际上是时间和空间的权衡(使用函数节省空间,使用宏节省时间)。宏生成内联代码,即在程序中生成语句。如果调用20次宏,即在程序中插入20行代码。如果调用函数 20次,程序中只有一份函数语句的副本,所以节省了空间。然而另一方面, 程序的控制必须跳转至函数内,随后再返回主调程序,这显然比内联代码花费更多的时间。 宏的一个优点是,不用担心变量类型(这是因为宏处理的是字符串,而不是实际的值)。因此,只要能用int或float类型都可以使用SQUARE(x)宏。 C99提供了第3种可替换的方法——内联函数。

简单的函数通常使用宏,如下所示:

#define MAX(X,Y) ((X) > (Y) ? (X) : (Y)) /* 输出大的 */
#define ABS(X) ((X) < 0 ? -(X) : (X)) /* 输出参数的绝对值 */
#define ISSIGN(X) ((X) == '+' || (X) == '-' ? 1 : 0) /* 0正1负 */

16.5 文件包含:#include

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

#include <stdio.h>  //←文件名在尖括号中,查找系统目录
#include "mystuff.h"  //←文件名在双引号中,查找当前工作目录
#include "/usr/biff/p.h" ←查找/usr/biff目录中的p.h文件

在 UNIX 系统中,尖括号告诉预处理器在标准系统目录中查找该文件双引号告诉预处理器首先在当前目录中(或文件名中指定的其他目录)查找该文件,如果未找到再查找标准系统目录

为什么要包含文件?因为编译器需要这些文件中的信息。例如,stdio.h 文件中通常包含EOF、NULL、getchar()和 putchar()的定义。getchar()和 putchar()被定义为宏函数。此外,该文件中还包含C的其他I/O函数。

C语言习惯用.h后缀表示头文件,这些文件包含需要放在程序顶部的信息。头文件经常包含一些预处理器指令。有些头文件(如stdio.h)由系统提供,当然你也可以创建自己的头文件。 包含一个大型头文件不一定显著增加程序的大小。在大部分情况下,头文件的内容是编译器生成最终代码时所需的信息,而不是添加到最终代码中的材料。

16.5.1 头文件示例

假设你开发了一个存放人名的结构,还编写了一些使用该结构的函数。 可以把不同的声明放在头文件中。

声明和指令放在nems_st.h头文件中,函数实现放在names_st.c源代码文件中。

头文件:

#pragma once
// 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);

头文件函数实现:

#define _CRT_SECURE_NO_WARNINGS 1
// names_st.c -- 定义 names_st.h中的函数
#include <stdio.h>
#include "names_st.h" // 包含头文件
// 函数定义
//设置姓名
void get_names(names* pn) //该函数参数为一个结构指针
{
    
    
	printf("请输入您的名字:");
	s_gets(pn->first, SLEN);
	printf("请输入您的姓氏:");
	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;
}

测试使用names_st.h头文件:

#define _CRT_SECURE_NO_WARNINGS 1
// useheader.c -- 使用 names_st 结构
#include <stdio.h>
#include "names_st.h"
// 记住要链接 names_st.c
int main(void)
{
    
    
	//使用names_st.h头文件中typedef定义的names结构类型声明结构变量candidate
	names candidate;
	get_names(&candidate);
	printf("Let's welcome ");
	show_names(&candidate);
	printf(" to this program!\n");
	return 0;
}

16.5.2 使用头文件

头文件中最常用的形式如下。

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

  • 宏函数——例如,getc(stdin)通常用getchar()定义,而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类型也定义在头文件中。

许多程序员都在程序中使用自己开发的标准头文件。如果开发一系列相关的函数或结构,那么这种方法特别有价值。 另外,还可以使用头文件声明外部变量供其他文件共享。例如,如果已经开发了共享某个变量的一系列函数,该变量报告某种状况(如,错误情况),这种方法就很有效。这种情况下,可以在包含这些函数声明的源代码文件定义一个文件作用域的外部链接变量:

  • 如果是要定义全局变量,先在一个.c文件中定义,然后在头文件中用extern关键字引用式声明;
/*【源代码文件,具有文件作用域】 在一个包含了该头文件的.C文件中定义式声明: */
int status = 0; //同时可以在该.c文件中创造一系列操作该变量的函数,后续在包含了该头文件的其他源代码文件中可以直接使用变量status
/*【头文件】 头文件中引用式声明: */
extern int status; // extern关键字指明使用的外部变量
  • 如果是要声明一个不想被其他文件使用、只能被本文件使用的变量,可以用static关键字在头文件中进行定义,如果不想该变量的值改变,可以使用const关键字定义.
/*【头文件】static内部链接,此变量作用域当前文件,如果有其他源代码文件包含了此头文件,那么此头文件和该源代码文件就是同一个文件域 */
const static int arr = 11111; //如果该变量的值可以被改变,就可以省略const关键字
/* 【源代码文件包含了此头文件】 */
extern int arr; //此变量只能引用式声明并且在声明时不能进行初始化,声明之后如果没有const关键字修饰那么就可以改变值
printf("arr = %d \n", arr);

16.6 其他指令

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

16.6.1 #undef指令

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

 #define LIMIT 400
#undef LIMIT  // 将移除上面的定义

现在就可以把LIMIT重新定义为一个新值。即使原来没有定义LIMIT,取消LIMIT的定义仍然有效。如果想使用一个名称,又不确定之前是否已经用过,为安全起见,可以用#undef 指令取消该名字的定义。

#define LIMIT 100
#undef LIMIT //清除上面的定义或者在其他头文件中的定义,【注意undef指令不能为标识符设置值,他只能清除标识符之前的定义】
#define LIMIT 122
int main(void)
{
    
    
	printf("LIMIT =%d \n", LIMIT); //LIMIT = 122
	return 0;
}

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

处理器在识别标识符时,遵循与C相同的规则:标识符可以由大写字母、小写字母、数字和下划线字符组成,且首字符不能是数字。当预处理器在预处理器指令中发现一个标识符时,它会把该标识符当作已定义的或未定义的。这里的已定义表示由预处理器定义。如果标识符是同一个文件中由前面的#define指令创建的宏名,而且没有用#undef 指令关闭,那么该标识符是已定义的如果标识符不是宏,假设是一个文件作用域的C变量,那么该标识符对预处理器而言就是未定义的。 已定义宏可以是对象宏,包括空宏或类函数宏:

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

注意,#define宏的作用域从它在文件中的声明处开始,直到用#undef指令取消宏为止,或延伸至文件尾。另外还要注意,如果宏通过头文件引入,那么#define在文件中的位置 取决于#include指令的位置。有些预定义宏image-20221215182115347是已定义的,而且不能取消定义。

16.6.3 条件编译

可以使用其他指令创建条件编译(conditinal compilation)。也就是说,可以使用这些指令告诉编译器根据编译时的条件执行或忽略信息(或代码) 块

1.#ifdef、#else和#endif指令

#ifdef指令说明,如果预处理器已定义了后面的标识符(MAVIS),则执行#else或#endif指令之前的所有指令并编译所有C代码(先出现哪个指令 就执行到哪里)。

如果预处理器未定义MAVIS,且有 #else指令,则执行 #else和#endif指令之间的所有代码。

#ifdef MAVIS //if条件编译开始和判断条件指令
#include "horse.h"// 如果已经用#define定义了 MAVIS,则执行下面的指令
#define STABLES 5
#else 
#include "cow.h" //如果没有用#define定义 MAVIS,则执行下面的指令
#define STABLES 15
#endif  //if条件编译结束指令

也可以用这些指令标记C语句块:

#define _CRT_SECURE_NO_WARNINGS 1
/* 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  /* 如果预处理器已定义了JUST_CHECKING标识符,则执行#ifdef条件内容体,否则不执行 */
		printf("i=%d, running total = %d\n", i, total); //此源代码文件已经定义了JUST_CHECKING标识符所以会执行此条语句
#endif
	}
	printf("Grand total = %d\n", total);
	return 0;
}

2.#ifndef指令

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

#ifndef SIZE /* 如果预处理器没有定义SIZE标识符,则执行下面的指令 */
#define SIZE 100
#endif
//#ifndef指令通常用于防止多次包含一个文件。
/* things.h */
#ifndef THINGS_H_
#define THINGS_H_
/* 省略了头文件中的其他内容*/
#endif

3.#if和#elif指令

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

#if SYS == 1
#include "ibm.h"
#endif
//可以按照if else的形式使用#elif
#if SYS == 1   //在使用SYS前要在预处理器中定义了此标识符
#include "ibmpc.h"
#elif SYS == 2
#include "vax.h"
#elif SYS == 3
#include "mac.h"
#else
#include "general.h"
#endif
//这里,defined是一个预处理运算符,如果它的参数是用#defined定义过,则返回1;否则返回0。
#if defined (IBMPC)
#include "ibmpc.h"
#elif defined (VAX)
#include "vax.h"
#elif defined (MAC)
#include "mac.h"
#else
#include “genral.h"
#endif

16.6.4 预定义宏

C标准规定了一些预定义宏,如表所列。

image-20221215185354632
#define _CRT_SECURE_NO_WARNINGS 1
// 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 E:\vs2019_code\Chapter16_22-12-15\predef.c.
The date is Dec 15 2022.
The time is 18:55:55.
This is line 11.
This function is main
This function is why_me
This is line 19.
*/

16.6.5 #line和#error

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

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

#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

16.6.6 #pragma

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

#pragma c9x on

一般而言,编译器都有自己的编译指示集。例如,编译指示可能用于控制分配给自动变量的内存量,或者设置错误检查的严格程度,或者启用非标准语言特性等。

C99还提供**_Pragma预处理器运算符,该运算符把字符串转换成普通的编译指示。**

_Pragma("nonstandardtreatmenttypeB on")
//等价于下面的指令:
#pragma nonstandardtreatmenttypeB on

16.6.7 泛型选择(C11)

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

/* _Generic是C11的关键字。_Generic后面的圆括号中包含多个用逗号分隔的项。
	第一个项是一个表达式,第一个项的类型匹配那个标签,那么整个表达式的值就是该标签后面的值,否则就是default标签后面的值
*/
_Generic(x, int: 0, float: 1, double: 2, default: 3);
/* 泛型选择语句和宏定义组合 */
#define MYTYPE(X) _Generic((X),
int: "int",
float : "float",
double: "double",
default: "other"
)

对一个泛型选择表达式求值时,程序不会先对第一个项求值,它只确定类型。只有匹配标签的类型后才会对表达式求值。

#define _CRT_SECURE_NO_WARNINGS 1
// 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;
}

16.7 内联函数(C99)

16.7.1 什么是内联函数?

通常,函数调用都有一定的开销,因为函数的调用过程包括建立调用、 传递参数、跳转到函数代码并返回。使用宏使代码内联,可以避免这样的开销。C99还提供另一种方法:内联函数(inline function)。

内联函数是在 C99 中增加的一个功能,可以提高程序执行效率。如果函数是内联的,编译器在编译时,会把内联函数的实现体替换到每个调用内联函数的地方,可以与宏函数作类比,但宏函数不会进行类型检查

内联函数一般要求如下:

  1. 函数简短,通常3-5行,把较长的函数变成内联并未节约多少时间,因为执行函数体的时间比调用函数的时间长得多。

  2. 函数内没有复杂的实现,比如:包含while、for 循环,递归等;

  3. 通常在多处有调用;

  4. 编译器优化内联函数必须知道该函数定义的内容。这意味着内联函数定义与函数调用必须在同一个文件中。鉴于此,一般情况下内联函数都具有内部链接。

注意:函数声明为内联,仅仅是对编译器的建议,如果函数比较复杂,编译器会将其看做普通函数。

16.7.2 内联函数的声明

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

  • 由于并未给内联函数预留单独的代码块,所以无法获得内联函数的地址(实际上可以获得地址,不过这样做之后,编译器会生成一个非内联函数)。另外,内联函数无法在调试器中显示。

  • 如果内联函数定义省略了static关键字,那么inline定义被视为可替换的外部定义即既可以使用当前文件的内联定义函数也可以使用其他文件中的外部链接定义函数

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

如果编译器将函数eatline编译成内联函数,那么在代码执行时的效果为:

#include <stdio.h>
inline static void eatline() //内联函数定义/原型
{
    
    
while (getchar() != '\n')
continue;
}
int main()
{
    
    
...
/* 在此处调用内联函数 */
while (getchar() != '\n') //在函数调用的位置将函数调用语句替换成函数体中的代码
continue;
...
}

如果程序有多个文件都要使用某个内联函数,那么这些文件中都必须包含该内联函数的定义。最简单的做法是,把内联函数定义放入头文件,并在使用该内联函数的文件中包含该头文件即可。

// eatline.h
#ifndef EATLINE_H_
#define EATLINE_H_  /* 如果标识符EATLINE_H_是未定义的,那么执行下面语句 */
/* 当函数声明为内联并且函数简短、声明、定义、调用都在同一个文件中时,编译器才会将函数编译为内联函数,会把内联函数的实现体替换到每个调用内联函数的地方 */
inline static void eatline()
{
    
    
while (getchar() != '\n')
continue;
}
#endif

一般都不在头文件中放置可执行代码,内联函数是个特例。因为内联函数具有内部链接,所以在多个文件中定义同一个内联函数不会产生什么问题。 与C++不同的是,C还允许混合使用内联函数定义和外部函数定义(具有外部链接的函数定义)。例如,一个程序中使用下面3个文件:

//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); //编译器既可以使用该文件中square()函数的内联定义,也可以使用file2.c文件中的外部链接定义。
...
}    

16.8 _Noreturn函数(C11)

C99新增inline关键字时,它是唯一的函数说明符(关键字extern和static是存储类别说明符,可应用于数据对象和函数)。C11新增了第2个【函数说明符_Noreturn】,表明调用完成后函数不返回主调函数。exit()函数是 _Noreturn 函数的一个示例,一旦调用exit(),它不会再返回主调函数。注意,这与void返回类型不同。void类型的函数在执行完毕后返回主调函数, 只是它不提供返回值。 _Noreturn的目的是告诉用户和编译器,这个特殊的函数不会把控制返回主调程序。告诉用户以免滥用该函数,通知编译器可优化一些代码。

16.9 C库

最初,并没有官方的C库。后来,基于UNIX的C实现成为了标准。ANSI C委员会主要以这个标准为基础,开发了一个官方的标准库。在意识到C语 言的应用范围不断扩大后,该委员会重新定义了这个库,使之可以应用于其他系统。 我们讨论过一些标准库中的 I/O 函数、字符函数和字符串函数。本章将介绍更多函数。不过,首先要学习如何使用库。

16.9.1 访问C库

如何访问C库取决于实现,因此你要了解当前系统的一般情况。首先,可以在多个不同的位置找到库函数。例如,getchar()函数通常作为宏定义在 stdio.h头文件中,而strlen()通常在库文件中。其次,不同的系统搜索这些函数的方法不同。下面介绍3种可能的方法。

1.自动访问

在一些系统中,只需编译程序,就可使用一些常用的库函数。 记住,在使用函数之前必须先声明函数的类型,通过包含合适的头文件即可完成。在描述库函数的用户手册中,会指出使用某函数时应包含哪个头文件。过去,不同的实现使用的头文件名不同。ANSI C标准把库函数分为多个系列,每个系列的函数原型都放在一个特定的头文件中。

2.文件包含

如果函数被定义为宏,那么可以通过#include 指令包含定义宏函数的文件。通常,类似的宏都放在合适名称的头文件中。例如,许多系统都有ctype.h文件,该文件中包含了一些确定字符性质(如 大写、数字等)的宏。

3.库包含

在编译或链接程序的某些阶段,可能需要指定库选项。即使在自动检查标准库的系统中,也会有不常用的函数库。必须通过编译时选项显式指定这些库。注意,这个过程与包含头文件不同。头文件提供函数声明或原型,而库选项告诉系统到哪里查找函数代码。

16.10 数学库

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

image-20221216111331181

16.10.1 三角问题

我们可以使用数学库解决一些常见的问题:把x/y坐标转换为长度和角度。把直角坐标转换为极坐标函数示例:

#define _CRT_SECURE_NO_WARNINGS 1
/* 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("输入直角坐标值(x,y); enter q to quit:");
	while (scanf("%lf %lf", &input.x, &input.y) == 2)
	{
    
    
		result = rect_to_polar(input);
		printf("长度 = %0.2f, 角度 = %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;  //如果极坐标长度为0,则角度也为0
	else
		pv.angle = RAD_TO_DEG * atan2(rv.y, rv.x); //计算机坐标角度
	return pv;
}

16.10.2 类型变体

基本的浮点型数学函数接受double类型的参数,并返回double类型的值。当然,也可以把float或 long double 类型的参数传递给这些函数,它们仍然能正常工作,因为这些类型的参数会被转换成double类型。这样做很方便,但并不是最好的处理方式。如果不需要双精度,那么用float类型的单精度值来计算会更快些。而且把long double类型的值传递给double类型的形参会损失精度,形参获得的值可能不是原来的值。为了解决这些潜在的问题, C标准专门为float类型和long double类型提供了标准函数,即在原函数名前加上f或l前缀。因此,sqrtf()是sqrt()的float版本,sqrtl()是sqrt()的long double 版本。

根据函数实参类型选择最合适的数学函数版本:

#define _CRT_SECURE_NO_WARNINGS 1
// generic.c -- 定义泛型宏
#include <stdio.h>
#include <math.h>
#define RAD_TO_DEG (180/(4 * atanl(1)))
// 泛型平方根函数  例如X为float x = 45.0f; 那么函数宏的结果为:sqrtf(x)即sqrtf(45.0f)
#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;
}

16.10.3 tgmath.h库(C99)

C99标准提供的tgmath.h头文件中定义了泛型类型宏,其效果与上面的程序类似。如果在math.h中为一个函数定义了3种类型(float、double和long double)的版本,那么tgmath.h文件就创建一个泛型类型宏,与原来 double 版本的函数名同名。例如,泛类型函数宏根据提供的参数类型,定义 sqrt()宏展开为 sqrtf()、sqrt()或 sqrtl()函数。换言之,sqrt()宏的行为和上面的程序中的 SQRT()宏类似。

如果编译器支持复数运算,就会支持complex.h头文件,其中声明了与复数运算相关的函数。例如,声明有 csqrtf()、csqrt()和 csqrtl(),这些函数分别返回 float complex、double complex和long double complex类型的复数平方根。如果提供这些支持,那么tgmath.h中的sqrt()宏也能展开为相应的复数平方根函数。

如果包含了tgmath.h,要调用sqrt()函数而不是sqrt()宏,可以用圆括号把被调用的函数名括起来:

#include <tgmath.h>
...
float x = 44.0;
double y;
y = sqrt(x); // 调用宏,因为x为float类型所以宏展开为:sqrtf(x);
y = (sqrt)(x); // 调用函数 sqrt()
y = (*sqrt)(x); //也可以这样使用函数指针调用函数sqrt()

16.11 通用工具库

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

16.11.1 exit()和atexit()函数

atexit()函数 <stdlib.h>

  • 描述

C 库函数 int atexit(void (*func)(void)) 当程序正常终止时,调用指定的函数 func。您可以在任何地方注册你的终止函数,但它会在程序终止的时候被调用。

  • 声明
/* 注册终止函数 */
int atexit(void (*func)(void)) //atexit函数使用函数指针,函数名作为函数参数时相当于该函数的地址
  • 参数

func – 在程序终止时被调用的函数。

  • 返回值

如果函数成功注册,则该函数返回零,否则返回一个非零值。

  • 其他
  1. atexit注册的终止函数的执行顺序与注册顺序相反,类似于栈结构。(最后注册的终止函数,在程序终止时最先执行);
  2. ANSI保证,atexit至少可以注册 32 个终止函数;
  3. atexit()注册的函数应该不带任何参数且返回类型为void。
  • 函数作用

通常,atexit()函数会执行一些清理任务,例如更新监视程序的文件或重置环境变量。 注意,即使没有显式调用exit(),还是会调用sign_off(),因为main()结束 时会隐式调用exit()。

exit()函数 <stdlib.h>

  • 描述

C 库函数 void exit(int status) 立即终止调用进程。任何属于该进程的打开的文件描述符都会被关闭,该进程的子进程由进程 1 继承,初始化,且会向父进程发送一个 SIGCHLD 信号。main()返回系统时将自动隐式调用exit()函数。

  • 声明
void exit(int status)
  • 参数

status – 返回给父进程的状态值。C为了可移植性的要求,定义了 一个名为EXIT_FAILURE(值为1)的宏表示终止失败、EXIT_SUCCESS(值为0)表示终止成功,exit()函数也接受0表示成功终止

  • 返回值

该函数无返回值。

  • 其他

在 ANSI C中,在非递归的main()中使用exit()函数等价于使用关键字return。尽管如此,在main()以外的函数中使用exit()也会终止整个程序

  • 函数作用

exit()执行完atexit()指定的函数后,会完成一些清理工作:刷新所有输出流、关闭所有打开的流和关闭由标准I/O函数tmpfile()创建的临时文件。然后 exit()把控制权返回主机环境,如果可能的话,向主机环境报告终止状态。

exit()和atexit()函数使用示例:

#define _CRT_SECURE_NO_WARNINGS 1
/* byebye.c -- atexit()示例 */
#include <stdio.h>
#include <stdlib.h>
void sign_off(void);
void too_bad(void);
int main(void) /* main()返回系统时将自动调用exit()函数,所以sign_off函数不管main执行成功与否都会被调用执行 */
{
    
    
	int n;
	atexit(sign_off); /* 注册 sign_off()函数,当调用exit()时就会执行too_bad函数 */
	puts("输入整数:");
	if (scanf("%d", &n) != 1)
	{
    
    
		puts("这不是整数!");
		atexit(too_bad); /* 注册 too_bad()函数,当调用exit()时就会执行too_bad函数 */
		exit(EXIT_FAILURE);
	}
	printf("%d is %s.\n", n, (n % 2 == 0) ? "even" : "odd");
	return 0;
}
void sign_off(void)
{
    
    
	puts("因此,SeeSaw软件终止了另一个宏伟的计划!");
}
void too_bad(void)
{
    
    
	puts("SeeSaw Software对您的程序失败表示衷心的哀悼。");
}

16.11.2 qsort()函数

对较大型的数组而言,“快速排序”方法是最有效的排序算法之一。它把数组不断分成更小的数组,直到变成单元素数组。首先,把数组分成两部分,一部分的值都小于另一部分的值。 这个过程一直持续到数组完全排序好为止。 快速排序算法在C实现中的名称是qsort()。

qsort()函数 <stdlib.h>

  • 描述

C 库函数 void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void , const void)) 对数组进行排序。

  • 声明
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))
  • 参数

base – 指向要排序的数组的第一个元素的指针。可以是任意类型的数组首元素指针(ANSI C允许把指向任何 数据类型的指针强制转换成指向void的指针)。
nitems – 由 base 指向的数组中元素的个数。
size – 数组中每个元素的大小,以字节为单位。
compar --比较函数即指向函数的指针:用来比较两个元素的函数,该函数应接受两个参数(qsort()函数会把这两个数组元素的地址传递给比较函数):分别指向待比较数组的两个元素地址。如果第1项的值大于第2项,比较函数则返回正数;如果两项相同,则返回0;如果第1项的值小于第2项,则返回负数。

  • 返回值

该函数不返回任何值。

qsort()函数使用示例:

#define _CRT_SECURE_NO_WARNINGS 1
/* qsorter.c -- 用 qsort()排序一组数字 */
#include <stdio.h>
#include <stdlib.h> /* 内含sqort函数原型 */
#define NUM 42
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("排序前的数组:");
	showarray(vals, NUM);
    //对数组进行排序
	qsort(vals, NUM, sizeof(double), mycomp); //mycomp比较函数不需要传入参数,因为qsort函数会自动传入给比较函数
	puts("\n排序后的数组:");
	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; /* 赋值时可以不进行强制类型转换,不能直接使用p1、p2因为他们是指向void的编译器会认为他们指向的值不存在 */
	const double* a2 = p2;
	//const double* a2 = (const double*)p2;
	if (*a1 < *a2)
		return -1;
	else if (*a1 == *a2)
		return 0;
	else
		return 1;
}

16.12 断言库

assert.h文件支持的断言库是一个用于辅助调试程序的小型库。**它由 assert()宏组成,接受一个整型表达式作为参数。如果表达式求值为假(非 零),assert()宏就在标准错误流(stderr)中写入一条错误信息,并调用 abort()函数终止程序(abort()函数的原型在stdlib.h头文件中)。**assert()宏是为了标识出程序中某些条件为真的关键位置,如果其中的一个具体条件为假,就用assert()语句终止程序。通常,assert()的参数是一个条件表达式或逻辑表达式。如果 assert()中止了程序,它首先会显示失败的测试、包含测 试的文件名和行号。

16.12.1 assert的用法

assert()宏基本使用:

#define _CRT_SECURE_NO_WARNINGS 1
/* assert.c -- 使用 assert() */
#include <stdio.h>
#include <math.h>
#include <assert.h> /* 提供assert()宏 */
int main()
{
    
    
	double x, y, z;
	puts("输入一对数字(00退出): ");
	while (scanf("%lf%lf", &x, &y) == 2 && (x != 0 || y != 0))
	{
    
    
		z = x * x - y * y;
		/* 在程序运行时进行检查,如果assert()宏中的参数表达式为假则宏在标准错误流(stderr)中写入一条错误信息,并调用abort()函数终止程序 */
		assert(z >= 0); //错误信息:Assertion failed: z >= 0, file E:\vs2019_code\Chapter16_22-12-15\assert.c, line 13
		printf("答案是 %f\n", sqrt(z));
		puts("下一对数字: ");
	}
	puts("Done");
	return 0;
}

16.12.2 _ Static_assert(C11)

assert()表达式是在运行时进行检查。C11新增了一个特性: _ Static_assert声明,可以在编译时检查assert()表达式。因此,assert()可以导致正在运行的程序中止,而_Static_assert()可以导致程序无法通过编译。

_Static_assert()接受两个参数。第1个参数是整型常量表达式,第2个参数是一个字符串。如果第 1 个表达式求值为 0(或image-20221216194007221),编译器会显示字符串,而且不编译该程序。

#define _CRT_SECURE_NO_WARNINGS 1
// 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

16.13 string.h库中的memcpy()和memmove()

不能把一个数组赋给另一个数组,所以要通过循环把数组中的每个元素赋给另一个数组相应的元素(有一个例外的情况是:使用strcpy()和strncpy() 函数来处理字符数组)。memcpy()和memmove()函数提供类似的方法处理任意类型的数组。下面是这两个函数的原型:

memcpy()函数 <string.h>

  • 描述

C 库函数 void *memcpy(void *str1, const void *str2, size_t n) 从存储区 str2 复制 n 个字节到存储区 str1。memcpy()的参数带关键字restrict,即memcpy()假 设两个内存区域之间没有重叠;

如果使用 memcpy()时,两区域出现重叠会怎样?其行为是未定义的,这意味着该函数可能正常工作,也可能失败。编译器不会在本不该使用 memcpy()时禁止你使用,作为程序员,在使用该函数时有责任确保两个区域 不重叠。

  • 声明
void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
  • 参数

str1 – 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
str2 – 指向要复制的数据源,类型强制转换为 void* 指针。
n – 要被复制的字节数。

  • 返回值

该函数返回一个指向目标存储区 str1 的指针。

memmove()函数 <string.h>

  • 描述

C 库函数 void *memmove(void *str1, const void *str2, size_t n) 从 str2 复制 n 个字符到 str1,但是在重叠内存块这方面,memmove() 是比 memcpy() 更安全的方法。如果目标区域和源区域有重叠的话,memmove() 能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中,复制后源区域的内容会被更改。如果目标区域与源区域没有重叠,则和 memcpy() 函数功能相同。

  • 声明
void *memmove(void *s1, const void *s2, size_t n);
  • 参数

str1 – 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
str2 – 指向要复制的数据源,类型强制转换为 void* 指针。
n – 要被复制的字节数。

  • 返回值

该函数返回一个指向目标存储区 str1 的指针。

#define _CRT_SECURE_NO_WARNINGS 1
// 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)); //将数组values拷贝给数组target
	puts("target (copy of values):");
	show_array(target, SIZE);

	puts("\n使用具有重叠范围的memmove():");
	memmove(values + 2, values, 5 * sizeof(int));
	puts("values -- elements 0-4 copied to 2-6:");
	show_array(values, SIZE);

	puts("\n使用memcpy()将double复制到int:");
    /* 从 double 类型数组中把数据拷贝到 int 类型数组中,这演示了memcpy()函数不知道也不关心数据的类型,它只负责从一个位置把一些字节拷贝到另一个位置并且拷贝过程中不会进行数据转换, */
	memcpy(target, curious, (SIZE / 2) * sizeof(double)); 
	puts("target--5倍于10个int位置:");
	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');
}

16.14 可变参数: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。传递给形参parmN的实际参数意义是省略号部分代表的参数数量。例如,可以这样使用前面声明的f1()函数:

f1(2, 200, 400); // parmN=2表示2个额外的参数
f1(4, 13, 117, 18, 23); // parmN=4表示4个额外的参数

接下来,声明在stdarg.h中的va_list类型代表一种用于储存形参对应的形参列表中省略号部分的数据对象。变参函数的定义起始部分类似下面这样:

double sum(int lim,...)
{
    
    
va_list ap; //声明一个储存参数的对象

在该例中,lim是parmN形参,它表明变参列表中参数的数量。 然后,该函数将使用定义在stdarg.h中的va_start()宏,把参数列表拷贝到 va_list类型的变量中。该宏有两个参数:va_list类型的变量和parmN形参。 接着上面的例子讨论,va_list类型的变量是ap,parmN形参是lim。所以,应这样调用它:

va_start(ap, lim); // 把ap初始化为参数列表

下一步是访问参数列表的内容,这涉及使用另一个宏va_arg()。该宏接受两个参数:一个va_list类型的变量和一个类型名。第1次调用va_arg()时,它返回参数列表的第1项;第2次调用时返回第2项,以此类推。表示类型的参数指定了va_arg()返回值的类型。例如,如果参数列表中的第1个参数是double类 型,第2个参数是int类型,可以这样做:

注意,传入的参数类型必须与宏参数的类型相匹配。如果第1个参数是10.0,上面tic那行代码可以正常工作。但是如果参数是10,这行代码可能会出错。这里不会像赋值那样把double类型自动转换成int类型。

double tic;
int toc;
...
tic = va_arg(ap, double); // 检索第1个参数,double表明第一个参数的类型并且指定了va_arg返回值的类型
toc = va_arg(ap, int); //检索第2个参数

最后,要使用va_end()宏完成清理工作。例如,释放动态分配用于储存参数的内存。该宏接受一个va_list类型的变量:

va_end(ap); // 清理工作

调用va_end(ap)后,只有用va_start重新初始化ap后,才能使用变量ap。 因为va_arg()不提供退回之前参数的方法,所以有必要保存va_list类型变量的副本。C99新增了一个宏用于处理这种情况:va_copy()。该宏接受两 个va_list类型的变量作为参数,它把第2个参数拷贝给第1个参数:

va_list ap;
va_list apcopy;
double
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个参数

此时,即使删除了ap,也可以从apcopy中检索两个参数。

可变参数函数示例:

可变参数sum()对传入的参数求和并返回结果值:

#define _CRT_SECURE_NO_WARNINGS 1
//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); // 访问参数列表中的每一项,double表明参数列表中每个参数的类型并且指定了va_arg返回值的类型
	va_end(ap); // 清理工作
	return tot;
}

猜你喜欢

转载自blog.csdn.net/m0_52729352/article/details/128347766
今日推荐