C语言-文件操作-一些我想到的、见到的奇怪的问题

博客主页:【夜泉_ly

本文专栏:【C语言

欢迎点赞👍收藏⭐关注❤️

C语言-文件操作-一些我想到的、见到的奇怪的问题

前言

关于C语言文件操作的文章在CSND上很多很多,我自身才疏学浅,补充不了什么内容,因此,我决定换一个角度,分享一下我想到的、见到的奇怪的问题。

这个奇怪当然也是我单方面认为的

注:本篇只讨论数据文件,且只讨论纯文本文件(.txt)。

1.在不关闭文件的情况下,连续多次调用 fopen() 打开同一个文件,会发生什么?

1.1过程

为了验证这个问题,我当然不想创建很多的文件指针来一对一,因此,我尝试使用同名指针两次打开同一个文件:

c 复制代码
#include <stdio.h>
int main()
{
    FILE* pf = fopen("test.txt", "w");
    printf("time 1:%p\n", pf);

    pf = fopen("test.txt", "w");
    printf("time 2:%p\n", pf);
    return 0;
}

运行结果如下图:

这说明,即便是同一个文件指针,同一个文件,只要使用fopen,系统都会再用一个新的文件描述符。

那么要验证这个问题就很简单了,代码如下:

c 复制代码
#include <stdio.h>
int main()
{
    FILE* pf;
    int count = 1;
    while (1)
    {
        pf = fopen("test.txt", "w");
        if (pf == NULL)
        {
            printf("time %d::", count);
            perror("");
            break;
        }
        printf("time %d:%p\n", count++, pf);
    }
    return 0;
}

运行结果如下图:

可以看见当打开第510次时,以及没有更多的文件描述符了。

当然,我用的是VS2022,而在其他环境下具体次数可能会改变,但应该还是会有个限制的。

在现代操作系统中,每个进程可以同时打开的文件数是有限的(通常可以通过 ulimit -n 查看限制)。每次调用 fopen(),操作系统都会为文件分配一个文件描述符,文件描述符是操作系统为进程管理文件资源的句柄。当打开的文件数量超过系统允许的最大数量时,fopen() 将返回 NULL

1.2结论

对文件只开不关,会使得系统提供的文件描述符被耗尽,最后fopen会返回一个空指针。

1.3意义

如果有人只会开文件,不会关文件,那么那个人多半在开文件时,也不会检查文件是否打开成功🤣。

那么这样的错误,就有可能出现了:

c 复制代码
fputs("HaHa", NULL);

具体场景如下:

c 复制代码
#include <stdio.h>
int main()
{
    FILE* pf;
    int count = 1;
    while (1)
    {
        pf = fopen("test.txt", "w");
        if (pf == NULL)
        {
            printf("time %d::", count);
            perror("");
            break;
        }
        printf("time %d:%p\n", count++, pf);
    }
    fputs("HaHa", pf);// pf 此时已经为 NULL了
    return 0;
}

如果运行,程序最终会崩掉:

因此,在使用完文件后,一定要fclose

2.fseek如果设置到文件前的位置会发生什么?

2.1过程

有这样的问题是因为有一天我写了这样一个代码:

c 复制代码
#include <stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (!pf)return 1;
	
	int ch = 'a';
	fputc(ch,pf);

	fseek(pf, -1000000, SEEK_SET);
	
	ch = 'a';
	fputc(ch, pf);
	
	fclose(pf);
	pf = NULL;
	return 0;
}

我在fgetc之后用了fseek,并把位置设置到了开始位置的-10000,之后再次使用了fgetc

然后程序运行成功了,并且在文件中输入了两个a

复制代码
aa

为什么不是一个a或者报错?

其实非常简单,因为刚进fseek就被弹出来了:

c 复制代码
#include <stdio.h>
int main()
{
    FILE* pf = fopen("test.txt", "w");
    if (!pf)return 1;

    int ch = 'a';
    fputc(ch, pf);
    printf("fseek前的偏移量:%d\n", ftell(pf));
    if (fseek(pf, -1000000, SEEK_SET)) 
    {
        perror("fseek failed");
    }
    printf("fseek后的偏移量:%d\n", ftell(pf));
    ch = 'a';
    fputc(ch, pf);

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

运行结果如下图:

1.2结论

什么都不会发生,fseek会返回一个非零值,并设置错误码(对应的错误信息就是 Invalid argument),但偏移量不会改变。

3.fseek设置到单个汉字的中间会发生什么?

3.1过程

众所周知,一个汉字占多个字节,那我用fseek设置到这多个字节的中间会发生什么呢?

代码如下:

c 复制代码
#include <stdio.h>
int main()
{
    FILE* pf = fopen("test.txt", "w");
    if (!pf)return 1;

    fprintf(pf, "今天的日期:20240921");

    fclose(pf);

    pf = fopen("test.txt", "r");
    if (!pf)return 1;

    fseek(pf, 1, SEEK_SET);
    char ch[100];
    fscanf(pf, "%s", ch);
    printf("%s\n", ch);

    return 0;
}

运行结果如下:

3.2结论

可能会输出乱码。

3.3意义

在C语言中,有很多地方使用汉字会导致未定义行为,因此尽量避免使用汉字。

4.同时读、写同一个文件会发生什么?

4.1过程

代码如下:

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

int main() {
    FILE* write = fopen("test.txt", "w");
    FILE* read = fopen("test.txt", "r");
    if (!read || !write)return 1;

    fputs("Hello World!", write);

    char ch[100];
    fgets(ch, 100, read);
    printf("%s\n", ch);

    fclose(write);
    write = NULL;
    fclose(read);
    read = NULL;
    return 0;
}

这里,我在对文件写入之后立刻读取,最终得到下图的结果:

但如果我在fputs语句之后,刷新缓冲区,则会正常输出:

c 复制代码
    fputs("Hello World!", write);
    fflush(write);

如果我将fclose提前,也能正常输出:

c 复制代码
    fputs("Hello World!", write);
    fclose(write);
    write = NULL;

4.2结论

可能缓冲区没被刷新,导致读到乱码或不完整的信息。

5.在追加模式下使用fseek会不会覆盖原文件?

5.1过程

先写,再追加,最后读:

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

int main() 
{
    FILE* write = fopen("test.txt", "w");
    if (!write)return 1;
    fputs("Hello World!", write);
    fclose(write);
    write = NULL;
    
    FILE* add = fopen("test.txt", "a");
    if (!add)return 1;
    fseek(add, -10, SEEK_SET);
    perror("");
    fputs("xxxxxxxxxx", add);
    fclose(add);
    add = NULL;

    FILE* read = fopen("test.txt", "r");
    char ch[100];
    fgets(ch, 100, read);
    printf("%s\n", ch);

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

结果如下图:

5.2结论

并不会覆盖原文件,fseek又是一进去就被弹出来了(rewind也不行,会设置在追加的起始位置)。

6.在只读模式下写入会发生什么?

6.1过程

我先写入"Hello World!",然后在只读模式下尝试写入信息(还加了一个perror打印错误信息),最后读取文件信息。

代码如下:

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

int main() {
    FILE* write = fopen("test.txt", "w");
    if (!write)return 1;
    fputs("Hello World!", write);
    fclose(write);
    write = NULL;

    FILE* pf = fopen("test.txt", "r");
    if (!pf)return 1;

    fprintf(pf, "HaHa");
    perror("");

    fclose(pf);

    FILE* read = fopen("test.txt", "r");
    char ch[100];
    fgets(ch, 100, read);
    printf("%s\n", ch);

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

运行结果如下图:

错误信息是坏的文件描述符,可能是指我的文件指针用错了吧。

6.2结论

会拒绝写入,原文件信息不会改变。

7.FILE类型的结构体是开在堆上的吗?如果是,free它一下会发生什么?

7.1过程

先简单验证一下VS2022中是不是开在堆上的:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* a = (int*)malloc(sizeof(int));
	int* b = (int*)malloc(sizeof(int));
	FILE* pf = fopen("test.txt", "w");
	printf("%p\n%p\n%p", a, b, pf);
	return 0;
}

运行结果如下图:

可以发现,这三个变量的地址相近,因此,可以认为FILE类型的结构体是开在堆上的。

既然如此,那么free(文件指针)应该是可以运行的:

c 复制代码
#include <stdio.h>
#include <string.h>
int main()
{
    FILE* write = fopen("test.txt", "w");
    if (!write)return 1;
    fputs("Hello World!", write);
    fflush(write);
    free(write);
    
    FILE* read = fopen("test.txt", "r");
    char ch[100];
    fgets(ch, 100, read);
    printf("%s\n", ch);

    fclose(read);
    read = NULL;

    return 0;
}

运行结果如下图:

7.2结论

在VS2022中,FILE类型的结构体是开在堆上的,因此,free(结构体指针)时不会报错。

但是,FILE类型的结构体并不是通过 malloccalloc 分配的内存,所以使用 free 会导致未定义行为,如,在下一次读取时输出一堆乱码。

7.3意义

虽然 FILE* 在堆上分配,但由于它是由C标准库通过 fopen() 处理的,不应直接使用 free(),而应该使用 fclose() 来正确释放资源!!!


希望本篇文章对你有所帮助!并激发你进一步探索编程的兴趣!

本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!

相关推荐
xyq20248 分钟前
Memcached stats items 命令详解
开发语言
Evand J9 分钟前
【MATLAB例程】多传感器协同DOA目标跟踪与EKF滤波,输出动态目标轨迹、轨迹误差对比分析
开发语言·matlab·目标跟踪·滤波·定位·导航
csbysj20209 分钟前
《jEasyUI 自定义分页》
开发语言
初心未改HD11 分钟前
Go语言Context深度解析与工程实践
开发语言·golang
SilentSamsara15 分钟前
Python 内存管理:引用计数、循环垃圾回收与内存泄漏排查
开发语言·vscode·python·青少年编程·pycharm
Rabitebla1 小时前
【C++】string 类:原理、踩坑与对象语义
linux·c语言·数据结构·c++·算法·github·学习方法
傻啦嘿哟2 小时前
如何在 Python 中使用 colorama 库来给输出添加颜色
开发语言·python
geovindu3 小时前
go: Visitor Pattern
开发语言·设计模式·golang·访问者模式
宣宣猪的小花园.3 小时前
C语言重难点全解析:内存管理到位运算
c语言·开发语言·单片机
方安乐7 小时前
python之向量、向量和、向量点积
开发语言·python·numpy