文件IO流

IO使用函数

标准IO 文件IO(低级IO)
打开 fopen, freopen, fdopen open
关闭 fclose close
getc, fgetc, getchar, fgets, gets, fread printf fprintf read
putc, fputc, putchar, fputs, puts, fwrite scanf fscanf write
操作文件指针 fseek lseek
其它 fflush rewind ftell
文件描述符

当打开文件时,文件相关信息(如创建时间、所属者、操作权限等)会被加载到内存,操作系统用struct file结构体维护这些信息,多个打开的文件对应的struct file通过双向链表管理。

每个进程的进程控制块task_struct中,有一个指针files指向files_struct表。该表包含一个指针数组,数组中的每个指针指向内存中的一个struct file,实现进程与文件的连接。文件描述符是该数组的下标,通过它可定位内存中的文件。

数组前三个下标通常固定对应标准输入、标准输出和标准错误(perror)。

这三个文件描述符(0号-标准输入,1号-标准输出,2号-标准错误)在UNIX/Linux系统中被预设为进程的输入和输出通道,是为了方便程序与用户或者其他程序进行通信。

  • 标准输入(stdin):默认情况下,这是键盘输入的数据。如果程序需要从用户处接收输入,它就会从这个描述符读取数据。

  • 标准输出(stdout):默认情况下,这是显示器的输出。如果程序需要向用户显示信息,它就会写入这个描述符。

  • 标准错误(stderr):默认情况下,这同样是显示器的输出。不过,它专门用于输出错误信息。这样做的好处是,即使标准输出被重定向到其他位置(如文件),错误信息仍然可以显示给用户看。

这种设计可以大大增加程序的灵活性。例如,你可以通过重定向机制,将一个程序的标准输出作为另一程序的标准输入,这就形成了所谓的"管道"(pipe),能将多个程序连接起来,协同完成一项工作。这也是UNIX/Linux广受欢迎的一个重要原因。

总结:文件修饰符本质是一个数字,用于表示要操作文件在系统里抽象的文件指针数组里的下标,通过下标(文件修饰符)就可以通过函数来间接操作文件。

1:open函数

调用低级io相关函数一般我们会引入以下头文件:

复制代码
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
  • <fcntl.h> : 定义了open函数和相关的常量,如O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL等。

  • <unistd.h> : 定义了read, write, lseek, 和close函数,以及很多UNIX标准的系统调用。

  • <sys/types.h> : 通常包含了一些用于系统调用的数据类型定义,比如off_t, size_t等。

  • <sys/stat.h> : 如果你在调用open函数时需要设置文件权限,这个头文件中定义了文件权限相关的宏,比如S_IRUSR, S_IWUSR

调用open()函数可打开或创建一个文件,其函数原型如下:

复制代码
int open(const char *pathname, int flags)  
int open(const char *pathname, int flags, mode_t mode)  
  • 返回值:返回一个新的文件描述符,指向打开的文件,如果调用失败则返回-1。如果返回-1,那么c程序会同时将错误信息状态码设为文件打开失败的对应信息,可以通过perror来显示

  • 参数

    • pathname:要打开或创建的文件的路径.相对于当前目录或使用绝对路径

    • flags:指定程序对文件的操作权限和其他的设置

      • O_RDONLY 以只读方式打开文件:

      • O_WRONLY 以只写方式打开文件

      • O_RDWR 以可读写方式打开文件。上述三种旗标是互斥的,也就是不可同时使用,但可与下列的旗标**利用OR(|)**运算符组合。

      • O_CREAT 若欲打开的文件不存在则自动建立该文件。

      • O_EXCL 如果O_CREAT 也被设置,此指令会去检查文件是否存在。文件若不存在则建立该文件,否则将导致打开文件错误。

        此外,若O_CREAT与O_EXCL同时设置,并且欲打开的文件为符号连接,则会打开文件失败。

      • O_APPEND 当读写文件时会从文件尾开始移动,也就是所写入的数据会以附加的方式加入到文件后面。追加模式(写入时指针位于文件末尾)。

      • O_SYNC 以同步的方式打开文件。

  • mode:用于设置当文件创建时的权限,只有使用了O_CREAT,即创建文件才会生效。比如0664指所属用户以及用户组都是读写权限,其他用户则只有读权限(r:4 w:2 X:1)(x执行)

    • 第一个6表示文件创建者的权限

    • 第二个6表示文件所属组的权限

    • 第三个4表示其他用户的权限

  • 示例代码:

复制代码
int main()
{
    char *filename = "new_file.txt";
    int fd = open(filename, O_RDWR | O_CREAT , 0666);
    if (fd == -1)
    {
        perror("无法创建文件");
        return 1;
    }
    return 0;
}
2:write函数

调用函数write()可向已打开的文件中写入数据,其函数模型如下:

复制代码
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数说明
  • fd(文件描述符)

    :这是一个整数,作用是标识要写入数据的目标文件或设备。

    • STDOUT_FILENO(标准输出,一般对应的值是 1),数据会默认输出到控制台。

    • STDERR_FILENO(标准错误,通常值为 2),用于输出错误信息。

    • 还可以是通过open()函数打开文件时返回的文件描述符。

  • buf:它是一个指向内存缓冲区的指针,里面存放着要写入磁盘的数据,一般是数组的地址。

  • count:表示要写入数据的字节大小,通常就是数组的大小。

返回值情况
  • 成功时 :函数会返回实际写入的字节数,这个返回值通常和参数count的值相同。

  • 失败时 :函数会返回 - 1,并且会设置errno变量来指示具体的错误类型。

  • 部分写入情况 :要是返回值大于 0 但小于count,这表明只写入了部分数据。此时,你需要再次调用write()函数,从缓冲区的剩余部分继续写入。

示例代码
1. 向标准输出(控制台)写入数据
复制代码
#include <unistd.h>
​
int main()
{
    char message[] = "Hello, World!\n";
    write(STDOUT_FILENO, message, sizeof(message) - 1);
    // 也可以直接使用1代替STDOUT_FILENO
    return 0;
}
2. 向文件写入数据
复制代码
int main() 
{
    char *filename = "new_file.txt";
    int fd = open(filename, O_RDWR | O_CREAT , 0666);
    if (fd == -1)
    {
        perror("无法创建文件");
        return 1;
    }
    char buffer[] = "这是一个测试文件\n";
    ssize_t bytes_written = write(fd, buffer, sizeof(buffer) - 1);//sizeof(buffer) - 1 等价于 strlen(buffer),减去‘\0',不需要写入文件,只写入有效字符。
    if (bytes_written == -1) 
    {
        perror("写入失败");
    } 
    else 
    {
        printf("成功写入 %zd 字节\n", bytes_written);
    }
​
    close(fd);
    return 0;
}
3. 处理部分写入的情况
复制代码
int main() 
{
    int fd = open("data.txt", O_RDWR | O_CREAT, 0666);
    char buffer[] = "这是一个较长的数据,可能需要多次写入才能完成\n";
   
    size_t total_bytes = sizeof(buffer) - 1;//sizeof(buffer)计算数组总大小(包含字符串结尾的\0),减 1 后得到有效数据长度(不包含\0)。
    size_t bytes_written = 0;//记录已成功写入的字节数,初始化为 0。
   
    while (bytes_written < total_bytes)//只要未写入的字节数大于 0,就继续循环。
    {
        ssize_t result = write(fd, buffer + bytes_written, total_bytes - bytes_written);
        //buffer + bytes_written:指针偏移到未写入部分的起始位置。
        //total_bytes - bytes_written:计算剩余未写入的字节数。
        //result:本次实际写入的字节数(可能小于请求写入的字节数)。
        if (result == -1)
        {
            perror("写入失败");
            break;
        }
        bytes_written += result;
    }
​
    close(fd);
    return 0;
}

关键注意事项

  1. 文件打开模式的影响 :如果文件是以O_APPEND模式打开的,那么每次调用write()时,数据都会被追加到文件的末尾,而文件偏移量会自动更新。

  2. 错误处理的重要性 :在调用write()函数后,一定要检查返回值,以此来确保数据成功写入。常见的错误原因有磁盘已满、达到文件大小限制或者权限不足等。

  3. 写入二进制数据write()函数可以用于写入任意类型的数据,包括二进制数据。不过在处理二进制数据时,要特别注意字节顺序和数据对齐的问题。

  4. 缓冲区和系统调用 :由于write()是一个系统调用,频繁调用会带来一定的开销。所以,在实际应用中,通常会先将数据缓存到内存中,然后再批量写入。

3:read函数

调用函数read()可从打开的文件中读取数据:

复制代码
#include<unistd.h>
ssize_t read(int fd, void *buf, size_t count);
​
  • 参数

    • fd 文件描述符,这个文件描述符可以通过open()获得,也可以是标准输入STDIN_FILENO(值为 0)。

    • buf 读取到的数据要存放到的地方(缓冲区地址),一般是一个数组的地址

    • count 要读取的最大字节数

  • 返回值

    • 成功:

      • 返回实际读取的字节数(范围是 0 到count

      • 若返回值为 0,说明已经到达文件末尾(EOF),没有更多数据可读取。

    • 失败:

      • 返回-1
  • 注意

    • 读普通文件时,在读到要求字节数之前已经达到了文件结尾。eg:若在达到文件尾端之前有30个字节,而要求读50个字节,则read返回30,下次在调用read时将返回0;

    • 当从终端设备读时, 通常一次最多读一行。

    • 当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。

  • 示例代码:

    1. 从标准输入读取数据
    复制代码
    int main() 
    {
        char buffer[100] = { 0 };
        ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer));
        if (bytes_read == -1)
        {
            perror("读取失败");
            return 1;
        }
        printf("成功读取 %zd 字节\n", bytes_read);
        return 0;
    }
2. 从文件读取数据(循环读取直到文件末尾)
复制代码
int main()
{
    int fd = open("data.txt", O_RDONLY);
    if (fd == -1)
    {
        perror("文件打开失败");
        return 1;
    }
​
    char buffer[50];
    ssize_t bytes_read;
​
    while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0) 
    {
        // 处理读取到的数据,例如打印
        write(STDOUT_FILENO, buffer, bytes_read);
    }
​
    if (bytes_read == -1)
    {
        perror("读取失败");
    }
​
    close(fd);
    return 0;
}

总结:

  • 单次调用read()返回 0 → 未读取到任何数据。

  • 循环中read()返回 0 → 文件已全部读完。

4:close函数

当打开一个文件之后,就会占据系统资源,如果我们使用完之后不及时清理,会造成资源的浪费,所以一般使用之后就会关闭文件,释放占用的资源

复制代码
#include <unistd.h>
int close(int fd);

注意:关闭已打开文件时会释放该进程加在该文件中上的所有记录锁。当一个进程终止时,内核自动关闭其所有已打开的文件。很多程序都利用这一功能而不是在程序中用函数close()关闭已打开的文件

5:lseek函数

每个文件都有一个与之相关联的文件偏移量,用以度量文件起始位置到当前位置的字节数。通常情况下,读写操作都是从当前文件偏移量处开始的,读写完成后,文件偏移量会自动增加所读写的字节数。打开一个文件后,该文件偏移量默认设置为0。通过设置偏移量就可以实现文件指定位置进行插入内容。

调用函数lseek()可显式的为打开的文件设置文件偏移量,用于改变文件的读写位置。其函数原型如下:

复制代码
off_t lseek(int fd, off_t offset, int whence);
  • 参数

    • fd: 文件描述符,是一个整数,表示要操作的文件。

    • offset: 表示相对于whence所指定位置的字节数。这个值可以是正数(向文件末尾方向移动)、负数(向文件开头方向移动)或者 0(保持当前位置不变)。

    • whence: 参考点,表示偏移量的起始位置。

      • whence的值可以是以下三个常量之一: SEEK_SET:从文件开始处计算偏移量(文件起始位置加offset个字节)。

    SEEK_CUR:从当前位置计算偏移量(文件当前位置加offset个字节)。 ​ SEEK_END:从文件结束处计算偏移量(文件末尾位置加offset个字节)。

  • 返回值

    • 成功: 返回新的文件偏移量,这个值是从文件起始位置开始计算的字节数。

    • 失败:返回-1

函数lseek()仅将当前文件的文件偏移量记录在内核中,并不会引起任何I/O的操作,该文件偏移量用于下一次读写操作 。

示例代码:

1. 将文件偏移量设置到文件开头
复制代码
int main()
{
    int fd = open("data.txt", O_RDWR);
    if (fd == -1)
    {
        perror("文件打开失败");
        return 1;
    }
​
    // 将文件偏移量设置到文件开头
    off_t new_offset = lseek(fd, 0, SEEK_SET);
    if (new_offset == -1)
    {
        perror("lseek失败");
        close(fd);
        return 1;
    }
​
    // 现在可以从文件开头开始读取或写入
    close(fd);
    return 0;
}
2. 计算文件大小
复制代码
int main() 
{
    int fd = open("data.txt", O_RDONLY);
    if (fd == -1) 
    {
        perror("文件打开失败");
        return 1;
    }

    // 将文件偏移量设置到文件末尾,并获取新的偏移量(即文件大小)
    off_t file_size = lseek(fd, 0, SEEK_END);
    if (file_size == -1)
    {
        perror("lseek失败");
        close(fd);
        return 1;
    }

    printf("文件大小为 %lld 字节\n", (long long)file_size);
    close(fd);
    return 0;
}
3. 定位到文件中间位置进行读取
复制代码
int main()
{
    int fd = open("data.txt", O_RDONLY);
    if (fd == -1) 
    {
        perror("文件打开失败");
        return 1;
    }
​
    // 定位到文件中间位置
    off_t file_size = lseek(fd, 0, SEEK_END);
    lseek(fd, file_size / 2, SEEK_SET);
​
    // 从文件中间位置开始读取数据
    char buffer[100];
    ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
    if (bytes_read > 0) 
    {
        // 处理读取到的数据
    }
​
    close(fd);
    return 0;
}
写一个程序来实现文件复制功能:
复制代码
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
​
int main(int argc, char *argv[]) 
{
    char *fileName = "test.txt";
    char *otherName = "copy.txt";
    
    // 1: 打开文件:------------------------------
    int fd = open(fileName, O_RDWR );
    int fd2 = open(otherName, O_RDWR | O_CREAT, 0666 );
    
    if(fd == -1 || fd2 == -1)
    {
        perror("文件创建失败!");
        exit(EXIT_FAILURE);
    }
    
    // 2: 对文件进行读或写操作:--------------------
    // 2-1: 读进数据
    char buf[64] = {0};
    int length = sizeof(buf) / sizeof(buf[0]);
    
    ssize_t size;
    while((size = read( fd, buf, length )) != 0)
    {
        // 2-2: 写出数据
        write( fd2, buf , size );
    }
    
    // 3: 关闭文件 ------------------------------
    close(fd);
    close(fd2);
    
    puts("---end---");
    
 return 0;
}
相关推荐
wqqqianqian31 分钟前
国产linux系统(银河麒麟,统信uos)使用 PageOffice在线编辑word文件保存数据同时保存文件
linux·word·信创·国产·保存·pageoffice·在线编辑
心之语歌43 分钟前
ubuntu24.04 搭建 java 环境服务,以及mysql数据库
linux·ubuntu
愚润求学1 小时前
【Linux】POSIX信号量
linux·运维
进阶的DW1 小时前
新手小白使用VMware创建虚拟机安装Linux
java·linux·运维
jz_ddk1 小时前
[zynq] Zynq Linux 环境下 AXI BRAM 控制器驱动方法详解(代码示例)
linux·运维·c语言·网络·嵌入式硬件
Magnum Lehar1 小时前
vulkan游戏引擎启动环境配置1
c语言
深思慎考2 小时前
Linux网络——socket网络通信udp
linux·网络·udp
一介草民丶2 小时前
Jenkins | Linux环境部署Jenkins与部署java项目
java·linux·jenkins
字节高级特工2 小时前
【Linux篇】0基础之学习操作系统进程
linux·运维·服务器·数据结构·windows·学习·list
待什么青丝2 小时前
【TMS570LC4357】之相关驱动开发学习记录1
c语言·arm开发·驱动开发·学习