C_Primer_Plus16.preprocessor_and_library
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,它的工作是把一些文本转换成另外一些文本。
在预处理之前,编译器必须对程序进行一些翻译处理。首先编译器把源代码中出现的字符映射到源字符集。
明示常量: #define
manifest constant, 也叫符号常量
预处理器指令:
#号前后允许有空格(旧版本不允许)- 其定义从指令出现的地方到该文件末尾有效
#define ONE 1
#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(){
int x = TWO;
PX;
x = FOUR;
printf(FMT, x);
printf("%s\n", OW);
printf("TWO: OW\n");
return 0;
}
output:
X is 2.
X is 4.
Consistency is the last refuge of the unimaginative. - Oscar Wilde
TWO: OW
#define 使用规则:
- 每行
#define(逻辑行)由3部分组成 - 第一部分是
#define本身 - 第二部分是选定的缩写,也叫作 宏(macro)
- 宏的名称中不允许有空格,而且必须遵循C变量的命名规则
- 有的宏代表的是值,叫做 类对象宏(object-like macro);有些代表函数,叫做 类函数宏(function-like macro)
- 第三部分称为 替换列表或替换体
- 一旦预处理器在程序中找到宏的示例,就会用替换体代替该宏。唯一的例外是双引号中的宏
- 从宏编程最终替换文本的过程称为 宏展开 (macro expansion)
- 宏所在行的注释会被替换成一个空格
注意,宏中的运算不会被执行,而是直接替换到程序里,比如:
#define TWO 2
#define FOUR TWO+TWO
// main:
int x = TWO; // 2
int y = 3*FOUR; // y = 3*2+2 = 6+2 = 8
与 const 关键字区别
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; // 无效
非自动数组的大小应该是整形常量表达式,意味着表示数组大小的必须是整形常量的组合、枚举常量和 sizeof 表达式,不包括 const 声明的值(与 C++ 不同,C++ 会把 const 值作为常量表达式的一部分)。但是有些C编译器接受其他形式的常量表达式,比如 Clang4.6 允许 data2 的声明。
记号
从技术角度看,可以把宏的替换体看做是记号(token)型字符串,而不是字符型字符串。C 预处理器记号是宏定义的替换体中单独的“词”。用空白把这些词分开:
#define FOUR 2*2 // 该宏有一个记号:2*2 序列
#define SIX 2 * 3 // 该宏有3个记号:2, *, 3
- 字符型字符串:
把空格视为替换体的一部分 - 记号型字符串:
把空格视为替换体中各记号的分隔符
C编译器处理记号的方式比预处理器复杂。实际应用中,一些C编译器使用字符串而不是记号。在更复杂的情况下,两者的区别才有实际意义。
重定义常量
重复定义相同的常量,叫做重定义常量
不同的实现采用不同的重定义方案。除非新定义与旧定义相同,否则有些编译器会将其视为错误。而另外一些编译器允许重定义,但会给出警告。ANSI 标准采用前者。
#define SIX 2 * 3
#define SIX 2 * 3 // 允许,与上次定义相同
#define SIX 2*3 // 不允许,因为上次的定义有3个记号,而这里只有一个记号
如果确实需要重定义常量,使用 const 关键字和作用域更容易。
#define 中使用参数
类函数宏,带参数的宏,形式上很像函数,因为这样的宏也使用圆括号,可以有一个或多个参数。
注意,这类的宏也只是做简单的替换,不会做任何计算:
#include <stdio.h>
#define SQUARE(X) X*X
#define PR(X) printf("The result is %d.\n", X)
int main(){
int x = 5;
int z;
printf("x = %d\n", x);
z = SQUARE(x);
printf("Evaluating SQUARE(z): ");
PR(z);
z = SQUARE(2);
printf("Evaluating SQUARE(2): ");
PR(z);
printf("Evaluating SQUARE(x+2): ");
PR(SQUARE(x + 2));
printf("Evaluating 100/SQUARE(x): ");
PR(100 / SQUARE(2));
printf("x is %d.\n", x);
printf("Evaluating SQUARE(++x): ");
PR(SQUARE(++x));
printf("After incrementing, x is %d.\n", x);
return 0;
}
output:
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.
计算过程:
SQUARE(x + 2) = x + 2*x + 2 = 5 + 2*5 + 2 = 17
100 / SQUARE(2) = 100 / 2*2 = 100
SQUARE(++x) = ++x*++x = 6*7 = 42
若要减少这种误解,可以使用括号:
#define SQUARE(x) ((x) * (x))
尽管如此,还是解决不了 SQUARE(++x) 这样的问题。并且,由于C标准未定义这类运算的顺序,所以有些编译器得到 7*6 这样的结果。有些编译器得出 7*7=49 这样的结果。问题还未结束,在使用了 SQUARE(++x) 后,x 的值增加了两次,这显然不是程序想要的结果。
一般而言,不要在宏中使用递增或递减运算符。但是 ++x 可以作为函数参数,因为编译器会对 ++x 求值得到6,再把6传递给函数
用宏参数创建字符串: #运算符
C允许在字符串中包含宏参数。在类函数宏的函数体中,# 作为一个预处理运算符,可以把记号转换成字符串:
#define PSQR(x) printf("The square of " #x " is %d.\n", ((x) * (x)))
// main() 中:
int y = 5;
PSQR(y); // The square of 5 is 25.
PSQR(2 + 4); // The square of 2 + 4 is 36.
预处理粘合剂: ##运算符
可用于类函数宏的替换部分,也可用于对象宏的替换部分。作用是把两个记号组合成一个记号:
#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; // x1 = 14;
int XNAME(2) = 14; // 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;
}
变参宏: ... 和 __VA_ARGS__
一些函数接受数量可变的参数(比如 printf()). stdvar.h 头文件提供了工具,让用户自定义带可变参数的函数。C99/C11 也对宏提供了这样的工具:
#define PR(...) printf(__VA_ARGS__)
// main()中:
PR("Howdy");
PR("weight = %d, shipping = $%.2f\n", wt, sp);
// 等价于:
printf("Howday");
printf("weight = %d, shipping = $%.2f\n", wt, sp);
例:
#include <stdio.h>
#include <math.h>
#define PR(X, ...) printf("Message " #x ": " __VA_AEGS__)
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;
}
output:
Message 1: x = 48
Message 2: x = 48.00, y = 6.9282
需要注意的是,省略号只能放在参数列表的最后
宏和函数的选择
使用宏比使用普通函数更复杂一些,有时会有奇怪的副作用。有些编译器规定宏只能定义成一行,不过,即使编译器没有这个限制,也应该这样做。
宏和函数的选择实际上是时间和空间的权衡。
- 宏生成内敛代码,在程序中生成语句
- 如果调用宏20次,即在程序中插入20行代码;而函数只有一份代码。所以函数节省了空间
- 程序的控制必须跳转至函数内,再返回主调函数,这比内联代码花费更多的时间
宏实际上处理的是字符串而不是实际的值,所以宏中不必担心变量类型。
内联函数
有些简单的函数,可以用宏代替:
#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))
#define ABS(X) ((X) < 0 ? -(X) : (X))
#define ISSIGN(X) ((X) == '+' || (X) == '-' ? 1 : 0)
使用宏需要注意的地方
- 宏名中不允许有空格,但在替换字符串中允许有空格;参数列表中允许有空格
- 用圆括号把宏的参数和整个替换体括起来
- 用大写字母表示红函数的名称。用于提醒程序员,宏可能产生副作用
- 如果打算使用宏来加快程序的运行速度,那么首先要确定使用宏和使用函数是否会导致较大差异
- 程序中只是用一次的宏无法明显减少程序的运行时间。在嵌套循环中使用宏有助于提高效率
想要在不同文件中使用宏,可以使用 #include 指令。
文件包含: #include
预处理器会把 #include 后面的文件中的内容替换到当前位置。#include 的使用有两种方式:
#include <stdio.h>
#include "mystuff.h"
UNIX 系统中,尖括号告诉预处理器在标准系统目录中查找该文件。双引号告诉预处理器首先在当前目录查找该文件,或文件名中指定的其他目录中查找,这取决于编译器的设定,有些编译器会搜索当前工作目录,有的会搜索项目所在的目录。如果未找到再查找标准系统目录:
#include <stdio.h>
#include "hot.h"
#include "/usr/biff/p.h"
包含一个大型头文件不一定会显著增加程序的大小。大部分情况下,头文件的内容包含的是编译器生成代码时所需的信息,而不是把整个文件全部包含到最终代码中。
头文件常用形式
- 明示常量
#define声明类对象宏
- 宏函数
#define声明类函数宏
- 函数声明
- 自定义函数声明
- 结构模板定义
- struct
- 类型定义
- typedef
- 声明外部变量
许多程序员在程序中使用自己开发的标准头文件。
外部链接
在与源代码文件相关联的头文件中使用 extern 可以把外部变量引用进来使用,这样该系列函数的文件都能使用这个变量:
extern int status;
虽然源代码文件中包含了该头文件,也包含了该声明,但只要声明的类型一致,在一个文件中同时使用定义式声明和引用式声明没有问题
内部链接
具有文件作用域、内部链接和const限定符的变量或数组,也可以通过头文件来声明。只是 static 意味着每个包含该头文件的文件都会获得一份副本。因此,不需要在一个文件中进行定义式声明,在其他文件中进行引用式声明。
其他指令
#undef 指令
用于“取消”已定义的 #define 指令。通常用于对常量重新定义新的值:
#define LIMIT 400
#undef LIMIT
#define LIMIT 300
即使原来没有定义 LIMIT,取消 LIMIT 的定义仍然有效。如果想使用一个名称,又不确定之前是否使用过,可以用 #undef 指令取消改名字的定义。
从 C 预处理器角度看已定义
预处理器在预处理指令中发现一个标识符时,会把它分类为已定义的或未定义的。已定义的表示由预处理器定义
- 使用 #define 指令创建的宏名,而且没有用 #undef 指令关闭,则是已定义的
- 标识符不是宏,而是一个文件作用域的 C 变量,则该标识符对于预处理器来说就是未定义的
#define LIMIT 100 // 已定义的
#define GOOD // 已定义的
#define A(X) ((-(X)) * (X)) // A 是已定义的
int q; // 未定义的
#undef GOOD // GOOD取消定义,是未定义的
条件编译
conditional compilation
可以使用这些指令告诉编译器根据编译时的条件执行或忽略代码块
#ifdef, #else 和 #endif 指令
#ifdef MAVIS
#include "horse.h"
#define STABLES 5
#else
#include "cow.h"
#define STABLES 15
#endif
如果未定义 MAVIS 常量,则使用上部分代码,否则执行下部分代码。对旧编译器,必须左对齐所有指令或至少左对齐#号
也可以用于语句块中,但是预处理器不识别用于标记块的花括号,因此 #endif 必须存在:
#include <stdio.h>
#define JUST_CHECKING
#define LIMIT 4
int main(void){
int i;
int total = 0;
for(i = 0; i <= LIMIT; ++i){
total += 2*i + 1;
#ifdef JUST_CHECKING
printf("i=%d, running total = %d\n", i, total);
#endif
}
printf("total = %d\n", total);
return 0;
}
这种方法可用于调试。调试结束后,将 JUST_CHECKING 的定义行注释,并重新编译
#ifndef 指令
与 #ifdef 意思相反:
#ifndef SIZE
#define SIZE 100
#endif
#ifndef 可以防止相同的宏被重复定义。它通常用于防止多次包含一个文件,通常做法是将头文件所有代码都包含在内,用于判断的宏名通常是文件名的大写,. 换为下划线,然后再加入若干下划线,防止名字冲突:
// names.h
#ifndef NAMES_H_
#define NAMES_H_
#define SLEN 32
struct names_st {
char first[SLEN];
char last[SLEN];
};
typedef stuct names_st names;
void get_names(names*);
void show_names(const names*);
char* s_gets(char* st, int n);
#endif
#if 和 #elif 指令
与 C 语言中的 if 很想,但 #if 后面是整型常量表达式,如果表达式非0,则为真:
#if SYS == 1
#include "ibm.h"
#elif SYS == 2
#include "vax.h"
#elif SYS == 3
#include "mac.h"
#else
#include "general.h"
#endif
较新的编译器提供另一种方法测试名称是否已定义:
#if defined (IBMPC)
#include "ibm.h"
#elif defined (VAX)
#include "vax.h"
#elif defined (MAC)
#include "mac.h"
#else
#include "general.h"
#endif
预定义宏
| 宏 | 含义 |
|---|---|
__DATA__ |
预处理的日期(Mmm dd yyyy 形式,比如 Nov 23 2013) |
__FILE__ |
当前源代码文件名字符串字面量 |
__LINE__ |
当前行号整型字面量 |
__STDC__ |
设置为1时,表明实现遵循C标准 |
__STDC_HOSTED__ |
本机环境设置为1;否则设置为0 |
__STD_VERSION__ |
支持C99标准,设置为 199901L;支持C11,设置为 201112L |
__TIME__ |
翻译代码的时间,格式为 "hh:mm:ss" |
C99 还提供了一个名为 __func__ 的预定义标识符,它是当前函数的名字的字符串,必须在函数中使用。所以它是C语言的预定义标识符,而不是预定义宏:
#include <stdio.h>
void why_me();
int main(){
printf("This function is %s\n", __func__);
return 0;
}
void why_me(){
printf("This function is %s\n", __func__);
}
output:
This function is main
This function is why_me
#line 和 #error
#line 指令重置 __LINE__ 和 __FILE__ 宏报告的行号和文件名:
#line 1000 // 把当前行号重置为1000
#line 10 "cool.c" // 把行号重置为10,把文件名重置为cool.c
#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
$
#pragma
#pragma 把编译器指令放入源代码中,用于修改编译器的一些设置。比如,在开发 C99 时,标准被称为 C9X,可以这样设置来让编译器支持 C9X:
#pragma c9x on
编译器都有自己的编译指示集,可以实现比如控制分配给自动变量的内存量这样的功能。
C99 还提供了 _Pragma 预处理器运算符,用于把字符串转换成普通的编译指示:
_Pragma("nonstandardtreatmenttypeB on");
// 等价于:
#pragma nonstandardtreatmenttypeB on
泛型选择 (C11)
泛型编程:generic programming. 指那些没有特定类型,但一旦指定一种类型,就可以转换成指定类型的代码。有点类似面向对象中的重载。
C11 新增了一种表达式,叫做 泛型选择表达式(generic selection expression),可根据表达式的类型(int, double 或其他)选择一个值。泛型选择表达式不是预处理器指令,但是在一些泛型编程中它常用作 #define 宏定义的一部分。
_Generic(x, int: 0, float: 1, double: 2, default:3)
根据 x 的类型,返回不同的值。
与 #define 一起使用:
#define MYTYPE(X) _Generic((X),\
int: "int",\
float: "float",\
double: "double",\
default: "other"\
)
使用方法:
int d = 5;
printf("%s\n", MYTYPE(d)); // int
printf("%s\n", MYTYPE(2.0*d)); // double
printf("%s\n", MYTYPE(3L)); // other
内联函数 (C99)
inline function
把函数编程内联函数,编译器可能会用内联代码替换函数调用,并执行一些其他优化,但也有可能不起作用。
具有内部链接的函数可以成为内联函数,内联函数的定义与调用必须在同一个文件中。因此最简单的定义内联函数的方法是使用函数说明符 inline 和存储类别说明符 static. 通常,内联函数应定义在首次使用它的文件中,所以内联函数也相当于函数原型:
#include <stdio.h>
inline static void eatline(){
while(getchar() != '\n')
continue;
}
int main(void){
...
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 + .5);}
void masp(double x){
double kw = square(w);
...
}
上述三个文件定义了同名的函数,参数列表和返回值类型也相同。file1.c 是内联函数;file2.c 是普通函数,具有外部链接;file3.c 是内联函数,但省略了 static。
file1.c 中的main() 使用内联函数,所以编译器有可能优化代码,也许会内联该函数。file2.c 中以正常函数对待之。file3.c 既可以使用本文件中的内联函数,也可以使用 file2.c 中的外部链接定义。file3.c 中的内联函数省略了 static,则 inline 定义被视为可替换的外部定义。
_Noreturn 函数 (C11)
_Noreturn 是继 inline 之后的第二个函数说明符,表明函数调用完成后不返回主调函数。exit() 是它的一个示例。
_Noreturn 的目的是告诉用户和编译器,这个特殊的函数不会把控制返回主调程序。告诉用户以免滥用该函数,通知编译器可优化一些代码。
C 库
最初,并没有官方的C库,后来,基于 UNIX 的C实现成为了标准。ANSI C 以此为基础,开发了一个官方的标准库。后来又重新定义了这个库,以使之可以应用于其他系统。
访问C库
例1:
根据不同的类型选择不同的数学函数
#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), \
double: sinl((X)/RAD_TO_DEG), \
float: sinf((X)/RAD_TO_DEG), \
default: sin((X)/RAD_TO_DEG) \
)
int main(){
float x = 45.0f;
double xx = 45.;
long double xxx = 45.L;
long double y = SQRT(x);
long double yy = SQRT(xx);
long double yyy = SQRT(xxx);
printf("%.17Lf\n", y);
printf("%.17Lf\n", yy);
printf("%.17Lf\n", yyy);
int i = 45;
yy = SQRT(i);
printf("%.17Lf\n", yy);
yyy = SIN(xxx);
printf("%.17Lf\n", yyy);
return 0;
}
output:
$ gcc -o 02math 02math.c -lm
$ ./02math
6.70820379257202148
6.70820393249936942
6.70820393249936909
6.70820393249936942
0.95605565732762954
带有 math.h 头文件的源代码编译时需要带参数 -lm. 因为 UNIX 系统没有 math.h 文件,math.h 中 sin 及其他函数的定义是在 /usr/lib/libm.so 中,所以应当把 libm.so 包含进来。-lm 的作用是在默认目录 /lib, /usr/lib 中查找名叫 libm.so 的文件,然后包含进来。-lm 中的 m 前缀加上 lib,后缀加上 .so 即为文件名 libm.so。如果想要改变默认路径,使用 -L 参数,比如:
gcc sin.c -lm -L/lib -L/usr/lib -o sin
tgmath.h 库 (C99)
tgmath.h 中定义了泛型类型宏,其效果与上节代码中的宏 SQRT 类似。比如定义 sqrt() 宏,根据参数类型(float, double, long double)展开为 sqrtf(), sqrt() 或 sqrtf() 函数。
如果编译器支持复数运算,就会支持 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); // 调用宏
y = (sqrt)(x); // 调用函数
也可以使用指针来调用函数:
(* sqrt)(x)
通用工具库
包含各种函数,包括随机数生成器,查找和排序函数,转换函数和内存管理函数(rand(), srand(), malloc(), free()),他们都在 stdlib.h 中
exit() 和 atexit() 函数
在 exit() 函数执行时,通过 atexit() 函数可以实现执行一些代码的功能。atexit() 函数接受一个函数,当程序退出(包括 exit() 退出和正常退出)时这行这个函数。
#include <stdio.h>
#include <stdlib.h>
void asgin_off(void);
void too_bad(void);
int main(){
int n;
atexit(sign_off);
if(scanf("%d", &n) != 1){
atexit(too_bad); // 可以注册多个函数
exit(EXIT_FAILURE);
}
return 0;
}
void sign_off(void){
...
}
void too_bad(void){
...
}
atexit() 注册的函数应该不带任何参数且返回类型为 void. 通常这些函数会执行一些清理任务,比如更新监视程序的文件或重置环境变量。ANSI 保证保存注册函数的列表至少容纳32个函数。
程序结束时,会隐式调用 exit().
exit() 执行完注册的函数后,会做一些清理工作:刷新所有输出流,关闭所有打开的流和关闭由标准 IO 函数 tmpfile() 创建的临时文件。然后 exit() 把控制权返回主机环境。如果可能的话,向主机环境报告终止状态。通常0代表成功终止,非零代表终止失败。由于 UNIX 返回值并不适合所有系统,ANSI C 为了可移植性,定义了一个 EXIT_FAILURE 的宏表示终止失败。在 ANSI C 中,在非递归的 main() 中使用 exit() 函数等价于使用关键字 return.
qsort() 函数
快速排序方法。1962 C.A.R.Hoare 开发。把数组不断分成更小的数组,直到变成单元素数组。首先把数组分成两部分,一部分的值全部小于另一部分的值。这个过程一直持续到数组完全排序好为止。
快速排序函数原型:
void qsort(void *base, size_t nmemb, size_t size, \
int (*compar)(const void*, const void*));
第一个参数是数组首元素地址,可以兼容任何类型;第二个参数元素数量;第三个参数是每个元素的大小(字节数);第四个参数是用于确定排序顺序的函数指针,如果第一个参数大于第二个参数则返回正数,相同返回0,否则返回负数。
例:
#include <stdio.h>
#include <stdlib.h>
#define NUM 40
void fillarray(double [], int);
void showarray(const double[], int);
int mycomp(const void*, const void*);
int main(void){
double ar[NUM];
fillarray(ar, NUM);
printf("original:\n");
showarray(ar, NUM);
qsort(ar, NUM, sizeof(double), mycomp);
printf("after sort:\n");
showarray(ar, NUM);
return 0;
}
void fillarray(double ar[], int n){
for(int i = 0; i < n; ++i)
ar[i] = (double) rand() / ((double) rand() + .1);
}
void showarray(const double ar[], int n){
for(int i = 0; i < n; ++i){
printf("%9.4f ", ar[i]);
if(i % 6 == 5)
putchar('\n');
}
putchar('\n');
}
int mycomp(const void* p1, const void* p2){
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;
}
output:
original:
2.1304 0.9808 4.6147 0.4364 0.5014 0.7591
0.7105 1.0393 0.8863 0.2333 0.0671 0.1706
0.3908 1.1928 4.5768 0.6113 2.0695 1.2160
0.5074 0.3792 0.6842 0.4489 0.8038 0.8789
0.0735 6.1123 0.2898 2.5516 3.2049 7.2541
0.2455 1.0603 0.4940 0.4935 0.7676 13.5337
0.4697 1.2911 0.3849 1.8076
after sort:
0.0671 0.0735 0.1706 0.2333 0.2455 0.2898
0.3792 0.3849 0.3908 0.4364 0.4489 0.4697
0.4935 0.4940 0.5014 0.5074 0.6113 0.6842
0.7105 0.7591 0.7676 0.8038 0.8789 0.8863
0.9808 1.0393 1.0603 1.1928 1.2160 1.2911
1.8076 2.0695 2.1304 2.5516 3.2049 4.5768
4.6147 6.1123 7.2541 13.5337
注意,C 和 C++ 的
void*的用法有所不同。
C++ 要求把void*指针赋给任何类型的指针时必须进行强制类型转换
而 C 中没有这样的要求,强转操作是可选的
结构的排序
struct names{
char first[40];
char last[40];
};
struct names staff[100];
比较函数的写法:
#include <string.h>
int cmp(const void* p1, const void* p2){
const struct names* n1 = (const struct names*) p1;
const struct names* n2 = (const struct names*) p2;
int res;
res = strcmp(n1->last, n2->last);
if(res != 0)
return res;
else
return strcmp(n1->first, n2->first);
}
断言库
assert.h 头文件支持的断言库是一个用于辅助调试程序的小型库。由 assert() 宏组成,接受一个整型表达式作为参数。
如果表达式求值为假(非零),assert() 宏就在标准错误流(stderr)中写入一条错误信息,并调用 abort() 函数(stdlib.h 中)终止程序。
通常用在程序中某些条件为真的关键位置,如果其中一个具体条件为假,就用 assert() 语句终止程序。由 assert() 终止后,首先会显示失败的测试、包含测试的文件名和行号。
assert 用法
#include <stdio.h>
#include <math.h>
#include <assert.h>
int main(){
double x, y, z;
printf("Enter a pair of numbers (0 0 to quit):\n");
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));
printf("Next pair of numbers (0 0 to quit):\n");
}
printf("Done.\n");
return 0;
}
output:
$ gcc -o 04assert 04assert.c -lm
$ ./04assert
Enter a pair of numbers (0 0 to quit):
2 1
answer is 1.732051
Next pair of numbers (0 0 to quit):
5 3
answer is 4.000000
Next pair of numbers (0 0 to quit):
2 6
04assert: 04assert.c:12: main: Assertion `z >= 0' failed.
Aborted (core dumped)
使用 if 语句也能完成相似的功能:
if (z < 0){
printf("z less than 0\n");
abort();
}
通常,assert() 用于调试,当程序通过了调试,认为已经排除了程序的bug,那么可以定义一个宏,并写在包含 assert.h 的位置前面:
#define NODIBUG
#include <assert.h>
_Static_assert (C11)
assert() 表达式是在运行时进行检查。而 _Static_assert 是在编译时检查 assert() 表达式。所以 assert() 可以导致正在运行的程序中止,而 _Static_assert() 可以导致程序无法通过编译。
_Static_assert 接受两个参数,第一个是整形常量表达式,第二个是一个字符串。如果第一个表达式为0或False,编译器会显示字符串,并且不编译该程序。
例:
#include <stdio.h>
#include <limits.h>
_Static_assert(CHAR_BIT == 16, "16-bit char falsely assumed");
int main(){
printf("char is 16 bits.\n");
return 0;
}
output:
$ gcc -o 05static_assert 05static_assert.c
05static_assert.c:3:1: error: static assertion failed: "16-bit char falsely assumed"
_Static_assert(CHAR_BIT == 16, "16-bit char falsely assumed");
^
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 拷贝到 s1,并返回 s1 的值,他们的区别是参数的关键字 restrict,即 memcpy() 假设两个内存区域之间没有重叠;而 memmove() 允许有重叠,它的拷贝过程类似于先把所有字节拷贝到一个临时缓冲区,再拷贝到最终目的地。当程序中两个指针没有遵循 restrict 约定,则使用 memcpy() 时可能会失败。
另外一个问题是指针类型。由于参数使用 void* 类型,它能兼容任意指针类型,所以 s1 和 s2 两个指针类型可能不同,函数不会检查它们的类型,也不关心类型。拷贝的内容的大小由第三个参数控制,它代表字节数。
可变参数: stdarg.h
定义一个可变参的函数比可变参的宏要复杂。实现步骤如下
- 提供一个使用省略号的函数原型
- 省略号必须写在参数列表的最后
- 最后一个参数(省略号之前的形参)是省略号代表的参数的个数
- 在函数定义中创建一个 va_list 类型的变量
- 用宏把该变量初始化为一个参数列表
- 用宏访问参数列表
- 用宏完成清理工作
函数原型:
void f1(int n, ...);
void f1(const char* s, int k, ...);
该函数使用定义在 stdarg.h 中的 va_start() 宏,把参数列表拷贝到 va_list 类型的变量中。该宏有两个参数:va_list 类型的变量和 paramN 形参。
然后是调用 va_arg() 函数返回参数列表,第一次调用它返回第一个参数项,第二次调用返回第二个参数项,以此类推。
最后调用 va_end() 函数完成清理工作,比如释放动态分配用于存储参数的内存,它接受一个 va_list 类型的变量。
因为 var_arg() 不提供退回之前参数的方法,所以有必要保存 va_list 类型变量的副本。C99 新增了一个宏用于处理这种情况:va_copy(). 接受两个参数,把第一个参数拷贝给第一个参数:
va_list ap, apcopy;
va_start(ap, lim);
va_copy(apcopy, ap);
例:
#include <stdio.h>
#include <stdarg.h>
double sum(int, ...);
int main(){
double s, t;
s = sum(3, 1.1, 2.5, 6.1);
t = sum(6, 1.1, 2.1, 3.2, 4.3, 5.6, 6.8);
printf("s: %f\n", s);
printf("t: %f\n", t);
return 0;
}
double sum(int lim, ...){
va_list ap;
double tot = 0;
va_start(ap, lim);
for(int i = 0, i < lim; ++i){
tot += va_arg(ap, double);
}
va_end(ap);
return tot;
}
output:
s: 9.700000
t: 23.100000

浙公网安备 33010602011771号