C语言之预处理,头文件

1 预处理

1.1 简介

预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP

所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。
这些预处理器命令必须在一行的开头以(#)开始,末尾不再加号,以区别于C语言的其他语句

下面列出了所有重要的预处理器指令:

指令 描述
#define 定义宏
#include 包含一个源代码文件
#undef 取消已定义的宏
#ifdef 如果宏已经定义,则返回真
#ifndef 如果宏没有定义,则返回真
#if 如果给定条件为真,则编译下面代码
#else #if 的替代方案
#elif 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个 #if……#else 条件编译块
#error 当遇到标准错误时,输出错误消息
#pragma 使用标准化方法,向编译器发布特殊的命令到编译器中

1.2 预处理器实例

分析下面的实例来理解不同的指令。

#define MAX_ARRAY_LENGTH 20

这个指令告诉 CPP 把所有的 MAX_ARRAY_LENGTH 定义为 20。使用 #define 定义常量来增强可读性。

#include <stdio.h>  
#include "myheader.h"

这些指令告诉 CPP 从系统库中获取 stdio.h,并添加文本到当前的源文件中。下一行告诉 CPP 从本地目录中获取 myheader.h,并添加内容到当前的源文件中。

#undef  FILE_SIZE
#define FILE_SIZE 42

这个指令告诉 CPP 取消已定义的 FILE_SIZE,并定义它为 42。

#ifndef MESSAGE
   #define MESSAGE "You wish!"
#endif

这个指令告诉 CPP 只有当 MESSAGE 未定义时,才定义 MESSAGE。

#ifdef DEBUG
   /* Your debugging statements here */
#endif

这个指令告诉 CPP 如果定义了 DEBUG,则执行处理语句。在编译时,如果向 gcc 编译器传递了 -DDEBUG 开关量,这个指令就非常有用。它定义了 DEBUG,可以在编译期间随时开启或关闭调试。

调试程序,在程序开头定义一个符号常数DEBUG(值为1或0),在程序任何需要设置调试信息之处,加入以下信息:

if DEBUG
    调试代码
endif

这里定义DEBUG为1时,为调试状态,即输出跟踪信息。在调试时,程序中定义DEBUG的语句为:#define DEBUG 1
而调试结束后,将这条语句改为:#define DEBUG 0
这样,只需修改#define DEBUG这一条语句,便达到了自动删除或增加程序中众多调试语句的效果

1.3 预定义宏

1.3.1 简介

ANSIC 定义了许多宏。在编程中可以使用这些宏,但是不能直接修改这些预定义的宏

描述
__DATE__ 当前日期,一个以 "MMM DD YYYY" 格式表示的字符常量。
__TIME__ 当前时间,一个以 "HH:MM:SS" 格式表示的字符常量。
__FILE__ 这会包含当前文件名,一个字符串常量。
__LINE__ 这会包含当前行号,一个十进制常量。
__STDC__ 当编译器以 ANSI 标准编译时,则定义为 1。

注意:上面前后都是两横线

让我们来尝试下面的实例:

#include <stdio.h>
main()
{
   printf("File :%s\n", __FILE__ );
   printf("Date :%s\n", __DATE__ );
   printf("Time :%s\n", __TIME__ );
   printf("Line :%d\n", __LINE__ );
   printf("ANSI :%d\n", __STDC__ );
 
}

结果:

File :test.c
Date :Jun 2 2012
Time :03:36:24
Line :8
ANSI :1

1.3.2 不带参数的宏

不带参数的宏定义命令行形式如下:

#define 宏名 替换文本

#define宏名替换文本之间用空格隔开
例如:

#define LENGTH 300

其中标识符LENGTH称为宏名,是用户自定义的标识符,不能与同在一段程序中的其他标识符同名。编译时,编译预处理程序对源程序中所有名为LENGTH的标识符用300来替换,这个替换过程称为宏展开。

使用宏名替代一个字符串,其中一个主要的目的是减少程序中重复书写某些字符串的工作量,比如在程序中一些不太好记忆的参数,重复书写容易出错且很繁琐,这时用宏名来代替该字符串就可以使程序简单明了。其次当程序的一些常量需要改变时,如果没有宏名,那么整个程序用到该常量的地方都需人工一 一修改,若用宏名,则只需改变宏定义命令行,一改全改

同一个宏名不能重复定义。
如:

#define PI 0.14159
#define PI 0.1416

二条语句不能同时出现在一段程序中。

宏定义是用宏名代替一个字符串,也就是简单的置换,并不作语法检查。如:

#define PI 0.14159

中的0.14159如果输入时不小心将数字1误输为字母i,那么在替换时也照样代入,只有在对已作宏展开后的源程序编译链接时才会出现错误。

宏定义与一般C语言语句不同,末尾不能加分号,否则会连分号一起置换

可以用undef命令终止宏定义的作用域。一般而言,一个宏定义,从被定义开始直至文件末尾,全程有效。若要更改其作用域则可以用undef命令。如:

define AAA 10
main()
{
...
}
undef AAA

undef AAA语句后,AAA的作用域终止,不能再使用未定义的标识符AAA

1.3.3 参数化的宏

在C语言程序设计中,宏定义还可以用于带参数的宏。带参数的宏定义的一般形式为:

define 宏名(参数表) 字符串

CPP 一个强大的功能是可以使用参数化的宏来模拟函数。例如,下面的代码是计算一个数的平方:

int square(int x) {
   return x * x;
}

我们可以使用宏重写上面的代码,如下:

#define square(x) ((x) * (x))

在使用带有参数的宏之前,必须使用 #define 指令定义。参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格

#include <stdio.h>
 
#define MAX(x,y) ((x) > (y) ? (x) : (y))
 
int main(void)
{
   printf("Max between 20 and 10 is %d\n", MAX(10, 20));  
   return 0;
}
结果:
Max between 20 and 10 is 20

1.3.4 变参宏:…和_ _VA_ARGS_ _

通过把宏参数列表中最后的参数写成省略号(即,3个点 )来实现这一功能。这样,预定义宏_ _VA_ARGS_ _可用在替换部分中,表明省略号代表什么。例如,下面的定义:
#define PR(…) printf(_ _VA_ARGS_ _)假设稍后调用该宏:PR(“Howdy”);那么_ _VA_ARGS_ _展开为1个参数:“Howdy”。

示例程序:

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

1.3.5 宏和函数的选择

使用宏比使用普通函数复杂一些,稍有不慎会产生奇怪的副作用。一些编译器规定宏只能定义成一行。不过,即使编译器没有这个限制,也应该这样做。

宏和函数的选择实际上是时间空间的权衡。宏生成内联代码,即在程序中生成语句。如果调用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);
  • 用大写字母表示宏函数的名称。该惯例不如用大写字母表示宏常量应用广泛。但是,大写字母可以提醒程序员注意,宏可能产生的副作用。

如果打算使用宏来加快程序的运行速度,那么首先要确定使用宏和使用函数是否会导致较大差异。在程序中只使用一次的宏无法明显减少程序的运行时间。在嵌套循环中使用宏更有助于提高效率。许多系统提供程序分析器以帮助程序员压缩程序中最耗时的部分。

另外主要有以下区别:

  • 函数在调用时,先求出实参表达式的值,再用这个值赋给形式参数;但带参数的宏定义仅仅进行字符替换。
  • 函数调用是在程序运行时分配临时内存单元;而宏展开是在编译时进行,展开时不分配内存单元,不进行值传递。
  • 宏替换不占运行时间,只占编译时间;函数调用占运行时间。
  • 宏替换后使源程序增长;而函数调用不会使源程序增长。
  • 在使用带参数的宏的时候,对带参数的宏的展开只是用语句中宏名后的实参字符串代替#define命令中的形参,这与函数参数传递时,实参将值传递给形参不同,在编写带参数的宏时需加以注意。

1.4 预处理器运算符

预处理器提供了下列的运算符来创建宏

1.4.1 宏延续运算符(\)

宏延续运算符(\):一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\)。例如:

#define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n")

1.4.2 字符串常量化运算符(#)

在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表。
C允许在字符串中包含宏参数。在类函数宏的替换体中,#号作为一个预处理运算符,可以把记号转换成字符串。例如,如果x是一个宏形参,那么#x就是转换为字符串x的形参名。这个过程称为字符串化(stringizing)

#include <stdio.h>
 
#define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n")
 
int main(void)
{
   message_for(Carole, Debra);
   return 0;
}

结果:

Carole and Debra: We love you!

1.4.3 标记粘贴运算符(##)

宏定义内的标记粘贴运算符(##)会合并两个参数。它允许在宏定义中 两个独立的标记被合并为一个标记
#运算符类似,##运算符可用于类函数宏的替换部分。而且,##还可用于对象宏的替换部分。##运算符把两个记号组合成一个记号。
#define XNAME(n) x ## n,然后 宏XNAME(4) 将展开为x4。
示例:

#include <stdio.h>
#define tokenpaster(n) printf ("token" #n " = %d", token##n)
int main(void)
{
   int token34 = 40;
   
   tokenpaster(34);
   return 0;
}

结果:
token34 = 40

这是怎么发生的,因为这个实例会从编译器产生下列的实际输出:

printf ("token34 = %d", token34);

这个实例演示了 token##n 会连接到 token34 中,在这里,我们使用了字符串常量化运算符(#)和标记粘贴运算符(##)。

1.4.4 defined() 运算符

预处理器 defined 运算符是用在常量表达式中的,用来确定一个标识符是否已经使用 #define 定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。下面的实例演示了 defined() 运算符的用法:

#include <stdio.h>
 
#if !defined (MESSAGE)
   #define MESSAGE "You wish!"
#endif
 
int main(void)
{
   printf("Here is the message: %s\n", MESSAGE);  
   return 0;
}

结果:
Here is the message: You wish!

2 头文件

2.1 头文件讲解

在编写C语言程序时,可以把宏定义语句按照功能不同分别存入到不同的文件,当需要某一类宏定义时,就无需在程序中重新定义,只要把这些宏定义所在的文件,包含在程序的开头就可以了。
文件包含就是在一个文件中,包含另外一个文件的全部内容,用include命令来实现文件包含的功能。

include命令的形式如下:

#include "文件名"或 include <文件名>
  • 如果文件名是用双引号括起来的,系统将先在源程序所在的目录内查找指定的包含文件,如果找不到,再按系统指定的标准方式到相关的目录去寻找。
  • 如果文件名用尖括号括起来,则系统将直接按照指定的标准方式到相关的目录中去查找。在预编译时,预编译程序将用指定文件中的内容来替换此命令行。

文件包含在使用时应注意:
一条文件包含命令只能包含一个文件。如果需要包含多个文件,就必须使用多条文件包含命令。 例如,如果要包含两个文件file1.h和file2.h就不能这样写:

#include "file1.h,file2.h"
或
#include "file1.h" ,"file2.h"
必须写成:
#include "file1.h"
#include "file2.h"

其先后顺序与文件内容无关。但如果一个文件要使用另一个文件的内容,则后者文件应写在前面,即如果file1.h要使用file2.h文件中的内容,则文件包含语句必须按如下顺序写:

include "file2.h"
include "file1.h"

文件包含的定义是可以嵌套的,文件包含的嵌套是指一个被包含的文件中还可以包含其他文件。
被包含的文件常以.h结尾,这类文件中往往是程序所需要的一些说明定义,如符号常量的定义类型定义带参数的宏定义数组结构共用体和枚举的定义等等,以及外部变量的定义、函数的定义或说明。但是被包含的文件可以是任意的文件,不一定是.h文件,也可以是C语言源文件

2.2 头文件引用

引用头文件相当于复制头文件的内容,但是我们不会直接在源文件中复制头文件的内容,因为这么做很容易出错,特别在程序是由多个源文件组成的时候。那么建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件

2.2.1 只引用一次头文件

如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:

#ifndef HEADER_FILE
#define HEADER_FILE
the entire header file file
#endif

这种结构就是通常所说的包装器 #ifndef。当再次引用头文件时,条件为假,因为 HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它

2.2.2 有条件引用

有时需要从多个不同的头文件中选择一个引用到程序中。例如,需要指定在不同的操作系统上使用的配置参数。可以通过一系列条件来实现这点,如下:

#if SYSTEM_1
   # include "system_1.h"
#elif SYSTEM_2
   # include "system_2.h"
#elif SYSTEM_3
   ...
#endif

但是如果头文件比较多的时候,这么做是很不妥当的,预处理器使用宏来定义头文件的名称。这就是所谓的有条件引用。它不是用头文件的名称作为 #include 的直接参数,只需要使用宏名称代替即可:

 #define SYSTEM_H "system_1.h"
 ...
 #include SYSTEM_H

SYSTEM_H 会扩展,预处理器会查找 system_1.h,就像 #include 最初编写的那样。SYSTEM_H 可通过 -D 选项的 Makefile 定义

2.3 宏指令

2.3.1 #undef指令

#undef 指令用于“取消”已定义的#define指令。 也就是说,假设有如下定义:#define LIMIT 400
然后,下面的指令: #undef LIMIT
将移除上面的定义。现在就可以把LIMIT重新定义为一个新值。即使原来没有定义LIMIT,取消LIMIT的定义仍然有效。

2.3.2 条件编译

2.3.2.1 #ifdef、#else和#endif指令

#ifdef MAVIS
#include "horse.h" // 如果已经用#define定义了 MAVIS,则执行下面的指令
#define STABLES 5
#else
#include "cow.h" //如果没有用#define定义 MAVIS,则执行下面的指令
#define STABLES 15
#endif

2.3.2.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

2.3.2.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

2.3.3 预定义宏

在这里插入图片描述
示例程序:

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

2.3.4 #line和#error

#line指令重置_ LINEFILE _宏报告的行号和文件名。可以这样使用#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

2.3.5 #pragma

#pragma把编译器指令放入源代码中。例如,在开发C99时,标准被称为C9X,可以使用下面的编译指示(pragma)让编译器支持C9X:
#pragma c9x on
C99还提供_Pragma预处理器运算符,该运算符把字符串转换成普通的编译指示。例如:_Pragma(“nonstandardtreatmenttypeB on”)
等价于下面的指令:#pragma nonstandardtreatmenttypeB on

2.3.6 泛型选择(C11)

C11新增了一种表达式,叫作泛型选择表达式(generic selection expression),可根据表达式的类型(即表达式的类型是int、double还是其他类型)选择一个值。泛型选择表达式不是预处理器指令,但是在一些泛型编程中它常用作#define宏定义的一部分。

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

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

_GenericC11的关键字。_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

2.4 函数部分

2.4.1 内联函数(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定义被视为可替换的外部定义。

2.4.2 _Noreturn函数(C11)

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

_exit()函数是_Noreturn函数的一个示例,一旦调用exit(),它不会再返回主调函数。注意,这与void返回类型不同。void类型的函数在执行完毕后返回主调函数,只是它不提供返回值。

_Noreturn的目的是告诉用户和编译器,这个特殊的函数不会把控制返回主调程序。告诉用户以免滥用该函数,通知编译器可优化一些代码。

2.4.3 数学库

数学函数库,一些数学计算的公式的具体实现是放在math.h里,具体有:

  • 三角函数
    double sin(double);正弦
    double cos(double);余弦
    double tan(double);正切
  • 反三角函数
    double asin (double); 结果介于[-PI/2,PI/2]
    double acos (double); 结果介于[0,PI]
    double atan (double); 反正切(主值),结果介于[-PI/2,PI/2]
    double atan2 (double,double); 反正切(整圆值),结果介于[-PI,PI]
    double sinh (double);
    double cosh (double);
    double tanh (double);
  • 指数与对数
    double frexp(double value,int * exp);这是一个将value值拆分成小数部分f和(以2为底的)指数部分exp,并返回小数部分f,即f * 2^exp。其中f取值在0.5~1.0范围或者0。
    double ldexp(double x,int exp);这个函数刚好跟上面那个frexp函数功能相反,它的返回值是x * 2^exp
    double modf(double value,double * iptr);拆分value值,返回它的小数部分,iptr指向整数部分。
    double log (double); 以e为底的对数
    double log10 (double);以10为底的对数
    double pow(double x,double y);计算x的y次幂
    float powf(float x,float y); 功能与pow一致,只是输入与输出皆为单精度浮点数
    double exp (double);求取自然数e的幂
    double sqrt (double);开平方根
  • 取整
    double ceil (double); 取上整,返回不比x小的最小整数
    double floor (double); 取下整,返回不比x大的最大整数,即高斯函数[x]
  • 绝对值
    double fabs (double);求实型的绝对值
    double cabs(struct complex znum);求复数的绝对值
  • 标准化浮点数
    double frexp (double f,int *p); 标准化浮点数,f = x * 2^p,已知f求x,p (x介于[0.5,1])
    double ldexp (double x,int p); 与frexp相反,已知x,p求f
  • 取整与取余
    double modf (double,double*); 将参数的整数部分通过指针回传,返回小数部分
    double fmod (double,double); 返回两参数相除的余数
  • 其他
    double hypot(double x,double y);已知直角三角形两个直角边长度,求斜边长度
    double ldexp(double x,int exponent);计算x*(2的指数幂)
    double poly(double x,int degree,double coeffs []);计算多项式
    int matherr(struct exception *e);数学错误计算处理程序

2.4.3.1 三角问题

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.

2.4.3.2 类型变体

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(),先对泛型选择表达式求值得一个指针,然后通过该指针调用它所指向的函数。

2.4.4 通用工具库

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

2.4.4.1 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!
  • atexit()函数的用法
    要使用atexit()函数,只需把退出时要调用的函数地址传递给atexit()即可。函数名作为函数参数时相当于该函数的地址,所以该程序中把sign_off或too_bad作为参数。然后,atexit()注册函数列表中的函数,当调用exit()时就会执行这些函数。ANSI保证,在这个列表中至少可以放32个函数。最后调用exit()函数时,exit()会执行这些函数(执行顺序与列表中的函数顺序相反,即最后添加的函数最先执行)。
    atexit()注册的函数(如sign_off()和too_bad())应该不带任何参数且返回类型为void。通常,这些函数会执行一些清理任务,例如更新监视程序的文件或重置环境变量。
    注意,即使没有显式调用exit(),还是会调用sign_off(),因为main()结束时会隐式调用exit()。
  • exit()函数的用法
    exit()执行完atexit()指定的函数后,会完成一些清理工作:刷新所有输出流、关闭所有打开的流和关闭由标准I/O函数tmpfile()创建的临时文件。然后exit()把控制权返回主机环境,如果可能的话,向主机环境报告终止状态。

2.4.5 断言库

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

2.4.5.1 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()语句。

2.4.5.2 _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),但这会在编译和运行程序后才生成一条错误信息,很没效率。

2.4.6 可变参数:stdarg.h

前面提到过变参宏。stdarg.h 头文件为函数提供了一个类似的功能,但是用法比较复杂。必须按如下步骤进行:

  • 提供一个使用省略号的函数原型;
  • 在函数定义中创建一个va_list类型的变量;
  • 用宏把该变量初始化为一个参数列表;
  • 用宏访问参数列表;
  • 用宏完成清理工作。

函数原型中,参数至少有一个形参和一个省略号:

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类型变量的副本。

示例程序:

//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
posted @ 2022-10-30 19:11  上善若泪  阅读(97)  评论(0编辑  收藏  举报