文件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;
}
相关推荐
Android系统攻城狮6 小时前
Linux Gstreamer深度解析之gst_audio_ring_buffer_delay调用流程与实战(七十二)
linux·运维·服务器·gstreamer音视频·音视频进阶
快乐的韭菜根根6 小时前
ubuntu 安装slurm并配置
linux·运维·ubuntu
Navigator_Z7 小时前
LeetCode //C - 1289. Minimum Falling Path Sum II
c语言·算法·leetcode
himobrinehacken7 小时前
取模运算的汇编奥秘揭秘
c语言·安全
倒头就睡的小比特8 小时前
Linux基础指令3
linux·服务器
Yiran_G8 小时前
LoRa串口模组和SoC模组怎么选?从MCU架构看无线通信方案设计
嵌入式硬件·物联网
梦帮科技8 小时前
Kali Linux Scapy 畸形报文构造学——深度解剖非法 TCP Flags 与反常序列号
linux·爬虫·神经网络·算法·数学建模·系统架构·模拟退火算法
半杯咖啡半行码9 小时前
Linux 驱动开发入门:内核模块编程基础
linux·服务器·网络
别动我齐刘海10 小时前
Linux 系统调试学习2
linux·运维·c++·人工智能·学习·机器学习·机器人
青岚_Nox10 小时前
C语言实战:带你手写一个带"计时功能"的猜数字小游戏
c语言