×
Featured image of post 换个姿势学C语言第7.2节 文件访问的基础知识

换个姿势学C语言第7.2节 文件访问的基础知识

换个姿势学C语言 第7章 获取全部外币牌价数据并保存为文件

0. 说明

《换个姿势学C语言》由何旭辉 著,清华大学出版社2022年出版。感谢何老师!

Snipaste_2024-03-10_14-51-10.png

这是一本非常不错的书!

7. 获取全部外币牌价数据并保存为文件

变量、数组都是存储在内存RAM中的,这些数据所占用的内存在程序结束以后会被操作系统回收,其中的数据也就丢失了。因此我们需要将数据保存到外部存储器上(通常是硬盘),以便下次使用。

在一些较底层的语言里,程序员可以直接访问硬盘的某个扇区并进行数据读写,但这种方式一般不被推荐,因为这种方式除了效率比较低外还具有较大的危险,不恰当的磁盘访问可能会引起严重的故障(例如操作系统崩溃或者数据丢失)。

因此通常是以“文件”来组织磁盘上的数据。文件系统由操作系统管理,程序员通过操作系统间接地访问磁盘上的数据,不恰当的文件访问会被操作系统阻止(例如文件被其他程序占用或程序没有访问这个文件的权限),这样一来就安全得多,同时操作系统也会采取一些机制来提高文件访问的效率。

本章将会将取得的外汇牌价数据保存到磁盘文件中,但是在学习磁盘文件访问之前先学习结构体的使用方法。

结构体可以将多种不同类型的数据“组合”到一起,然后再将其存储到磁盘文件中。

7.2 文件访问的基础知识

磁盘上的数据是以文件的形式来存储的,程序员对文件的操作主要包括读和写。

  • 写文件的本质,就是将内存中的一块数据复制到磁盘上来实现长期保存;而读文件的本质就是将磁盘上的一块数据复制到内存中。

读写文件的步骤如下:

  • 第1步:创建(或打开)文件。
  • 第2步:向文件中写数据或从文件中读数据。
  • 第3步:关闭文件。

C语言标准库提供了访问文件的函数,较常用的如下:

  • fopen函数–用于创建或打开文件;
  • fread函数–用于从文件中读数据;
  • fwrite函数–用于写数据到文件;
  • fclose函数–用于关闭文件。
7.2.1 使用fopen函数创建或打开文件

将数据保存到磁盘文件时首先要创建文件。如果一个文件已经创建,只需要打开它(有时候是直接覆盖它,这由具体情况来决定)。在C语言中创建和打开文件都使用fopen函数,fopen函数的原型如下:

1
FILE* fopen( const char* filename, const char* mode );
  • 第一个参数是filename,它是一个指定文件名的字符串;
  • 第二个参数是mode,也是一个字符串,它的作用是决定打开这个文件的方式。

第二个参数,表示打开文件的模式,有以下几种:

模式C 语言 fopenPython open()意思
只读"r""r"打开读(文件必须存在)
只写"w""w"清空创建写(文件不存在就创建)
追加"a""a"末尾追加写
读写"r+""r+"可读可写
读写创建"w+""w+"创建读写,文件存在则会清空
追加读写“a+”“a+”写到末尾 + 可以读
  • 当指定的路径不存在,试图打开 的文件不存在或文件被其他程序占用、对指定路径的访问权限不够、磁盘空间已满都可能会引起fopen函数打开或创建文件失败!
  • fopen函数在操作失败时会返回NULL,以便程序员知晓这些错误并采取处理措施。
  • 如果fopen函数打开文件成功,则会返回一组关于这个文件的数据,这些数据被存储在一个结构体中,这个结构体是标准库中已经定义好的,别名为FILE
  • 我们不用关心这个结构体各项成员的含义,因为在不同的库中实现这个结构体的定义不同。
1
FILE* fp = fopen("data.json", "r");

这行代码中的fp是一个指向这种结构体的指针。fopen函数内部在打开文件成功后就会创建这个结构体,将文件的相关数据存入其中并返回这个结构体的指针。后面的程序要访问该文件就需要通过指针fp获取结构体存储的数据。习惯上,我们把这个结构体指针称为文件句柄

此处写fopen的伪代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 这是 fopen 内部的伪代码!!!
FILE* fopen(const char* filename, const char* mode)
{
    // 1. 打开文件(系统调用)
    int handle = os_open_file(filename, mode);

    // 2. 自己 malloc 一个 FILE 结构体!!!
    FILE* file = (FILE*)malloc(sizeof(FILE));

    // 3. 把信息存进去
    file->handle = handle;
    file->position = 0;
    file->eof = 0;
    ...

    // 4. 返回指针给你!
    return file;
}

就是说,当我使用FILE* fp = fopen("data.json", "r");去打开一个文件时,是fopen函数在内部创建了一个结构体,并将这个文件句柄返回,FILE* fp只是接收了fopen返回的这个内存地址。

下面演示fopen文件占用情况:

 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
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable: 6031)

#include <stdio.h>
#include <windows.h>

int main()
{
    // 1. 打开文件
    FILE* fp = fopen("data.json", "r");
    if (fp == NULL)
    {
        printf("打开文件失败!\n");
        return 1;
    }
    printf("打开文件成功!此时你不能删除文件\n");

    // 在Windows环境下,最常用的延时函数是Sleep函数。
    // Sleep函数定义在windows.h头文件中,它的参数是延时的毫秒数
    // 程序暂停期间,文件句柄一直被程序占用,你不能删除文件
    Sleep(1000 * 3600);

    // 2. 关闭文件
    fclose(fp);

    return 0;
}

运行程序后,尝试手动删除文件,会提示无法删除:

Snipaste_2026-04-28_23-26-48.png

7.2.1.1 模拟无权限打开文件

给文件设置「只读 + 拒绝读取权限」:

  • 右键data.json属性
  • 切到 安全 选项卡 → 点 编辑
  • 选中当前登录的用户 / 管理员组
  • 在【 拒绝】 那一列,勾选: 读取 & 执行 和- 读取
  • 确定保存

Snipaste_2026-04-29_20-08-57.png

先上代码:

 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
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable: 6031)

#include <stdio.h>
// windows.h提供 Sleep 函数
#include <windows.h>
// errno.h 提供错误码,如 EACCES          13
#include <errno.h>
// string.h 提供 strerror 函数,获取错误码对应的异常说明
#include <string.h>

int main()
{
    // 1. 打开文件
    FILE* fp = fopen("data.json", "r");
    printf("文件句柄地址:%p\n", fp);
    if (fp == NULL)
    {
        printf("打开文件失败!错误编号:%d, 原因: %s\n", errno, strerror(errno));
        return 1;
    }
    printf("打开文件成功!此时你不能删除文件\n");

    // 在Windows环境下,最常用的延时函数是Sleep函数。
    // Sleep函数定义在windows.h头文件中,它的参数是延时的毫秒数
    // 程序暂停期间,文件句柄一直被程序占用,你不能删除文件
    Sleep(1000 * 3600);

    // 2. 关闭文件
    fclose(fp);

    return 0;
}

运行程序,输出如下:

1
2
文件句柄地址:00000000
打开文件失败!错误编号:13, 原因: Permission denied

Snipaste_2026-04-29_21-53-25.png

说明因为权限异常,读取不到文件!

修改文件属性,将刚才选择的拒绝勾选去掉,然后再执行程序,就可以正常读文件:

Snipaste_2026-04-29_21-56-19.png

7.2.1.2模拟文件不存在打开文件

直接将data.json文件重命名为data1.json,然后再运行程序,此时输出为:

1
2
文件句柄地址:00000000
打开文件失败!错误编号:2, 原因: No such file or directory

Snipaste_2026-04-29_21-59-30.png

data1.json命名重新改回为data.json

7.2.1.3 文件打开模式异常的处理

https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fopen-wfopen?view=msvc-170 这里面定义了fopen的打开模式:

Snipaste_2026-04-29_22-40-00.png

如果我们使用一个非法的模式,如将模式"r"换成"invalid",尝试运行程序:

Snipaste_2026-04-29_22-43-38.png

程序直接崩溃了,提示Expression:(“Invalid file open mode”,0) 无效的文件打开模式!

因此,在打开文件前,应对文件打开模式进行校验!

下面是优化后的代码,对文件打开模式提前进行校验:

 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
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable: 6031)

#include <stdio.h>
// windows.h提供 Sleep 函数
#include <windows.h>
// errno.h 提供错误码,如 EACCES          13
#include <errno.h>
// string.h 提供 strerror 函数,获取错误码对应的异常说明
#include <string.h>
// stdbool.h 提供布尔逻辑true和false
#include <stdbool.h>

// 函数声明
bool is_valid_fopen_mode(const char* mode);

int main()
{
    const char* mode = "invalid";  // 随便写文件打开模式

    // 1. 检查模式对不对
    if (!is_valid_fopen_mode(mode))
    {
        printf("错误:文件打开模式( %s )不合法!\n", mode);
        return 1;
    }
    printf("文件打开模式( %s )合法!\n", mode);

    // 2. 打开文件
    // 直接用/替换\\作为路径分隔符
    FILE* fp = fopen("D:/BC101/Examples/L07/L07_07_FILE_READ_AND_WRITE/data.json", mode);
    printf("文件句柄地址:%p\n", fp);
    // 3. 检查文件是否打开成功
    if (fp == NULL)
    {
        printf("打开文件失败!错误编号:%d, 原因: %s\n", errno, strerror(errno));
        return 1;
    }
    // 4. 打开成功,进行等待
    printf("打开文件成功!此时你不能删除文件\n");

    // 在Windows环境下,最常用的延时函数是Sleep函数。
    // Sleep函数定义在windows.h头文件中,它的参数是延时的毫秒数
    // 程序暂停期间,文件句柄一直被程序占用,你不能删除文件
    Sleep(1000 * 3600);

    // 5. 关闭文件,释放文件句柄
    fclose(fp);

    return 0;
}

// 检查文件打开模式是否合法
bool is_valid_fopen_mode(const char* mode)
{
    // 合法模式列表
    const char* valid_modes[] = {
        // 文本模式
        "r",  "r+",
        "w",  "w+",
        "a",  "a+",

        // 二进制 b 模式
        "rb",  "rb+", "r+b",
        "wb",  "wb+", "w+b",
        "ab",  "ab+", "a+b"
    };

    // 计算一共有多少个合法模式
    int len = sizeof(valid_modes) / sizeof(valid_modes[0]);
    printf("合法字符个数:%d\n", len);
    // 逐个比对
    for (int i = 0; i < len; i++)
    {
        // strcmp 也是 string.h 中的,表示对两个字符串进行比较,相等就等于0
        if (strcmp(mode, valid_modes[i]) == 0)
        {
            return true;  // 找到合法模式 → 返回真
        }
    }
    return false;  // 没找到 → 非法
}

运行程序,可以看到,对非法模式进行了拦截:

Snipaste_2026-04-29_23-19-20.png

此时,如果将invalid改成r,然后再启动程序,可以看到能正常运行:

Snipaste_2026-04-29_23-23-33.png

7.2.4 使用fread读取文件内容

在使用fread前,我们先用fgets一行一行读取文件内容。

7.2.4.0 使用fgets一行一行读取文件内容

基本用法:

fgets函数用于从文件中读取一行数据,并将其存储在指定的缓冲区中。其原型如下:

1
char *fgets(char *str, int n, FILE *stream);
  • str:指向字符数组的指针,用于存储读取的字符串。
  • n:要读取的最大字符数,包括空字符\0
  • stream:指向FILE对象的指针,表示要读取的文件。

返回值:

  • 成功时返回str指针。
  • 遇到文件结束符(EOF)或读取错误时返回NULL

注意事项:

  • buf设置的要读取的最大字符数n越小时,如果文件内容一行越长,一行越会分几次读取。应尽量将buf设置大一点。

看示例,如果将buf的长度设置为8,代码如下:

 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
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable: 6031)

#include <stdio.h>
// windows.h提供 Sleep 函数
#include <windows.h>
// errno.h 提供错误码,如 EACCES          13
#include <errno.h>
// string.h 提供 strerror 函数,获取错误码对应的异常说明
#include <string.h>
// stdbool.h 提供布尔逻辑true和false
#include <stdbool.h>

// 函数声明
bool is_valid_fopen_mode(const char* mode);

int main()
{
    const char* mode = "r";  // 随便写文件打开模式

    // 1. 检查模式对不对
    if (!is_valid_fopen_mode(mode))
    {
        printf("错误:文件打开模式( %s )不合法!\n", mode);
        return 1;
    }
    printf("文件打开模式( %s )合法!\n", mode);

    // 2. 打开文件
    // 直接用/替换\\作为路径分隔符
    FILE* fp = fopen("D:/BC101/Examples/L07/L07_07_FILE_READ_AND_WRITE/data.json", mode);
    printf("文件句柄地址:%p\n", fp);
    // 3. 检查文件是否打开成功
    if (fp == NULL)
    {
        printf("打开文件失败!错误编号:%d, 原因: %s\n", errno, strerror(errno));
        return 1;
    }
    // 4. 打开成功,进行等待
    printf("打开文件成功!此时你不能删除文件\n");

    char buf[8] = { 0 };
    // 循环逐行读取, fgets(缓冲区, 缓冲区大小, 文件指针)
    printf("sizeof(buf): %d\n", sizeof(buf));
    while (fgets(buf, sizeof(buf), fp) != NULL)
    {
        printf("%s", buf);
        printf("开始读一行\n");
    }

    // =============================================================

    printf("\n文件读取完毕\n");

    // 在Windows环境下,最常用的延时函数是Sleep函数。
    // Sleep函数定义在windows.h头文件中,它的参数是延时的毫秒数
    // 程序暂停期间,文件句柄一直被程序占用,你不能删除文件
    // Sleep(1000 * 3600);

    // 5. 关闭文件,释放文件句柄
    fclose(fp);

    return 0;
}

// 检查文件打开模式是否合法
bool is_valid_fopen_mode(const char* mode)
{
    // 合法模式列表
    const char* valid_modes[] = {
        // 文本模式
        "r",  "r+",
        "w",  "w+",
        "a",  "a+",

        // 二进制 b 模式
        "rb",  "rb+", "r+b",
        "wb",  "wb+", "w+b",
        "ab",  "ab+", "a+b"
    };

    // 计算一共有多少个合法模式
    int len = sizeof(valid_modes) / sizeof(valid_modes[0]);
    // 逐个比对
    for (int i = 0; i < len; i++)
    {
        // strcmp 也是 string.h 中的,表示对两个字符串进行比较,相等就等于0
        if (strcmp(mode, valid_modes[i]) == 0)
        {
            return true;  // 找到合法模式 → 返回真
        }
    }
    return false;  // 没找到 → 非法
}

运行程序,输出如下:

Snipaste_2026-05-02_12-38-06.png

可以看到,当第2-5行的长度比较长时,fgets每次读8-1=7个字符,一行就会被分几次读取!

buf长度设置成1024,然后再运行代码,此时输出如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
文件打开模式( r )合法!
文件句柄地址:015263B0
打开文件成功!此时你不能删除文件
sizeof(buf): 1024
{
开始读一行
    "sites": [
开始读一行
    { "name":"菜鸟教程" , "url":"www.runoob.com" },
开始读一行
    { "name":"google" , "url":"www.google.com" },
开始读一行
    { "name":"微博" , "url":"www.weibo.com" }
开始读一行
    ]
开始读一行
}开始读一行

文件读取完毕
7.2.4.1 使用fread读取文件

fread函数基本用法:

fread函数用于从文件中读取数据块。其原型如下:

1
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
  • ptr:指向用于存储读取数据的内存块的指针。
  • size:每个数据块的字节数。
  • count:要读取的数据块数量。
  • stream:指向FILE对象的指针,表示要读取的文件。

返回值:

  • 成功时返回读取的数据块数量。
  • 出错或遇到文件结束符时返回0

示例:

  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
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable: 6031)

#include <stdio.h>
// windows.h提供 Sleep 函数
#include <windows.h>
// errno.h 提供错误码,如 EACCES          13
#include <errno.h>
// string.h 提供 strerror 函数,获取错误码对应的异常说明
#include <string.h>
// stdbool.h 提供布尔逻辑true和false
#include <stdbool.h>

// 函数声明
bool is_valid_fopen_mode(const char* mode);

int main()
{
    const char* mode = "r";  // 随便写文件打开模式

    // 1. 检查模式对不对
    if (!is_valid_fopen_mode(mode))
    {
        printf("错误:文件打开模式( %s )不合法!\n", mode);
        return 1;
    }
    printf("文件打开模式( %s )合法!\n", mode);

    // 2. 打开文件
    // 直接用/替换\\作为路径分隔符
    FILE* fp = fopen("D:/BC101/Examples/L07/L07_07_FILE_READ_AND_WRITE/data.json", mode);
    printf("文件句柄地址:%p\n", fp);
    // 3. 检查文件是否打开成功
    if (fp == NULL)
    {
        printf("打开文件失败!错误编号:%d, 原因: %s\n", errno, strerror(errno));
        return 1;
    }
    // 4. 打开成功,进行等待
    printf("打开文件成功!此时你不能删除文件\n");

    //char buf[1024] = { 0 };
    //// 循环逐行读取, fgets(缓冲区, 缓冲区大小, 文件指针)
    //printf("sizeof(buf): %d\n", sizeof(buf));
    //while (fgets(buf, sizeof(buf), fp) != NULL)
    //{
    //    printf("%s", buf);
    //    printf("开始读一行\n");
    //}

    char buf[1024] = { 0 };
    int count;
    while ((count = fread(buf, sizeof(char), sizeof(buf) - 1, fp)) > 0)
    {
        buf[count] = '\0';   // 手动加字符串结束符,避免乱码
        printf("%s", buf);
    }

    // =============================================================

    printf("\n文件读取完毕\n");

    // 在Windows环境下,最常用的延时函数是Sleep函数。
    // Sleep函数定义在windows.h头文件中,它的参数是延时的毫秒数
    // 程序暂停期间,文件句柄一直被程序占用,你不能删除文件
    // Sleep(1000 * 3600);

    // 5. 关闭文件,释放文件句柄
    fclose(fp);

    return 0;
}

// 检查文件打开模式是否合法
bool is_valid_fopen_mode(const char* mode)
{
    // 合法模式列表
    const char* valid_modes[] = {
        // 文本模式
        "r",  "r+",
        "w",  "w+",
        "a",  "a+",

        // 二进制 b 模式
        "rb",  "rb+", "r+b",
        "wb",  "wb+", "w+b",
        "ab",  "ab+", "a+b"
    };

    // 计算一共有多少个合法模式
    int len = sizeof(valid_modes) / sizeof(valid_modes[0]);
    // 逐个比对
    for (int i = 0; i < len; i++)
    {
        // strcmp 也是 string.h 中的,表示对两个字符串进行比较,相等就等于0
        if (strcmp(mode, valid_modes[i]) == 0)
        {
            return true;  // 找到合法模式 → 返回真
        }
    }
    return false;  // 没找到 → 非法
}

可以看到,freadfgets的返回结果是不一样的!

也可以使用二进制方式读取文件:

 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
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable: 6031)

#include <stdio.h>
// windows.h提供 Sleep 函数
#include <windows.h>
// errno.h 提供错误码,如 EACCES          13
#include <errno.h>
// string.h 提供 strerror 函数,获取错误码对应的异常说明
#include <string.h>
// stdbool.h 提供布尔逻辑true和false
#include <stdbool.h>

// 函数声明
bool is_valid_fopen_mode(const char* mode);

int main()
{
    const char* mode = "rb";  // 随便写文件打开模式

    // 1. 检查模式对不对
    if (!is_valid_fopen_mode(mode))
    {
        printf("错误:文件打开模式( %s )不合法!\n", mode);
        return 1;
    }
    printf("文件打开模式( %s )合法!\n", mode);

    // 2. 打开文件
    // 直接用/替换\\作为路径分隔符
    FILE* fp = fopen("D:/BC101/Examples/L07/L07_07_FILE_READ_AND_WRITE/data.json", mode);
    printf("文件句柄地址:%p\n", fp);
    // 3. 检查文件是否打开成功
    if (fp == NULL)
    {
        printf("打开文件失败!错误编号:%d, 原因: %s\n", errno, strerror(errno));
        return 1;
    }
    // 4. 打开成功,进行等待
    printf("打开文件成功!\n");

    // 接收读取数据的缓冲区
    char buf[1024] = { 0 };
    int count;
    while ((count = fread(buf, sizeof(char), sizeof(buf) - 1, fp)) > 0)
    {
        buf[count] = '\0';   // 手动加字符串结束符,避免乱码
        printf("%s", buf);
    }

    // =============================================================

    printf("\n文件读取完毕\n");

    // 在Windows环境下,最常用的延时函数是Sleep函数。
    // Sleep函数定义在windows.h头文件中,它的参数是延时的毫秒数
    // 程序暂停期间,文件句柄一直被程序占用,你不能删除文件
    // Sleep(1000 * 3600);

    // 5. 关闭文件,释放文件句柄
    fclose(fp);

    return 0;
}

// 检查文件打开模式是否合法
bool is_valid_fopen_mode(const char* mode)
{
    // 合法模式列表
    const char* valid_modes[] = {
        // 文本模式
        "r",  "r+",
        "w",  "w+",
        "a",  "a+",

        // 二进制 b 模式
        "rb",  "rb+", "r+b",
        "wb",  "wb+", "w+b",
        "ab",  "ab+", "a+b"
    };

    // 计算一共有多少个合法模式
    int len = sizeof(valid_modes) / sizeof(valid_modes[0]);
    // 逐个比对
    for (int i = 0; i < len; i++)
    {
        // strcmp 也是 string.h 中的,表示对两个字符串进行比较,相等就等于0
        if (strcmp(mode, valid_modes[i]) == 0)
        {
            return true;  // 找到合法模式 → 返回真
        }
    }
    return false;  // 没找到 → 非法
}

此时运行程序,输出如下:

Snipaste_2026-05-03_18-18-42.png

7.2.2 使用fwrite函数写入数据到文件

7.2.1节学会了使用fopen打开文件,现在我们考虑如何将数据写入创建的磁盘文件,fwrite函数用于向已经打开的文件中写入数据。写入数据到磁盘文件的本质是将内存中的数据复制到磁盘文件,因此fwrite函数至少需要3项信息才能将文件写入磁盘。

  • 要复制的数据从哪里开始?

    • 你需要指定数据在内存中的起始位置,这往往是一个指针。它可以是数组的首地址、结构体的首地址,或者使用malloc函数分配的内存地址。
  • 要写入多少字节?

    • 指针只能说明首地址,接下来要指定有多少字节要传输到磁盘文件。fwrite函数使用2个参数来指定写入的字节数。
  • 要写到哪个文件里去?

    • 程序中可能同时打开了多个文件,fwrite函数当然不能自动判断要写到哪个文件里,此时可以使用之前创建的文件句柄来标识要写入的文件。

    fwrite函数的原型是:

    1
    
    size_t fwrite( const void* buffer, size_t size, size_t count, FILE* stream );
    
  • 第1个参数const void* buffer,参数名名buffer,类型是const void*,这个参数的作用是指定要写入文件的数据的内存地址。void*表示一个无类型指针,const限定符表示这个指针指向一个只读的内存区域。

  • 第2个参数size_t size,指定单个数据项的大小。

  • 第3个参数size_t count,表示要写入磁盘文件的数据项个数,单个数据项的大小乘以数据项的个数,即为要写入磁盘文件的总字节数。

  • 第4个参数FILE* stream,即为文件句柄,标识要写入的文件。

  • fwrite的返回值:fwrite函数返回成功写入磁盘文件的数据项个数。如果fwrite的返回值不等于参数count,则意味着写入的过程中发生了错误。

以下是一个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
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable: 6031)

#include <stdio.h>
// windows.h提供 Sleep 函数
#include <windows.h>
// errno.h 提供错误码,如 EACCES          13
#include <errno.h>
// string.h 提供 strerror 函数,获取错误码对应的异常说明
#include <string.h>
// stdbool.h 提供布尔逻辑true和false
#include <stdbool.h>

// 函数声明
bool is_valid_fopen_mode(const char* mode);

int main()
{
    const char* mode = "w";  // 随便写文件打开模式

    // 1. 检查模式对不对
    if (!is_valid_fopen_mode(mode))
    {
        printf("错误:文件打开模式( %s )不合法!\n", mode);
        return 1;
    }
    printf("文件打开模式( %s )合法!\n", mode);

    // 2. 打开文件
    // 直接用/替换\\作为路径分隔符
    FILE* fp = fopen("D:/BC101/Examples/L07/L07_07_FILE_READ_AND_WRITE/data.txt", mode);
    printf("文件句柄地址:%p\n", fp);
    // 3. 检查文件是否打开成功
    if (fp == NULL)
    {
        printf("打开文件失败!错误编号:%d, 原因: %s\n", errno, strerror(errno));
        return 1;
    }
    // 4. 打开成功,进行等待
    printf("打开文件成功!\n");

    int count;
    char buf[1024] = { 0 };   // 超大缓冲区,安全
    strcpy(buf, "ABCDEFG\n");   // 变量赋值
    int expect_len = strlen(buf);   // 计算真实要写入的长度(不含\0)
    count = fwrite(buf, sizeof(char), expect_len, fp);
    if (count != expect_len) {
        printf("\n文件写入异常,写入个数:%d,期望个数:%d\n", count, expect_len);
        printf("文件写入失败!错误编号:%d, 原因: %s\n", errno, strerror(errno));
        fclose(fp);
        return 1;
    }

    printf("\n文件写入完毕,写入个数:%d\n", count);

    // 5. 关闭文件,释放文件句柄
    fclose(fp);

    return 0;
}

// 检查文件打开模式是否合法
bool is_valid_fopen_mode(const char* mode)
{
    // 合法模式列表
    const char* valid_modes[] = {
        // 文本模式
        "r",  "r+",
        "w",  "w+",
        "a",  "a+",

        // 二进制 b 模式
        "rb",  "rb+", "r+b",
        "wb",  "wb+", "w+b",
        "ab",  "ab+", "a+b"
    };

    // 计算一共有多少个合法模式
    int len = sizeof(valid_modes) / sizeof(valid_modes[0]);
    // 逐个比对
    for (int i = 0; i < len; i++)
    {
        // strcmp 也是 string.h 中的,表示对两个字符串进行比较,相等就等于0
        if (strcmp(mode, valid_modes[i]) == 0)
        {
            return true;  // 找到合法模式 → 返回真
        }
    }
    return false;  // 没找到 → 非法
}

查看写入的文件内容:

Snipaste_2026-05-03_22-33-35.png

代码中关键信息说明:

  • int expect_len = strlen(buf); 获取真实要写入到文件的长度。
  • count = fwrite(buf, sizeof(char), expect_len, fp);写字符串写入到fp文件句柄对应的文件中,返回写入的个数count
  • if (count != expect_len) 判断实际写入数和期望数是否相同,相同则说明写入正常。

以下是一些可能导致写入失败的原因:

  • 磁盘满了
  • U 盘突然拔掉
  • 程序被杀死
  • 系统崩溃
7.2.3 文件缓冲区

内存是速度很快的电子器件,CPU可以直接访问 内存中的数据,但诸如硬盘这些外部存储器的速度远远不如内存。

因此当我们要往磁盘文件中多次写入数据时,每一次写入操作后需要等待较长时间才能将数据写入磁盘,在这段时间里无法进行下一次写入。这样一来程序的速度就被降低了!读取数据也一样,每次读取数据都要等到硬盘上的磁头运动完毕才能读取完成。

为了减少这种等待以提高访问外存的速度,操作系统使用了“文件缓冲区”技术,即通过在RAM中占用一块空间以换得文件读写性能的提升。

文件缓冲区是操作系统为每一个打开的文件开辟的一块内存区域。

在打开一个文件后,磁盘文件的部分或全部会被读入缓冲区,读取文件内容时优先从缓冲区中读取,只有文件缓冲区中不存在要读取的内容时才从磁盘上读入。缓冲区命中率越高,读取文件的速度就越快。

同样的,程序在写文件时,也是优先写到缓冲区中(并不一定及时写入硬盘),由于RAM的读写速度远远高于对磁盘的读写速度,使用文件缓冲区会大大提高磁盘文件的读写的效率。

文件缓冲区由操作系统管理,程序员一般不用对其进行干预。但需要注意的是,在一个文件读写文件后,应该使用fclose函数关闭文件缓冲区。关闭缓冲区使操作系统将最新的缓冲区数据写入磁盘。防止因意外事故造成数据丢失。

Licensed under the GNU General Public License v3.0
最后更新于 2026年05月04日 00:10