1.文件I/O
文件通常是在磁盘或固态硬盘上的一段已命名的存储区,可以存储程序、文档、数据、书信、图片、视频等信息。有时,程序需要从文件中读取信息或者把信息写入文件,这种程序与文件交互的形式就是文件的重定向。C把文件看作是一系列连续的字节,每个字节都能被单独读取。C提供两种文件模式:文本模式和二进制模式。我们知道,所有文件的内容都以二进制数字的形式存储在计算机中。但是,如果文件最初使用二进制编码字符(如ASCII码)表示文本(就像C字符串那样),该文件就是文本文件,其中包含文本内容。如果文件中的二进制值代表机器语言代码或数值数据或图片或音乐编码,该文件就是二进制文件,其中包含二进制内容。
为了规范文本文件的处理,C提供两种访问文件的途径:二进制模式和文本模式。在文本文件中,程序所见的内容和文件的实际内容不同。因为程序以文本模式读取文件时,把本地环境表示的行末尾或文件末尾映射为C模式,而不同的系统这种转换是不同的。在二进制模式中,程序可以访问文件的每一个字节。程序所见内容和实际内容相同,不会发生映射。值得一提的是,在UNIX和Linux系统中,都使用同一种文件格式处理文本文件和为二进制文件内容。
除了选择文件的模式,大多数情况下,还可以选择I/O的两个级别(即处理文件访问的两个级别)。底层I/O使用操作系统提供的基本I/O服务,标准高级I/O使用C库的标准包和stdio.h头文件定义。因为无法保证所有的操作系统都使用相同的底层I/O模型,C标准只支持标准的I/O包。有些实现会提供底层库,但是C标准建立了可移植的I/O模型,我们主要讨论这些I/O。
C程序会自动打开3个文件,它们被称为标准输入(standard input)、标准输出(standard output)和标准错误输出( standard error output)。在默认情况下,标准输入是系统的普通输入设备,通常为键盘;标准输出和标准错误输出是系统的普通输出设备,通常为显示屏。与底层I/O相比,标准I/O包除了可移植以外还有两个好处:第一,标准I/O有许多专门的函数简化了处理不同I/O的问题。第二,输入和输出都是缓冲的。也就是说,一次转移一大块信息而不是一字节信息(通常至少512字节)。
2. 文件I/O函数
2.1. fopen()和fclose()函数
fopen()和fclose()函数都定义在stdio.h头文件里。fopen()函数接受两个参数,第1个参数是待打开文件的名称,更确切的说是一个包含该文件名的字符串地址,第2个参数是一个字符串,指定待打开文件的模式。下表列出C库提供的一些模式:
模式字符串 | 含义 |
---|---|
“r” | 以只读模式打开文件 |
“w” | 以写模式打开文件,把现有文件长度截为0(即删除原内容),如果文件不存在,则创建一个新文件 |
“a” | 以写模式打开文件,在现有文件末尾添加内容,如果文件不存在,则创建一个新文件 |
“r+” | 以更新模式打开文件(即可以读写文件) |
“w+” | 以更新模式打开文件(即可以读、写),若文件存在,将其长度截为0,如果文件不存在,则新建一个文件 |
“a+” | 以更新模式打开文件(即可以读、写),在现有文件末尾添加内容,如果文件不存在,则创建一个新文件;可以读整个文件,但是只能从末尾添加内容 |
“rb”、“wb”、“ab”、“rb+”、“r+b”、“wb+”、“w+b”、“ab+”、“a+b” | 与上一个模式类似,但是以二进制模式而不是文本模式打开文件 |
“wx”、“wbx”、“w+x”、“wb+x”、“w+bx” | C11新增的,类似不带x的模式,但是如果文件已存在或以独占模式打开文件,则打开文件失败 |
补充:
- 带x字母的写模式即使fopen()打开文件失败,也不会删除原文件的内容。如果环境允许,x模式的独占特性使得其它程序或线程无法访问正在被打开的文件;
- fopen()将返回文件指针(file pointer),其它I/O函数可以使用这个指针指向该文件。文件指针的类型是指向FILE的指针,FILE是stdio.h中的派生类型。它不指向实际的文件,指向一个包含文件信息的数据对象,其中,包括操作文件的I/O函数所用的缓冲区信息。可以这样
FILE * fp;
声明文件指针。
fclose()关闭指定的文件,必要时刷新缓冲区。对于较正式的程序,应该检查是否成功关闭文件:fclose(fp) !=0 ;
?如果成功关闭,fclose()函数返回0,否则返回EOF(表示文件结尾的符号)。
2.2. 其它的文件I/O函数
函数 | 含义 |
---|---|
getc(),putc() | 与getchar()和putchar()函数类似,所不同的是,要告诉getc()和putc()函数使用哪一个文件 |
fprintf()、fscanf() | 工作方式和printf()、scanf()函数类似,区别在于前者需要用第1个参数指定待处理的文件 |
fgets()、fputs() | 前面介绍过,fgets()遇到EOF时返回NULL |
fseek()、ftell() | fseek()函数有3个参数,第1个是FILE指针,指向待查找的文件,第2个是偏移量,表示从起点开始要移动的距离,第3个参数确定起始点(见下表起始点模式)。如果一切正常,fseek()返回0,出现错误则返回-1。ftell()返回当前位置距文件开始处的字节数。 |
fread()、fwrite() | 用于以二进制形式处理数据 |
文件的起始点模式:
模式 | 偏移量的起始点 |
---|---|
SEEK_SET | 文件开始处 |
SEEK_CUR | 当前位置 |
SEEK_END | 文件末尾 |
3. C预处理器的指令
C预处理器在程序执行之前查看程序,根据预处理命令,把符号缩写替换成其表示的内容。下面来介绍它们。
#define预处理指令由3部分组成。第1部分是#define指令本身。第2部分是选定的缩写,也称为宏(宏名不允许有空格)。有些宏代表值,这些宏被称为类对象宏(object-like macro)。宏的名称中不允许有空格,而且必须遵循C变量的命名规则。第3部分(指令行的其余部分)称为替换列表或替换体。一旦预处理器在程序中找到宏的示实例后,就会用替换体代替该宏(也有例外,稍后解释)。从宏变成最终替换文本的过程称为宏展开(macro expansion)。
在#define中使用参数可以创建外形和作用与函数类似的类函数宏(function-like macro)。下面是一个类函数宏的示例:
#define SQUARE(X) X*X
在函数中可以这样使用:
z = SQUARE(2);
虽然看上去像函数调用,但是它的行为和函数调用完全不同。这里,SQUARE是宏标识符,SQUARE(X)中的X是宏参数,X*X是替换列表。程序中出现SQUARE(X)的地方都会被X*X替换掉,例如上面例子中被替换为:z = 2 * 2;
。需要注意的是,如果令X=5,然后程序中这样写:SQUARE(X+2)
,那么得到的值将会是17,因为C预处理器只做替换,并不做运算。这里被替换成X+2*X+2
,得到结果为17,因此,我们在写这样的宏定义时要注意圆括号的位置,以保证能得到期望的值。还有使用递增、递减运算符时,可能会得到一些不确定的值,所以一般在宏定义中不要使用递增、递减运算符。
C允许在字符串中包含宏参数,在类函数宏的替换体中,#号作为一个预处理运算符,可以把记号转换成字符串。例如,如果x是一个宏形参,那么#x就是转换为字符串“x”的形参名。与x类似,##运算符可用于类函数宏的替换部分,也可用于对象宏的替换部分。下面程序演示##运算符的用法(程序来源于《C Primer Plus》第六版中文版第16章,书528面程序清单16.4 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
C99/C11中提供了接受变参宏的工具,定义在stdvar.h头文件中。具体例子(程序来源于《C Primer Plus》第六版中文版第16章,书529面程序清单16.5 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;
}
得到结果:
那么使用函数还是使用宏?可以从几点考虑:
- 使用宏比使用普通函数复杂一些,稍有不慎就会产生奇怪的副作用。而且我们在定义宏时,应该只定义成一行。
- 宏节省时间,函数节省空间。如果打算使用宏来加快程序的运行速度,那么首先要确定使用宏和函数是否会导致较大的差异,在程序中只是用一次的宏无法明显减少程序的运行时间。
- 宏的一个优点是不用担心变量类型(因为宏处理的是字符串,不是实际的值)。
C标准规定了一些预定义宏可直接使用(两个下滑线之间实际上没有空格,这里为了显示清楚打了个空格):
宏 | 含义 |
---|---|
_ _DATE_ _ | 预处理的日期(“Mmm dd yyyy”形式的字符串字面量) |
_ _FILE_ _ | 表示当前源代码文件名的字符串字面量 |
_ _LINE_ _ | 表示当前源代码文件中行号的整型常量 |
_ _STDC_ _ | 设置为1时,表明实现遵循C标准 |
_ _STDC_HOSTED_ _ | 本机环境设置为1,否则设置为0 |
_ _STDC_VERSION_ _ | 支持C99标准设置为1999901L,支持C11标准设置为201112L |
_ _TIME_ _ | 翻译代码的时间,格式为“hh:mm:ss” |
#include指令在系列2中介绍过了。头文件中最常用的形式有:明示常量、宏函数、函数声明、结构模板定义、类型定义。
4. 其它指令
预处理器提供一些其它的指令
指令 | 含义 |
---|---|
#undef | 取消已定义的#define指令,即使原来没有定义,该命令仍然有效 |
#ifdef、#else、#endif | #ifdef指令说明,如果预处理器已定义了后面的标识符,则执行#else或#endif指令之前的所有指令并编译所有C代码。#ifdef和#endif要配套使用 |
#ifndef | 和#ifdef用法类似,用于判断后面的标识符是否是未定义的,常用于定义之前未定义的常量。防止多次包含一个文件,与#endif配套使用 |
#if、#elif | #if指令类似于C中的if,#if后面跟整型常量表达式,如果表达式非0,则为真。 |
#line | 重置_ _LINE_ _和_ _FILE_ _宏报告的行号和文件名 |
#error | 让预处理器发出一条错误消息,该消息包含指令中的文本,如果可能的话,编译过程应该中断 |
#pragma | 把编译器指令放入源代码中,如#pragma c9x on 让编译器支持C9X |
看几个例子更容易明白:
#ifdef MAVIS
#include "horse.h" //如果已经用#define定义了MAVIS,则执行下面的命令
#define STABLES 5
#else
#include "cow.h" //如果没有用#define定义了MAVIS,则执行下面的命令
#define STABLES 5
#endif
可以在#if、#elif指令中使用关系运算符和逻辑运算符,可以这样写:
#if SYS == 1
#include "ibmpc.h"
#elif SYS == 2
#include "vax.h"
#elif SYS == 3
#include "mac.h"
#endif
在较新的编译器中,也可以这么写:
#if define (IBMPC)
#include "ibmpc.h"
#elif define (VAX)
#include "vax.h"
#elif define (MAC)
#include "mac.h"
#endif
这里define是一个预处理运算符,如果它的参数是用#define定义过,则返回1,否则返回0。
#line 1000 "test.c"//把行号重置为1000,文件名重置为test.c
#if __STDC_VERSION__ != 201112L
#error Not C11
#endif
前面介绍过内联函数,inline是C99新增的唯一的函数说明符,C11又新增了第2个函数说明符——_Noreturn,表明调用函数后不返回主调函数。exit()函数就是_Noreturn函数的一个示例。
5. 几个函数
C库有许多函数可供使用,前面的字符串函数,文件I/O函数都属于库里的函数。这里再介绍几个通用库里的函数。
函数 | 含义 |
---|---|
exit() | 终止程序,0表示正常退出,1表示意外退出 |
qsort() | 快速排序算法 |
memcpy()、memmove() | 处理数组,都从s2指向的位置拷贝n字节到s1指向的位置,而且都返回s1。但是memcpy()假设两个存储区之间没有重叠,memmove()不做这样的假设 |
assert.h头文件支持的断言库是一个用于辅助调试程序的小型库。由assert()宏组成,接受一个整型表达式作为参数。
6. 结束语
在这里,C语言的基础知识总结(笔记)就写完了,限于目前水平,其实还有很多知识点没有包括进去,有些地方也是模棱两可。在接下来的学习中,希望能不断完善。