一、Linux文件IO的说明

(一)、文件的概念(Linux下一切皆文件)

  • 狭义:指普通的文本文件,或二进制文件,包括日常所见的txt文档、源代码、word文档、压缩包、图片、视频、音频文件等

  • 广义:除了狭义上的文件外,几乎所有可操作的设备或结构都可视为文件。包括键盘、鼠标、硬盘、串口、显示屏、触摸屏等,也包括网络通讯端口(多机通信要用到的文件)、进程间通讯管道(单机通信)等抽象概念

(二)、Linux中文件的分类

普通文件(符号:-)(popular):      存在于外部存储器中,用于存储普通数据               --- 标准IO、文件属性与目录操作(*****)
目录文件(符号:d)(directory):    用于存放目录项,是文件系统管理的重要文件类型
管道文件(符号:p)(pipeline):     一种用于进程间通信的特殊文件,也被称为命名管道FIFO  --- 系统编程
套接字文件(符号:s)(socket):     一种用于网络间通信的特殊文件                       --- 网络编程
链接文件(符号:l)(link):         用于间接访问另外一个目标文件,相当于windows系统快捷方式
字符设备文件(符号:c)(character):字符设备在应用层的访问接口(以字符为单位,跟系统进行数据交换的设备,比如:键盘、鼠标、触摸屏等)   --- 系统IO、设备操作(*****)
块设备文件(符号:b)(block):      块设备在应用层的访问接口  (以块为单位(256、512、1024字节为一块)),跟系统进行数据交换的设备,比如:U盘、内存、硬盘等
  • 使用ls -l命令,即可列出文件的详细信息,并从其第一个字符看出是什么文件

(三)、系统IO与标准IO说明

  • 说明:对文件的操作,基本上就是输入输出,因此也一般称为IO接口
    • 在操作系统层面上:这一组专门针对文件的IO接口就被称为系统IO --- 偏向于底层(设备文件)
    • 在标准库的层面上:这一组专门针对文件的IO接口就被称为标准IO --- 偏向于上层(软件程序文件)
  • 图解:
    • IO控制文件的方式(系统IO控制):

  • 标准IO:

  • 解析:
    • 系统IO:是众多系统调用中专用于文件操作的一部分接口
    • 标准IO:是众多标准函数当中专用于文件操作的一部分接口
  • 总结:
    • 标准IO实际上是对系统IO的封装,系统IO是更接近底层的接口,如果把系统IO比喻为菜市场,提供各式肉蛋奶菜,那么标准IO就是对这些基本原理的进一步封装,是品类和功能更加丰富的酒庄饭店

(四)、如何选择系统IO与标准IO

  • 系统IO:
    • 由操作系统直接提供的函数接口,特点是简洁,功能单一
    • 没有提供缓冲区,因此对海量数据的操作效率较低
    • 套接字socket、设备文件的访问只能使用系统IO
  • 标准IO:
    • 由标准C库提供的函数接口,特点是功能丰富
    • 由提供缓冲区,因此对海量数据的操作效率高
    • 编程开发中尽量选择标准IO,但许多场合只能用系统IO
  • 总结:这两则函数接口在实际编程开发中都经常会用到,都是基本开发技能

二、系统IO的基本API

(一)、文件的打开

  • 关键点:
    • open函数有两个版本,一个有两个参数,一个有三个参数
    • 当打开一个已存在的文件时,指定两个参数即可(使用两个参数的那个open即可)
    • 当创建一个新文件,需要用第三个参数指定新文件的权限,否则新文件的权限是随机值(系统会帮你配置)
    • 模式flags,可以使用位或的方式,来同时指定多个模式
    • 模式flags,O_NOCTTY主要用在后台精灵进程,阻止这些精灵进程拥有控制终端。 --- 系统编程的时候才讲的,现在不必看
  • linux创建的文件权限:
    • 注意:umask值和open函数设置得文件权限值
      • 第一个数:表示八进制的意思
      • 第二个数:文件所有者的权限
      • 第三个数:文件所属组的权限
      • 第四个数:其它用户的权限
  • 示例代码:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char const *argv[])
{
    int fd = 0;     // 文件描述符

    // 一、以下三种打开方式,都要求文件已存在,否则失败返回 (知道文件存在)
    fd = open("1.txt", O_RDWR);      // 以可读可写方式打开文件
    fd = open("1.txt", O_RDONLY);    // 以只读方式打开文件
    fd = open("1.txt", O_WRONLY);    // 以只写方式打开文件

    // 二、以下三种打开方式,如果文件不存在,则创建文件,并设置其权限为0644。 存在则返回错误信息 (你想要确保文件存在)
    fd = open("1.txt", O_RDWR|O_CREAT|O_EXCL,   0644);    // 以可读可写方式打开文件, 若文件不存在,就创建该文件,若文件存在则返回错误信息
    fd = open("1.txt", O_RDONLY|O_CREAT|O_EXCL, 0644);    // 以只读方式打开文件, 若文件不存在,就创建该文件,若文件存在则返回错误信息
    fd = open("1.txt", O_WRONLY|O_CREAT|O_EXCL, 0644);    // 以只写方式打开文件, 若文件不存在,就创建该文件,若文件存在则返回错误信息

    // 三、以下三种打开方式,如果文件不存在,则创建文件,并清空里面的信息,并设置其权限为0644   (你想要确保文件存在,并且里面是没有东西的)
    fd = open("1.txt", O_RDWR|O_CREAT|O_TRUNC,   0644);    // 以可读可写方式打开文件, 若文件不存在,就创建该文件,并清空里面的信息
    fd = open("1.txt", O_RDONLY|O_CREAT|O_TRUNC, 0644);    // 以只读方式打开文件, 若文件不存在,就创建该文件,并清空里面的信息
    fd = open("1.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644);    // 以只写方式打开文件, 若文件不存在,就创建该文件,并清空里面的信息

    // 四、以下两种打开方式,都要求文件已存在,否则失败返回,并追加文件内容   (知道文件存在,并且想要追加数据到里面去)      
    fd = open("1.txt", O_RDWR|O_APPEND);                   // 以可读可写方式打开文件, 并追加文件内容
    fd = open("1.txt", O_WRONLY|O_APPEND);                 // 以只写方式打开文件,并追加文件内容

    return 0;
}

(二)、标准库函数的错误处理

  • 说明:在所有的库函数中,如果调用过程中出错了,那么该函数除了会返回一个特定的数据来告诉用户调用失效之外,还都会去修改一个大家共同的全局错误码errno,我们可以通过这个错误码,来进一步确认究竟是什么错误
  • 关键点:
    • 如果库函数,系统调用出错了,全局错误码errno会随之改变
    • 如果库函数,系统调用没出错,全局错误码errno不会改变
    • 一个库函数,系统调用出错后,若未及时处理错误码,则错误码可能会被随后的其它函数修改

(三)、文件描述符的本质

  • 说明:函数open()的返回值,是一个整型int数据,这个整型数据,实际上是内核中的一个称为fd_array的数组的下标
  • 图解:

  • 解释:打开文件时,内核产生一个指向file{}的指针,并将该指针放入一个位于file_struct{}的数组fd_array[]中,而该指针所在的数组的下标,就被open()函数返回给用户,用户把这个数组下标称为文件描述符(fd)
  • 结论:
    • 文件描述符从0开始,每打开一个文件,就产生一个新的文件描述符
    • 可以重复打开同一个文件,每次打开文件都会使用内核产生系列结构体,并得到不同的文件描述符
    • 由于系统在每一个进程开始运行时,都默认打开了一次键盘、两次屏幕,因此0、1、2描述符分别代表标准输入、标准输出和标准出错

(四)、文件的读取、写入

  • 关键点:
    • 参数count是读写字节数的愿望值,实际读写成功的字节数由返回值决定
    • 读取普通文件时,如果当读到了文件末尾,read()会返回0
  • 示例代码1:不断地读取文件里面的数据,并打印出来
#include <stdio.h>
// open函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// read函数、close
#include <unistd.h>

// 全局错误码声明所在的文件
#include <errno.h>  

// strerror函数
#include <string.h>  


// 主函数
int main(int argc, char const *argv[])
{

    // 0、判断命令行参数
    if (argc != 2)
        printf("命令行参数错误,格式为(./可执行文件 要打开文件)!\n");
    
    // 1、打开文件(以只读的方式打开)
    int fd = open(argv[1], O_RDONLY);
    if ( -1 == fd )
    {
        printf("错误信息:打开%s文件失败,  错误原因:%s,  错误位置:%d行\n", argv[1], strerror(errno), __LINE__);
        return -1;
    }

    // 2、读取文件里面的信息,并打印出来
    char buf[128] = {0};
    int  ret = 0;

    while (1)
    {
        // 清空buf里面的数据,防止对下一次的读取数据造成干扰
        bzero(buf, sizeof(buf));

        // 每次最多读取128个字节
        ret = read(fd, buf, sizeof(buf)); 

        // 如果返回0,则证明文件已被读取完毕
        if (ret == 0)
        {
            printf("啊,我读完了!\n");
            break;
        }

        // 打印读取的数据
        printf("%s", buf);
    }

    // 3、关闭文件
    close(fd);

    return 0;
}
  • 示例代码2:向文件写入数据
#include <stdio.h>
// open函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// write函数、close函数
#include <unistd.h>

// 全局错误码声明所在的文件
#include <errno.h> 

// strerror函数
#include <string.h> 

int main(int argc, char const *argv[])
{
    // 0、判断命令行参数
    if (argc != 2)
        printf("命令行参数错误,格式为(./可执行文件 要打开文件)!\n");

    // 1、打开文件(以只写的方式打开,如果没有这个文件就创建,并清空文件)
    int fd = open(argv[1], O_WRONLY|O_CREAT|O_TRUNC, 0644);
    if (-1 == fd)
    {
        printf("错误信息:打开%s文件失败,  错误原因:%s,  错误位置:%d行\n", argv[1], strerror(errno), __LINE__);
        return -1;
    }

    // 2、将数据写入
    char buf[1024] = {0};

    // 清空buf里面的数据,防止对下一次的写入数据造成干扰
    bzero(buf, sizeof(buf));

    // 从键盘输入数据
    printf("作者,请继续您的作品,读者催更中!:\n");
    scanf("%s", buf);
    while(getchar()!='\n');

    // 向文件写入数据
    write(fd, buf, strlen(buf));

    // 3、关闭文件
    close(fd);

    return 0;
}
  • 注意:当我们对文件读写操作时,系统会为我们记录操作的位置,以便于下次继续进行读写操作的时候吗,从适当的地方开始
    • 每当打开一个文件,系统就会维护一套包括文件操作位置在内的相关
    • 对同一个文件描述符进行读写操作时,使用的同一套文件信息,影响的是同一个位置参数
    • 对同一个文件的多个文件描述符进行读写操作时,使用的是不同的文件信息,影响的是不同的位置参数,彼此互相之间独立,这往往会导致文件信息的错乱
  • 示例代码3:读写作用于一个读写位置
#include <stdio.h>
// open函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// read函数、 write函数、close函数
#include <unistd.h>
// 全局错误码声明所在的文件
#include <errno.h>  
// strerror函数
#include <string.h>  

int main(int argc, char const *argv[])
{
   // 0、判断命令行参数
    if (argc != 2)
        printf("命令行参数错误,格式为(./可执行文件 要打开文件)!\n");

    // 1、打开文件(以可读可写的方式打开)
    int fd = open(argv[1], O_RDWR);
    if ( -1 == fd )
    {
        printf("错误信息:打开%s文件失败,  错误原因:%s,  错误位置:%d行\n", argv[1], strerror(errno), __LINE__);
        return -1;
    }

    // 2、读取文件里面的信息,并打印出来
    char buf[128] = {0};
    int  ret = 0;

    while (1)
    {
        // 清空buf里面的数据,防止对下一次的读取数据造成干扰
        bzero(buf, sizeof(buf));

        // 每次最多读取128个字节
        ret = read(fd, buf, sizeof(buf)); 

        // 如果返回0,则证明文件已被读取完毕
        if (ret == 0)
        {
            printf("啊,我读完了!\n");
            break;
        }

        // 打印读取的数据
        printf("%s", buf);
    }
    
    // 2、将数据写入文件中
    // 清空buf里面的数据,防止对下一次的写入数据造成干扰
    bzero(buf, sizeof(buf));

    // 从键盘输入数据
    printf("作者,请继续您的作品,读者催更中!:\n");
    scanf("%[^\n]", buf);      // 直到遇到'\n'才退出
    while(getchar()!='\n');

    // 向文件写入数据
    write(fd, buf, strlen(buf));

    // 4、关闭文件
    close(fd);

    return 0;
}

(五)、文件的读写位置的设置

  • 关键点:
    • lseek函数可以将文件位置调整到任意的位置,可以是已有数据的地方,也可以是未有数据的地方,假设调整到文件末尾之后的某个地方,那么文件将会形成所谓的"空洞" 
    • lseek函数只能对普通文件调整文件位置,不能对管道文件调整
    • lseek函数的返回值是调整后的文件位置距离文件开头的偏移量,单位是字节
  • 示例代码1(拓展):形成一个空洞文件
#include <stdio.h>
// open函数、lseek函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// lseek函数
#include <unistd.h>
// 全局错误码声明所在的文件
#include <errno.h> 
// strerror函数
#include <string.h> 

// 主函数
int main(int argc, char const *argv[])
{
    // 0、判断命令行参数
    if (argc != 2)
        printf("命令行参数错误,格式为(./可执行文件 要打开文件)!\n");

    // 1、打开文件(以可读可写的形式打开,如果没有这个文件就创建,并清空文件)
    int fd = open(argv[1], O_RDWR|O_CREAT|O_TRUNC, 0644);
    if (-1 == fd)
    {
        printf("错误信息:打开%s文件失败,  错误原因:%s,  错误位置:%d行\n", argv[1], strerror(errno), __LINE__);
        return -1;
    }

    // 2、在空洞前,写入数据(头信息数据)
    char w_buf1[128] = "文件的大小+文件的名字+文件的格式";
    write(fd, w_buf1, strlen(w_buf1));

    // 3、利用lseek形成一个空洞文件
    lseek(fd, 10*1024*1024, SEEK_CUR);

    // 4、在空洞后,写入数据(尾信息数据)
    char w_buf2[128] = "文件的末尾";
    write(fd, w_buf2, strlen(w_buf2));

    // 5、关闭文件
    close(fd);

    return 0;
}
  • 示例代码(拓展):获取文件的大小
#include <stdio.h>
// open函数、lseek函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// lseek函数
#include <unistd.h>

// 全局错误码声明所在的文件
#include <errno.h> 

// strerror函数
#include <string.h> 

/**
 * @brief: 获取文件的大小
 * @note:  None
 * @param: pathname:要计算大小的文件路径
 * @retval: 成功:返回计算的文件的大小
 *          失败:返回-1
*/
off_t GetFileSize(const char *pathname)
{
    // 1、打开文件(以读写的权限打开文件)
    int fd = open(pathname, O_RDWR);
    if (-1 == fd)
    {
        printf("错误信息:打开%s文件失败,  错误原因:%s,  错误位置:%d行\n", pathname, strerror(errno), __LINE__);
        return -1;
    }
    
    // 2、获取文件的大小
    off_t ret_len = lseek(fd, 0, SEEK_END); // 将文件位置偏移到文件末尾,并只移动0字节
    
    // 3、关闭文件
    close(fd);

    // 4、返回文件的大小
    return ret_len;
}

// 主函数
int main(int argc, char const *argv[])
{
    // 0、判断命令行参数
    if (argc != 2)
        printf("命令行参数错误,格式为(./可执行文件 要打开文件)!\n");

    // 1、打印文件的大小
    printf("%s文件的大小为: %ld字节\n", argv[1], GetFileSize(argv[1]));
    
    return 0;
}

(六)、文件的关闭

  • 关键点:
    • 当不再使用一个文件时,应当关闭文件,防止系统资源浪费
    • 对同一文件重复执行关闭操作(或其它操作)会失败返回,不会由其它副作用

三、标准IO的基本API

(一)、文件的打开与关闭

  • 说明:不管用系统IO函数还是标准IO函数,操作文件的第一步,都是打开文件(open/fopen)
  • 注意:
    • 系统IO:打开文件得到的是一个整数,称为文件描述符
    • 标准IO:打开文件得到的是一个指针,称为文件指针
  • 图解:文件指针指向结构体FILE,该结构体内部包含了文件描述符

1、文件的打开:

  • 注意:总共6种打开模式,不能自创别的模式,比如"rw"是非法的
2、文件的关闭

  • 注意:fclose函数涉及内存释放,不可对同一个文件多次关闭
3、示例代码
#include <stdio.h>
// 全局错误码声明所在的文件
#include <errno.h> 
// strerror函数
#include <string.h> 


int main(int argc, char const *argv[])
{

    FILE *fp = NULL;

    // 一、打开文件
    // (1)、打开文件(方式1:要求文件必须存在)
    // 1、以只读方式打开
    fp = fopen("./1.txt", "r");
    if (fp == NULL)
    {
        printf("错误信息:打开1.txt文件失败,  错误原因:%s,  错误位置:%d行\n", strerror(errno), __LINE__);
        return -1;
    }
    else
    {
         printf("使用fopen函数,打开文件成功!\n");
    }


    // 2、以读写方式打开
    fp = fopen("./1.txt", "r+");
    
    // (2)、打开文件(方式2:文件可以不存在,如果文件不存在就创建文件,并清空文件)
    // 1、以只写方式打开
    fp = fopen("./1.txt", "w");
    
    // 2、以读写方式打开
    fp = fopen("./1.txt", "w+");

    // (3)、打开文件(方式3:文件可以不存在,如果文件不存在就创建文件,并清空文件,以追加的方式打开)
    // 1、以只写方式打开
    fp = fopen("./1.txt", "a");
    
    // 2、以读写方式打开
    fp = fopen("./1.txt", "a+");


    // 二、关闭文件
    int ret = fclose(fp);         // 关闭一般不判断返回值
    if (ret == EOF)                
    {
        printf("错误信息:打开1.txt文件失败,  错误原因:%s,  错误位置:%d行\n", strerror(errno), __LINE__);
        return -2;
    }
    else
    {
        printf("使用fclose函数,关闭文件成功!\n");
    }
    
    return 0;
}

(二)、文件位置的获取与设置

  • 说明:与系统IO接口类似,标准IO也可以设置文件的读写位置,获取读写位置,只不过对于标准IO而言,这两个功能是分开实现的

  • 关键点:设置或获取的文件位置都是long类型;可以将文件位置设置到已有数据范围之外,形成"空洞"文件
  • 示例代码
#include <stdio.h>
// 全局错误码声明所在的文件
#include <errno.h> 
// strerror函数
#include <string.h> 
// fseek函数
#include <sys/ioctl.h> 

int main(int argc, char const *argv[])
{
    
    // 0、判断命令行参数
    if (argc != 2)
        printf("命令行参数错误,格式为(./可执行文件 要打开文件)!\n");

    // 1、打开文件(文件可以不存在,如果文件不存在就创建文件,并清空文件,以读写权限)
    FILE *fp =  fopen(argv[1], "w+");
    if (fp == NULL)
    {
        printf("错误信息:打开%s文件失败,  错误原因:%s,  错误位置:%d行\n", argv[1], strerror(errno), __LINE__);
        return -1;
    }

    // 2、在空洞前,写入数据(头信息数据)
    char w_buf1[128] = "文件的大小+文件的名字+文件的格式";
    fwrite(w_buf1, 1, strlen(w_buf1), fp);

    // 3、利用lseek形成一个空洞文件
    fseek(fp, 10*1024*1024, SEEK_CUR);

    // 4、在空洞后,写入数据(尾信息数据)
    char w_buf2[128] = "文件的末尾";
    fwrite(w_buf2, 1, strlen(w_buf2), fp);

    // 5、关闭文件
    fclose(fp);

    return 0;
}

(三)、文件内容的读写操作

  • 说明1:标准IO和系统IO不同的地方在于,标准IO的读写操作提供了很多不同的函数接口,以满足不同的需求:
  • 说明2:对于读操作而言,返回EOF意味着操作失败,这有两种情况
    • 如果feof(fp)为真,此时意味着读到了文件末尾,没有数据可读了
    • 如果ferror(fp)为真,此时意味着遇到了错误
1、按字节读写文本文件

  • 关键点1:
    • fgetc()函数与getc()函数完全一样,区别是fgetc是函数,而getc是宏
    • fputc()函数与putc()函数完全一样,区别是fputc是函数,而putc是宏
    • getchar()函数与putchar()函数只能针对键盘输入和屏幕输出,不能指定别的文件
    • 读操作函数接口的返回值是int,而不是char,原因是当读操作失败是返回EOF的数值是-1,而char型数据可能无法表达-1(而且char型数据本来就是单字节的int型)
  • 示例代码:
#include <stdio.h>
// 全局错误码声明所在的文件
#include <errno.h> 
// strerror函数
#include <string.h> 

int main(int argc, char const *argv[])
{

    // 0、判断命令行参数
    if (argc != 2)
        printf("命令行参数错误,格式为(./可执行文件 要复制的文件路径 要粘贴的文件路径)!\n");

    // 1、打开文件(文件可以不存在,如果文件不存在就创建文件,并清空文件,以读写权限)
    FILE *fp =  fopen(argv[1], "w+");
    if (fp == NULL)
    {
        printf("错误信息:打开%s文件失败,  错误原因:%s,  错误位置:%d行\n", argv[1], strerror(errno), __LINE__);
        return -1;
    }
    

    // 2、写
    // a、fputc:   函数(推荐)
    // b、putc:    宏
    // c、putchar: 只针对标准输出(屏幕)
    char w_buf[128] = "世界那么大,我想去浪浪!";

    int i = 0;
    while (w_buf[i] != '\0')            // 没有数据写入也退出
    {
        int ret = fputc(w_buf[i], fp);
        if (ret == EOF )                // 写入出错就退出
            break;
        i++;
    }

    // 获取当前位置的偏移量,重新移位至文件开头处
    long ret_t =  ftell(fp);
    printf("ret_t == %ld\n", ret_t);
    fseek(fp, 0, SEEK_SET);


    // 3、读
    // a、fgetc:   函数(推荐)
    // b、getc:    宏
    // c、getchar: 只针对标注输入(键盘)
    char r_buf[128] = {0};
    int len = sizeof(r_buf)-1;
    int j = 0;

    while (j <= len)                    // 防止溢出
    {
        int ret = fgetc(fp);
        if (ret == EOF)                 // 已经读到文件末尾,或者出错就退出
            break;
        r_buf[j] = ret;
        
        j++;
    }
    printf("r_buf == %s\n", r_buf);
    
    // 4、关闭文件
    fclose(fp);

    return 0;
}
2、按行读写文本文件

  • fgets()函数和gets()函数的区别:
    • fgets()函数可以读取指定的任意文件,而gets()函数只能从键盘读取
    • fgets()函数有内存边界判断,而gets()函数没有,因此后者是不安全的,不建议使用
    • fgets()函数在任何情形下都按原样读取数据,但gets()函数会自动去除数据末尾的'\n'
  • fputs()函数和puts()函数的区别:
    • fputs()函数可以写入指定的任意文件,而ptus()函数只能将数据输出到屏幕
    • fputs()函数在任何情形下都按原样写入数据,但puts()函数会自动给写入数据末尾加上'\n'
  • 示例代码
#include <stdio.h>
// 全局错误码声明所在的文件
#include <errno.h> 
// strerror函数
#include <string.h> 

int main(int argc, char const *argv[])
{
    // 0、判断命令行参数
    if (argc != 2)
        printf("命令行参数错误,格式为(./可执行文件 要复制的文件路径 要粘贴的文件路径)!\n");

    // 1、打开文件(文件可以不存在,如果文件不存在就创建文件,并清空文件,以读写权限)
    FILE *fp =  fopen(argv[1], "w+");
    if (fp == NULL)
    {
        printf("错误信息:打开%s文件失败,  错误原因:%s,  错误位置:%d行\n", argv[1], strerror(errno), __LINE__);
        return -1;
    }


    // 2、写
    // a、fputs:函数(推荐)
    // b、puts: 宏
    char w_buf[128]     = {0};
    char w_buf_tmp[256] = {0};

    while (1)
    {
        // 1、从键盘输入要写入的数据
        printf("请输入要写入的字符串:\n");
        scanf("%[^\n]", w_buf);
        while(getchar()!='\n');

        if (strcmp(w_buf, "exit") == 0)
            break;

        // 2、将w_buf数据写入到文本中
        sprintf(w_buf_tmp, "%s\n", w_buf);
        fputs(w_buf_tmp, fp);

        // 3、清空w_buf数据
        bzero(w_buf, sizeof(w_buf));
        bzero(w_buf_tmp, sizeof(w_buf_tmp));
        
    }
    

    // 3、获取当前位置的偏移量,重新移至文件开头处
    long ret_t =  ftell(fp);
    printf("ret_t == %ld\n", ret_t);
    fseek(fp, 0, SEEK_SET);

    // 4、读
    // a、fgets:函数(推荐)
    // b、gets: 宏、
    char r_buf[256] = {0};
    while (1)
    {
        fgets(r_buf, sizeof(r_buf), fp);
        printf("r_buf == %s\n", r_buf);

        // 判断文件是否读取完毕
        if (feof(fp))
        {
            printf("文件内容已读完!\n");
            break;
        }

        // 判断文件是否读取有误
        if (ferror(fp))
        {
            printf("读操作遇到错误!\n");
            break;
        }
        
    }
    
    // 4、关闭文件
    fclose(fp);

    return 0;
}
3、按指定格式读写文本

  • 注意:
    • fprintf()函数不仅可以像printf()函数一样向标准输出设备输出信息,也可以像由stream指定的任何有相应权限的文件写入数据
    • sprintf()函数和snprintf()函数都是向一块自定义缓冲区写入数据,不同的是后者第二个参数提供了这块缓冲区的大小,避免缓冲区溢出,因此应尽量使用后者,放弃使用前者
    • fscanf()函数不仅可以像从由stream指定的任何由相应权限的文件读入数据,也可以从标准输入中读入数据
    • scanf()函数一样从标准输入设备读入信息,也可以从一块由指定的自定义缓冲区中读入数据
    • 这些函数的读写带个是,格式由下表决定:

  • 示例代码:
#include <stdio.h>
// 全局错误码声明所在的文件
#include <errno.h> 
// strerror函数
#include <string.h> 

int main(int argc, char const *argv[])
{
    
    // 0、判断命令行参数
    if (argc != 2)
        printf("命令行参数错误,格式为(./可执行文件 要复制的文件路径 要粘贴的文件路径)!\n");

    // 1、打开文件(文件可以不存在,如果文件不存在就创建文件,并清空文件,以读写权限)
    FILE *fp =  fopen(argv[1], "w+");
    if (fp == NULL)
    {
        printf("错误信息:打开%s文件失败,  错误原因:%s,  错误位置:%d行\n", argv[1], strerror(errno), __LINE__);
        return -1;
    }

    // 2、写
    // a、fprintf: 不仅可以像printf()函数一样向标准输出设备输出信息,也可以像由stream指定的任何有相应权限的文件写入数据(推荐)
    // b、printf:  只能将数据输出到输出设备上(屏幕)
    // c、sprintf: 处理有格式的字符串数据
    // d、snprintf:处理有格式的字符串数据(有规定范围的)
    int num = 0;
    printf("请输入用户名:\n");
    char account_buf[128] = {0};
    scanf("%s", account_buf);

    fprintf(fp, "[server_account]\n");
    fprintf(fp, "account[%d]=%s\n", num, account_buf);
    fprintf(fp, "[un_server_account]\n");


    printf("请输入密码:\n");
    char passwd_buf[128] = {0};
    scanf("%s", passwd_buf);
    
    fprintf(fp, "\n");
    fprintf(fp, "[server_passwd]\n");
    fprintf(fp, "passwd[%d]=%s\n", num, passwd_buf);
    fprintf(fp, "[un_server_passwd]\n");
    

    // 3、获取当前位置的偏移量,重新移位至文件开头处
    long ret_t =  ftell(fp);
    printf("ret_t == %ld\n", ret_t);
    fseek(fp, 0, SEEK_SET);

    // 4、读
    // a、fscanf:不仅可以像从由stream指定的任何由相应权限的文件读入数据,也可以从标准输入中读入数据(推荐)
    // b、scanf: 只能从标准输入设备上(键盘)获取数据
    // c、sscanf:处理有格式的字符串数据
    int  num_tmp = -1;
    char account_buf_tmp[128] = {0};
    char passwd_buf_tmp[128] = {0};


    fscanf(fp, "[server_account]\n");
    fscanf(fp, "account[%d]=%s\n", &num_tmp, account_buf_tmp);
    fscanf(fp, "[un_server_account]\n");

    fscanf(fp, "\n");
    fscanf(fp, "[server_passwd]\n");
    fscanf(fp, "passwd[%d]=%s\n",  &num_tmp, passwd_buf_tmp);
    fscanf(fp, "[un_server_passwd]\n");

    printf("num_tmp         == %d\n", num_tmp);
    printf("account_buf_tmp == %s\n", account_buf_tmp);
    printf("passwd_buf_tmp  == %s\n", passwd_buf_tmp);


    // 5、关闭文件
    fclose(fp);
    

    return 0;
}
4、按数据块读写文本

  • 注意:
    • 以上两个函数既能处理文本,也能处理二进制文本
    • 返回值均是成功读写的"块"数,而非字节数
  • 示例代码:
#include <stdio.h>
// 全局错误码声明所在的文件
#include <errno.h> 
// strerror函数
#include <string.h> 

int main(int argc, char const *argv[])
{
    // 0、判断命令行参数
    if (argc != 2)
        printf("命令行参数错误,格式为(./可执行文件 要复制的文件路径 要粘贴的文件路径)!\n");

    // 1、打开文件(文件可以不存在,如果文件不存在就创建文件,并清空文件,以读写权限)
    FILE *fp =  fopen(argv[1], "w+");
    if (fp == NULL)
    {
        printf("错误信息:打开%s文件失败,  错误原因:%s,  错误位置:%d行\n", argv[1], strerror(errno), __LINE__);
        return -1;
    }

    // 2、写
    char w_buf[128] = "三十年河东,三十年河西,莫欺少年穷";
    size_t ret_w = fwrite(w_buf, 1, strlen(w_buf), fp);    // 等价于:系统IO函数的write(fd, w_buf, strlen(w_buf));
    if (ret_w < strlen(w_buf) || ret_w == 0)
    {
        printf("写入数据失败!\n");
        return -2;
    }

    // 3、获取当前位置的偏移量,重新移位至文件开头处
    long ret_t =  ftell(fp);
    printf("ret_t == %ld\n", ret_t);
    fseek(fp, 0, SEEK_SET);

    // 4、读
    char r_buf[128] = {0};
    size_t ret_r = fread(r_buf, 1, ret_w, fp);    // 等价于:系统IO函数的write(fd, w_buf, strlen(w_buf));
    if (ret_r < ret_w || ret_r == 0)
    {
        printf("读取到了文件末尾或读取文件失败!\n");
        return -3;
    }
    printf("r_buf == %s\n", r_buf);
    /*
        // 判断文件是否读取完毕
        if (feof(fp))
        {
            printf("文件内容已读完!\n");
            break;
        }

        // 判断文件是否读取有误
        if (ferror(fp))
        {
            printf("读操作遇到错误!\n");
            break;
        }
    */
    

    // 5、关闭文件
    fclose(fp);

    return 0;
}

欢迎在评论区分享你的实践心得或提出疑问。典型问题将汇总至下期内容统一回复,复杂问题可能通过单独教程深入讲解。留言时注明使用环境和预期目标,有助于更快获得针对性建议

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐