Linux应用开发(一)——Linux的标准IO

一、标准IO的理解

  • IO的理解,I : input , 读, 输入, 接收;O : output , 写, 输出, 发送
  • 标准IO库是C语言的标准库。
  • 不仅在Linux系统,在很多操作系统上都实现了标准I/O库。
  • 标准I/O库由ANSI C标准说明 , 是C语言的标准库
  • 标准I/O库处理很多细节,如缓存分配、以优化长度执行I/O等,这样使用户不必关心如何选择合适的块长度。
  • 标准I/O在系统调用(文件IO)函数基础上构造的,它便于用户使用。
  • 标准I/O库及其头文件stdio.h为底层I/O系统调用提供了一个通用的接口。

1、文件指针

FILE指针:每个被打开的文件,操作系统都在内存中开辟一个区域,用来存放被打开文件的有关信息,这些信息是保存在一个结构体类型的变量中,该结构体类型是由系统定义的,取名为FILE。FILE类型定义如下:

  • 标准I/O库的所有操作都是围绕流(stream)来进行的,在标准I/O中,流用FILE *来描述。
  • 标准I/O库是由Dennis Ritchie在1975年左右编写的。

2、流(stream)

  • 定义:所有的I/O操作仅是简单的从程序移进或者移出,这种字节流,就称为流。
  • 分类:文本流/二进制流。
  • **文本流定义:在流中处理的数据是以字符出现。**在文本流中,'\n'被转换成换行符LF的ASCII码是0AH。而当输出时,0AH被转换成'\n',数字2001在文本流中的表示方法为 '2' '0' '0' '1',ASCII: 50 48 48 49
  • 二进制流定义:流中处理的是二进制序列。 若流中有字符,则用一个字节的二进制。ASCII码表示;若是数字,则用对应的二进制数表示,对'\n'不进行变换。数字2001在二进制流中的表示方法为 00000111 11010001。

3、缓冲文件系统(高级磁盘IO)

  • 目的:尽量减少使用read/write的调用。
  • 定义:操作系统自动的在内存中为每一个正在使用的文件开辟一个缓冲区,从内存向磁盘输出数据必须先送到内存缓冲区,装满缓冲区在一起送到磁盘中去。从磁盘中读数据,则一次从磁盘文件将一批数据读入到内存缓冲区中,然后再从缓冲区逐个的将数据送到程序的数据区。
  • 类比景区的摆渡车
  • 分类:全缓存,行缓存,不缓存。

4、非缓冲文件系统 (低级磁盘IO)

  • 定义:依靠于操作系统,通过操作系统的功能对文件进行读写,是系统级的输入输出。
  • 没有缓存的概念。
  • 类比城市的公交车

5、标准I/O (stdin,stdout,stderr)

标准I/O预定义3个流,他们可以自动地为进程所使用。

二、打开流 fopen

1、fopen打开流

cpp 复制代码
FILE *fopen(const char *pathname, const char *mode);

函数的功能:打开一个文件 , 并返回一个流(文件指针)

函数的参数:【pathname】 : 要打开的文件名 , 例如"/home/linux/1.txt"。【mode】: 打开文件的方式

|------------|-------------------------------------------------------------------|
| 命令 | 解释 |
| b | binary的缩写, 表示以二进制的形式打开文件, 操作的是二进制流 |
| r或rb | 打开只读文件,该文件必须存在,文件不存在报错。 |
| r+或r+b | 打开可读写的文件,该文件必须存在。 |
| w或wb | 打开只写文件,若文件存在则文件长度清为0,即会擦些文件以前内容。若文件不存在则建立该文件。 |
| w+或w+b或wb+ | 打开可读写文件,若文件存在则文件长度清为零,即会擦些文件以前内容。若文件不存在则建立该文件。 |
| a或ab | 以附加的方式打开只写文件。若文件不存在,则会建立该文件, 如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。 |
| a+或a+b或ab+ | 以附加方式打开可读写的文件。若文件不存在,则会建立该文 件,如果文件存在,写入的数据会被加到文件尾后,即文件原先 的内容会被保留。 |

函数返回值:成功返回FILE 类型的指针(内存中创建的结构体首地址)。失败返回NULL指针并设置errno, 设置errno后可以使用perror 打印出错信息

2、实例fopen打开一个文件

源文件fopen_test.c,依旧是使用VS Code进行编程,需要安装下面的几个插件

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
    FILE *fp;
    // 程序运行需要2个参数 ./01-fopen filename
    if (argc != 2)
    {
        printf("错误:运行程序时请带入参数(./fopen filename)\n");
        exit(-1);
    }
    // argv[0] = ./01-fopen
    // argv[1] = filename
    // fp = fopen(argv[1],"r") ; //只读方式打开文件, 文件不存在, 报错
    // fp = fopen(argv[1],"r+") ; //可读可写方式打开文件, 文件不存在, 报错
    // fp = fopen(argv[1],"w") ; //只写方式打开文件, 文件不存在则创建文件, 文件存在清空文件内容
    // fp = fopen(argv[1],"w+") ; //可读可写方式打开文件, 文件不存在则创建文件, 文件存在清空文件内容
    // fp = fopen(argv[1],"a") ; //只写方式打开文件, 文件不存在则创建文件, 文件存在不清空文件内容, 光标移动文件末尾
    fp = fopen(argv[1], "a+"); // 可读可写方式打开文件, 文件不存在则创建文件, 文件存在不清空文件内容, 光标移动文件末尾
    if (fp == NULL)
    {
        perror("fopen");
        exit(-1);
    }
    return 0;
}

三、关闭流 fclose

cpp 复制代码
int fclose(FILE *stream);

函数功能:关闭一个已经打开的文件(流)

函数参数:stream : 要关闭文件的流

函数返回值:成功返回: 返回为0;失败返回 : EOF(end of file) , 并设置errno

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

int main(int argc, char const* argv[])
{
    FILE* fp = NULL;
    int i = 0;
    char filename[128];
    while(1)
    {
        sprintf(filename, "file%d", i);
        printf("filename: %s\n", filename);

        fp = fopen(filename, "w");
        if(fp == NULL)
        {
            perror("fopen");
            return -1;
        }
        i++;
    }
    return 0;
}

在Ubuntu上一个进程最多可以打开1024个文件,这个程序需要在ubuntu的终端种执行,这个程序不能使用ssh登录并运行,因为ssh的最大打开文件个数比较多。

【分析】最多创建了file0 ... file1020 , 一共是1021个文件, 系统默认给我们打开了3个文件, 分别是stdin, stdout,stderr, 因此一个进程最多可以打开文件的个数为 1024 。

四、读写流 fgetc/fgets/fread

  • 调用fopen()成功打开流之后,可在三种不同类型的非格式化I/O中进行选择,对其进行读、写操作:
  • **每次一个字符的I/O。**使用fgetc()/fputc()一次读或写一个字符,如果流是带缓存的,则标准I/O函数处理所有缓存。
  • **每次一行的I/O。**使用fgets()和fputs()一次读或写一行。每行都以一个新行符终止。当调用fgets()时,应说明能处理的最大行长。
  • **直接I/O。**fread()和fwrite()函数支持这种类型的I/O。每次I/O操作读或写某种数量的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件中读或写一个结构。

1、每次读一个字符 fgetc

cpp 复制代码
int fgetc(FILE *stream);

函数功能:从已经打开的流(文件指针)中读取一个字符

函数参数:【stream】要读文件的流

函数返回值:成功返回读回来的字符;失败返回EOF(end of file)

2、实例fgetc实现cat

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

int main(int argc, char const* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s filename\n", argv[0]);
        return -1;
    }

    FILE* fp = fopen(argv[1], "r");//只读方式打开文件, 文件不存在, 报错
    if(fp == NULL)
    {
        perror("fopen");
        return -1;
    }

    char ch;
    while((ch = fgetc(fp)) != EOF)//读取文件内容, 直到文件结束
    {
        printf("%c", ch);
    }
    fclose(fp);//关闭文件
    return 0;
}

3、每次写一个字符 fputc

cpp 复制代码
int fputc(int c, FILE *stream);

函数功能:把一个字符写入到一个已经打开的流(文件指针)中

函数参数:【c】要写入文件的字符;【stream】要写文件的流

函数返回值:成功返回: 返回写入文件的字符;失败返回 : EOF(end of file)

4、实例fputc和fgetc实现cp

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

int main(int argc, char const* argv[])
{
    //argv[0]是文件名
    //argv[1]是 src file1
    //argv[2]是 dst file2
    if(argc != 3)
    {
        printf("Usage: %s filename\n", argv[0]);
        return -1;
    }

    FILE* fp_src = fopen(argv[1], "r");//只读方式打开文件, 文件不存在, 报错
    if(fp_src == NULL)
    {
        perror("fopen");
        return -1;
    }

    FILE* fp_dst = fopen(argv[2], "w");//只写方式打开文件, 文件不存在则创建,存在则清空
    if(fp_dst == NULL)
    {
        perror("fopen");
        return -1;
    }

    char ch;
    while((ch = fgetc(fp_src)) != EOF)//读取文件内容, 直到文件结束
    {
        fputc(ch, fp_dst);//写入文件
    }

    fclose(fp_src);
    fclose(fp_dst);
    
    return 0;
}

5、每次读一行的字符串 fgets()

cpp 复制代码
char *fgets(char *s, int size, FILE *stream);

函数功能:从流(文件指针)读取指定长度的字符串到s指定的内存中,从stream中最多读入一个小于size的字符,并将它们存储到指向的内存s中 , 读取在EOF或换行符(\n)之后停止。如果读取了换行符,则将其存储到缓冲区中。使用('\0')存储在缓冲区的最后一个字符之后。fgets遇到\n 停止读入, '\n'会被读取进来, 保存到s中, 最后会加上一个'\0', 做为字符串结束标志 。

函数参数:【s】保存读回来的字符串的内存地址;size : 要读多少个字节;【stream】要读文件的流。

函数返回值:成功返回: 返回保存字符串的内存地址s;失败返回 : 错误时返回NULL, 或者是到达文件末尾时也返回NULL。

6、实例fgets实现cat

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

int main(int argc, char const* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s filename\n", argv[0]);
        return -1;
    }

    FILE* fp = fopen(argv[1], "r");//只读方式打开文件, 文件不存在, 报错
    if(fp == NULL)
    {
        perror("fopen");
        return -1;
    }

    char buf[1024]={0};
    while(fgets(buf,1024,fp) != NULL)//读取文件内容, 直到文件结束
    {
        printf("%s", buf);
    }
    fclose(fp);//关闭文件
    return 0;
}

7、每次写一行的字符串 fputs()

cpp 复制代码
int fputs(const char *s, FILE *stream);

函数功能:把s保存的字符串写入到流(文件指针)内

函数参数:【s】要写入流内的字符串;【stream】要写文件的流

函数返回值:成功返回写入流中的字节数, 一个正数;失败返回错误时返回EOF

8、实例使用fgets和fputs实现cp

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

int main(int argc, char const* argv[])
{
    //argv[0]是文件名
    //argv[1]是 src file1
    //argv[2]是 dst file2
    if(argc != 3)
    {
        printf("Usage: %s filename\n", argv[0]);
        return -1;
    }

    FILE* fp_src = fopen(argv[1], "r");//只读方式打开文件, 文件不存在, 报错
    if(fp_src == NULL)
    {
        perror("fopen");
        return -1;
    }

    FILE* fp_dst = fopen(argv[2], "w");//只写方式打开文件, 文件不存在则创建,存在则清空
    if(fp_dst == NULL)
    {
        perror("fopen");
        return -1;
    }

    char buf[1024]={0};
    while(fgets(buf,1024,fp_src) != NULL)//读取文件内容, 直到文件结束
    {
        fputs(buf, fp_dst);//写入文件
    }

    fclose(fp_src);
    fclose(fp_dst);
    
    return 0;
}

9、直接读指定字节数 fread

cpp 复制代码
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

函数功能:从流(文件指针)中读取指定字节数保存到ptr所指向的内存,可以读文本文件,也可以读二进制文件。fgetc 和fgets 只能读取文本文件 。

函数参数,【ptr】要保存数据的内存地址;【size】每一个元素的字节数;【nmemb】要读取元素的个数;【stream】要读文件的流

函数返回值:成功返回实际读回来的字节数或元素数;失败返回到达文件末尾返回 0,错误时返回 0

10、实例fread实现cat

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

int main(int argc, char const* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s filename\n", argv[0]);
        return -1;
    }

    FILE* fp = fopen(argv[1], "r");//只读方式打开文件, 文件不存在, 报错
    if(fp == NULL)
    {
        perror("fopen");
        return -1;
    }

    char buf[1024]={0};
    while(fread(buf,1,1024,fp) != 0)//读取文件内容, 直到文件结束
    {
        printf("%s", buf);
    }
    fclose(fp);//关闭文件
    return 0;
}

这个程序有问题, 需要使用fwrite去解决也可以使用清空数组的办法。使用清除数组内容的办法, 不能彻底解决乱码的问题, 最终还是需要使用fwrite(设置nmemb128可以复现该现象)

11、直接写指定字节数 fwrite

cpp 复制代码
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);

函数功能:把ptr保存的数据写入到指定的流中

函数参数:【ptr】要写入文件的数据;【size】每一个元素的字节数;【nmemb】要写元素的个数;【stream】要写文件的流

函数返回值:成功返回实际写入文件的字节数或元素数;失败返回错误时返回 0

12、解决fread的问题

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

int main(int argc, char const* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s filename\n", argv[0]);
        return -1;
    }

    FILE* fp = fopen(argv[1], "r");//只读方式打开文件, 文件不存在, 报错
    if(fp == NULL)
    {
        perror("fopen");
        return -1;
    }

    char buf[1024]={0};
    int nbytes = 0;
    while((nbytes = fread(buf,1,1024,fp)) != 0)//读取文件内容, 直到文件结束
    {
        fwrite(buf,1,nbytes,stdout);//写入文件
    }
    fclose(fp);//关闭文件
    return 0;
}

13、 使用fread和fwrite实现cp命令功能

五、定位流 ftell/fseek

1、定位标准I/O流的两种方式

  • 文件的位置可以理解为文件的光标的位置
  • ftell()和fseek(): 这两个函数自V7以来就存在了,但是它们都假定文件的位置可以存放在一个长整型中。
  • fgetpos()和fsetpos()。这两个函数是新由ANSI C引入的。它们引进了一个新的抽象数据类型fpos_t,它记录文件的位置。
  • 需要移植到非UNIX系统上运行的应用程序应当使用fgetpos()和fsetpos()

2、获取文件位置 ftell

cpp 复制代码
long ftell(FILE *stream);

函数功能:获取文件的位置

函数参数:【stream】要操作的文件指针

函数返回值:成功返回: 文件的位置。失败返回 :错误时返回 -1 , 并设置errno,Otherwise, -1 is returned and errno is set to indicate the error.

fopen打开文件的方式,"r" : 文件位置在开头。"w" : 文件位置在开头。"a" : 文件位置在末尾。

3、实例使用ftell计算文件的大小

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

int main(int argc, char const* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s filename\n", argv[0]);
        return -1;
    }

    FILE* fp = fopen(argv[1], "a");//追加方式打开文件, 文件不存在则创建,存在则追加, 光标在文件末尾
    if(fp == NULL)
    {
        perror("fopen");
        return -1;
    }

    long offset = ftell(fp);//获取当前文件指针的偏移量
    printf("size = %ld\n", offset);

    fclose(fp);//关闭文件
    return 0;
}

4、设置文件的位置 fseek

cpp 复制代码
int fseek(FILE *stream, long offset, int whence);

函数功能:设置文件的位置

函数参数:【stream】要操作的文件指针。【offset】基于 whence 的偏移量, 以字节计算偏移量。【whence】SEEK_SET : 设置文件指定的位置;SEEK_CUR : 基于当前的文件位置;SEEK_END : 基于文件末尾的位置。

函数返回值,成功返回: 0。失败返回 :错误时返回 -1 , 并设置errno,Otherwise, -1 is returned and errno is set to indicate the error.

cpp 复制代码
// 1. 把文件位置设置到开始
fseek(fp,0,SEEK_SET)

// 2. 把文件位置设置到末尾
fseek(fp,0,SEEK_END )

// 3. 把文件位置设置到末尾的前一个元素
fseek(fp,-1,SEEK_END )

// 4. 基于文件位置的前1个元素
fseek(fp,-1,SEEK_CUR )

// 5. 基于文件位置的后1个元素
fseek(fp,1,SEEK_CUR )

5、fseek合并文件

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

int main(int argc, char const* argv[])
{
    if(argc != 4)
    {
        printf("Usage: %s filename file1 file2 file3\n", argv[0]);
        return -1;
    }

    FILE* fp1 = fopen(argv[1], "r");//只读方式打开文件, 文件不存在报错
    if(fp1 == NULL)
    {
        perror("fopen");
        return -1;
    }

    FILE* fp2 = fopen(argv[2], "r");//只读方式打开文件, 文件不存在报错
    if(fp2 == NULL)
    {
        perror("fopen");
        return -1;
    }

    FILE* fp3 = fopen(argv[3], "w");//只写方式打开文件, 文件不存在则创建,存在则清空
    if(fp3 == NULL)
    {
        perror("fopen");
        return -1;
    }

    // 使用fgetc从file1中读取内容, 写入file3中
    char ch;
    while((ch = fgetc(fp1)) != EOF)//读取文件内容, 直到文件结束
    {
        fputc(ch, fp3);//写入文件
    }
    // 从file2中读取内容, 写入file3中
    while((ch = fgetc(fp2)) != EOF)//读取文件内容, 直到文件结束
    {
        fputc(ch, fp3);//写入文件
    }

    fclose(fp3);
    fclose(fp1);
    fclose(fp2);

    return 0;
}

6、fseek拆分文件

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

int main(int argc, char const* argv[])
{
    if(argc != 4)
    {
        printf("Usage: %s filename file1 file2 file3\n", argv[0]);
        return -1;
    }

    FILE* fp1 = fopen(argv[1], "r");//只读方式打开文件, 文件不存在报错
    if(fp1 == NULL)
    {
        perror("fopen");
        return -1;
    }

    FILE* fp2 = fopen(argv[2], "w");//只读方式打开文件, 文件不存在报错
    if(fp2 == NULL)
    {
        perror("fopen");
        return -1;
    }

    FILE* fp3 = fopen(argv[3], "w");//只写方式打开文件, 文件不存在则创建,存在则清空
    if(fp3 == NULL)
    {
        perror("fopen");
        return -1;
    }

    //使用fseek和ftell计算file1的大小
    fseek(fp1, 0, SEEK_END);//将文件指针移动到文件末尾
    long offset = ftell(fp1);//获取当前文件指针的偏移量
    printf("size = %ld\n", offset);
    fseek(fp1, 0, SEEK_SET);//将文件指针移动到文件开头

    //使用fgetc函数把file1的前一半复制到file2中
    for(int i = 0; i < offset/2; i++)
    {
        char ch = fgetc(fp1);//读取文件内容
        fputc(ch, fp2);//写入文件
    }

    //使用fgetc函数把file1的前一半复制到file3中
    for(int i = 0; i < offset/2; i++)
    {
        char ch = fgetc(fp1);//读取文件内容
        fputc(ch, fp3);//写入文件
    }   

    fclose(fp3);
    fclose(fp1);
    fclose(fp2);

    return 0;
}

7、fseek文件逆序

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

int main(int argc, char const* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s filename file1\n", argv[0]);
        return -1;
    }

    FILE* fp1 = fopen(argv[1], "r+");//只读方式打开文件, 文件不存在报错
    if(fp1 == NULL)
    {
        perror("fopen");
        return -1;
    }

    //使用fseek和ftell计算file1的大小
    fseek(fp1, 0, SEEK_END);//将文件指针移动到文件末尾
    long offset = ftell(fp1);//获取当前文件指针的偏移量
    printf("size = %ld\n", offset);
    fseek(fp1, 0, SEEK_SET);//将文件指针移动到文件开头

    int t1, t2;

    //使用fseek和ftell函数把file1进行逆序输出到file1中
    for(long i = 0; i < offset/2 ; i++)
    {
        //将光标移动到第i个字符
        fseek(fp1, i, SEEK_SET);
        
        //读取光标位置的字符
        t1 = fgetc(fp1);

        //把光标移动到第offset-i-1个字符
        fseek(fp1, offset-i-1, SEEK_SET);

        //读取光标位置的字符
        t2 = fgetc(fp1);

        //把光标移动到i的位置
        fseek(fp1, i, SEEK_SET);

        //把t2写入到光标的位置
        fputc(t2, fp1);

        //把光标移动到第offset-i-1个字符
        fseek(fp1, offset-i-1, SEEK_SET);

        //把t1写入到光标的位置
        fputc(t1, fp1);
    }

    fclose(fp1);

    return 0;
}
相关推荐
路弥行至1 小时前
Linux (ARM64 / Jetson) 挂载 exFAT 移动硬盘排障与离线安装指南
linux·运维·服务器·经验分享·笔记·jetson·exfat
漫谈数据智理1 小时前
主动元数据为何是数据编织的核心——从“连接数据”走向“感知、理解与行动”的 Data Fabric
运维·fabric
数据库小学妹1 小时前
MySQL长事务复盘:Sleep连接、MDL排队与undo滞留
运维·数据库·mysql
云泽8082 小时前
Linux 系统编程实战:从零手写一个高性能终端进度条
linux·运维·服务器
Mapleay2 小时前
设备树/ACPI 与 fwnode 内核基础设施
linux
半摆烂日常2 小时前
自建WMS和买成品:三年成本对比
大数据·服务器·数据库·python·深度学习·低代码·numpy
宋军涛3 小时前
Windows 操作系统【任务计划程序】简单使用
服务器·windows
蒸蒸yyyyzwd3 小时前
cpp 选手秋招学习笔记 day27
服务器·c++·面试·八股
waiting9711183 小时前
Ubuntu 26.04 + Android14安装与编译教程
android·linux·ubuntu