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,最重要的一条:这些函数的读写都是带格式的,这些所谓的格式由下表规定:

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

相关推荐
LateBloomer77711 分钟前
FreeRTOS——信号量
笔记·stm32·学习·freertos
legend_jz15 分钟前
【Linux】线程控制
linux·服务器·开发语言·c++·笔记·学习·学习方法
Komorebi.py16 分钟前
【Linux】-学习笔记04
linux·笔记·学习
fengbizhe1 小时前
笔试-笔记2
c++·笔记
余为民同志1 小时前
mini-lsm通关笔记Week2Day4
笔记
墨染风华不染尘1 小时前
python之开发笔记
开发语言·笔记·python
徐霞客3202 小时前
Qt入门1——认识Qt的几个常用头文件和常用函数
开发语言·c++·笔记·qt
澜世2 小时前
2024小迪安全基础入门第三课
网络·笔记·安全·网络安全
Bald Baby2 小时前
JWT的使用
java·笔记·学习·servlet
rellvera3 小时前
【强化学习的数学原理】第02课-贝尔曼公式-笔记
笔记·机器学习