Unix C:文件标准IO

文件是数据的集合,文件操作分为标准IOstdio)和系统调用IOsysio);

  • 系统调用IO根据操作系统的实现方式而定,例如打开文件,Linux的系统调用为open,而Windows的系统调用为opendir
  • 标准IO提供了一套标准的IO实现的库函数(例如pringtffopen等),它实际上也是调用了系统IO进行操作。

常用的标准IO库函数

打开关闭文件 输入输出流 文件指针操作 缓存相关
fopen fgetcfputc fseek fflush
fclose fgetsfputs ftell
freadfwrite rewind
printf族,scanf

注意FILE类型贯穿始终;

打开文件 - fopen

FILE *fopen(const char *filename, const char *mode)
  • fopen函数的功能是打开文件,该函数需要两个参数
    • filename文件路径,需要打开的文件位置(文件名)
    • mode打开方式,程序可以对文件做的操作
  • fopen函数的返回值是FILE*类型,表示打开的文件
    • 如果文件打开失败,fopen的返回值为NULL
    • 打开成功后,使用FILE *类型指针来表示打开的文件
打开方式 作用 解释
r 只读 文件必须存在,从头开始读
w 只写 文件不存在就创建,存在就清空,从头开始写
a 追加 文件不存在就创建,存在就清空,从头开始写
r+ 读写 文件必须存在,从头开始读写
w+ 读写 文件不存在就创建,存在就清空,从头开始读写
a+ 追读 文件不存在就创建,存在不清空,从头开始读,从尾开始写
  • 模式rr+要求文件必须存在,其他模式都是有则清空,无则创建;

  • mode也可以包含字母b,放在最后或者中间,表示二进制流。例如“rb”“r+b”

  • 操作成功返回一个FILE指针。否则返回 NULL,且设置全局变量 errno 来标识错误(该全局变量在头文件errno.h中声明)。

      #define EPERM 1 /* Operation not permitted */
      #define ENOENT 2 /* No such file or directory */
      #define ESRCH 3 /* No such process */
      #define EINTR 4 /* Interrupted system call */
      #define EIO 5 /* I/O error */
      #define ENXIO 6 /* No such device or address */
      #define E2BIG 7 /* Argument list too long */
    

示例代码

#include<stdio.h>
#include <stdlib.h>
#include <errno.h>
/*
FILE *fopen(const char* path, const char *mode);  */
/*例如:以只读的方式打开当前目录下的text.txt文件,如果文件存在则返回打开成功,否则打开失败*/
FILE* fp = fopen("text.txt","r");
if(fp == NULL){
	// printf("file open failed\n");
	// fprintf(stderr, "fopen() failed! errno = %d.\n", errno);
	perror("fopen()");
	return -1;
}
else
	printf("file open success\n");
	
	
/*
若文件不存在则输出:
fopen(): No such file or directory
*/

fopen函数解析

由函数原型可知,fopen函数返回的是一个FILE类型的指针,FILE是一个结构体,由typedef进行了重命名,而指针实际上是指向结构体的指针。

FILE指针指向的结构体在内存空间的位置是那一块

  • :若fopen函数中定义的结构体在栈中创建,则fopen函数执行完之后该结构体就被释放了。
  • 静态区:若使用static关键字,则将该结构体保存在静态区,但这样只能存在一个FILE实例,不符合实际情况
  • :每次调用fopen函数都动态申请一块内存,在使用完成之后手动free释放内存即可,而fclose函数中就会调用free
// 简单的fopen源码分析
FILE *fopen(const char *filename, const char *mode) {
    FILE *tmp = NULL;
    tmp = malloc(sizeof(FILE));
    
    // 给结构体成员赋值初始化
    tmp->xxx = xxx;
    tmp->yyy = yyy;
    ...
        
    return tmp;
}

关闭文件 - fclose

int fclose(FILE *stream)
  • fclose函数的功能是关闭文件
    • stream:这是指向FILE 对象的指针,该FILE`对象指定了要被关闭的流。
    • 如果流成功关闭,则该方法返回零。如果失败,则返回 EOF。
  • fclose函数的参数是文件指针,即fopen函数的返回值
    • 如果文件正常关闭,fclose函数返回值为0,如果文件关闭异常,fclose函数返回值为非0
  • 对文件操作结束后,一定要使用fclose关闭文件,同时将文件指针置为NULL
    • 如果打开文件后不去关闭就会一直占用内存空间,造成资源浪费
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main(void) {
    FILE *fp;
    fp = fopen("tmp", "r");
    if(fp == NULL) {
        perror("fopen()");
        exit(1);
    }
    puts("OK!");
    fclose(fp);		//关闭文件操作
    fp = NULL;		//文件关闭后,文件指针置为NULL
    exit(0);
}


操作文件

fgetc & fputc

  • getcharputchar
int getchar(void); // 从标准输入 stdin 获取一个字符(一个无符号字符)。这等同于 getc 带有 stdin 作为参数
int putchar(int char); // 把参数 char 指定的字符(一个无符号字符)写入到标准输出 stdout 中。这等同于 putc 带有 stdout 作为参数
  • getcputc
int getc(FILE *stream); // 从指定的流 stream 获取下一个字符(一个无符号字,并把位置标识符往前移动。
int putc(int char, FILE *stream); // 把参数 char 指定的字符(一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动。
  • fgetcfputc
int fgetc(FILE *stream); // 从指定的流 stream 获取下一个字符(一个无符号字符),并把位置标识符往前移动。
// 该函数以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF。
int fputc(int char, FILE *stream); // 把参数 char 指定的字符(一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动。
// 如果没有发生错误,则返回被写入的字符。如果发生错误,则返回 EOF,并设置错误标识符。

getcputcfgetcfputc的区别

  • 两者的使用完全相同,只是实现不同。这里的f指的是function,而不是file
  • getcputc是通过宏定义实现,而fgetcfputc是通过函数来实现。

宏只占用编译时间,不占用调用时间,而函数相反,因此内核的实现通常使用宏来定义函数,减少调用时间。

代码示例

需求:拷贝文件

./mycpy src dest

实现代码:

#include <stdio.h>
#include <stdlib.h>

// 命令行传参
int main(int argc, char **argv) {

    FILE *fps, *fpd;
    int ch; // 存储读入的字符

    if(argc < 3) {
        fprintf(stderr, "Usage:%s <src_file> <dest_file>\n", argv[0]);
        exit(1);
    }

    fps = fopen(argv[1], "r");
    if(fps == NULL) {
        perror("fopen()");
        exit(1);
    }

    fpd = fopen(argv[2], "w");
    if(fpd == NULL) {
        fclose(fps);
        perror("fopen()");
        exit(1);
    }

    while(1) {
        ch = fgetc(fps);
        if(ch == EOF) { // 读到文件末尾结束循环
            break;
        }
        fputc(ch, fpd);
    }
    
	// 释放内存,后开的先关
    fclose(fpd);
    fclose(fps);

    exit(0);
}

使用:

./mycpy /usr/local/test /temp/out

读写文件 - fread & fwrite

fread函数的功能是将文件钟的数据读取到内存中以使用。

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
  • ptr — 从文件中读取数据要存储到的存储区首地址。

  • size— 这是要读取的每个元素的大小,以字节为单位。

  • memb— 这是元素的个数,每个元素的大小为 size 字节。

  • tream— 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流。

  • 成功读取的元素总数会以size_t对象返回,size_t对象是一个整型数据类型。如果总数与nmemb参数不同,则可能发生了一个错误或者到达了文件末尾。


fwrite函数功能是将内存中的数据写入到文件所在硬盘中。

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
  • ptr — 要向文件写入数据的存储区首地址。
  • size — 这是要被写入的每个元素的大小,以字节为单位。
  • nmemb — 这是元素的个数,每个元素的大小为 size 字节。
  • stream — 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流。
    • 如果成功,该函数返回一个size_t象,表示元素的总数,该对象是一个整型数据类型。如果该数字与 memb参数不同,则会显示一个错误。

简单的实例

fread(buf, size, nmemb, fp);

// 情况1:数据量足够
// 情况2:文件只有5个字节

// 读10个对象,每个对象1个字节
fread(buf, 1, 10, fp);

// 情况1:
// 第一次读:返回10(读到10个对象),读到10个字节
// 情况2:
// 第一次读:返回5(读到5个对象),读到5个字节

//--------------------------------

// 读1个对象,每个对象10个字节
fread(buf, 10, 1, fp);

// 情况1:
// 第一次读:返回1(读到1个对象),也读到10个字节
// 情况2:
// 第一次读:返回0(读不到1个对象,因为1个对象要10字节,而文件只有5个字节)

代码示例

freadfwrite代替fgtecfputc

#include <stdio.h>
#include <stdlib.h>
#define SIZE 1024

int main(int argc, char **argv) {

    FILE *fps, *fpd;
    char buf[SIZE];
    int n;

    if(argc < 3) {
        fprintf(stderr, "Usage:%s <src_file> <dest_file>\n", argv[0]);
        exit(1);
    }
    fps = fopen(argv[1], "r");
    if(fps == NULL) {
        perror("fopen()");
        exit(1);
    }
    fpd = fopen(argv[2], "w");
    if(fpd == NULL) {
        fclose(fps);
        perror("fopen()");
        exit(1);
    }
    // 如果成功读到n(n>0)个对象,则返回n
    // 将这n个对象写入流中
    while((n = fread(buf, 1, SIZE, fps)) > 0) 
            fwrite(buf, 1, n, fpd);

    fclose(fpd);
    fclose(fps);

    exit(0);
}

文件读写位置

  • 操作系统为每个打开的文件保留一个整数,表示下一次文件读写操作的开始位置
    • 这个整数就是文件头到这个位置直接包含的字节个数
    • 这个整数也叫做文件的位置指针
  • 每当从文件里获得n个字节或向文件里写入n个字节以后位置指针都会向后移动n个字节

fseek:设置文件位置,从whence位置开始根据偏移量offset设置文件位置。

int fseek(FILE *stream, long int offset, int whence)
  • stream — 这是指向 FILE 对象的指针,即操作的文件。

  • offset — 这是相对whence的偏移量,以字节为单位。

  • whence — 这是表示开始添加偏移offset的位置。它一般指定为下列常量之一:

    • SEEK_SET 0 把文件头作为基准位置
    • SEEK_CUR 1 把当前位置作为基准位置
    • SEEK_END 2 把文件尾作为基准位置
    int fseek(FILE *stream, long offset, int whence);
    
    FILE* fp = fopen("a.txt","w+");
    fseek(fp, 2, SEEK_SET);		//读写位置从文件头向后移动2个字节
    fseek(fp, 4, SEEK_CUR);		//读写位置从当前位置向后移动2个字节
    fseek(fp, -6, SEEK_END);	//读写位置从文件尾向前移动2个字节
    printf("位置在%d\n",ftell(fp));
    
    
  • 如果成功,则该函数返回零,否则返回非零值。


ftell:返回给定流 stream 的当前文件位置。

long int ftell(FILE *stream)
  • stream — 这是指向FILE对象的指针,即操作的文件。
  • 该函数返回位置标识符的当前值。如果发生错误,则返回-1L,全局变量errno被设置为一个正值。

程序实例——求程序的有效字节

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv){

    FILE *fp;
    if(argc < 2) {
        fprintf(stderr, "Usage...\n");
        exit(1);
    }

    fp = fopen(argv[1], "r");
    if(fp == NULL) {
        perror("fopen()");
        exit(1);
    }
	// 将指针定位在文件末尾
    fseek(fp, 0, SEEK_END);

    printf("%d\n", ftell(fp));

    exit(0);
}

rewind:设置文件位置为给定流 stream 的文件的开头。

void rewind(FILE *stream)

相当于

(void) fseek(stream, 0, SEEK_SET);

注意

fseekftell中偏移offset的修饰类型是long,因此只能对2G左右大小的文件进行操作,否则会超出long的范围;

fseekoftello则将偏移的修饰类型使用typedef定义为offset_t,具体类型交由系统决定,因此不存在文件大小的限制。但是这两个函数不是C标准库函数,而是隶属于POSIX标准(POSIX是标准C库的超集,或者说,C库是普通话,而POSIX是方言)。


fprintf和fscanf

这两个函数执行效率较低,不适合处理大数据量的文件

  • fprintf函数可以把数字直接记录到文件中

    • 参数:
      • 文件指针;
      • printf函数的参数
    fprintf(fp,"%d\n",a);
    
  • fscan函数可以从文本文件里获得数字并记录到内存里

    • 参数:
      • 文件指针;
      • 同scanf函数的参数
    fscanf(fp,"%d",&a);
    

刷新流 - fflush

fflush:刷新 流stream输出缓冲区。刷新,指的是将缓冲区(内存上的一片区域)的内容写入到磁盘(外存)中,或者输出到终端上显示。

int fflush(FILE *stream)
  • 如果参数为NULL,则刷新所有的已打开的流;
  • 如果成功,该函数返回零值。如果发生错误,则返回EOF,且设置错误标识符(即feof)。

场景一:程序执行完printf后没有输出打印内容

#include <stdio.h>

int main() {
    printf("Before while(1)");
    while(1);
    printf("After while(1)");
    return 0;
}

//进入到了while循环,但是程序没有输出"Before while(1)"
  • 因为对于标准输出,输出缓冲区刷新的时机:
    • 输出缓冲区满
    • 或者遇到换行符\n
    • 强制刷新
    • 进程结束

因此,可以修改为:

#include <stdio.h>
#include <stdlib.h>
int main() {
    // 换行刷新
    printf("Before while(1)\n");

    printf("Before while(1)");
    // 强制刷新
    fflush(stdout);// 或者 fflush(NULL);
    while(1);
    printf("After while(1)\n");
    exit(0);
}

缓冲的分类:

  • 行缓冲line buffered:针对标准输出(终端设备),有换行刷新,缓冲满刷新,强制刷新三种,后两个和全缓冲一致;
  • 全缓冲fully buffered默认缓冲机制(除标准输出【终端设备】,例如重定向到文件),有缓冲满刷新,强制刷新两种,强制刷新例如调用fflush函数,或者进程结束时也会强制刷新;此时换行符仅仅只是个换行符,没有刷新功能;
  • 无缓冲unbuffered:例如stderr,需要立即输出,数据会立即读入内存或者输出到外存文件和设备上;

setvbuf:定义流 stream 应如何缓冲。理解即可。

int setvbuf(FILE *stream, char *buffer, int mode, size_t size)
  • stream — 这是指向 FILE 对象的指针,该 FILE 对象标识了一个打开的流。
  • buffer — 这是分配给用户的缓冲。如果设置为 NULL,该函数会自动分配一个指定大小的缓冲。
  • mode — 这指定了文件缓冲的模式:
    • _IOFBF全缓冲:对于输出,数据在缓冲填满时被一次性写入。对于输入,缓冲会在请求输入且缓冲为空时被填充。
    • _IOLBF行缓冲:对于输出,数据在遇到换行符或者在缓冲填满时被写入,具体视情况而定。对于输入,缓冲会在请求输入且缓冲为空时被填充,直到遇到下一个换行符。
    • _IONBF无缓冲:不使用缓冲。每个 I/O 操作都被即时写入。buffersize 参数被忽略。

getline

之前介绍的函数,都不能获得完整的一整行(有缓冲区大小的限制),而下面介绍的getline函数则可以动态分配内存,当装不下完整一行时,又会申请额外的内存来存储。

getline是C++标准库函数,但不是C标准库函数,而是POSIX所定义的标准库函数(在POSIX IEEE Std 1003.1-2008标准出来之前,则只是GNU扩展库里的函数)。在gcc编译器中,对标准库stdio进行了扩展,加入了一个getline函数。

getline会生成一个包含一串从输入流读入的字符的字符串,直到以下情况发生会导致生成的此字符串结束:

  • 到文件结束
  • 遇到函数的定界符
  • 输入达到最大限度

函数原型:

#define _GNU_SOURCE // 通常将这种宏写在makefile中,现在的编译器没有了该宏,直接使用即可
#include <stdio.h>
ssize_t getline(char **lineptr, size_t *n, FILE *stream);
  • lineptr:指向存放该行字符的指针,如果是NULL,则有系统帮助malloc,请在使用完成后free释放。该参数是一个二级指针,因此传参需要一级指针的地址。即函数会把读取到的字符串的首地址存放在一级指针中。

    // 传参:
    char *ptr;
    // 函数内的实际操作:
    // 假设读取到的字符串Hello的首地址为0x000
    &ptr = 0x000; // 此时ptr就指向了Hello
    
  • n:如果是由系统malloc的指针填0;

  • stream:函数需要读取的FILE流

  • 返回值:成功返回读取的字节数,失败或读完返回-1

代码示例

int main(int argc, char **argv) {
    FILE *fp;
    // 一定要初始化,否则指针会指向内存中的随机位置
    char *linebuf = NULL;
    size_t linesize = 0;
    if(argc < 2) {
        fprintf(stderr, "Usage...\n");
    }
    fp = fopen(argv[1], "r");
    if(fp == NULL) {
        perror("fopen()");
        exit(1);
    }
    while(1) {
        // 当返回-1时则读完
    	if(getline(&linebuf, &linesize, fp) < 0)
            break;
       	printf("%d\n", strlen(linebuf));
    }
    fclose(fp);
    exit(0);
}

标准文件指针

标准文件指针 作用 作用位置
stdin 标准输入 键盘
stdout 标准输出 终端窗口
stderr 标准错误 终端窗口
int a = 100;
/*两种输出到终端窗口的方法是等效的*/
printf("%d\n",a);
fprintf(stdout, "%d\n", a);
/*两种从键盘输入的方法是等效的*/
scanf("%d",&a);
fscanf(stdin, "%d", &a);

临时文件

临时文件产生的问题:

  • 如何命名不冲突
  • 如何保证及时销毁

tmpnam:生成并返回一个有效的临时文件名,该文件名之前是不存在的。如果 str 为空,则只会返回临时文件名。

存在并发问题,可能会产生两个或多个名字相同的临时文件。

char *tmpnam(char *str)
  • str — 这是一个指向字符数组的指针,其中,临时文件名将被存储为 C 字符串。
  • 返回一个指向 C 字符串的指针,该字符串存储了临时文件名。如果 str 是一个空指针,则该指针指向一个内部缓冲区,缓冲区在下一次调用函数时被覆盖。
  • 如果 str 不是一个空指针,则返回 str。如果函数未能成功创建可用的文件名,则返回一个空指针。

tmpfile:以二进制更新模式(wb+)创建临时文件。被创建的临时文件会在流关闭的时候或者在程序终止的时候自动删除。

该文件没有名字(匿名文件)只返回指向FILE的指针,因此不存在命名冲突的问题,同时会自动删除,因此可以及时销毁。

FILE *tmpfile(void)
  • 如果成功,该函数返回一个指向被创建的临时文件的流指针。如果文件未被创建,则返回 NULL。
posted @   -O-n-e-  阅读(37)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
点击右上角即可分享
微信分享提示