linux系统编程05-标准IO1

介绍

IO是一切实现的基础

stdio :标准io

sysio :系统调用io(文件io)

  • 关系:标准io是用系统调用io实现的
  • 使用原则:能用标准io就用标准io(移植性好、可以加速)

Untitled

标准IO:

FILE 类型贯穿始终

fopen();
fclose();

fgetc();
fputc();
fgets();
fputs();
fread();
fwrite();

printf();
scanf();

fseek();
ftell();
rewind();

fflush();

getline();

临时文件:
tmpnam();
tmpfile();

man手册:1章基本命令;2章系统调用;3章标准io;7章机制(epoll/tcp/socket) man 7 epoll

fopen

#include<stdio.h>

FILE *fopen(const char *pathname, const char *mode);
FILE *fopen(int fd, const char *mode);
FILE *fopen(const char *pathname, const char *mode, FILE *stream);

目前只介绍第一个原型

  • 返回值:成功返回 FILE * ,失败返回 errno
    • perror("提示信息")
    • strerror(errno)
  • 参数: 文件路径 pathname ; 打开方式 mode

示例代码:

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

int main()
{
        FILE *fp;

        fp = fopen("tmp", "r");
        if(fp == NULL)
        {
								//#include<errno.h>
                fprintf(stderr, "fopen() failed! errno = %d\n", errno);
                perror("fopen()");
								
								//#include<string.h>
                fprintf(stderr, "fopen()%s\n:", strerror(errno));
                exit(1);
        }
				puts("OK");

				flcose();
				exit(0);
}

运行结果:

Untitled

关于 mode 的要点:

  • 权限描述:

    r 只读;文件指针在开头;无不创建,报错
    r+ 读并写;文件指针在开头;无不创建,报错
    w 写;文件指针在开头;有则清空,无则创建
    w+ 读并写;文件指针在开头;有则清空,无则创建
    a 读并写;文件指针末尾的下一个;有则添加,无则创建
    a+ 读并写;文件指针看情况;有则添加,无则创建
  • mode is a string begining with one of above sequences 所以 fopen("tmp", "readwrite") 不会报错,而是会被识别为 fopen("tmp", "r")

  • 是否加 b :表示二进制:在windows环境下,stream分为”文本stream”和”二进制stream”,但是在linux环境下统一为stream,所以可以不添加

  • 关于 errno

    errno 在一开始是一个 全局 的整型变量,不同的值对应不同的错误。

    问题在于共用,所以如果不立刻打印就会被其他进程占据。

    现在的 errno 是一个私有的宏,映射到私有空间,不会产生数据冲突。

    //验证代码
    #include<errno.h>
    errno;
    

    终端执行: gcc -E errno.c 其中 -E 表示预处理

    运行结果:

    Untitled

  • mode 手册内容

    Untitled

  • fopen 创建文件时,被创建文件的权限

    linux中,默认文件创建权限为 0666 (0表示八进制),文件夹创建权限为 0777

    系统中有一个掩码 umask ,一般为 0002 ,文件创建权限为 0666 & ~umask 【或简单理解为 0666 - umask

    tmp权限为0664 = 0666-0002

    tmp权限为0664 = 0666-0002

fclose

fopen 的返回的指针存放在哪里:栈、静态区、堆

  • 如果存放在栈中,那么 fopen 执行结束后栈的空间会被销毁,那么必然拿不到指针,所以不会在栈中
  • 如果存放在静态区中 static FILE* ,那么由于静态区的变量只会被创建一次,那么打开多个文件时,就会使用同一个 FILE * ,这也不合理,所以不会放在静态区中
  • 放在堆中,可以解决上述问题,但是需要手动 mallocfree , mallocfopen 中,那么 free 必然是在 fclose

*fclose 一般不会失败,所以不检查返回值*

是资源一定有上限, fopen 最大能打开的文件数量也有限

测试代码:

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
                                                                                                                                           
int main()
{
    FILE *fp = NULL;
    int cnt = 0;
    while(1)
    {   
        fp = fopen("tmp","w");
        if(fp == NULL)
        {
            perror("fopen()");
            break;
        }
        cnt++;
    }   
    printf("cnt = %d\n", cnt);

    exit(0);
}

执行结果:

Untitled

由于进程产生时默认打开三个stream, stdin stdout stderr ,所以最大打开的流数目是1024个

可以通过 ulimit -a 查看,通过 ulimit -n xxx 修改

Untitled

fgetc\fputc

man fgetcman fputc 可知:

getchar = getc(stdin) = fgetc(stdin)

putchar = putc(c,stdout) = fputc(c,stdout)

并且根据man手册,getc和fgetc,putc和fputc的函数原型是一样的,那么二者有什么区别呢,实际上没有区别,只不过最早的时候getc是作为宏,而fgetc是作为函数。

宏和函数什么区别?宏占用编译时间,函数占用调用时间,内核中的链表大多使用宏和内联函数,就是为了节省调用时间。

函数原型:

int fgetc(FILE *stream);
int fputc(int ch, FILE *stream); 

Untitled

Untitled

注意,虽然读的是字符,但是为了防止出错,会被转换为 int 类型,所以需要用 int 类型接受 fgetc 的返回值,以及传给 fputc

例子1:编写 mycopy.c 实现 cp 的功能

示例代码:

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

int main(char argc, char **argv)
{
    FILE *fps, *fpd;
    int ch; 

    if(argc < 3)
    {   
        fprintf(stderr, "usage: %s <sourc_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)
    {   
        perror("fopen()");
        exit(1);
		}
	
    while(1)
    {
        ch = fgetc(fps);
        if(ch == EOF)
            break;
        fputc(ch, fpd);
    }
    fclose(fpd);
    fclose(fps);

    exit(0);
}

运行结果:

Untitled

例子2:编写一个程序,计算一个文件中的有效字符

示例代码:

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

int main(char argc, char **argv)
{   
    if(argc < 2)
    {   
        fprintf(stderr, "Usage: %s <file_name>", argv[0]);
        exit(1);
    }
    
    FILE *fp;
    int cnt = 0;
    
    fp = fopen(argv[1], "r");
    if(fp == NULL)
    {   
        perror("fopen()");
        exit(1);
    }   
    while(fgetc(fp) != EOF)
        cnt++;

    printf("cnt = %d\n", cnt);
		fclose(fp);
    
    exit(0);
}

运行结果:

Untitled

fgets\fputs

函数原型:

char *fgets(char *s, int size, FILE *stream);
int fputs(const char *s, FILE *stream);

fgets()

  • 参数:缓冲区指针,缓冲区大小,读取的流
  • 返回值:成功返回读取到的字符串,失败返回 NULL

fputs()

  • 参数:待写入的字符串、流
  • 返回值:成功返回一个非负数,失败返回 EOF 【一般不检查】

fgets()gets 的关系 : char *gets() 没有指定缓冲区大小,所以会出现缓冲区溢出等问题, fgets() 通过设定缓冲区大小解决了这一点。

fputs()puts() 的关系: puts(char *s) 无法输出流,默认 stdoutfputs() 可以

fgets() 正常结束有两种情况:

  1. 读取到 size-1 个字符,最后添加 \0
  2. 读取到 \n

缓冲区大小为5时,abcd实际上要读两次

缓冲区大小为5时,abcd实际上要读两次

例子:用 fgets()fputs() 实现指令 cp

示例代码:

#include<stdio.h>                                                                                                                                             
#include<stdlib.h>
##define BUFSIZE 1024
    
int main(char argc, char **argv)
{       
    FILE *fps, *fpd;
    char buf[BUFSIZE];

    if(argc < 3)
    {
        fprintf(stderr, "usage: %s <sourc_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)
    {   
        perror("fopen()");
        exit(1);
		}
		while(fgets(buf, BUFSIZE, fps) != NULL)
        fputs(buf, fpd);
    fclose(fpd);
    fclose(fps);

    exit(0);
}

运行结果:

Untitled

fread\fwrite

函数原型:数据块读写

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

fread 从stream中读取数据到ptr,每次读取的大小为size,读取nmemb个数据块

fwrite 将ptr中的数据写入到stream中,每次写入的大小为size,写入nmemb个数据块

返回值:成功读取或写入的 数据块 的个数

举例说明:

Untitled

  • 对于 fread(buf, 1, 10, fp)

数据量足够时返回10, 表示成功读取10个数据块,即读取10字节

数据量不足时返回成功读取的数据块个数x,即读取x个字节

  • 对于 fread(buf, 10, 1, fp)

数据量足够时返回1, 表示成功读取1个数据块,即读取10个字节

数据量不足时返回0, 此时读取0个字节

结论:一般场景下,推荐1个字节一个字节地读取,当作 fgetc() 来用,来保证读入文件的所有内容。

例子:用 fread()fwrite() 实现指令 cp

示例代码:

while((n = fread(buf, 1, BUFSIZE, fps)) > 0)
        fwrite(buf,1,n,fpd);

执行结果:

Untitled

其他的话:

在linux中io很重要:一切皆文件,所有的一切都抽象为文件进行控制,而程序员控制文件的方式就是io。比如socket传的是fd,fd可以通过 fdopen() 转换为 FILE,从而传唤为一个stream,然后程序员通过对stream的io来操控socket。

posted @ 2025-09-17 00:04  Miaops  阅读(14)  评论(0)    收藏  举报