基于存储的C语言文件操作常规问题分析(文本文件与二进制文件)
问题描述
我们都知道计算机中的数据都是以二进制存储的,也对不同的编码解码有所了解,比如ASCII、UNICODE、UTF-8编码等。 但当使用C语言对文件进行读写,会遇到了各种各样的问题,比如从文件中读取到的数据不是我们想要的数据,向文件中写入数据变成乱码,对C语言读写文件的各种参数不甚了解,读写位置与各种符号不好判断等问题。在程序编写的过程中只能不断查询各种读写函数的用法,不同数据类型的读写方式等。其主要原因是我们对文件的存储、编码以及读写文件的函数没有进行过系统性的了解。
文本文件与二进制文件
文本文件和二进制文件在本质上没有任何区别,都是二进制数据流。当我们使用文本工具打开一个文件时,会根据编码方式将文本数据解码,翻译成字符显示出来。所以我们使用文本工具打开文件,看到的其实都是字符。即便整个文本都是数字,那也是数字字符,所以当我们读取文件中的数据时,我们可能想读取的是该数字字符表示的值,但是使用fread和read读取到的,是文本数据本质上(物理上)存储的值,所以会发现读取到的值并不是我们想要的值。什么意思呢?
如图,我们建立一个文件,写入四行数据,第一行1 2 4 a (中间用空格隔开),但是,当我们将该文件以十六进制方式打开,会发现该文件是下图这样的:
上图是根据ASCII编码方式将二进制数据翻译成字符,显示出来给我们看的。下图是该文件真实的数据,该文件的真面目,也就是该文件的二进制形式。
包括空格,换行符等,在文件中也都对应一个字符。
现在我们写一个程序读取该文件,程序如下:
#include<stdio.h>
#include<malloc.h>
typedef struct{
int num1;
int num2;
int num3;
char cz;
}Data,*PData;
int main(int argc,char* argv[]){
FILE* fp=fopen("data.txt","rb");
PData pData=(PData)malloc(sizeof(Data)*2);
fseek(fp,0,SEEK_SET);
fread(pData,sizeof(Data),2,fp);
for(int i=0;i<2;i++){
printf("%x %x %x %c\n",pData[i].num1,pData[i].num2,pData[i].num3,pData[i].cz);
}
fclose(fp);
}
该程序我们预期得到的结果是前两行的数据1 2 4 a 和 2 2 2 b,但是执行程序之后我们得到的数据见下图,和预期不符,反而和十六进制打开该文件的数据是一致的。
这是因为fread从起始位置,根据要读取数据量,将文本的二进制数据读取到我们定义的buf中。注意,读取到的不是我们用文本工具打开看到的数字字符,是该数字字符对应ASCII数据。比如结构体中的num1-num3是int型数据,四字节,第一个num1的值就是原文件中前四个字节(两个空格和字符1,字符2)对应的值。(如果不明白为什么顺序是反的,请看小端存储和大端存储)
数据写入文本乱码问题
用fwrite和write将字符串写入文件,不会出现问题,但是当我们将数据写入到文件的时候,打开文件会发现乱码了,也是同样的道理。
#include<stdio.h>
#include<malloc.h>
typedef struct{
int num1;
int num2;
char cz;
}Data,*PData;
int main(int argc,char* argv[]){
FILE* fp=fopen("data.txt","a+");
PData pData=(PData)malloc(sizeof(Data)*2);
for(int i=0;i<3;i++){
pData[i].num1 = 1;
pData[i].num2 = 2;
pData[i].cz = 'f';
}
fseek(fp, 0, SEEK_END);
fwrite(pData, sizeof(Data), 3, fp);
char* str = "aabbccdd";
fwrite(str, sizeof(str), fp);
fclose(fp);
}
上述代码向文件中写三次int型数据 1 ,2,字符 f ,再在末尾写入字符串aabbccdd,运行后打开文件如下图。
发现出现乱码,而字符f成功写入,字符串aabbccdd也没有乱码,也就是说乱码的数据是int型数据。 再查看该文件的十六进制数据,得下图
发现数据是按照int型数据的存储方式写入文件,之所以乱码是因为文本将该int型的数据按照ASCII编码的方式一个字节一个字节的解码显示,所以会出现乱码。
至于为什么字符f也会占四个字节,是因为代码运行的时候自动4字节对齐,所以结构体Data的大小sizeof(Data)的值是12。
综上所述,我们在写C语言读写文件的时候,会出现读取的数据不是想要的数据,写入的数据乱码,都是因为我们看到的是解码后的字符,即便是数字也是字符,读取的时候是该字符对应的ASCII码值。
所以,C语言对文件的读写是基于二进制文件的,当数据是字母字符和字符串时,读取写入不会存在太大问题,因为字符不存在二义性,当数据是数字时,就需要注意,数字字符和数字存在二义性。
fopen和open
fopen和open的区别主要在于C语言对文件的两种操作方式,流式文件操作和直接文件操作。
流式文件操作是将文件读取到缓冲区中,然后读写时都是和缓冲区进行交互。直接I/O文件操作不需要经过缓冲区,当然并不是全过程都不需要。
fopen和open区别详解.
流式文件操作常用函数
函数 | 描述 | 返回值 |
---|---|---|
fopen | FILE *fopen(const char *filename, const char *mode) ; 打开流 | 返回指向该流的文件指针。如果文件打开失败则返回 NULL |
fclose | int fclose( FILE *fp ); 关闭一个流 | 成功关闭,fclose 返回 0,否则返回EOF(-1) |
fread | size_t fread( void *buffer, size_t size, size_t count, FILE *stream );//C99前 。从流中读指定个数的字符(相当于对每个对象调用size次fgetc) | 返回成功读取的对象个数,若出现错误或到达文件末尾,则可能小于count。 |
fwrite | size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) ,向流中写指定个数的字符。该函数以二进制形式对文件进行操作,不局限于文本文件。 | 返回实际写入的数据块数目 |
fgetc | int fgetc(FILE *stream); 从流中读一个字符 | 字符的ASCII值作为函数的返回值,若返回值为EOF,说明文件结束 |
fputc | int fputc (int c, File *fp) 写一个字符到流中 | 函数返回写入文件的字符的ASCII码值,出错时,返回EOF(-1) |
fgets | char *fgets(char *str, int n, FILE *stream); 从流中读一行或指定个字符 | 如果成功,该函数返回相同的 str 参数如果发生错误,返回一个空指针 |
fputs | int fputs(const char *str, FILE *stream); 写字符串到流 | 该函数返回一个非负值,如果发生错误则返回 EOF(-1) |
fprintf | int fprintf (FILE* stream, const char*format, [argument]) ;按格式输出到流 | fprintf()的返回值是输出的字符数,发生错误时返回一个负值. |
fscanf | int fscanf(FILE * stream, const char * format, [argument…]);从流中按格式读取 | 整型,成功返回读入的参数的个数,失败返回EOF(-1) |
feof() | int feof(FILE *stream); | 到达文件尾时返回真值 |
ferror | int ferror(FILE *stream); | 如果ferror返回值为0(假),表示未出错。如果返回一个非零值,表示出错 |
fseek | int fseek(FILE *stream, long offset, int fromwhere);函数设置文件指针stream的位置 | 成功,返回0,失败返回非0值,并设置error的值,可以用perror()函数输出错误。 |
rewind | void rewind(FILE *stream);将文件内部的指针重新指向一个流的开头 ,rewind函数作用等同于 (void)fseek(stream, 0L, SEEK_SET); | 无 |
remove | int remove(cha r*filename)删除文件 (通用) | 成功则返回0,失败则返回-1 |
tmpfile | FILE *tmpfile(void); 生成一个临时的文件流 | 指向该流的文件指针 |
直接I/O文件操作常用函数
函数 | 描述 | 返回值 |
---|---|---|
open | int open(constchar*pathname,intflags);打开和创建文件。open是UNIX系统(包括LINUX、Mac等)的系统调用函数 | 成功则返回文件描述符,否则返回-1 |
close | int close(int fd); 关闭一个句柄 | 成功返回0,出错返回-1 |
lseek | off_t lseek(int handle, off_t offset, int fromwhere); 定位到文件的指定位置 | 当调用成功时则返回目前的读写位置,也就是距离文件开头多少个字节。若有错误则返回-1 |
read | ssize_t read (int fd, void *buf, size_t count); 块读文件 | 成功返回读取的字节数,出错返回-1 |
write | int write(int handle, void *buf, int nbyte)。把参数buf所指向的内存空间,写入count个字节,任何赋给参数fd所指的文件内。当然,文件读写位置也会随之移动。 | 如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1 |
eof | EOF(filenumber) ;判断文件读写位置是否已经到达了文件尾 | 返回一个数值(整数类型Integer),它包含布尔值Boolean值True,表明已经到达为Random(Open语句方法)或顺序Input (Open 语句方法)打开的文件的结尾 |
filelength | 获取文件的长度,但是最大只能获取2g的文件大小,因为返回值类型long使用4个字节大小来表示,最大为2的31次方也就是2G的大小。 | 文件的大小,单位为字节 |
rename | int rename(char *oldname, char *newname);重命名文件,把一个文件的完整路径的盘符改一下就实现了这个文件的移动 | 成功则返回0,失败则返回-1 |
chsize | int chsize(int handle, long size);改变文件大小,为文件预分配空间等。 | chsize 当文件大小被成功改变时返回 0 值,当发生错误时返回 -1。 |