C语言基础 Day11 文件(下)
1. fprintf和fscanf
在说fprintf函数和fscanf函数之前,首先说说什么是变参函数。变参函数是形参中带有...
,最后一个固定参数通常是格式描述串,包含了格式匹配符,函数的参数个数、类型、顺序由这个固定参数决定。比如举例最简单的printf函数:
printf("hello world\n");
printf("%s\n", "hello world");
printf("%s %s\n", "hello", "world");
在这三个函数调用语句中,第一个固定参数由于没有格式匹配字符,所以就一个参数。而第二个中固定参数有一个格式匹配字符,所以就两个参数,依次类推。
对于fprintf函数和fscanf函数,在上一节的表中就已经写明了其用法。接下来看看关于这两个函数的使用例子。
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
// 使用fprintf向文件中写数据
void write_file(char *filename)
{
srand((unsigned int)time(NULL));
FILE *fp = fopen(filename, "w");
if (fp == NULL)
{
perror("fopen error");
return;
}
for (int i = 0; i < 10; i++)
{
fprintf(fp, "%d\n", rand() % 10);
}
fclose(fp);
}
// 使用fscanf从文件中读数据
void read_file1(char *filename)
{
FILE *fp = fopen(filename, "r");
if (fp == NULL)
{
perror("fopen error");
return;
}
int number = 0;
// 读次数大于数字个数
for (int i = 0; i < 12; i++)
{
fscanf(fp, "%d\n", &number);
printf("%d\n", number);
// 每次读完打印后重置为0
number = 0;
}
fclose(fp);
}
// 使用fscanf循环读文件
void read_file2(char *filename)
{
FILE *fp = fopen(filename, "r");
if (fp == NULL)
{
perror("fopen error");
return;
}
int number = 0;
while (1)
{
fscanf(fp, "%d\n", &number);
printf("%d\n", number);
if (feof(fp))
break;
}
fclose(fp);
}
// fgets循环读文件
void read_file3(char *filename)
{
FILE *fp = fopen(filename, "r");
if (fp == NULL)
{
perror("fopen error");
return;
}
char buff[1024];
while (1)
{
fgets(buff, 1024, fp);
if (feof(fp))
break;
printf("%s", buff);
}
fclose(fp);
}
int main(int argc, char* argv[])
{
char *filename = "test01-1.txt";
write_file(filename);
read_file1(filename);
printf("==============================\n");
read_file2(filename);
printf("==============================\n");
read_file3(filename);
system("pause");
return 0;
}
#endif
在使用fscanf函数的时候,特别需要注意一点的是fscanf函数每次在调用的时候都会判断下一次的调用是否匹配参数format
,如果匹配就会提前结束文件,也就是feof
为真。因此对于fscanf函数,我们都是先获取数据之后再进行判断。
下面给一个关于文件版排序的例子。利用计算机生成随机数并写入到文件中,再将文件中的随机数进行从小到大排序写入到文件。
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
#define N 15
void write_file(char *filename)
{
srand((unsigned int)time(NULL)); // 随机数种子
FILE *fp = fopen(filename, "w");
if (fp == NULL)
{
perror("fopen error");
return;
}
for (int i = 0; i < N; i++)
{
fprintf(fp, "%d\n", rand() % 100); // 将生成的随机数写入文件
}
fclose(fp);
}
void bubble_sort(int a[], int size)
{
for (int i = 0; i < size - 1; i++)
{
for (int j = 0; j < size - i - 1; j++)
{
if (a[j] > a[j + 1])
{
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
}
}
}
}
void read_file(char *filename)
{
FILE *fp = fopen(filename, "r");
if (fp == NULL)
{
perror("fopen error");
return;
}
int *p = (int *)malloc(sizeof(int)* 100);
int i = 0;
while (1)
{
fscanf(fp, "%d\n", &p[i ++]); // 从文件中读取一个随机数,存入数组p
if (feof(fp)) // 先存储后判断,防止最后一个元素丢失
{
break;
}
}
for (int j = 0; j < i; j++)
{
printf("%d ", p[j]);
}
putchar('\n');
fclose(fp);
bubble_sort(p, i); // 对读取到的乱序数组进行排序
fp = fopen(filename, "w"); // 用w的方式打开文件,清空原来的内容
if (fp == NULL)
{
perror("fopen error");
return;
}
for (int j = 0; j < i; j++)
{
printf("%d ", p[j]);
fprintf(fp, "%d\n", p[j]); // 写排序好的数组到文件
}
putchar('\n');
fclose(fp);
}
int main(int argc, char* argv[])
{
char *filename = "test02-1.txt";
//write_file(filename);
read_file(filename);
system("pause");
return 0;
}
#endif
2. fwrite与fread
这两个函数主要二进制的文件输入和输出。由于是二进制操作,所以对于文本文件和二进制文件都可以进行操作,因为文本文件在进行存储的时候底层也是用二进制进行存储的。对于这两个函数在也在上面一节的表中说明了用法。对于这两个函数的返回值,成功永远都是nmemb
的值,读写的总大小是size*nmemb
。
关于两个函数的示例如下。示例主要是给了一些列的学生的姓名、年龄和电话,并按照年龄从小到大的顺序进行了冒泡排序。
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
#define MAX_SIZE 100
#define STUDENT_CNT 15
#define N STUDENT_CNT
typedef struct Student
{
char *name;
unsigned int age;
char *tel;
} Student;
// 将数据写入文件中
void write_file(char *filename)
{
// 以写的方式打开文件判断是否打开成功
FILE *fp = fopen(filename, "w");
if (fp == NULL)
{
perror("fopen error");
return;
}
srand((unsigned int)time(NULL));
Student *std_list;
std_list = (Student *)malloc(sizeof(Student)* MAX_SIZE);
char buff[15] = {0};
// 生成学生的基本信息
for (int i = 0; i < N; i++)
{
// 在堆上开辟学生的基本信息存储空间
std_list[i].name = (char *)malloc(sizeof(char)* 15);
std_list[i].tel = (char *)malloc(sizeof(char)* 15);
std_list[i].age = rand() % 30 + 20;
memset(std_list[i].name, 0, 15);
memset(std_list[i].tel, 0, 15);
// 初始化信息
int name_len = rand() % 6 + 3;
for (int i = 0; i < name_len; i++)
{
buff[i] = rand() % 26 + 'a';
}
strcpy(std_list[i].name, buff);
sprintf(std_list[i].tel, "%d", rand() % 100000 + 100000000);
// 将结构体的信息写入文件中
int ret = 0;
// 学生姓名
ret = fwrite(std_list[i].name, 1, 15, fp);
if (ret == 0)
{
perror("fwrite error");
return;
}
// 学生年龄
ret = fwrite(&std_list[i].age, 1, sizeof(unsigned int), fp);
if (ret == 0)
{
perror("fwrite error");
return;
}
// 学生电话
ret = fwrite(std_list[i].tel, 1, 15, fp);
if (ret == 0)
{
perror("fwrite error");
return;
}
}
fclose(fp);
}
// 对堆上的元素进行冒泡排序
void bubble_sort(Student *p, int size)
{
for (int i = 0; i < size - 1; i++)
{
for (int j = 0; j < size - 1 - i; j++)
{
if (p[j].age > p[j + 1].age)
{
Student s = p[j];
p[j] = p[j + 1];
p[j + 1] = s;
}
}
}
}
// 从文件中使用fread读取数据
void read_file(char *filename)
{
// 打开文件
FILE *fp = fopen(filename, "r");
if (fp == NULL)
{
perror("fopen error");
return;
}
// 在堆上开辟学生数组
Student *stu = (Student *)malloc(sizeof(Student)* MAX_SIZE);
int i = 0;
// 读取数据
while (1)
{
// 开辟姓名和电话的存储空间
stu[i].name = (char *)malloc(sizeof(char)* 15);
stu[i].tel = (char *)malloc(sizeof(char)* 15);
fread(stu[i].name, 1, 15, fp);
// 判断是否文件末尾
if (feof(fp))
{
break;
}
fread(&stu[i].age, 1, sizeof(unsigned int), fp);
fread(stu[i].tel, 1, 15, fp);
printf("学生姓名: %s 年龄: %u 电话: %s\n", stu[i].name, stu[i].age, stu[i].tel);
i++;
}
bubble_sort(stu, i);
printf("按照年龄从小到大排序后:\n");
for (int j = 0; j < i; j++)
{
printf("学生姓名: %s 年龄: %u 电话: %s\n", stu[j].name, stu[j].age, stu[j].tel);
}
fclose(fp);
}
int main(int argc, char* argv[])
{
char *filename = "test03-1.txt";
write_file(filename);
read_file(filename);
system("pause");
return 0;
}
#endif
会使用这两个函数之后,我们可以进行一些其它文件的操作。比如文件的复制。下面给出一个关于文件复制的代码。在文件复制中需要注意的是fwrite中第三个参数传的数据应该是fread的返回值,代码如下。
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
int file_copy(char *filename1, char *filename2)
{
FILE *rfp = fopen(filename1, "rb");
if (rfp == NULL)
{
perror("file1 fopen error");
return -1;
}
FILE *wfp = fopen(filename2, "wb");
if (wfp == NULL)
{
perror("file2 fopen error");
return -1;
}
char buff[256];
int ret = 0;
while (1)
{
ret = fread(buff, 1, sizeof(buff), rfp);
printf("ret = %d\n", ret);
if (feof(rfp))
{
break;
}
fwrite(buff, 1, ret, wfp);
}
fclose(wfp);
fclose(rfp);
return 0;
}
int main(int argc, char* argv[])
{
if (argc != 3) return -1;
printf("%s\n%s\n", argv[1], argv[2]);
file_copy(argv[1], argv[2]);
system("pause");
return 0;
}
#endif
3. 随机读写
对于文件的随机读写,我们主要控制的是读写光标的位置,读写光标也就是我们说的文件读写指针。在程序中,读写光标我们定义了三个关键的宏代表了起始、当前、结束。
宏名 | 含义 |
---|---|
SEEK_SET | 文件起始位置 |
SEEK_CUR | 读写指针当前位置 |
SEEK_END | 文件结束位置 |
对于文件位置的操作,我们有三个函数:
函数名 | 原型 | 返回值 | 参数 | 作用 | tip |
---|---|---|---|---|---|
fseek | int fseek(FILE *stream, long offset, int whence); | 成功返回0,失败返回-1 | stream: 已经打开的文件指针 offset: 根据whence移动的偏移量,可以是正数,也可以是负数。正数往后移动,负数往前移动 whence: 取值为上述的三个宏 | 移动文件流的读写位置 | - |
ftell | long ftell(FILE *stream); | 成功返回当前文件流的读写位置,失败返回-1 | stream: 已经打开的文件指针 | 获取文件流的读写位置 | - |
rewind | void rewind(FILE *stream); | - | stream: 已经打开的文件指针 | 把文件流的读写位置移动到文件开头 | - |
下面给出关于随机读写文件的示例代码。
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
typedef struct Student
{
char name[12];
unsigned int age;
unsigned int sno;
} Student;
void test1()
{
Student s_list[4] = {
"张三", 16, 26123,
"李四", 20, 26433,
"王麻子", 26, 24733,
"老六", 18, 62356
};
Student stu;
FILE *fp = fopen("test05-1.txt", "wb+");
if (fp == NULL)
{
perror("fopen error");
return;
}
int ret = fwrite(s_list, 1, sizeof(s_list), fp); // 以二进制的方式写入
printf("ret = %d\n", ret);
fseek(fp, sizeof(stu) * 2, SEEK_SET);
ret = ftell(fp); // 获取读写指针到文件起始位置的偏移量
printf("ret = %d\n", ret);
ret = fread(&stu, 1, sizeof(stu), fp);
printf("ret = %d\n", ret);
printf("姓名: %s, 年龄: %u, 编号: %u\n", stu.name, stu.age, stu.sno);
fseek(fp, 0, SEEK_END); // 将读写指针放在文件结尾
ret = ftell(fp);
printf("文件大小为: %d\n", ret);
rewind(fp); // 回卷读写指针
ret = fread(&stu, 1, sizeof(stu), fp);
printf("ret = %d\n", ret);
printf("姓名: %s, 年龄: %u, 编号: %u\n", stu.name, stu.age, stu.sno);
fclose(fp);
}
void test2()
{
FILE *fp = fopen("test05-2.txt", "w+");
int ret = fputs("11111", fp);
printf("ret1 = %d\n", ret);
ret = fputs("22222", fp);
printf("ret2 = %d\n", ret);
ret = fputs("33333", fp);
printf("ret3 = %d\n", ret);
char buff[1024] = {0};
/*char *ptr = fgets(buff, sizeof(buff), fp);
if (feof(fp))
{
printf("error\n");
}
if (ptr == NULL)
printf("ptr = NULL\n", ptr);
else
printf("ptr = %s\n", ptr);
memset(buff, 0, sizeof(buff));*/
fseek(fp, -10, SEEK_CUR);
char *ptr = fgets(buff, sizeof(buff), fp);
if (ptr == NULL)
printf("ptr = NULL\n", ptr);
else
printf("ptr = %s\n", ptr);
memset(buff, 0, sizeof(buff));
rewind(fp);
ptr = fgets(buff, sizeof(buff), fp);
if (ptr == NULL)
printf("ptr = NULL\n", ptr);
else
printf("ptr = %s\n", ptr);
fclose(fp);
}
void test3()
{
FILE *fp = fopen("test05-3.txt", "r+");
if (fp == NULL)
{
perror("fopen error");
return;
}
char buff[6] = { 0 };
char *ptr = fgets(buff, 6, fp);
printf("ptr = %s, buff = %s\n", ptr, buff);
fseek(fp, 0, SEEK_CUR);
int ret = fputs("12345", fp);
printf("ret = %d\n", ret);
fclose(fp);
}
int main(int argc, char* argv[])
{
//test1();
//test2();
test3();
system("pause");
return 0;
}
#endif
在test1中我们使用了fseek(fp, 0, SEEK_END) 与 ftell(fp)联合使用获取到了文件的大小。在test2中我们需要知道文件以w+打开时候到文件末尾要进行读操作会发生乱码的错误,此时应该先移动文件读写的指针再进行读操作就不会出错。在test3中我们使用了r+的方式打开了文件,此时会发现进行写操作的时候在Windows中无法写入,在Linux中不受影响。因此在Windows中如果需要写需要在写操作前添加fseek(fp, 0, SEEK_CUR);
,虽然看着是废话,但是是必须写的。
4. Linux和Windows文件的区别与文件的其它操作
Linux和window文件的区别主要在于以下三点:
- 对于二进制文件的操作,Windows下必须使用“b”,而Linux下二进制文件和文本文件没有任何区别。
- Windows下回车用
\r
,换行用\n
,而在Linux中回车换行都是\n
。 - 对文件指针来说,先写后读的操作在Windows和Linux中的效果是一样的。但是如果是先读后写,Linux中无需修改,而在Windows中必须在写操作前加上
fseek(fp, 0, SEEK_CUR);
来获取文件读写指针,使之生效。
在前面,我们使用了fseek函数与ftell函数计算了文件的大小。但是这种算法需要打开文件进行操作。打开文件对于系统而言,系统资源消耗巨大。所以此处我们引入了一个新的操作,即获取文件的状态。使用该函数和结构体的使用在sys/stat.h
和sys/types.h
头文件中。函数原型为int stat(const char *path, struct stat *buf);
,其中返回值表示是否成功获取到了文件的状态,0表示成功,-1表示失败。其中形式参数中path表示的是文件路径,buf是一个关于stat的结构体,该结构体的定义如下:
struct stat {
dev_t st_dev; //文件的设备编号
ino_t st_ino; //节点
mode_t st_mode; //文件的类型和存取的权限
nlink_t st_nlink; //连到该文件的硬连接数目,刚建立的文件值为1
uid_t st_uid; //用户ID
gid_t st_gid; //组ID
dev_t st_rdev; //(设备类型)若此文件为设备文件,则为其设备编号
off_t st_size; //文件字节数(文件大小)
unsigned long st_blksize; //块大小(文件系统的I/O 缓冲区大小)
unsigned long st_blocks; //块数
time_t st_atime; //最后一次访问时间
time_t st_mtime; //最后一次修改时间
time_t st_ctime; //最后一次改变时间(指属性)
};
通过成员变量就可以获取到文件相应的一些信息。下面给出一个关于获取文件大小与最后一次修改时间的例子。
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc, char* argv[])
{
struct stat buf;
int ret = stat("06-获取文件属性.c", &buf);
printf("文件大小: %d\n", buf.st_size);
time_t tm = buf.st_mtime;
struct tm *ptm = localtime(&tm);
printf("文件的最后一次修改时间: %04d/%02d/%02d %02d:%02d:%02d\n", 1900 + ptm->tm_year, 1 + ptm->tm_mon, ptm->tm_mday, ptm->tm_hour, ptm->tm_min, ptm->tm_sec);
system("pause");
return 0;
}
#endif
除了关于文件状态信息的获取,有时候我们也需要对文件进行删除或者重命名。下面给出这两个函数的原型:
int remove(const char *pathname); // 删除文件。
int rename(const char *oldpath, const char *newpath); // 重名文件
最后谈谈关于缓冲区刷新的问题。标准输出对应着标准输出缓冲区,写给屏幕都数据,都是先存在缓冲区中,由缓冲区一次性刷新到物理设备上。标准输入对应着标准输入缓冲区,从键盘上读取的数据,直接读到缓冲区中,由缓冲区提供给程序数据。预读入,缓输出。缓冲分为行缓冲、全缓冲和无缓冲。
- 行缓冲:printf对应的就是行缓冲,遇到\n就会将缓冲区的数据刷新到物理设备上。
- 全缓冲:文件就是全缓冲。缓冲区存满,数据才会刷新到物理设备上。
- 无缓冲:perror就是无缓冲。只要缓冲区有数据就会立即刷新到物理设备。
由于文件时全缓冲,所以缓冲区没有满的时候就只有关闭的时候才能让缓冲区自动刷新。如果想要手动刷新,则需要用到fflush()
函数。fflush的函数原型为int fflush(FILE *stream);
,其中返回值为0表示成功,-1表示失败。下面给出一个关于文件缓冲区刷新的例子。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
int main0701(void)
{
FILE *fp = fopen("test07.txt", "w+");
if (!fp)
{
perror("fopen error");
return -1;
}
char m = 0;
while (1)
{
scanf("%c", &m);
if (m == ':')
{
break;
}
fputc(m, fp);
fflush(fp); // 手动刷新文件缓冲到物理磁盘。
}
// 当文件关闭时,会自动刷新缓冲区。
fclose(fp);
system("pause");
return EXIT_SUCCESS;
}