C和指针:第十五,十六章
第15章 输入/输出函数
1. 错误报告:perror函数
操作系统在执行任务时,都存在失败的可能。标准库函数在一个外部整型变量errno(在errno.h中定义)中保存错误代码之后把这个信息传递给用户这些特定错误的过程。函数原型为:
void perror(char const *message); // 如果message不是NULL并且指向一个非空字符串,perror就打印这个字符串。
即使是那些十拿九稳不会失败的操作也应进行检查,因为它们迟早可能失败。这种检查需要稍许额外的工作,但和付出大量调试时间相比,它们还是非常值得的。
2. 终止执行:exit函数
函数原型:
void exit(int status);
status参数返回给操作系统,用于提示程序是否正常完成。预定义符号:EXIT_SUCCESS和EXIT_FAILURE分别提示程序的终止是成功还是失败。
这个函数没有返回值,因为当exit函数结束后,程序已经消失,所以无处可返。
3. 标准I/O
1.) 流
C语言所有的I/O操作只是简单地从程序移进或移出字节的事情,而这些字节流便称为流(stream)。绝大多数流是完全缓冲的(fully buffered),这意味着“读取”和“写入”实际上是从一块被称为缓冲区(buffer)的内存区域来回复制数据。
对于输出流的缓冲区只有当它写满时才会被刷新(flush,物理写入)到设备或文件中。一次性把写满的缓冲区写入和逐片把程序产生的输出写入相比效率更高。
只有当操作系统可以断定它们与交互设备并无联系时才会进行完全缓冲,否则,它们的缓冲状态将因编译器而定。在调试时一个常见的策略是,将printf函数散布于程序中,确定错误出现的具体位置。但这些函数调用的输出结果被写入到缓冲区中,并不立即显示在屏幕上。事实上,如果程序失败,缓冲输出可能不会被实际写入,这样可能等到关于错误出现位置的不正确结论。解决方法是,在每个用于调试的printf函数后立即调用 fflush,如:
printf(“something or other”);
fflush(stdout); // 迫使缓冲区的数据立即写入,不管它是否已满
流分两种,文本(text)流和二进(binary)流。文本流的有些特性在不同系统中可能不同。二进制流的字节将完全根据程序编写它们的形式写入到文件或设备中,而且完全根据它们从文件或设备读取的形式读入到程序中,它们并未作任何改变。
2.) 文件(FILE)结构
FILE结构是一个数据结构,每个流都有一个对应的FILE与它关联。对于每个ANSI C程序,运行时系统都必须提供至少三个流:标准输入(standard input),标准输出(standard output)和标准错误(standard error)。这些流的名字分别是:stdin, stdout和stderr,它们都指向FILE结构的指针。
程序为每个处于活动状态的文件声明一个指针,类型为FILE *,这个指针指向FILE结构。I/O函数以三种基本的形式处理数据:单个字符,文本行和二进制数,具体如下:
数据类型 | 输入 | 输出 | 描述 |
字符 | getchar | putchar | 读取(写入)单个字符 |
文本行 | gets scanf | puts printf | 文本行未格式化的输入(输出) 格式化的输入(输出) |
二进制数据 | fread | fwrite | 读取定(写入)二进制数据 |
3.) 打开流
fopen函数打开一个特定的文件,并把一个流和这个文件相关联。函数原型为:
FILE *fopen (char const *name, char const *mode);
两个参数都是字符串,第一个字符串是你要打开的文件或设备名,第二个参数是指定打开的模式是只读,只写还是既读又写,以及以文本流还是二进制流打开。以下是常用模式:
读取 | 写入 | 添加 | |
文本 | "r" | "w" | "a" |
二进制 | "rb" | "wb" | "ab" |
关于文件的读取:如果一个文件打开是用于读取的,那么这个文件必须是已存在的。
关于文件的写入:如果一个文件打开是用于写入的,这个文件如果已存在,那么它原来的内容就会被删除;如果该文件不存在,就创建一个以该文件名为名字的文件。
关于文件的添加:如果一个文件打开是用于添加的,这个文件如果已存在,那么它原来的内容不会被删除;如果该文件不存在,就创建一个以该文件名为名字的文件。
如果fopen函数执行成功,返回一个指向FILE结构的指针,该结构代表这个新创建的流。如果函数执行失败,返回一个NULL指针,errno会提示问题的性质。
注意:你应该始终检查fopen函数的返回值!如果函数失败,会返回一个NULL值,如果程序不检查错误,这个NULL指针会传给后续的I/O函数,并将对这个指针执行间接访问,这样将导致错误。如:
FILE *input;
input = fopen( “data3”, “r”);
if(input == NULL) // 检查fopen函数打开的结果
{
perror(“data3”);
exit(EXIT_FAILURE);
}
4.) 关闭流
fclose函数原型:
int fclose(FILE *f);
fclose函数在文件关闭之前刷新缓冲区,如果关闭成功,返回值为0,如果失败,返回EOF
同样,对fclose函数也要做返回值的检查,以防止程序出错。
4. 字符I/O
1.)字符的输入需要用到getchar 函数家庭,原型如下:
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);
fgetc, getc 用于所有的流,getchar只用于stdin流。每个函数都从流中读取下一个字符,并把它作为函数的返回值返回(返回值都是int型而不是char型)。如果流中不存在更多的字符,函数就返回常量值EOF。
2.) 字符的输出可以用putchar函数家庭,原型如下:
int fputc(int character, FILE *stream);
int putc(int character, FILE *stream);
int putchar(int character);
fputc, getc 用于所有的流,putchar只用于stdout流。以上函数中第一个参数是被打印的字符,在打印之前,函数把这个整形参数裁剪为一个无符号字符型值,所以:
putchar(‘abc’); // 只打印一个字符,至于打印哪一个,不同的编译器处理结果不一样
3.) 撤销字符读入:ungetc 函数
函数原型:int ungetc(int character, FILE *stream); // 它把一个先前读入的字符返回到流中,这样它可在后面重新被载入
“退回”的字符和流的当前位置有关,如果用fseek, fsetops或rewind函数改变了流的位置,所有退回的字符都将被丢弃
5. 未格式化的行I/O
未格式化的I/O只是简单读取或写入字符串。gets和puts函数家庭用于处理字符串而不是单个字符,函数原型如下:
char *fgets(char *buffer, int buffer_size, FILE *stream); // 从指定的stream读取字符并把它们复制到buffer中,直到读取到换行符或读取的字符数达到buffer_size - 1个时就停止。如果在读取字符前就碰到文件尾,缓冲区就未修改,函数并返回NULL指针,否则返回第一个参数的指针。另外,fgets无法把字符串读入到一个长度小于2个字符的缓冲区,因为其中的一个字符需要以NUL字节保留。
char *gets(char *buffer);
int fputs(char const *buffer, FILE *stream); // 第一个参数中缓冲区必须包含一个以NUL结尾的字符串
int puts(char const *buffer);
gets和puts函数几乎与fgets和fputs相同,它们之间的一个主要功能性区别是:当gets读取一行后,不在缓冲区中存储结尾的换行符NUL。且gets函数没有缓冲区大小,也就是如果一个读取的字符串长度大于缓冲区时,多出来的字符将写入到缓冲区后面的内存中,这将破坏一个或多个不相关变量的值;puts写入一个字符串时,它在字符串写入之后向输出再添加一个换行符。
6. 格式化的行I/O
1.) scanf家庭
函数原型如下:
int fscanf(FILE *stream, char const *format, …); // 从第一个参数的文件结构流读取,可以是任何输入流
int scanf(char const *format, …); // 从标准输入读取
int sscanf(char const *string, char const *format, …); // 从第一个参数给出的字符串中读取
以上函数读取到达末尾或输入不匹配格式字符串所指定的类型时,输入就停止。函数返回被转换输入值的数目,如果在读取前就已到达尾部,函数返回EOF
函数无法验证以上函数中格式化参数的正确性,所以函数默认它们都是正确的,如果执行时,指针参数的类型与格式化参数的类型不匹配,结果将是垃圾值,并且邻近的内存变量也可能被修改。
scanf函数的格式化参数以一个百分号开始,后面可以是:a. 一个可选的星号;b. 一个可选的宽度;c. 一个可选的限定符;d. 格式代码。
a. 星号将使转换后的值被丢弃而不进行存储,这个技巧可用于跳过不需要的输入字符;
b. 宽度以一个非负的整数给出,它限制被读取用于转换输入的字符个数。
c. 限定符如下:
格式符 | h | l | L |
d, i, n | short | long | |
o, u, x | unsigned short | unsigned long | |
e, f, g | double | long double |
在缺省的整形长度和short相同的机器上,转换一个short值时限定符h并非必需的,但对于那些缺省的整形长度比short长的机器上时,这个限定符是必须的。所以,如果在转换所有short和long型和long double型变量时都使用适当的限定符,将增强程序的可移植性。
2.) printf家庭
函数原型如下:
int fprintf(FILE *stream, char const *format, …); // 从第一个参数FILE流中输出,可以是任何输出流
int printf(char const *format, …); // 从标准输出流中输出
int sprintf(char *buffer, char const *format, …); // 将输出结果存储到buffer缓冲区中(buffer是一个指向用于存储数据的内存位置的指针),而不是输出到流中。它有一个潜在的危险,不对缓冲区的大小做检查,如果输出结果很长溢出缓冲区时,可能改写缓冲区后面内存位置中的数据。 可以使用snprintf函数来解决这一问题(snprintf为C99中新增的函数)。
和scanf家庭函数一样,printf家庭函数对格式化参数不做检查,如果格式化参数与被输出的数据类型不匹配,结果也将是垃圾值或导致程序失败。
7. 二进制I/O
数据读写效率最高的是二进制读写,因为二进制避免了数据转换过程中所涉及的开销和精度损失。
fread函数用于读取二进制数据,fwrite函数用于写入二进制数据,函数原型如下:
size_t fread(void *buffer, size_t size, size_t count, FILE *stream);
size_t fwrite(void *buffer, size_t size, size_t count, FILE *stream);
buffer是一个指向用于存储数据的内存位置的指针,size是缓冲区中每个元素的字节数,count是读取或写入的元素数,stream是数据读取或写入的流。函数返回值是实际读取或写入的元素(非字节)数目。如果输入过程中遇到文件尾或输入过程中出现错误,返回值可能比请求的元素数目小。
8. 刷新和定位函数
1.) fflush函数
函数原型: int fflush (FILE *stream); // 迫使一个输出流的缓冲区内数据做物理写入,不管该缓冲区是否已被写满
调用fflush函数可以保证调试信息实际打印出来,而不是保存在缓冲区中直到后面才打印。
2.) 随机访问函数:ftell, fseek
函数原型如下:
long ftell(FILE *stream);
int fseek(FILE *stream, long offset, int from);
ftell 函数返回流的当前位置,也就是下一个读取或写入将要开始的位置距文件起始位置的偏移量。在二进制中,这个值就是当前位置距文件起始位置之间的字节数。在文本流中,这个值并不一定准确地表示当前位置和文件起始位置之间的字符数,因为有些系统对末字符进行翻译转换。但,ftell 函数返回的值总是可以用于fseek 函数中,作为一个距离文件起始位置的偏移量。
fseek 函数允许在一个流中定位。这个操作将改变一个读取或写入操作的位置,第1个参数是需要改变的流,第2,3个参数标识文件中需要定位的位置,它们可以是如下:
如果from是… | 你将定位到… |
SEEK_SET | 从流的起始位置起offset个字节,offset必须是一个非负值 |
SEEK_CUR | 从流的当前位置起offset个字节,offset的值可正可负 |
SEEK_END | 从流的尾部位置起offset个字节,offset的值可正可负。如果为正,它将定位到文件尾的后面 |
a. 试图定位到一个文件的起始位置之前是一个错误。定位到文件尾之后并进行写入将扩展这个文件。
b. 定位到文件尾之后并进行读取将导致返回一条“到达文件尾”的信息。
c. 在二进制流中,从SEEK_END进行定位可能不被支持,所以应避免。
d. 在文本流中,如果from是SEEK_CUR或SEEK_END,offset的值必须是零。如果from是SEEK_SET,offset必须是一个从同一个流中以前调用ftell所返回的值。
加外,使用fseek 改变一个流的位置会带来三个副作用:一是,行末指示字符被清除;二是,如果在fseek 之前使用ungetc 把一个字符返回到流中,那么这个被返回的字符会被丢弃,因为定位操作后,它不再是“下一个字符”;三是,定位允许从写入模式切换到读取模式,或者回到打开的流以便更新。
9. 其它流操作
1.) 改变缓冲方式
当在流上执行的缓冲方式不适合时,有以下两个函数可以对缓冲方式进行修改,前提是被打开的流未执行任何操作。
函数原型如下:
void setbuf(FILE *stream, char *buf);
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
2.) 流错误函数
int feof (FILE *stream);
int ferror(FILE *stream);
void clearerr(FILE *stream);
可用以上函数对流状态做出判断:如果流处于文件尾,feof 函数返回真。这个状态可以通过对流执行fseek, rewind 或 fsetpos 函数来清除。ferror 函数报告流的错误状态,如果现出任何的读/写错误就返回真。clearerr 函数对流的错误标志进行设置。
第16章 标准函数库
标准库函数是C 的一个功能扩展,可大大提高程序的功能。
1. 算术函数<stdlib.h>
标准库函数包含了四个整形算术函数:
int abs(int value); // 返回参数的绝对值,如果结果不能用一个整数表示,结果是未定义的
long int labs(long int value); // 功能同上,只是参数可以是长整型
div_t div(int numerator, int denominator); // 把第2个参数(分母) 除以第1个参数(分子),产生商和余数,并用一个div_t 结构返回,这个结构包含:int quot; int rem; 商和余数两个字段,这两个字段不一定以这个顺序出现。另外,‘/’ 操作符除法运算结果未精确定义,当两个参数中任何一个为负时,结果由编译器决定。
ldiv_t ldiv(long int numer, long int denom); // 功能同上,只是参数可处理长整形数
2. 随机数<stdlib.h>
以下函数能够产生伪随机数(pseudo-random number),之所以称为伪随机数,是因为产生的随机数有可能会重复出现。
int rand(void); // 返回一个范围在0-RAND_MAX(至少为32767)之间伪随机数,当重复调用时,函数返回这个范围内的其他数。
void srand(unsigned int seed); // 产生随机数的种子(seed)
3. 字符串转换<stdlib.h>
int atoi(char const *string); // 将字符转换为整型并返回,要求字符是10进制形式的字符,如果不是0-9内字符,返回值为0
long int atol(char const *string); // 将字符转换为长整型并返回,要求字符是10进制形式的字符,如果不是0-9内字符,返回值为0
long int strtol(char const *string, char **unused, int base); // 将第1个参数按第3个参数为基数转换为长整形数,第3个参数范围从2-36,或0,它代表了进制的方式。如值为10时,采用10进制转换,若值为16就按16进制转换。另外,当值为0时是采用10进制做转换的,但遇到‘0x’前置的字符时还是采用16进制转换,遇到‘0’前置而不是0x则采用8进制转换。一开始strol会扫描第1个参数的字符串,并跳过前导空格,直到遇上数字或正负符号才做转换,再遇到非数字或字符串的结束符‘\0’结束转换,并返回结果。如果第2个参数不为NULL,会指向第1个参数终止的位置。
unsigned long int strtoul(char const *string, char **unused, int base); // 功能同上,只是返回一个无符号的整形数
4. 三角函数<math.h>
double sin(double angle); // 返回正弦值
double cos(double angle); // 返回余弦值
double tan(double angle); // 返回正切值
double asin(double angle); // 返回反正弦值(弧度)
double acos(double angle); // 返回反余弦值(弧度)
double atan(double angle); // 返回反正切值(弧度)
double atan2(double x, double y); // 返回表达式x/y 的反正切值,范围值范围在-pi - pi之间的弧度
5. 双曲函数<math.h>
double sinh(double angle); // 返回双曲正弦值
double cosh(double angle); // 返回双曲余弦值
double tanh(double angle); // 返回双曲正切值
6. 对数和批数函数<math.h>
double exp(double x); // 返回e值的x次幂
double log(double x); // 返回以x以e为底的对数,也就是自然对数
double log10(double x); // 返回x以10为底的对数
7. 浮点表示形式<math.h>
double frexp(double value, int *exponent); // 把value分解为数字部分x 和以2为底的指数部分n。即 value = x * 2n,其中n存放在exp指向的变量中,x的范围在0.5 <= x < 1
double ldexp(double fraction, int exponent); // 计算x * 2n,是上面的函数的逆过程
double modf(double value, double *ipart); // 把浮点值value分成整数和小数两部分,每部分都和原值一样的符号。整数以doulbe类型存储在第2个参数所指向的内存空间,小数部分作为函数返回值返回
8. 幂<math.h>
double pow(double x, double y); // 返回xy 的值
double sqrt(double x); // 返回参数的平方根,如果参数为负,函数定义域错误
9. 底数,顶数,绝对值和余数<math.h>
double floor(double x); // 返回不大于参数的最大整数,这个值以double值返回
double ceil(double x); // 返回不小于参数的最小整数值
double fabs(double x); // 返回参数的绝对值
double fmod(double x, double y); // 返回x 除以 y 所生产的余数,这个除法的商被限制为一个整数
10. 字符转换<stdlib.h>
double atof(char const *string); // 将字符串参数转换为double值并返回,忽略前导空白符,和忽略后缀的非法字符,如果没有可转换字符,返回0
double strtod(char const *string, char **unused); // 和上面函数功能类似,另外,如果第2个参数不是NULL,它保存第一个参数停止转换的位置
11. 处理器时间<time.h>
clock_t clock(void); // 返回程序开始执行处理器所耗的时间,这个值是个近似值,如果无法提供处理器时间,或时间值太大,无法用clock_t 表示,返回-1。
12. 当天时间<time.h>
time_t time(time_t *returned_value); // 如果参数是一个非 NULL的指针,时间值将通过这个指针进行存储。如果机器无法提供当前的日期和时间,或时间值太大,无法用time_t表示,返回-1。另外,调用time函数两次并把两个值相减,来判断流逝的时间是很危险的,因为标准并未要求函数的结果值用秒来表示。
13. 日期和时间的转换<time.h>
char *ctime(time_t const *time_value); // 返回一个指向字符串的指针,字符串格式为:Sun Oct 19 15:28:48 2010\n\0。字符串内部的空格是固定的。
double difftime(time_t time1, time_t time2); // 返回time1 – time2 的差,并把结果转换为秒返回,返回类型为double
struct tm *gmtime(time_t const *time_value); // 把时间值转换为世界协调时间(Coordinated Universal Time, UTC),UTC以前被称为格林尼治标准时间(Greenwith Mean Time)
struct tm *localtime(time_t const *time_value); // 把一个时间值转换为当地时间
char *asctime(struct tm const *tm_ptr); // 把函数参数所表示的时间值转换为字符串,格式为:Sun Oct 19 15:28:48 2010\n\0
size_t strftime(char (char *string, size_t maxsize, char const *format, struct tm const *tm_ptr); // 把一个tm结构转换为一个根据某个格式字符串而定的字符串
time_t mktime(struct tm *tm_ptr); // 把一个tm结构转换为一个time_t的值
13. 终止执行<stdlib.h>
void abort(void); // 终止一个不正在执行的程序
void atexit(void (func) (void)); // 把一些函数注册为退出函数(exit function),当程序将要正常终止时,退出函数被调用,退出函数不能接受任何参数
void exit(int status); // 正常终止程序,当exit被调用时,所有被atexit函数注册为退出函数的函数将按照所注册的顺序被反顺序依次调用
14. 断言<assert.h>
void assert (int expression);
函数被执行时,宏对参数表达式进行测试,如果值为假,就向标准打印一条诊断信息并终止程序,这条信息的格式由编译器决定,它将包含这个表达式和源文件的名字以及断言所在的行号;如果表达式为真,不打印任何东西,程序继续执行。如:
assert(value != NULL); // 如果函数错误地接受了一个NULL参数,程序将打印一条类似下面形式的信息:
Assertion failed: value != NULL, file.c line 24
另外,assert只适用于验证必须为真的表达式,由于它会终止程序,所以你无法用它检查那些你试图进行处理的情况,如检测非法的输入并要求用户重新输入一个值。
15. 排序和查找<stdlib.h>
void qsort(void *base, size_t n_elements, size_t el_size, int (*compare) (void const *, void const *));
第1个参数指向需要排序的数组,第2个参数指定数组中元素的数目,第3个参数指定每个元素的长度(以字符为单位),第4个参数是一个函数指针,它指向的函数对需要排序的元素类型进行比较。 比较函数接受两个参数,它们指向两个需要进行比较值,该函数返回一个整数,>0, == 0, <0,分别表示第1个参数大于,等于和小于第2个参数。且此比较函数与类型无关,因此必须使用强制类型转换把它们转换为适合的指针类型。
void *bsearch(void const *key, void const *base, size_tn_elements, size_t el_size, int (*compare) (void const *, void const *));
第1个参数指向需要查找的值,第2个参数指向查找所在的数据,第3个参数指定数组中元素的数目,第4个参数是每个元素的长度(以字符为单位),最后一个参数是和qsort中相同的指向比较函数的指针。bsearch 返回一个指向查找到数组元素的指针,如果需查找的值不存在,返回一个NULL指针。