本文讲述Linux C 中对文件的操作。分为以下几部分内容:

  1. 打开方式 (包含 创建 能力)
  2. 读、写操作

    读、写单个字符
    读、写字符串
    格式化读、写操作

  3. 数据块的读写
  4. 错误码判断与操作
  5. 位置指针移动
  6. 关闭文件
  7. 删除文件

以上操作涉及的函数#include <stdio.h>即可。


#打开方式

FILE *fopen(const char *path, const char *mode);
FILE *fdopen(int fd, const char *mode);
FILE *freopen(const char *path, const char *mode, FILE *stream);

打开一个文件可以用以上三个函数来实现,该函数执行后则会有相应的流与FILE*绑定。
如果函数正常执行则会返回FILE *,否则返回NULL,且具体的失败原因可以从errno中查询。

#####FILE *fopen(const char *path, const char *mode); #####

fopen()的第一个参数path用于指定要打开的文件名,第二个参数mode指定打开的方式模式。打开文件的模式可以是下列表中的某一项,或者某几项的组合。下表中给出了子模式的含义,且与其它模式的区别。

|模式|文件不存在|读写能力|读位置|写位置|写操作对原文件内容影响|
|—-|—-|
|r|—-|只读|文件开头|—-|—-|
|r+|—-|读写|文件开头|文件开头|原内容被新内容从文件开头按顺序被逐个字符地覆盖|
|w|创建|只写|—-|文件开头|原内容被清空,新内容从文件开头写入|
|w+|创建|读写|文件开头|文件开头|原内容被清空,新内容从文件开头写入|
|a|创建|只写|—-|文件结尾|新内容写在原内容后面|
|a+|创建|读写|文件开头|文件结尾|新内容写在原内容后面|

r: read w:write a:append b:binary

有些时候会看到文件打开模式除了上面的几种情况或由这几种情况的任意组合外,还会有和b的组合,含义是:
出现b表示以 二进制文件方式 操作文件,否则以 文本文件方式 操作。

模式w+a+虽然都具有读权限,但实践中有可能出现无法读出文件中的内容,原因如下:

  • 使用fopenw+一开始会把文件清空,这时候去读是没有内容的;写操作完毕后文件已经有内容了,这时候直接去读取,由于文件指针位置在文件末尾,所以仍然无法读取到内容,得把文件指针位置移动到文件之前的位置,才有内容可以读出。
  • 使用a+时,情形同w+相似,文件指针位置是在文件的结束位置,导致读不出内容。

示例代码将在文章的下半部分文件位置指针移动中给出。

#####FILE *fdopen(int fd, const char *mode);#####

fdopen()是使用文件描述符fd来操作文件流的。
功能几乎与fopen()相似。差别在于

  • fdopen()在使用模式ww+时,并不会对原有的文件内容进行清空操作;
  • fdopen()可以直接使用STDIN_FILENO(mode要具备r权限)、STDOUT_FILENO(mode要具备w权限)、STDERR_FILENO(mode要具备w权限)来操作标准输入输出流。

相关:函数fileno()可以获取某个文件的fd

int fileno(FILE *stream);

示例,以下代码演示从终端命令行中敲入一个字符并打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#incluce <unistd.h>
#incluce <stdio.h>
#incluce <stdlib.h>

int main() {
FILE* file = fdopen(STDIN_FILENO, "r");
if (file == NULL) {
printf("fopen fail:%s ", filePath);
perror("");
exit(EXIT_FAILURE);
}
int ch = fgetc(file);
if (ch == EOF) {
if (errno != 0) {
perror("fgetc fail:");
printf("feof %d \n", feof(file));
} else {
printf("read file end. %d \n", feof(file));
}
} else {
printf("fgetc:%c \n", ch);
}

fclose(file);
return EXIT_SUCCESS;
}

#####FILE *freopen(const char *path, const char *mode, FILE *stream); #####
freopen()的功能是将一个已经打开的流(参数中的stream,可以是文件也可以是标准输出输入流)重定向到另一个流(函数返回值)中去,重定向的位置由参数path决定。

示例代码:将标准输出流重定向到指定文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#incluce <unistd.h>
#incluce <stdio.h>
#incluce <stdlib.h>

int main() {
FILE * tmp;
// 重定向标准输出流到指定的文件中去
if((tmp = freopen("/Users/sodino/reopen.txt", "w", stdout)) == NULL) {
// 重定向失败,打印失败日志
fprintf(stderr,"error1:redirecting stdout \n");
} else {
// 重定向成功,下面这个语句将会输出到上面指定的文件中去,且不会在命令行终端中显示
printf("This will go into a file. %d\n", (tmp == stdout));
}
// 重定向后,可以继续重定向流到其它文件,之前的文件不再接受后续的写入
if((tmp = freopen("/Users/sodino/reopen.2.txt", "w", stdout)) == NULL) {
fprintf(stderr,"error1 redirecting stdout\n");
} else {
printf("2This will go into a file. %d\n", (tmp == stdout));
}
// 关闭流:输出流,则xcode的控制台区域会自动关闭
fclose(tmp);
return EXIT_SUCCESS;
}

#读、写操作
#####
读、写单个字符#####
读:

int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);

fgetc()从指定的流中读取下一个字符,把unsigned char强制转换成int。
getc()功能与fgetc()一致,只是在函数执行时可能会对流多做一次评估验证。
getchar()功能等同于getc(stdin)

以上三个函数在正常执行时都会返回读取到的字符int,在发生错误或已经读到流的结束位置则返回EOF

写:

int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);

fputc()将int转化为unsigned char并写入流中。
putc()功能与fputc()一致,只是在函数执行时可能会对流多做一次评估验证。
putchar()功能等同于putc(c, stdout)

三个函数在正常执行时会将写入的字符c返回,在执行发生错误时则返回EOF

Copy Demo

下面的示例代码演示复制一个文件的过程,copyFile_str()是逐字节复制,而copyFile_data()是每次8KB的数据块在复制,所以效率会高一些。fread()fwrite()将会在下文中继续提到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#include <stdio.h>
#include <stdlib.h>

/**复制成功返回复制的文件大小;否则返回-1。*/
static long copyFile(FILE*src, FILE*dest) {
if (src == NULL || dest == NULL) {
return -1;
}

if (ftell(src) > 0) {
rewind(src);
}

int res = 0;
long count = 0; // 进度。
while (1) {
int ch = fgetc(src);

if (ch == EOF) {
if (!feof(src)) {
printf("fgetc() break res=-1. count=%ld \n", count);
res = -1; // 失败
} else {
// 正常的结尾
}
break;
} else {
int tmp = fputc(ch, dest);
if (tmp == EOF) {
res = -1;
printf("fputc() break res=-1. count=%ld \n", count);
break;
}
count ++;
continue; // 继续复制
}
}

return (res == -1)?-1L:count;
}

static long copyFile_str(const char *srcPath, const char *destPath) {
FILE * src = fopen(srcPath,"r");
FILE * dest = fopen(destPath,"w");

long res = copyFile(src, dest);

fclose(dest);
fclose(src);

return res;
}

static long copyFileData(FILE*src, FILE*dest) {
if (src == NULL || dest == NULL) {
return -1;
}

// 指针移动到文件末尾
fseek(src, 0, SEEK_END);
// 获取文件大小
long file_size = ftell(src);
// 指针回到文件头
rewind(src);

int res = 0;
long count = 0; // 进度。

const int LEN_DATA = 8*1024;
unsigned char data[LEN_DATA];
// printf("size: %lu \n", sizeof(data));
while (1) {
// 获取当前位置
long current = ftell(src);
// 计算剩余量
long leave = file_size - current;
int readSize = LEN_DATA;
if (leave == 0) {
// 拷贝结束了。
printf("leave == 0 feof.\n");
break;
} else if (leave < LEN_DATA) {
// 拷贝快结束了,但数据量已经小于LEN_DATA了
readSize = (int)leave;
}
// 读取数据块
size_t res_t = fread(&data, 1, readSize, src);
if (res_t == readSize) {
// 正常读取
size_t w_res = fwrite(&data, 1, res_t, dest);
count += w_res;
if (res_t == w_res) {
printf("write=%ld res_t=%zu \n", count, res_t);
} else {
printf("fwrite fail. \n");
res = -1;
break;
}
} else if (feof(src) != 0){
printf("feof res_t=%zu \n", res_t);
break;
} else {
printf("fread fail...\n");
res = -1;
break;
}
}


return (res == -1)?-1:count;
}

static long copyFile_data(const char * srcPath, const char *destPath) {
FILE * src = fopen(srcPath,"r");
FILE * dest = fopen(destPath,"w");

long res = copyFileData(src, dest);

fclose(dest);
fclose(src);

return res;
}

int main(int argc, char **argv) {
char *srcPath = "/Users/sodino/workspace/xcode/CString/CString/a.out";
char *destPath = "/Users/sodino/workspace/xcode/CString/CString/a1.out";
long res = copyFile_str(srcPath, destPath);

if (res == -1){
printf("copyFile_str() fail. \n\n\n");
} else {
printf("copyFile_str() count=%d. \n\n\n", res);
}

printf("do copyFile_data() \n");
res = copyFile_data(srcPath, destPath);
printf ("copy.file.length=%ld \n", res);
if (res == -1) {
return EXIT_FAILURE;
} else {
return EXIT_SUCCESS;
}
}

#####读、写字符串#####
读:

char *fgets(char *s, int size, FILE *stream);

从流中读取一个小于size长度的字符串,并将读到的字符串s返回。在读取过程中如果碰到EOF或换行,则把已经读到的字符串返回不再继续往下读取。返回的字符串会自动加上字符串终止符/0,字符串终止符是算在size里面的,所以得到的字符串一直是小于size的。
当执行发生错误或已经读到流的结束位置了则返回NULL。

写:

int fputs(const char *s, FILE *stream);
int puts(const char *s);

fputs()会将参数中字符串s去掉字符串终止符/0然后写入指定的流中去。
puts()相当于fputs(s, stdout),即把字符串输出到标准输出流中去,且会在字符串后面自动加上换行符。

当函数正常执行时,则返回一个非0的数字代表写入的字符个数;否则返回EOF

#####格式化读、写操作#####
#include <stdio.h>

int fscanf(FILE *stream, const char *format, …); // 格式化读
int fprintf(FILE *stream, const char *format, …); // 格式化写

之前的文章已经讲过了printf()强大的日志打印功能,在文件流的读、写上fprintf()相当于是把printf()的内容写入到指定的流中去;fscanf()则是fprintf()的反操作,读。

示例代码将在文件位置指针移动中给出。


#数据块的读写

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

fread()用于流操作中数据块的读取。将读到的数据存储于第一个参数ptr中,第二个参数size告诉函数每个数据块的大小,第三个参数nmemb指明要读取数据块的数量,第四个参数stream指明读取的数据流。
本函数正常执行完毕后返回数据块的读取数量,即nmemb;当执行出现错误或遇到了文件结束的位置时,则返回0或比nmemb小的数。至于是出现错误还是文件结束导致的非正常执行,则要通过feof()ferror()来判断。

fwrite()用于流操作中的数据块写入。第一个参数ptr表示要写入的数据,第二个参数size告诉函数每个数据块的大小,第三个参数nmemb指明要写入数据块的数量,第四个参数stream指明写入的数据流。
本函数正常执行完毕后返回数据块的读取数量,即nmemb;当执行出现错误,则返回0或比nmemb小的数。

这两个方法经常用于二进制文件的操作,所以打开方式上要与b模式组合。

示例代码:批量读写 sturct student:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <stdlib.h>

struct student {
char name[25];
char sex;
struct {
int year;
int month;
int day;
}birthday;
float BWH[3];// bust, waist and hips
};

int main(int argc, char ** argv) {
struct student pes[3] = {{"jack ma",'M',1985,2,22,80,95,60.5},
{"pony",'F',1984,1,2,57,60,78},
{"robin",'F',1995,3,2,77.0,90,77.5}
};

int i;
FILE *fpr;
struct student b;
fpr = fopen("W.DAT","wb+");
for(i=0;i<3;i++) {
fwrite(pes+i,sizeof(struct student),1,fpr);
}
// 上面的for循环等价于下面这一句
// fwrite(pes,sizeof(struct student),3,fpr);

// 将文件指针移动到初始位置进行读操作
fseek(fpr,0L,SEEK_SET);

for(i=0;i<3;i++) {
fread(&b,sizeof(struct student),1,fpr);
printf("%7s ",b.name); // 打印名字默认使用7个字符的空间
printf("%c ",b.sex);
printf("%04dY %02dM %02dD ",b.birthday.year,b.birthday.month,b.birthday.day);
printf("%5.1f %5.1f %5.1f",b.BWH[0],b.BWH[1],b.BWH[2]);
printf("\n");
}

fclose(fpr);

return EXIT_SUCCESS;
}

#错误码判断与操作
有时候在执行流的读操作时,发现返回为NULL或者EOF,这时需要判断是到了流(文件)的结束位置了?还是出现错误了?这就涉及到如下函数:

int feof(FILE *stream);
int ferror(FILE *stream);
void clearerr(FILE *stream);

feof()判断文件是否处于文件结束位置,如文件结束,则返回值为1,否则为0。
ferror(文件指针)检查文件在用各种输入输出函数进行读写时是否出错。 如ferror返回值为0表示未出错,否则表示有错。

clearerr()用于清除出错标志和文件结束标志,使它们为初始值0。


#文件位置指针移动
在读流(文件)时,已经读到文件结束的位置时,或者想以当前位置为准跳过指定长度的数据继续往下读,则需要移动文件位置指针。一般常用的涉及到以下三个函数:

long ftell(FILE *stream);
int fseek(FILE *stream, long offset, int whence);
void rewind(FILE *stream);

ftell()用于获知当前文件指针的位置。
fseek()用于改变文件指针的位置,第二个参数offset是指参照whence后指针进一步的偏移量。第三个参数whence有三种赋值及其含义分别为:
SEEK_SET: 文件指针回到文件初始位置。
SEEK_CUR: 文件指针不变,仍处于当前位置。一般配合offset移动文件指针到当前位置前后若干字节。
SEEK_END: 文件指针跳到文件结束的位置。
函数正常执行时则返回0,否则返回-1且会把错误码设置在errno中。
rewind()相当于fseek(file, 0, SEEK_SET)。即直接跳转到文件开头位置。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
char *filePath = "/Users/sodino/workspace/xcode/CString/CString/test.txt";

FILE* file = fopen(filePath, "w+");
printf("integer descriptor:%d \n", fileno(file));
if (file == NULL) {
printf("fopen fail:%s ", filePath);
perror("");
exit(EXIT_FAILURE);
}

fprintf(file, "%s %d", "My_Age_is", 19); // '_'为空格的话则无法正常读出
fseek(file, 0, SEEK_SET); // 跳转到文件开头才能正常读取内容
char arrRead[100];
int age;
fscanf(file, "%s %d", arrRead, &age);
// 打印出:read: My_Age_is 19
printf("read: %s %d \n", arrRead, age);

fclose(file);

return EXIT_SUCCESS;
}

#关闭文件

int fclose(FILE *stream);
关闭文件(流),如果流中仍有缓存内容未进行写操作,则会执行完写操作后再关闭。
关闭文件(流)之后,任何函数再有对stream进行操作的行为结果则都是未知的。所以请及时对stream置NULL。


#删除文件

int remove(const char *pathname); Thread-Safe
remove()函数可以从文件系统中删除指定路径下的文件(夹),内部实现中使用unlink()删除文件,使用rmdir()删除文件夹。

如果指定的路径是其所代表的文件(夹)的最后一个链接(参照硬链接和软链接)且没有被其它进程后引用,则该文件(夹)会被立即删除,其所占用的空间也可以被重新利用。但如果有其它进程仍在引用着该文件(夹),则删除和空间的回收操作要等到其它进程释放了当前文件(夹)的引用之后才会继续进行。

如果指定的路径是一个软链接文件,则只会删除该软链接文件,而非其指向的原始文件。