目录

一、翻译程序的第一步

二、明示常量:#define

2.1 记号

2.2 重定义常量

三、在#define中使用参数

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

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

3.3 变参宏:...和_ _VA_ARGS

四、宏和函数的选择

五、文件包含:#include

5.1 头文件示例

5.2 使用头文件

六、其他指令

6.1 #undef指令

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

6.3 条件编译

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

2.#ifndef指令

3.#if和#elif指令

6.4 预定义宏

6.5 #line和#error

6.6 #pragma

6.7 泛型选择(C11)

七、内联函数(C99)

八、_Noreturn函数(C11)

九、C库

9.1 访问C库

1.自动访问

2.文件包含

3.库包含

9.2 使用库描述

十、数学库

10.1 三角问题

10.2 类型变体

10.3 tgmath.h库(C99)

十一、通用工具库

11.1 exit()和atexit()函数

1.atexit()函数的用法

2.exit()函数的用法

11.2 qsort()函数

1.qsort()的用法

2.mycomp()的定义

十二、断言库

12.1 assert的用法

12.2 Static_assert(C11)

十三、string.h库中的memcpy()和memmove()

十四、可变参数:stdarg.h


C语言建立在适当的关键字、表达式、语句以及使用它们的规则上。然而,C标准不仅描述C语言,还描述如何执行C预处理器、C标准库有哪些函数,以及详述这些函数的工作原理。

C预处理器在程序执行之前查看程序(故称之为预处理器)。根据程序中的预处理器指令,预处理器把符号缩写替换成其表示的内容。预处理器可以包含程序所需的其他文件,可以选择让编译器查看哪些代码。

一、翻译程序的第一步

在预处理之前,编译器必须对该程序进行一些翻译处理

首先,编译器把源代码中出现的字符映射到源字符集。

第二,编译器定位每个反斜杠后面跟着换行符的实例,并删除它们。把下面两个物理行(physical line)

printf("That's wond\
erful!\n");

转换成一个逻辑行(logical line):

printf("That's wonderful\n!");

一个逻辑行可以是多个物理行。

第三,编译器把文本划分成预处理记号序列空白序列注释序列(记号是由空格、制表符或换行符分隔的项)。这里要注意的是,编译器将用一个空格字符替换每一条注释。而且,实现可以用一个空格替换所有的空白字符序列(不包括换行符)。

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

将变成:

int fox;

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

二、明示常量:#define

指令可以出现在源文件的任何地方,其定义从指令出现的地方到该文件末尾有效。我们大量使用#define指令来定义明示常量(manifest constant)(也叫做符号常量),但是该指令还有许多其他用途。预处理器指令从#开始运行,到后面的第1个换行符为止。也就是说,指令的长度仅限于一行(逻辑行)。在预处理开始前,编译器会把多行物理行处理为一行逻辑行。

/* 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;
}

运行结果:

X is 2.
X is 4.
Consistency is the last refuge of the unimaginative.- Oscar Wilde
TWO: OW

每行#define(逻辑行)都由3部分组成。第1部分是#define指令本身。第2部分是选定的缩写,也称为。有些宏代表值,这些宏被称为类对象宏(object-like macro)。C 语言还有类函数宏(function-like macro)。宏的名称中不允许有空格,而且必须遵循C变量的命名规则。第3部分(指令行的其余部分)称为替换列表或替换体。一旦预处理器在程序中找到宏的实例后,就会用替换体代替该宏(也有例外)。从宏变成最终替换文本的过程称为宏展开(macro expansion)注意,可以在#define行使用标准C注释,每条注释都会被一个空格代替

宏可以表示任何字符串,甚至可以表示整个 C 表达式。

由于编译器在编译期对所有的常量表达式(只包含常量的表达式)求值,所以预处理器不会进行实际的乘法运算,这一过程在编译时进行。预处理器不做计算,不对表达式求值,它只进行替换

注意,宏定义还可以包含其他宏(一些编译器不支持这种嵌套功能)。

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

printf("TWO: OW");

打印的是TWO: OW

对于绝大部分数字常量,应该使用字符常量。如果在算式中用字符常量代替数字,常量名能更清楚地表达该数字的含义。

用const可以创建在程序运行过程中不能改变的变量,可具有文件作用域或块作用域。另一方面,宏常量可用于指定标准数组的大小和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值作为常量表达式的一部分)。

2.1 记号

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

#define FOUR 2*

该宏定义有一个记号:2*2序列。但是,下面的宏定义中:

#define SIX 2 * 3

有3个记号:2、*、3。替换体中有多个空格时,字符型字符串和记号型字符串的处理方式不同。

解释为字符型字符串,把空格视为替换体的一部分;解释为记号型字符串,把空格视为替换体中各记号的分隔符。在实际应用中,一些C编译器把宏替换体视为字符串而不是记号。

C编译器处理记号的方式比预处理器复杂。由于编译器理解C语言的规则,所以不要求代码中用空格来分隔记号。例如,C编译器可以把2*2直接视为3个记号,因为它可以识别2是常量,*是运算符。

2.2 重定义常量

假设先把LIMIT定义为20,稍后在该文件中又把它定义为25。这个过程称为重定义常量

不同的实现采用不同的重定义方案。除非新定义与旧定义相同,否则有些实现会将其视为错误。另外一些实现允许重定义,但会给出警告。ANSI标准采用第1种方案,只有新定义和旧定义完全相同才允许重定义。具有相同的定义意味着替换体中的记号必须相同,且顺序也相同

下面两个定义相同:

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

这两条定义都有 3 个相同的记号,额外的空格不算替换体的一部分。而下面的定义则与上面两条宏定义不同:

#define SIX 2*3

如果需要重定义宏,使用#undef 指令。如果确实需要重定义常量,使用const关键字和作用域规则更容易些。

三、在#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;
}

运行结果:

x = 5
Evaluating SQUARE(x): The result is 25.    
Evaluating SQUARE(2): The result is 4.     
Evaluating SQUARE(x+2): The result is 49.  
Evaluating 100/SQUARE(2): The result is 25.
x is 5.
Evaluating SQUARE(++x): The result is 49.  
After incrementing, x is 7.

预处理器不做计算、不求值,只替换字符序列。函数调用在程序运行时把参数的值传递给函数。宏调用在编译之前把参数记号传递给程序。这两个不同的过程发生在不同时期。

必要时要使用足够多的圆括号来确保运算和结合的正确顺序。

++x*++x = 6*7 = 42

由于标准并未对这类运算规定顺序,所以有些编译器得 7*6。而有些编译器可能在乘法运算之前已经递增了x,所以7*7得49。在C标准中,对该表达式求值的这种情况称为未定义行为。

避免用++x 作为宏参数。一般而言,不要在宏中使用递增或递减运算符。但是,++x可作为函数参数,因为编译器会对++x求值得5后,再把5传递给函数。

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

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.

调用第1个宏时,用"y"替换#x。调用第2个宏时,用"2 + 4"替换#x。ANSI C字符串的串联特性将这些字符串与printf()语句的其他字符串组合,生成最终的字符串。

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

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

// 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

PRINT_XN()宏用#运算符组合字符串,##运算符把记号组合为一个新的标识符。

3.3 变参宏:...和_ _VA_ARGS

stdvar.h 头文件提供了工具,让用户自定义带可变参数的函数。通过把宏参数列表中最后的参数写成省略号(即,3个点...)来实现这一功能。这样,预定义宏_ _VA_ARGS_ _可用在替换部分中,表明省略号代表什么。

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

假设稍后调用该宏:

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

对于第1次调用,_ _VA_ARGS_ _展开为1个参数:"Howdy"。对于第2次调用,_ _VA_ARGS_ _展开为3个参数:"weight = %d, shipping = $%.2f\n"、wt、sp。因此,展开后的代码是:

printf("Howdy");
printf("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);//%g 小数点右侧的尾数0不被显示;显示小数点仅当输出的小数部分不为0。PR(2, "x = %.2f, y = %.4f\n", x, y);return 0;
}

运行结果:

Message 1: x = 48
Message 2: x = 48.00, y = 6.9282

记住,省略号只能代替最后的宏参数:

#define WRONG(X, ..., Y) #X #_ _VA_ARGS_ _ #y //不能这样做

四、宏和函数的选择

使用宏比使用普通函数复杂一些,稍有不慎会产生奇怪的副作用。一些编译器规定宏只能定义成一行。

宏和函数的选择实际上是时间和空间的权衡。宏生成内联代码,即在程序中生成语句。另一方面,程序的控制必须跳转至函数内,随后再返回主调程序,这显然比内联代码花费更多的时间。

宏的一个优点是,不用担心变量类型(这是因为宏处理的是字符串,而不是实际的值)。因此,只要能用int或float类型都可以使用SQUARE(x)宏。C99提供了第3种可替换的方法——内联函数

宏名中不允许有空格,但是在替换字符串中可以有空格。ANSI C允许在参数列表中使用空格。用圆括号把宏的参数和整个替换体括起来。这样能确保被括起来的部分在下面这样的表达式中正确地展开。

用大写字母表示宏函数的名称。

在程序中只使用一次的宏无法明显减少程序的运行时间。在嵌套循环中使用宏更有助于提高效率。许多系统提供程序分析器以帮助程序员压缩程序中最耗时的部分。

五、文件包含:#include

当预处理器发现#include指令时,会查看后面的文件名并把文件的内容包含到当前文件中,即替换源文件中的#include指令。#include指令有两种形式:

#include <stdio.h> ←文件名在尖括号中

#include "mystuff.h" ←文件名在双引号中

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

#include <stdio.h> ←查找系统目录

#include "hot.h" ←查找当前工作目录

#include "/usr/biff/p.h" ←查找/usr/biff目录

ANSI C不为文件提供统一的目录模型,因为不同的计算机所用的系统不同。一般而言,命名文件的方法因系统而异,但是尖括号和双引号的规则与系统无关。

为什么要包含文件?因为编译器需要这些文件中的信息。包含一个大型头文件不一定显著增加程序的大小。在大部分情况下,头文件的内容是编译器生成最终代码时所需的信息,而不是添加到最终代码中的材料。

5.1 头文件示例

useheader.c

// 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.c

// 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';        // 在此处放置一个空字符elsewhile (getchar() != '\n')continue; // 处理输入行中的剩余字符}return ret_val;
}

names_st.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.h头文件包含了一些头文件中常见的内容:#define指令、结构声明、typedef和函数原型。注意,这些内容是编译器在创建可执行代码时所需的信息,而不是可执行代码。可执行代码通常在源代码文件中,而不是在头文件中。

两个源代码文件都使用names_st类型结构,所以它们都必须包含names_st.h头文件。必须编译和链接names_st.c和useheader.c源代码文件。声明和指令放在nems_st.h头文件中,函数定义放在names_st.c源代码文件中。

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类型也定义在头文件中。

另外,还可以使用头文件声明外部变量供其他文件共享。例如,如果已经开发了共享某个变量的一系列函数,该变量报告某种状况(如,错误情况),这种方法就很有效。

int status = 0; // 该变量具有文件作用域,在源代码文件

然后,可以在与源代码文件相关联的头文件中进行引用式声明:

extern int status; // 在头文件中

这行代码会出现在包含了该头文件的文件中,这样使用该系列函数的文件都能使用这个变量。虽然源代码文件中包含该头文件后也包含了该声明,但是只要声明的类型一致,在一个文件中同时使用定义式声明和引用式声明没问题。

需要包含头文件的另一种情况是,使用具有文件作用域、内部链接和 const 限定符的变量或数组。const 防止值被意外修改,static 意味着每个包含该头文件的文件都获得一份副本。因此,不需要在一个文件中进行定义式声明,在其他文件中进行引用式声明。

六、其他指令

#undef指令取消之前的#define定义。#if、#ifdef、#ifndef、#else、#elif#endif指令用于指定什么情况下编写哪些代码。

6.1 #undef指令

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

#define LIMIT 400

然后,下面的指令:

#undef LIMIT

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

6.2 从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指令的位置。

__DATE__和__FILE__一定是已定义的,而且不能取消定义。

6.3 条件编译

可以使用其他指令创建条件编译(conditinal compilation)

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

#ifdef MAVIS

#include "horse.h"// 如果已经用#define定义了 MAVIS,则执行下面的指令

#define STABLES 5

#else

#include "cow.h" //如果没有用#define定义 MAVIS,则执行下面的指令

#define STABLES 15

#endif

#ifdef 指令说明,如果预处理器已定义了后面的标识符(MAVIS),则执行#else或#endif指令之前的所有指令并编译所有C代码(先出现哪个指令就执行到哪里)。如果预处理器未定义MAVIS,且有 #else指令,则执行#else和#endif指令之间的所有代码。#ifdef #else很像C的if else。两者的主要区别是,预处理器不识别用于标记块的花括号({}),因此它使用#else(如果需要)和#endif(必须存在)来标记指令块。这些指令结构可以嵌套。也可以用这些指令标记C语句块。

/* 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_CHECKINGprintf("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

可以用这种方法在调试程序。调试结束后,可移除JUST_CHECKING定义并重新编译。如果以后还需要使用这些信息,重新插入定义即可。这样做省去了再次输入额外打印语句的麻烦。#ifdef还可用于根据不同的C实现选择合适的代码块。

2.#ifndef指令

#ifndef指令判断后面的标识符是否是未定义的,常用于定义之前未定义的常量。

/* arrays.h */
#ifndef SIZE
        #define SIZE 100
#endif

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

在被包含的文件中有某些项(如,一些结构类型的声明)只能在一个文件中出现一次。C标准头文件使用#ifndef技巧避免重复包含。

doubincl.c

// doubincl.c -- 包含头文件两次
#include <stdio.h>
#include "names.h"
#include "names.h" // 不小心第2次包含头文件
int main()
{names winner = {"Less", "Ismoor"};printf("The winner is %s %s.\n", winner.first,winner.last);return 0;
}

names.h

// 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

如果把#ifndef保护删除后,程序就无法通过编译。

3.#if和#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

条件编译还有一个用途是让程序更容易移植。

6.4 预定义宏

C标准规定了一些预定义宏

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

// predef.c -- 预定义宏和预定义标识符
#include <stdio.h>
#line 10 "cool.c"
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 cool.c.
The date is Apr 23 2023.
The time is 12:52:14.   
The version is 201710.  
This is line 17.        
This function is main   
This function is why_me 
This is line 25.

6.5 #line和#error

#line指令重置_ _LINE_ _和_ _FILE_ _宏报告的行号和文件名。

#line 1000 // 把当前行号重置为1000

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

#error 指令让预处理器发出一条错误消息,该消息包含指令中的文本。

#if _ _STDC_VERSION_ _ != 201112L
        #error Not C11
#endif

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

newish.c:14:2: error: #error Not C11

6.6 #pragma

在现在的编译器中,可以通过命令行参数或IDE菜单修改编译器的一些设置。#pragma把编译器指令放入源代码中。

下面的编译指示(pragma)让编译器支持C9X:

#pragma c9x on

一般而言,编译器都有自己的编译指示集。

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

_Pragma("nonstandardtreatmenttypeB on")

等价于下面的指令:

#pragma nonstandardtreatmenttypeB on

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

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

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

LIMRG ( ON )

下面的定义看上去没问题,但实际上无法正常运行:

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

问题在于这行代码依赖字符串的串联功能,而预处理过程完成之后才会串联字符串。

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

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

变成了:

#pragma use_bool "true "false

6.7 泛型选择(C11)

泛型编程(generic programming)指那些没有特定类型,但是一旦指定一种类型,就可以转换成指定类型的代码。C没有这种功能。然而,C11新增了一种表达式,叫作泛型选择表达式(generic selection expression),可根据表达式的类型(即表达式的类型是 int、double 还是其他类型)选择一个值。泛型选择表达式不是预处理器指令,但是在一些泛型编程中它常用作#define宏定义的一部分。

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

第1个项的类型匹配哪个标签,整个表达式的值是该标签后面的值。

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

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

// 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还提供另一种方法:内联函数 ( inline function)。C99和C11标准中叙述的是:“把函数变成内联函数建议尽可能快地调用该函数,其具体效果由实现定义”。因此,把函数变成内联函数,编译器可能会用内联代码替换函数调用,并(或)执行一些其他的优化,但是也可能不起作用。

创建内联函数的定义有多种方法。标准规定具有内部链接的函数可以成为内联函数,还规定了内联函数的定义与调用该函数的代码必须在同一个文件中。因此,最简单的方法是使用函数说明符 inline存储类别说明符static。通常,内联函数应定义在首次使用它的文件中,所以内联函数也相当于函数原型。由于并未给内联函数预留单独的代码块,所以无法获得内联函数的地址(实际上可以获得地址,不过这样做之后,编译器会生成一个非内联函数)。另外,内联函数无法在调试器中显示。内联函数应该比较短小。把较长的函数变成内联并未节约多少时间,因为执行函数体的时间比调用函数的时间长得多。最简单的做法是,把内联函数定义放入头文件,并在使用该内联函数的文件中包含该头文件即可。

// eatline.h
#ifndef EATLINE_H_
#define EATLINE_H_
inline static void eatline()
{
while (getchar() != '\n')
continue;
}
#endif

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

//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);
...

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类型的函数在执行完毕后返回主调函数,只是它不提供返回值。

九、C库

9.1 访问C库

首先,可以在多个不同的位置找到库函数。例如,getchar()函数通常作为宏定义在stdio.h头文件中,而strlen()通常在库文件中。其次,不同的系统搜索这些函数的方法不同。下面介绍3种可能的方法。

1.自动访问

在一些系统中,只需编译程序,就可使用一些常用的库函数。在使用函数之前必须先声明函数的类型,通过包含合适的头文件即可完成。

2.文件包含

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

3.库包含

在编译或链接程序的某些阶段,可能需要指定库选项。注意,这个过程与包含头文件不同。头文件提供函数声明或原型,而库选项告诉系统到哪里查找函数代码。

9.2 使用库描述

stddef.h 文件中包含了size_t 类型的typedef或#define定义。其他文件(包括stdio.h)通过包含stddef.h来包含这个定义。

ANSI C把指向void的指针作为一种通用指针,用于指针指向不同类型的情况。

十、数学库

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

10.1 三角问题

atan()无法区分角度相同但反向相反的线(实际上,atan()返回值的单位是弧度而不是度)。当然,C库还提供了atan2()函数。它接受两个参数:x的值和y的值。这样,通过检查x和y的正负号就可以得出正确的角度值。atan2()和atan()均返回弧度值。

/* 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;elsepv.angle = RAD_TO_DEG * atan2(rv.y, rv.x);return pv;
}

运行结果:

Enter x and y coordinates; enter q to quit:
4 5
magnitude = 6.40, angle = 51.34
1 1
magnitude = 1.41, angle = 45.00
-4 -5
magnitude = 6.40, angle = -128.66
q
Bye.

10.2 类型变体

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

// generic.c -- 定义泛型宏
#include <stdio.h>
#define printf __mingw_printf
//全局使用MinGW标准的printf函数进行输出。
//或者用__mingw_printf代替printf
#include <math.h>
#define RAD_TO_DEG (180 / (4 * atanl(1)))
// 泛型平方根函数
#define SQRT(X) _Generic((X),\long double: sqrtl,\float: sqrtf,\default: sqrt)(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);   // 匹配 floatprintf("%.17Lf\n", yy);  // 匹配 defaultprintf("%.17Lf\n", yyy); // 匹配 long doubleint i = 45;yy = SQRT(i);   // 匹配 defaultprintf("%.17Lf\n", yy);yyy = SIN(xxx); // 匹配 long doubleprintf("%.17Lf\n", yyy);return 0;
}

运行结果:

6.70820379257202148
6.70820393249936942
6.70820393249936909
6.70820393249936942
0.70710678118654752

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

10.3 tgmath.h库(C99)

C99标准提供的tgmath.h头文件中定义了泛型类型宏,其效果与上面的程序类似。

如果编译器支持复数运算,就会支持complex.h头文件,其中声明了与复数运算相关的函数。声明有 csqrtf()、csqrt()和 csqrtl(),这些函数分别返回 float complex、double complex和long double complex类型的复数平方根。

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

#include <tgmath.h>

...

float x = 44.0;

double y;

y = sqrt(x); // 调用宏,所以是 sqrtf(x)

y = (sqrt)(x); // 调用函数 sqrt()

这样做没问题,因为类函数宏的名称必须用圆括号括起来。圆括号只会影响操作顺序,不会影响括起来的表达式,所以这样做得到的仍然是函数调用的结果。实际上,在讨论函数指针时提到过,由于C语言奇怪而矛盾的函数指针规则,还也可以使用(*sqrt)()的形式来调用sqrt()函数。

十一、通用工具库

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

11.1 exit()和atexit()函数

在main()返回系统时将自动调用exit()函数。

atexit()函数通过退出时注册被调用的函数提供“指定在执行 exit()时调用的特定函数”的功能,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:
5
5 is odd.
Thus terminates another magnificent program from
SeeSaw Software!

(或者)

Enter an integer:
q
That's no integer!
SeeSaw Software extends its heartfelt condolences
to you upon the failure of your program.
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()把控制权返回主机环境,如果可能的话,向主机环境报告终止状态。ANSI C为了可移植性的要求,定义了一个名为EXIT_FAILURE的宏表示终止失败。类似地,ANSI C还定义了EXIT_SUCCESS表示成功终止。

11.2 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个参数是待排序项的数量。

由于qsort()把第1个参数转换为void指针,所以qsort()不知道数组中每个元素的大小。为此,函数原型用第 3 个参数补偿这一信息,显式指明待排序数组中每个元素的大小。

最后,qsort()还需要一个指向函数的指针,这个被指针指向的比较函数用于确定排序的顺序。qsort()原型中的第4个函数确定了比较函数的形式:

int (*compar)(const void *, 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;elsereturn 1;
}

运行结果:

Random list:
   0.0022    0.2390    1.2191    0.3910    1.1021    0.2027 
   1.3835   20.2830    0.2508    0.8880    2.2179   25.4866 
   0.0236    0.9308    0.9911    0.2507    1.2802    0.0939 
   0.9760    1.7217    1.2054    1.0326    3.7892    1.9635 
   4.1137    0.9241    0.9971    1.5582    0.8955   35.3798 
   4.0579   12.0460    0.0096    1.0109    0.8506    1.1529 
   2.3614    1.5876    0.4825    6.8749

Sorted list:
   0.0022    0.0096    0.0236    0.0939    0.2027    0.2390 
   0.2507    0.2508    0.3910    0.4825    0.8506    0.8880 
   0.8955    0.9241    0.9308    0.9760    0.9911    0.9971 
   1.0109    1.0326    1.1021    1.1529    1.2054    1.2191 
   1.2802    1.3835    1.5582    1.5876    1.7217    1.9635 
   2.2179    2.3614    3.7892    4.0579    4.1137    6.8749 
  12.0460   20.2830   25.4866   35.3798

1.qsort()的用法

qsort()函数排序数组的数据对象。该函数的ANSI原型如下:

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

第1个参数值指向待排序数组首元素的指针。第2个参数是待排序项的数量。第3个参数是数组中每个元素占用的空间大小,本例中为sizeof(double)。最后一个参数是mycomp,这里函数名即是函数的地址,该函数用于比较元素。

2.mycomp()的定义

qsort()的原型中规定了比较函数的形式:

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

这表明 qsort()最后一个参数是一个指向函数的指针,该函数返回int 类型的值且接受两个指向const void的指针作为参数。函数名作为参数时即是指向该函数的指针。为了比较指针所指向的值,必须解引用指针。因为值是 double 类型,所以要把指针解引用为 double 类型的值。然而,qsort()要求指针指向 void。要解决这个问题,必须在比较函数的内部声明两个类型正确的指针,并初始化它们分别指向作为参数传入的值。

为了让该方法具有通用性,qsort()和比较函数使用了指向 void 的指针。C++要求在把void*指针赋给任何类型的指针时必须进行强制类型转换。而C没有这样的要求:

const double * a1 = (const double *) p1;

这种强制类型转换,在C中是可选的,但在C++中是必须的。

下面再来看一个比较函数的例子。假设有下面的声明:

struct names {
        char first[40];
        char last[40];
};
struct names staff[100];

如何调用qsort():

qsort(staff, 100, sizeof(struct names), comp);

这里 comp 是比较函数的函数名。那么,应如何编写这个函数?假设要先按姓排序,如果同姓再按名排序,可以这样编写该函数:

#include <string.h>

int comp(const void *p1, const void *p2) /* 该函数的形式必须是这样 */

{

/* 得到正确类型的指针 */

const struct names *ps1 = (const struct names *)p1;

const struct names *ps2 = (const struct names *)p2;

int res;

res = strcmp(ps1->last, ps2->last); /* 比较姓 */

if (res != 0)

return res;

else /* 如果同姓,则比较名 */

return strcmp(ps1->first, ps2->first);

}

该函数使用 strcmp()函数进行比较。

十二、断言库

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

12.1 assert的用法

/* assert.c -- 使用 assert() */
#include <stdio.h>
#include <math.h>
#define NDEBUG//写在#include <assert.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; /* 应该用 + */// if (z < 0)// {// puts("z less than 0");// abort();// }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): 
3 5
Assertion failed: z >= 0, file C:\Users\HHH\Desktop\VS_PJ\CPPch16\assert.c, line 19

这条消息可能不是指明z >= 0,而是指明没有满足z >=0的条件。用if语句也能完成类似的任务:

if (z < 0)
{
puts("z less than 0");
abort();
}

使用 assert()有几个好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭 assert()的机制。如果认为已经排除了程序的 bug,就可以把下面的宏定义写在包含assert.h的位置前面:

#define NDEBUG

并重新编译程序,这样编译器就会禁用文件中的所有 assert()语句。

12.2 Static_assert(C11)

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;
}

编译:gcc statasrt.c -o start.c -o statasrt

结果:

根据语法,_Static_assert()被视为声明。因此,它可以出现在函数中,或者在这种情况下出现在函数的外部。

_Static_assert要求它的第1个参数是整型常量表达式,这保证了能在编译期求值(sizeof表达式被视为整型常量)。不能用程序中的assert代替_Static_assert,因为assert中作为测试表达式的z > 0不是常量表达式,要到程序运行时才求值。

十三、string.h库中的memcpy()和memmove()

不能把一个数组赋给另一个数组,所以要通过循环把数组中的每个元素赋给另一个数组相应的元素。有一个例外的情况是:使用strcpy()和strncpy()函数来处理字符数组。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()不作这样的假设,所以拷贝过程类似于先把所有字节拷贝到一个临时缓冲区,然后再拷贝到最终目的地。如果使用 memcpy()时,两区域出现重叠其行为是未定义的。

由于这两个函数设计用于处理任何数据类型,所有它们的参数都是两个指向 void 的指针。C允许把任何类型的指针赋给void *类型的指针。如此宽容导致函数无法知道待拷贝数据的类型。因此,这两个函数使用第 3 个参数指明待拷贝的字节数。

// 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

memcpy()函数不知道也不关心数据的类型,它只负责从一个位置把一些字节拷贝到另一个位置(例如,从结构中拷贝数据到字符数组中)。而且,拷贝过程中也不会进行数据转换。

十四、可变参数: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这个术语来描述该形参。传递给该形参的实际参数是省略号部分代表的参数数量。

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

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

lim是parmN形参,它表明变参列表中参数的数量。然后,该函数将使用定义在stdarg.h中的va_start()宏,把参数列表拷贝到va_list类型的变量中。该宏有两个参数:va_list类型的变量和parmN形参。

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

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

double tic;
int toc;
...
tic = va_arg(ap, double); // 检索第1个参数
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中检索两个参数。

// 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

使用变参函数比使用变参宏更复杂,但是函数的应用范围更广。

C | C预处理器和C库相关推荐

  1. 《C Primer Plus》第十六章——C预处理器和C库

    文章目录 C预处理器和C库 本章内容 翻译程序的第一步 明示常量:#define 记号 重定义常量 在#define中使用参数 用宏参数创建字符串:#运算符 预处理器黏合剂:##运算符 变参宏:... ...

  2. 【《C Primer Plus》读书笔记】第16章:C预处理器和C库

    [<C Primer Plus>读书笔记]第16章:C预处理器和C库 16.1 翻译程序的第一步 16.2 明示常量:#define 16.2.1 记号 16.2.2 重定义常量 16.3 ...

  3. 16.C预处理器和C库

    文章目录 C预处理器和C库 16.1翻译程序的第一步 16.2明示常量:#define 16.3在#define中使用参数 16.3.1用宏参数创建字符串:#运算符 16.3.2预处理器黏合剂:##运 ...

  4. 第16章 C预处理器和C库 16.3 在#define中使用参数

    2019独角兽企业重金招聘Python工程师标准>>> 通过使用参数,可以创建外形和作用都与函数相似的类函数宏(function-like macro). 类函数宏的定义中,用圆括号 ...

  5. 【C语言篇】C预处理器和C库

    友情链接:C/C++系列系统学习目录 知识总结顺序参考C Primer Plus(第六版)和谭浩强老师的C程序设计(第五版)等,内容以书中为标准,同时参考其它各类书籍以及优质文章,以至减少知识点上的错 ...

  6. fatal error C1021: 无效的预处理器命令“warning”

    这个没有预训练:https://github.com/jacke121/CenterNetPerson 这个是源码研读:https://github.com/takooctopus/CenterNet ...

  7. 16道嵌入式C语言面试题(经典) 预处理器(Preprocessor)

    16道嵌入式C语言面试题(经典) 预处理器(Preprocessor) 1. 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题) #define SECONDS_PER_ ...

  8. C语言再学习 -- C 预处理器

    gcc/cc xxx.c  可以编译链接C源程序生成一个可执行文件 a.out 整个过程中可以划分为以下的4步流程: (1)预处理/预编译: 主要用于包含头文件的扩展,以及执行宏替换等 //加上 -E ...

  9. 再谈 CSS 预处理器

    CSS 预处理器是什么?一般来说,它们基于 CSS 扩展了一套属于自己的 DSL,来解决我们书写 CSS 时难以解决的问题: 语法不够强大,比如无法嵌套书写导致模块化开发中需要书写很多重复的选择器: ...

最新文章

  1. AndroidStudio3.4.2 gradle5.1.1 apt注解解释器不执行
  2. 第一次react-native项目实践要点总结
  3. 【项目实战】:基于python的p2p运营商数据信息的特征挖掘
  4. 数据库系统原理(第5章:数据库编程)
  5. AjaxPro新发现-错误处理
  6. Linux基本操作——VI和VIM
  7. Springcloud服务如何在Eureka安全优雅的下线
  8. sqlite时间比较_一份经过时间检验的 Laravel PHPUnit 测试经验分享
  9. RadioGroup 的使用
  10. shell---字体颜色
  11. dh算法 理论依据_DH算法
  12. js 金额格式化 和 转成人民币大写金额形式
  13. 效果图是怎么做出来的?
  14. C/C++——set的基本操作总结
  15. 简单自定义MVC优化
  16. golang爬取免费代理IP
  17. 蓝牙协议栈HFP SCO连接流程
  18. NoSuchMethodError 错误——包冲突解决办法
  19. java.util.Date的getYear()
  20. 微信小程序 发布后强制更新版本 强制覆盖老版本

热门文章

  1. 五、校准你的测光表(转自 色影无忌)
  2. 常用的Essay写作句型怎么详细分析?
  3. web服务器理解和重要性
  4. 【EOS】区块链EOS到底是什么?
  5. Mybatis拦截器使用介绍
  6. c语言考试软件最新版,全国等级考试二级C语言上机模拟软件下载_全国等级考试二级C语言上机模拟软件官方下载-太平洋下载中心...
  7. 腾讯微博开放平台 android登录界面显示为pc登录界面 解决方案
  8. linux时间怎么老是延迟,实用技巧:Linux下时间延迟以及延缓操作
  9. 联合概率、边缘概率、条件概率
  10. 使用python的scapy库,提供一个可用的通过nbns获取主机名称的示例代码