Linux笔记 --- 标准IO

系统IO的最大特点一个是更具通用性,不管是普通文件、管道文件、设备节点文件、接字文件等等都可以使用,另一个是他的简约性,对文件内数据的读写在任何情况下都是带任何格式的,而且数据的读写也都没有经过任何缓冲处理,这样做的理由是尽量精简内API,而更加丰富的功能应该交给第三方库去进一步完善。

标准C库是最常用的第三方库,而标准IO就是标准C库中的一部分接口,这一部分口实际上是系统IO的封装,他提供了更加丰富的读写方式,比如可以按格式读写、按SCII码字符读写、按二进制读写、按行读写、按数据块读写等等,还可以提供数据读写冲功能,极大提高程序读写效率。

所有的系统IO函数都是围绕所谓的"文件描述符"进行的,这个文件描符由函数open()获取,而所有的标准IO都是围绕所谓的"文件指针"进的,这个文件指针则是由fopen()获取的,他是第一个需要掌握的标准IO函数:

打开/关闭

返回值的文件指针是一种指向FILE{}的结构体,在标准IO中被定义

可以看到,使用标准IO函数处理文件的最大特点是,数据将会先存储在一个标准IO缓冲区中,而后在一定条件下才被一并flush(冲洗,或称刷新)至内核缓冲区,而不是像系统IO那样,数据直接被 flush至内核。

注意到,标准IO函数 fopen()实质上是系统IO函数 open()的封装,他们是一一对应的,每一次fopen()都会导致系统分配一个file{}结构体和一个FILE{}来保存维护该文件的读写信息,每一次的打开和操作都可以不一样,是相对独立的,因此可以在多线程或者多进程中多次打开同一个文件,再利用文件空洞技术进行多点读写。

另外由上一节得知,标准输入输出设备是默认被打开的,在标准IO中也是一样,他们在程序的一开始就已经拥有相应的文件指针了:

跟fopen()配合使用的是fclose函数

读写

标准IO的读写函数接非常多,下面列出最常用的函数集合

第一组:读写一个字符的函数

需要注意的几点:

1,fgec()、getc()和getchar()返回值是int,而不是char,原因是因为他们在出 错或者读到文件末尾的时候需要返回一个值为---1的EOF标记,而char型数据有可能因为系统的差异而无法表示负整数。

2,当fgec()、getc()和getchar()返回EOF时,有可能是发生了错误,也有可能是读到了文件末尾,这是要用以下两个函数接口来进一步加以判断:

3,getchar()缺省从标准输入设备读取一个字符。

4,putchar()缺省从标准输出设备输出一个字符。

5,fgetc()和fputc()是函数,getc()和putc()是宏定义。

6,两组输入输出函数一般成对地使用,fgetc()和fputc(),getc()和putc(), getchar()和 putchar().

一个例子将这些函数的使用方法综合:

cpp 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>


int main(int argc, char const *argv[])
{
    FILE *fp1 = fopen("in.txt","w");
    FILE *fp2 = fopen("out.txt","r");

    if(fp1 == NULL || fp2 == NULL)
    {
        perror("fopen()");
        exit(1);
    }

    int c,total = 0;
    while (1)
    {
        c = fgetc(fp2);
        if(c == EOF && feof(fp2))
        {
            printf("copy compeleted,%d was been copied\n",total);
            break;
        }
        else if(ferror(fp2))
        {
            perror("fgetc()");
            break;
        }
        fputc(c,fp1);
        total++;
    }

    fclose(fp1);
    fclose(fp2);
    
    return 0;
}

第二组:每次一行的读写函数

值得注意的有以下几点:

1,fgets()跟fgetc()一样,当其返回NULL时并不能确定究竟是达到文件末尾还是碰到错误,需要用 feof()/ferror()来进一步判断。

2,fgets()每次读取至多不超过size个字节的一行,所谓"一行"即数据至多包含一个换行符\n′。

3,gets()是一个已经过时的接口,因为他没有指定自定义缓冲区s的大小,这样很容易造成缓冲区溢出,导致程序段访问错误。

4,fgets()和fputs(),gets()和puts()一般成对使用,鉴于gets()的不安全性, 一般建议使用前者。

下面是使用该组函数实现的普通文件拷贝示例代码:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

struct node //栈节点结构体
{
    int data;
    struct node *next;
};

struct linked_stack //管理结构体
{
    int size;
    struct node *top;   //指向栈顶节点
};

struct linked_stack *init_stack (void)
{
    struct linked_stack *s;
    s = malloc(sizeof(struct linked_stack));

    if(s != NULL)
    {
        s->size = 0;
        s->top = NULL;
    }

    return s;
}

//创建新节点
struct node *new_node(int data)
{
    struct node *new;
    new = malloc(sizeof(struct node));

    if(new != NULL)
    {
        new->data = data;
        new->next = NULL;
    }

    return new;
}

//压栈
bool push(struct linked_stack *s,struct node *new)
{
    if (s == NULL || new == NULL)
        return false;
    
    new->next = s->top;
    s->top = new;
    s->size++;

    return true;
}

bool stack_empty(struct linked_stack *s)
{
    return s->size == 0;
}

bool pop(struct linked_stack *s,struct node **p)
{
    if(s == NULL || p == NULL || stack_empty(s))
        return false;

    *p = s->top;
    s->top = s->top->next;
    (*p)->next = NULL;
    s->size--;

    return true;
}

void show(struct linked_stack *A,struct linked_stack *B,struct linked_stack *C)
{
    FILE *fp; /*建立文件指针*/
    fp = fopen("/mnt/e/GZ2112/程序入门/test_txt/hano.txt","a");

    int maxlen,len;

    maxlen = A->size > B->size ? A->size : B->size;
    maxlen = maxlen > C->size ? maxlen : C->size;
    len = maxlen;

    struct node *t1 = A->top;
    struct node *t2 = B->top;
    struct node *t3 = C->top;

    int i;
    for (i = 0; i < maxlen; i++)
    {
        if(t1 != NULL && len <= A->size)
        {
            fprintf(fp,"%d",t1->data);
            t1 = t1->next;
        }
        fprintf(fp,"\t");

        if(t2 != NULL && len <= B->size)
        {
            fprintf(fp,"%d",t2->data);
            t2 = t2->next;
        }
        fprintf(fp,"\t");

        if(t3 != NULL && len <= C->size)
        {
            fprintf(fp,"%d",t3->data);
            t3 = t3->next;
        }
        fprintf(fp,"\t");

        fprintf(fp,"\n");
        len--;
    }
    fprintf(fp,"A\tB\tC\n");
    fprintf(fp,"-----------------\n");
    
    fclose(fp);
}

void hano(struct linked_stack *A,struct linked_stack *B,struct linked_stack *C,int n)
{
    if(n <= 0)
        return;
    
    struct node *tmp;
    hano(A,C,B,n-1);    //将n-1块从A借助C移向B

    getchar();          //每回车一次进行一步
    show(A,B,C);

    pop(A,&tmp);
    push(C,tmp);        //将A最下面一块移动到C

    hano(B,A,C,n-1);    //将n-1块从B借助A移向C
}

int main(int argc, char const *argv[])
{
    struct linked_stack *A = init_stack();
    struct linked_stack *B = init_stack();
    struct linked_stack *C = init_stack();

    int hanois = 0;
    scanf("%d",&hanois);

    int i;
    for (i = 0; i < hanois; i++)
    {
        struct node *new = new_node(hanois-i);  //将汉诺塔中的块放入栈A(柱A)
        push(A,new);
    }
    
    hano(A,B,C,hanois);

    show(A,B,C);

    return 0;
}

第三组:每次读写若干数据块的标准IO接口函数

这一组标准IO函数被称为"直接IO函数"或者"二进制IO函数",因为他们对数据的读写严格按照规定的数据块数和数据块的大小来处理,而不会对数据格式做任何处理,而且当数据块中出现特殊字符,比如换行符"\n、字符串结束标记"\0等时不会受到影响。需要注意的几点:

1,如果fread()返回值小于nmemb时,则可能已达末尾,或者遇到错误,需要借助于feof()/ferror()来加以进一步判断。

2,当发生上述第1种情况时,其返回值并不能真正反映其读取或者写入的数据块数,而只是一个所谓的"截短值",比如正常读取5个数据块,每个数据块100个字节,在执行成功的情况下返回值是5,表示读到5个数据块总共500个字节,但是如果只读到499个数据块,那么返回值就变成4,而如果读到99个字节,那么fread()会返回0。因此当发生返回值小于nmemb时,需要仔细确定究竟读取了几个字节,而不能直接从返回值确定。

第四组:获取或设置文件当前位置偏移量

注意:

1.fseek函数的使用基本与lseek函数相同

2.rewind(fp)相当于fseek(fp,0L,SEEK_SET);

利用上面两组函数再次实现读写功能:

cpp 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>

#define BUFSIZE 100
#define NMEMB 5

int main(int argc, char const *argv[])
{
    FILE *fp1 = fopen("in.txt","w");
    FILE *fp2 = fopen("out.txt","r");

    if(fp1 == NULL || fp2 == NULL)
    {
        perror("fopen()");
        exit(1);
    }
    char buf[BUFSIZE];
    int total = 0;
    long pos1 , pos2;
    while (1)
    {
        bzero(buf,BUFSIZE);
        pos1 = ftell(fp2);
        if (fread(buf,BUFSIZE,NMEMB,fp2) < NMEMB)
        {
            if(feof(fp2))
            {
                //将未满NMENB的数据写入
                pos2 = ftell(fp2);
                fwrite(buf,pos2 - pos1,1,fp1);
                total += pos2-pos1; 
                printf("copy compeleted,%d was been copied\n",total);
                break;
            }
            else if(ferror(fp2))
            {
                perror("fgetc()");
                break;
            }
        }
        
        fwrite(buf,BUFSIZE,NMEMB,fp1);
        total += BUFSIZE * NMEMB;
    }

    fclose(fp1);
    fclose(fp2);
    
    return 0;
}

第五组:标准格式化IO函数

格式化IO函数中最常用的莫过于printf()和scanf()了,但从上表中可以看到,他们其实各自都有一些功能类似的兄弟函数可用,使用这些函数需要注意以下几点:

1,fprintf()不仅可以像printf()一样向标准输出设备输出信息,也可以向由stream指定的任何有相应权限的文件写入数据。

2,sprintf()和snprintf()都是向一块自定义缓冲区写入数据,不同的是后者第二个参数提供了这块缓冲区的大小,避免缓冲区溢出,因此应尽量使用后者,放弃使用前者。

3,fscanf()不仅可以像scanf()一样从标准输入设备读入信息,也可以从由stream指定的任何有相应权限的文件读入数据。

4,sscanf()从一块由s指定的自定义缓冲区中读入数据。

5,最重要的一条:这些函数的读写都是带格式的,这些所谓的格式由下表规定:

这一组函数与之前最大的区别在于带有格式控制,因此适用于有格式的文件处理,比如学生信息等等此类数据。

相关推荐
~在杰难逃~9 分钟前
Day23笔记-Day21和Day22作业讲解&单例类
开发语言·笔记·python·pycharm·数据分析
笑鸿的学习笔记1 小时前
git笔记之在多个分支中复用某个分支提交的更改
笔记·git
王俊山IT6 小时前
C++学习笔记----7、使用类与对象获得高性能(二)---- 理解对象生命周期(7)
开发语言·c++·笔记·学习
林九生7 小时前
【Redis】个人笔记
数据库·redis·笔记
清流君7 小时前
【自动驾驶】控制算法(八)横向控制Ⅳ | 调试与优化——让车辆行驶更平稳!
人工智能·笔记·算法·自动驾驶·控制算法
牢鹅出海7 小时前
Facebook开发者篇 - API拉取广告投放数据对接流程
经验分享·笔记·facebook
杳戢7 小时前
技术美术百人计划 | 《4.1 Bloom算法》笔记
人工智能·笔记·深度学习·计算机视觉·unity·图形渲染·技术美术
Pandaconda8 小时前
【计算机网络 - 基础问题】每日 3 题(十三)
开发语言·经验分享·笔记·后端·计算机网络·面试·职场和发展
一道秘制的小菜10 小时前
C++第八节课 日期类的补充
linux·开发语言·数据结构·c++·笔记·算法·链表
零星_AagT10 小时前
VulnHub-Bilu_b0x靶机笔记
笔记·网络安全·代码审计·vulnhub靶机