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语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!

相关推荐
杨荧37 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka
白子寰44 分钟前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++
王俊山IT1 小时前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
为将者,自当识天晓地。1 小时前
c++多线程
java·开发语言
小政爱学习!1 小时前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
k09331 小时前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
神奇夜光杯1 小时前
Python酷库之旅-第三方库Pandas(202)
开发语言·人工智能·python·excel·pandas·标准库及第三方库·学习与成长
Themberfue1 小时前
Java多线程详解⑤(全程干货!!!)线程安全问题 || 锁 || synchronized
java·开发语言·线程·多线程·synchronized·
plmm烟酒僧1 小时前
Windows下QT调用MinGW编译的OpenCV
开发语言·windows·qt·opencv
EricWang13582 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端