C语言梦开始的地方19:文件操作

文章目录

C语言梦开始的地方19:文件操作

什么是文件

磁盘(硬盘)上的文件就是文件。

那文件又有什么作用呢?

如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存 ,我们可以使用文件。

但是在程序设计上我们一般将文件分成两种:程序文件、数据文件(从文件功能的角度来分类的)。

程序文件 :程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。

数据文件:文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

本章节主要了解数据文件,比如C语言是怎么打开文件的,又是如何进行读写操作的。

程序文件我们在后面的预处理、编译和链接的时候再进行了解

文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用。

文件名包含3部分:文件路径+文件名主干+文件后缀

例如:c:\code\test.txt

为了方便起见,文件标识常被称为文件名。

二进制文件和文本文件

根据数据的组织形式,数据文件又称为文本文件 或者二进制文件

数据在内存中以二进制的形式存储,如果不加转换的输出到外存的文件中,就是二进制文件

如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件

那么一个数据在文件中是怎么存储的呢?

字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。

如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节。

c 复制代码
#include <stdio.h>

int main()
{
	int a = 10000;

	FILE* pf = fopen("test.txt", "wb");
	fwrite(&a, 4, 1, pf);
	fclose(pf);
	pf = NULL;

	return 0;
}

环境为vs2022,操作如下:

有可能程序运行后在对应地方看不到test.txt,此时我们点击源文件-》选择添加 -》选择现有项 ,最后找到并选中test.txt文件即可

文件的打开和关闭

流 和 标准流

(Stream)是C语言对输入/输出设备的抽象,程序通过流与外部设备交互。C程序启动时自动打开三个标准流:

  • stdin(标准输入流,通常键盘)-> scanf 从此读。
  • stdout(标准输出流,通常显示器)-> printf 写入此处。
  • stderr(标准错误流,通常显示器)-> 用于错误输出。

三者类型均为 FILE*,通常称为文件指针 。C语言中,就是通过FILE* 的文件指针来维护流的各种操作的。

文件指针

在C语言中每个打开的文件在内存中有一个 FILE 结构体,存放文件信息(位置、状态等)。我们使用 FILE* 指针指向该结构体,通过指针操作文件。

下面我们可以创建一个FILE*的指针变量:

c 复制代码
FILE* pf;//文件指针变量

定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件。

比如:

文件的打开和关闭

文件在读写之前应该先打开文件 ,在使用结束之后应该关闭文件

在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。

ANSI C 规定使用 fopen 函数来打开文件, fclose 来关闭文件。

c 复制代码
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );

mode表示文件的打开模式,下面都是文件的打开模式:

文件使用方式 含义 如果指定文件不存在
"r" (只读) 为了输入数据,打开一个已经存在的文本文件 出错
"w" (只写) 为了输出数据,打开一个文本文件 建立一个新的文件
"a" (追加) 向文本文件尾添加数据 建立一个新的文件
"rb" (只读) 为了输入数据,打开一个二进制文件 出错
"wb" (只写) 为了输出数据,打开一个二进制文件 建立一个新的文件
"ab" (追加) 向一个二进制文件尾添加数据 建立一个新的文件
"r+" (读写) 为了读和写,打开一个文本文件 出错
"w+" (读写) 为了读和写,建设一个新的文件 建立一个新的文件
"a+" (读写) 打开一个文件,在文件尾进行读写 建立一个新的文件
"rb+" (读写) 为了读和写打开一个二进制文件 出错
"wb+" (读写) 为了读和写,新建一个新的二进制文件 建立一个新的文件
"ab+" (读写) 打开一个二进制文件,在文件尾进行读写 建立一个新的文件
c 复制代码
#include <stdio.h>

int main()
{
    FILE* pf = fopen("myfile.txt","w"); // 打开一个叫myfile的.txt文件,以写的方式打开

    if(pf != NULL)
    {
        fputs("fopen example",pf); // 向pf指向的文件写入fopen example
        fclose(pf);  // 关闭文件
        pf = NULL;  // 因为pf 也是指针所以为了非法访问这里进行置NULL
    }

    return 0;
}

文件的顺序读写

顺序读写函数介绍

函数名 功能 适用于
fgetc 字符输入函数 所有输入流
fputc 字符输出函数 所有输出流
fgets 文本行输入函数 所有输入流
fputs 文本行输出函数 所有输出流
fscanf 格式化输入函数 所有输入流
fprintf 格式化输出函数 所有输出流
fread 二进制输入 文件输入流
fwrite 二进制输出 文件输出流

上面说的适用于所有输入流一般指适用于标准输入流和其他输入流(如文件输入流);所有输出流一般指适用于标准输出流和其他输出流(如文件输出流)。

c 复制代码
#include <stdio.h>

int main() {
    FILE* fp;
    char ch;
    char buffer[100];
    int num;

    // 1. fputc / fgetc
    fp = fopen("test1.txt", "w+");
    fputc('A', fp);
    rewind(fp);               // 回到文件开头
    ch = fgetc(fp);
    printf("读取到字符: %c\n", ch);
    fclose(fp);

    // 2. fputs / fgets
    fp = fopen("test2.txt", "w+");
    fputs("Hello World\n", fp);
    rewind(fp);
    fgets(buffer, sizeof(buffer), fp);
    printf("读取到字符串: %s", buffer);
    fclose(fp);

    // 3. fprintf / fscanf
    fp = fopen("test3.txt", "w+");
    fprintf(fp, "%d %f", 42, 3.14);
    rewind(fp);
    fscanf(fp, "%d %f", &num, &(float){0});
    printf("读取到整数: %d, 浮点数: %.2f\n", num, 3.14);
    fclose(fp);

    // 4. fwrite / fread (二进制)
    double arr[] = {1.1, 2.2, 3.3};
    fp = fopen("test4.bin", "wb+");
    fwrite(arr, sizeof(double), 3, fp);
    rewind(fp);
    double read_arr[3];
    fread(read_arr, sizeof(double), 3, fp);
    for (int i = 0; i < 3; i++)
        printf("%.1f ", read_arr[i]);
    putchar('\n');
    fclose(fp);

    return 0;
}

对比一组函数:

scanf/fscanf/sscanf

printf/fprintf/sprintf

  • scanf 从 stdin 读,printf 写至 stdout。
  • fscanf 从指定文件流读,fprintf 写至指定流。
  • sscanf 从字符串中解析数据,sprintf 将数据格式化到字符串。
c 复制代码
#include <stdio.h>

int main() {
    char str[] = "age=25 height=180";
    int age, height;
    sscanf(str, "age=%d height=%d", &age, &height);
    printf("解析出 age=%d, height=%d\n", age, height);

    char out[50];
    sprintf(out, "age=%d height=%d", age, height);
    printf("格式化字符串: %s\n", out);
    return 0;
}

这里就体现了输入输出流,sscanf是将str中的数据流向ageheightsprintf是将ageheight的数据流向out

文件的随机读写

fseek函数

根据文件指针的位置和偏移量来定位文件指针(文件内容的光标)。函数原型:

c 复制代码
int fseek ( FILE * stream, long int offset, int origin );

origin: SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件尾)。

示例:

c 复制代码
int main()
{
    FILE* pf = NULL;
    pf = fopen("myfile.txt","wb");
    fputs("This is an apple",pf);

    fseek(pf,9,SEEK_SET);
    fputs(" sam",pf);

    fclose(pf);
    pf = NULL;
    return 0;
}

解释 :从文件开头开始偏移9字节就将This is an apple 修改成了This is a sample

ftell函数

返回文件指针相对于起始位置的偏移量。函数原型:

c 复制代码
long int ftell ( FILE * stream );

示例:

c 复制代码
#include <stdio.h>

int main()
{
    FILE* pf = NULL;
    long size = 0;
    pf = fopen("myfile.txt","rb");

    if(pf == NULL)
    {
        perror("Error opening file");
        return -1;
    }
    else {
        fseek(pf,0,SEEK_END);
        size = ftell(pf);
        fclose(pf);
        pf = NULL;
        printf("%ld\n",size);
    }
    return 0;
}

解释 :fseek从偏移量0开始偏移到末尾,ftell就是获取偏移后的偏移量。从而获取该文件中字节长度为多少。

为什么这里不用r而是rb,因为rb是二进制的方式打开获取到的真实长度,如果用r打开在Windows上会将换行符 \r\n 转换为 \n从而计算不准确。但是在Linux/Mac上换行符本身就是 \n,文本模式和二进制模式没有区别 。所以这个问题只在 Windows 平台上暴露。

所以为了代码的跨平台最好是一律使用rb

rewind函数

让文件指针的位置回到文件的起始位置。函数原型:

c 复制代码
void rewind ( FILE * stream );

示例:

c 复制代码
#include <stdio.h>

int main()
{
    FILE *pf = NULL;
    pf = fopen("myfile.txt", "w+");
    char buffer[27];

    if (pf == NULL)
    {
        perror("Error opening file");
        return -1;
    }
    else{
        for(char ch = 'A'; ch <= 'Z';++ch)
        {
            fputc(ch,pf);
        }
        rewind(pf); // 等价于fseek(pf, 0, SEEK_SET)

        fread(buffer,1,26,pf);
        fclose(pf);
        pf = NULL;

        buffer[26] = '\0';
        printf(buffer);

    }
    return 0;
}

解释 :因为每次fputc都会让pf偏移1个字节,循环结束时pf指向的文件的末尾使用rewind,将pf指向末尾变成指向开头。

"w+" 模式会清空原有内容,如果 myfile.txt 原本有其他数据,它们会被覆盖丢失

文件读取结束的判定

被错误使用的feof

错误用法 :直接用 feof(fp) 判断是否结束。因为 feof 仅在读取失败后才返回真,用于区分结束原因,而非预测结束。

正确的判断方法:

  1. 文本文件读取是否结束,判断返回值是否为EOFfgetc ),或者NULLfgets
    例如:
    • fgetc 判断是否为EOF
    • fgets 判断返回值是否为NULL
  2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
    例如:
    • fread判断返回值是否小于实际要读的个数。
      文本文件示例:
c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int c; // 注意:int,非char,要求处理EOF
    FILE *fp = fopen("myfile.txt", "r");
    if (!fp)
    {
        perror("File opening failed");
        return EXIT_FAILURE;
    }
    // fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
    while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
    {
        putchar(c);
    }
    // 判断是什么原因结束的
    if (ferror(fp))
        puts("I/O error when reading");
    else if (feof(fp))
        puts("End of file reached successfully");
    fclose(fp);
    return 0;
}

fgetc 返回的是 int,它读取的是 ASCII 码值(0 ~ 255)。C语言规定,返回 EOF(即 -1)来表示"读失败或结束"。因为正常的字符值不可能是负数,所以 -1 可以安全地作为"终止哨兵",绝不会和正常数据混淆。

fgets 返回 char*(指针),用 NULL(空指针)当哨兵。后续的判断和fgetc差不多。

单纯是一个返回int,一个返回指针。

二进制文件示例

c 复制代码
int main()
{
    double a[5] = {1., 2., 3., 4., 5.};
    FILE *fp = fopen("test.bin", "wb");
    fwrite(a, sizeof *a, 5, fp); // 写 double 的数组
    fclose(fp);
    double b[5];
    fp = fopen("test.bin", "rb");
    size_t ret_code = fread(b, sizeof *b, 5, fp); // 读 double 的数组
    if (ret_code == 5)
    {
        puts("Array read successfully, contents: ");
        for (int n = 0; n < 5; ++n)
            printf("%f ", b[n]);
        putchar('\n');
    }
    else
    { // error handling
        if (feof(fp))
            printf("Error reading test.bin: unexpected end of file\n");
        else if (ferror(fp))
        {
            perror("Error reading test.bin");
        }
    }
    fclose(fp);
    return 0;
}

二进制文件由任意的0和1组合,所以使用fread读取时很可能读取到0xFF(即 -1 的补码表示),如果 fread也返回 -1 表示结束,那当它读到一个内容正好是 -1 的数据时,程序就会误以为文件结束了!那么此时就有两种可能一直就是老老实实的读取完毕从而退出,另一种就是中途退出只能返回"实际读到的元素个数"。你让它读 5 个,它只返回 3 个,就说明要么出错、要么提前结束了,这时你再回头用 feof 去查具体原因。

文件缓存区

C标准库采用缓冲文件系统 ,数据写入文件时先进入内存缓冲区,待缓冲区满或调用 fflush/fclose 时才真正写入磁盘。读取时也类似,从磁盘批量读入缓冲区再供程序使用。

  • 关闭文件(fclose)会自动刷新缓冲区。
  • 可调用fflush(stream)手动刷新(但较新VS中可能标记为弃用,建议使用 fflush 或直接 fclose)。

那么C语言为什么要采用缓存文件系统呢?

  1. 因为程序运行在内存中而内存的读写速率比磁盘快太多了,如果不采用缓存文件系统就会出现多次读写的情况。
  2. C语言的函数操作的都是封装操作系统的接口,那么没有缓存文件系统频繁读写就会进行多次的用户态转内核态的。操作系统为了防止用户的非法操作对这种用户切换内核的操作都是很严格的。

示例:观察缓冲区刷新时机。环境为vs2022,系统为Windows 11

c 复制代码
#include <stdio.h>
#include <windows.h>

int main()
{
	FILE* pf = fopen("test.txt", "w");
	fputs("abcdef", pf);//先将代码放在输出缓冲区
	printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
	Sleep(10000);
	printf("刷新缓冲区\n");
	fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
	//注:fflush 在高版本的VS上不能使用了
	printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
	Sleep(10000);
	fclose(pf);
	//注:fclose在关闭文件的时候,也会刷新缓冲区
	pf = NULL;

	return 0;
}

代码中注释fflush不能使用的是指fflush(stdin)高版本不能使用,因为C标准只承诺 fflush 对输出流(如 stdout)有效。fflush(stdin) 的行为在标准中是"未定义"的所以vs官方在后续版本的更新的中逐渐将撤回了fflush(stdin) 行为。

因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。

如果不做,可能导致读写文件的问题。

相关推荐
Dovis(誓平步青云)12 分钟前
折叠屏悬停看视频,上半屏和下半屏应该各做什么
android·java·服务器·开发语言·安全·音视频
benchmark_cc23 分钟前
Python量化实战:如何检测并剔除历史数据中的“闪崩/乌龙指”异常价格点?
开发语言·人工智能·python·量化·quantdash·量化数据源
2501_9378609427 分钟前
Java多线程初阶(下)—— synchronized、volatile、wait/notify与经典并发工具
java·开发语言·jvm
笨笨饿44 分钟前
#135_代码中的类型重定义艺术:从基础封装到函数类型设计
开发语言·stm32·单片机·嵌入式硬件·mcu·物联网·嵌入式实时数据库
统计学小王子1 小时前
R语言入门——ggplot2图例增加公式
开发语言·r语言
牛油果子哥q1 小时前
C++内存池与对象池精讲:内存碎片、自定义分配器、对象池实现、STL allocator原理、工程落地与性能对比
java·开发语言·c++
CodeStats1 小时前
《源纹天书》第三百六十三章至第三百六十四章
java·开发语言·源纹天书
万法若空1 小时前
C/C++ 类型别名定义方式对比
java·c语言·c++
Java后端的Ai之路1 小时前
03、Python - 抽象工厂模式
开发语言·python·设计模式·抽象工厂模式·通用智慧