《Linux C 智能 IO 矩阵:输入输出的自适应数据流转》

1. 标准库IO简介

标准库IO特点:通过操作系统提供的接口(API)和操作系统进行交互。(接近100个函数)

1.1. IO的过程

操作系统:向上为用户提供操作接口,向下为统筹控制硬件。

操作系统的组成:内核+外壳(shell)。

linux内核的五大功能:进程管理,内存管理,文件管理,网络管理,设备管理。

C语言基础最早接触的IO:

  • printf 标准输出
  • scanf 标准输入

1.2. IO的种类

  • 文件IO (系统调用)
  • 标准IO (库函数)

1.3. 文件IO(系统调用)

系统调用是受控的内核入口,我们通过系统调用从用户空间进入内核空间(另外一种说法:从用户空间进入内核空间的过程叫做系统调用),常用于linux系统。

特点:系统调用的可移植性差,效率相对较低,实时性强。

1.4. 标准IO(库函数)

简单可以理解成:库函数 = 系统调用+缓冲区

库函数的可移植性强,效率高,实时性差。

(库函数减少了系统调用的次数,从而提高的内核的工作效率)

1.5. 常用的函数接口

  • 文件IO:open,close,read,write,lseek
  • 标准IO:fopen,fclose,fread,fwrite,.......

2. 标准IO(库函数)

2.1. FILE指针

FILE是标准库中定义的一个结构体。

在这个结构体中包含了打开文件的相关信息。

FILE指针就是一个结构体指针。我们通过FILE指针去对文件进行操作。

当使用fopen函数成功打开文件的时候,就会返回一个FILE指针,后续我们对文件的读写操作都是通过这个FILE指针去进行的。

每个正在运行的程序都会维护自己的FILE指针,即使多个程序打开同一个文件,对应的FILE指针也是不同的。

每个正在运行的程序都有三个默认打开的FILE指针。

  • 标准输入:stdin -- scanf
  • 标准输出:stdout --- printf
  • 标准出错:stderr --- perror
cpp 复制代码
struct _IO_FILE 
{ 
    char *_IO_buf_base; /* 缓冲区的起始地址 */ 
    char *_IO_buf_end; /* 缓冲区的结束地址 */ 
    int _fileno; /*文件描述符*/ 
};

2.2. fopen函数

cpp 复制代码
#include <stdio.h>
FILE *fopen(const char *pathname, const char *mode);
功能:打开pathname对应的文件
参数:
    pathname:文件的路径和名字
    mode:打开文件的模式    "r"  "r+"
       r    以只读的方式打开文件,光标定位在文件的开头

       r+    以读写的方式打开文件,光标定位在文件开头

       w    以只写的方式打开文件,光标定位在文件开头。如果文件不存在就创建,如果文件存在就清空

       w+   以读写的方式打开文件,光标定位在文件开头。如果文件不存在就创建,如果文件存在就清空

       a   以追加的方式打开文件,光标定位在文件结尾,如果文件不存在就创建
       a+   以追加和读的方式打开文件,如果写光标会定位到文件的结尾,如果读,光标会定位在文件的开头。如果文件不存在就创建
返回值:成功返回文件对应的FILE指针,失败返回NULL,置位错误码

fopen使用实例

cpp 复制代码
#include<stdio.h>
int main()
{
    FILE *fp = NULL;//定义一个FILE指针
    //使用fopen 打开文件

    // //以只读的方式打开文件,文件不存在会报错
    // fp = fopen("./a.txt","r");
    // if(NULL == fp){
    //     printf("fopen a.txt error\n");
    //     return -1;
    // }

    //以只写的方式打开文件,文件不存在就创建,存在就清空
    fp = fopen("./a.txt","w");
    if(NULL == fp){
        printf("fopen a.txt error\n");
        return -1;
    }

    //不管以什么样的方式去打开文件,当前用户一定要有对该文件的操作权限
    return 0;
}

2.3. fclose函数

cpp 复制代码
#include <stdio.h> int fclose(FILE *stream); 
功能:关闭FILE指针 
参数: stream:通过fopen获取到的文件指针 
返回值:成功返回0,失败返回EOF,置位错误码

fclose代码实例

cpp 复制代码
#include<stdio.h>
int main()
{

    //1.先做一个正常的输入
    int a = 0;
    scanf("%d",&a);
    printf("第一次输出a = %d\n",a);
    fclose(stdin);  //关闭标准输入
    scanf("%d",&a);
    printf("第二次输出a = %d\n",a);

    //2.做一个正常的输出
    printf("111111111111\n");
    fclose(stdout); //关闭标准输出
    printf("2222222222222\n");


    return 0;
}

2.4. 错误码

2.4.1. 错误码的概念

  • 错误码是系统调用失败之后,内核向用户空间返回的错误信息的编号。
  • 错误码本质是一个整数,错误码保存在errno.h中的一个全局变量errno中。
  • 错误码在内核中有4k个。

/usr/include/asm-generic/errno-base.h

2.4.2. 错误的原理

2.4.3. 错误码使用实例1

cpp 复制代码
#include<stdio.h>
#include<errno.h>
int main(int argc,const char* argv[])
{
    //打开一个不存在的文件
    FILE *fp = fopen("b.txt","r");
    if(NULL == fp){
        printf("errno = %d\n",errno);//errno = 2,表示没有这个文件
        //return -1;
    }
    //成功调用函数,并不会置位错误码
    fp = fopen("a.txt","r");
    printf("errno = %d\n",errno);
    return 0;
}

2.4.4. 将错误码转换为错误信息的函数(strerror)

cpp 复制代码
 #include <string.h>
   char *strerror(int errnum);
    功能:将errnum转换为对应的错误信息
    参数:
        errnum:错误码
    返回值:错误码对应的错误信息,失败返回"Unknown error nnn"

2.4.5. 直接输出错误信息

cpp 复制代码
#include <stdio.h> void perror(const char *s); 
功能:直接输出错误信息,输出之后,会自动添加换行 
参数: s:错误信息的提示,后面会自动添加冒号 
返回值:空

#include<stdio.h> #include<errno.h> 
#include<string.h> 
int main(int argc,const char* argv[]) 
{ //打开一个不存在的文件 FILE *fp = fopen("b.txt","r"); 
if(NULL == fp)
{ perror("fopen b.txt error"); } 
return 0; }

2.5. fgetc函数

cpp 复制代码
#include <stdio.h> 
int fgetc(FILE *stream); 
功能:从文件中读取一个字符,以返回值的形式返回 
参数: stream:文件指针 
返回值:成功返回获取到的字符,失败或者读取到文件结尾返回EOF

fgetc使用实例

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

int main(int argc, const char *argv[])
{
    // 1.打开文件
    FILE *fp = fopen("./a.txt", "r");
    if (NULL == fp)
        PRINT_ERR("fopen a.txt error");

    // 2.从文件中读取一个字符,光标向后偏移一个字节
    char ch = 0;
    ch = fgetc(fp);
    printf("ch = %c\n", ch); //读取到 '1'
    //再次读取,读取到 '2'

    ch = fgetc(fp);
    printf("ch = %c\n", ch);

    //从终端获取一个字符
    ch = fgetc(stdin); //这种用法,等价于getchar()
    printf("ch = %c\n", ch);
    ch = fgetc(stdin); //这种用法,等价于getchar()
    printf("ch = %c\n", ch);

    return 0;
}

2.6. fputc函数

cpp 复制代码
#include <stdio.h> 
int fputc(int c, FILE *stream); 
功能:向文件中写入一个字符 
参数: c:要写入的字符 stream:文件指针 
返回值:成功返回写入的字符的ascii码,失败返回EOF

fputc使用实例

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

int main(int argc,const char* argv[])
{
    // //1.只写的方式打开文件
    // FILE *fp = fopen("./a.txt","w");
    // if(NULL == fp)
    //     PRINT_ERR("fopen a.txt error");
    
    // //2.fputc向文件中写入内容
    // fputc('h',fp);
    // fputc('e',fp);
    // fputc('l',fp);
    // fputc('l',fp);
    // fputc('o',fp);
    // //3.向终端输出内容
    // fputc('a',stdout);//'a'会输出到终端中,这种用法和putchar()完全一致


    //以读写的方式打开文件
    FILE *fp = fopen("./a.txt","r+");
    if(NULL == fp)
        PRINT_ERR("fopen a.txt error");
    //fputc向文件中写入内容
    fputc('h',fp); //如果文件中原本有内容,那么fputc会将光标后的字符给覆盖掉

    //光标问题:读写用的是同一个光标
    fgetc(fp);//光标向后偏移一个字节
    fputc('0',fp);//会将文件中第二个字符给覆盖掉

    return 0;
}

2.7. fgets函数

cpp 复制代码
char *fgets(char *s, int size, FILE *stream); 
功能:从文件中获取字符串,读取size-1个字节的数据 
参数: s:保存获取到的字符串的首地址 
size:要读取的字节数 stream:文件指针 
返回值:成功返回读取到的字符串,失败或者读取到文件结尾返回NULL

fgets的使用

2.8. sprintf/snprintf/fprintf

cpp 复制代码
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
功能:向文件中格式化输出内容
参数:
    stream:文件指针
    format:输出格式
    ...:可变参
返回值:成功返回格式化输出的字符的个数,失败返回负数
int sprintf(char *str, const char *format, ...);
功能:向数组中格式化输出内容
参数:
    str:输出的目标地址
    format:输出格式
    ...:可变参
返回值:成功返回格式化输出的字符的个数,失败返回负数
int snprintf(char *str, size_t size, const char *format, ...);
功能:向数组中格式化输出内容
参数:
    str:输出的目标地址
    size:格式化输出字符的个数
    format:输出格式
    ...:可变参
返回值:成功返回格式化输出的字符的个数,失败返回负数

printf家族使用实例

cpp 复制代码
#include <my_head.h>  // 包含自定义头文件

int main(int argc, const char* argv[])
{
    // 定义字符变量 var1 并初始化为 'a'
    char var1 = 'a';
    // 定义整数变量 var2 并初始化为 12345
    int var2 = 12345;
    // 定义字符数组 var3 并初始化为一个长字符串
    char var3[128] = {"12345121111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"};
    // 定义字符数组 buf 并初始化为 0
    char buf[128] = {0};

    // 1. 使用 sprintf 函数将格式化的字符串写入 buf
    // 注意: sprintf 不会检查缓冲区大小,可能导致数组越界
    // sprintf(buf, "var1 = %c var2 = %d var3 = %s", var1, var2, var3);
    // printf("buf = [%s]\n", buf);

    // 2. 使用 snprintf 函数将格式化的字符串写入 buf
    // snprintf 解决了数组越界问题,虽然有时会警告,但程序不会崩溃
    snprintf(buf, sizeof(buf), "var1 = %c var2 = %d var3 = %s", var1, var2, var3);
    printf("buf = [%s]\n", buf);

    // 3. 使用 fprintf 向文件中格式化输出内容
    FILE *fp = fopen("a.txt", "w");  // 以写模式打开文件 a.txt
    if (NULL == fp)  // 检查文件是否成功打开
        PRINT_ERR("fopen error");  // 如果文件打开失败,打印错误信息
    fprintf(fp, "var1 = %c var2 = %d var3 = %s", var1, var2, var3);  // 向文件写入格式化字符串
    fclose(fp);  // 关闭文件

    return 0;  // 返回 0 表示程序成功结束
}

2.9. fseek/ftell/rewind 光标相关函数

cpp 复制代码
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
功能:偏移光标位置
参数:
    stream:文件指针
    offset:偏移量
        >0:向后偏移
        =0:不偏移
        <0:向前偏移
    whence:偏移的基地址
        SEEK_SET:从文件开头开始偏移
        SEEK_CUR:从当前位置开始偏移
        SEEK_END:从文件结尾开始偏移
返回值:成功返回0,失败返回-1,置位错误码
long ftell(FILE *stream);
功能:获取光标当前位置到开头的字节数
参数:文件指针
返回值:成功返回当前位置到开头的字节数,失败返回-1,置位错误码


void rewind(FILE *stream);
功能:将光标偏移到文件开头
参数:文件指针

偏移光标实例

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

int main(int argc,const char* argv[])
{
    //1.以读写的方式打开文件
    FILE  *fp = fopen("./a.txt","r+");
    if(NULL == fp)
        PRINT_ERR("fopen error");
    
    char ch = 0;
    //2.从当前位置向后偏移光标
    fseek(fp,3,SEEK_CUR);
    ch = fgetc(fp);
    printf("ch = %c\n",ch);

    //3.从文件开头向后偏移
    fseek(fp,3,SEEK_SET);
    ch = fgetc(fp);
    printf("ch = %c\n",ch);

    //4.从文件结尾偏移光标
    fseek(fp,-3,SEEK_END);
    ch = fgetc(fp);
    printf("ch = %c\n",ch);

    //5.从文件结尾向后偏移
    int ret = fseek(fp,3,SEEK_END);
    printf("ret = %d\n",ret);//0,不会出错
    fputc('a',fp);  //会在文件结尾后空出三个字节,然后再将a写入

    //6.从文件开头向前偏移
     ret = fseek(fp,-3,SEEK_SET);
    printf("ret = %d\n",ret);//-1,出错

    //ftell函数的使用
    fseek(fp,0,SEEK_END);
    int pos = ftell(fp);
    printf("pos = %d\n",pos);  //如果光标位置在结尾,获取到的刚好是文件的大小

    //rewind函数
    rewind(fp);
    pos = ftell(fp);
    printf("pos = %d\n",pos);
    return 0;
}

3. 文件IO(系统调用)

3.1. 文件描述符

  • 在文件IO中我们文件描述符去操作文件。
  • 文件描述符本质就是一个非负的整数。
  • 文件描述符的最大的限制是1024.
  • 可以通过ulimit -a去查看
  • 可以通过 ulimit -n 2048 修改文件描述符的最大个数

每个正在行的程序都有三个默认打开的文件描述符

  • 标准输入:0
  • 标准输出:1
  • 标准出错:2
  • 文件描述符的分配,遵循最小分配原则。
  • 系统会把最小的没有被分配的文件描述符分配给你。
  • 文件描述符如何操作文件?

3.2. open函数的使用

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
功能:打开文件
参数:
    pathname:文件的路径和名字                                                                                                                   
    flags:打开文件的模式
        O_RDONLY:以只读的方式打开文件
        O_WRONLY:以只写的方式打开文件
        O_RDWR:以读写的方式打开文件    
        //以上三种打开文件的模式必须包含一个
        O_APPEND:以追加的方式打开文件
        O_CREAT:创建文件的标志
            如果flags中有O_CREAT这个参数,那么就需要使用到第三个参数
        O_TRUNC:清空文件的标志
        O_EXCL :以独占的方式打开文件
            必须配合O_CREAT使用
            加了这个参数之后,使用open创建文件的时候,如果文件存在,会返回文件已经存在的错误  错误码置为EEXIST
    mode:创建文件的权限
        如果第二个参数有O_CREAT那么就需要填充这个参数    
        比如:open("./a.txt",O_RDWR|O_CREAT|O_EXCL,0666)
        0666就表示我想要创建的文件的权限
        实际创建出来的文件的权限和umask相关
        mode & (~umask)
       umask:是创建文件的掩码
       umask查看的方式:umask 命令
       修改umask的方式: umask  掩码值
       mode  0666      110 110 110
       umask 0002      000 000 010
       实际: 0664      110 110 110
       
       mode  0666      110 110 110
       umask 0222      010 010 010
       实际: 0444      100 100 100
返回值:成功返回文件描述符,失败返回-1置位错误码
        文件描述符遵循最小分配原则

open("./a.txt",O_RDWR|O_CREAT|O_EXCL)

"r":O_RDONLY
"r+":O_RDWR
"w":O_WRONLY|O_CREAT|O_TRUNC
"w+":O_RDWR|O_CREAT|O_TRUNC
"a":O_APPEND|O_WRONLY|O_CREAT
"a+":O_APPEND|O_RDWR|O_CREAT

flags的原理

open使用实例

cpp 复制代码
#include <my_head.h>  // 包含自定义头文件

int main(int argc, const char* argv[])
{
    int fd = 0;

    // 打开文件,只读的方式打开
    // fd = open("./a.txt", O_RDONLY);
    // if (-1 == fd)
    //     PRINT_ERR("open file error");

    // 以"w+"的方式打开文件
    // fd = open("./a.txt", O_RDWR | O_CREAT | O_TRUNC, 0664);
    // if (-1 == fd)
    //     PRINT_ERR("open file error");

    // 以独占的方式打开文件
    // fd = open("./a.txt", O_RDWR | O_CREAT | O_EXCL, 0664);
    // if (-1 == fd) {
    //     if (errno == EEXIST) {
    //         printf("文件已经存在\n");
    //         // 发现文件存在,我换个名字,重新创建一下
    //         fd = open("./b.txt", O_RDWR | O_CREAT | O_EXCL, 0664);
    //         if (-1 == fd) {
    //             printf("再次创建文件失败\n");
    //             return -1;
    //         }
    //     } else {
    //         printf("打开失败\n");
    //     }
    // }

    // 文件描述符遵循最小分配原则
    int fd1, fd2, fd3;

    // 以只读方式打开文件 a.txt
    fd1 = open("./a.txt", O_RDONLY);
    if (-1 == fd1) 
    {
        PRINT_ERR("open file error");
        return -1;
    }
    printf("fd1 = %d\n", fd1);  // 打印文件描述符 fd1 的值

    // 以只读方式打开文件 b.txt
    fd2 = open("./b.txt", O_RDONLY);
    if (-1 == fd2) {
        PRINT_ERR("open file error");
        return -1;
    }
    printf("fd2 = %d\n", fd2);  // 打印文件描述符 fd2 的值

    // 关闭文件描述符 fd1
    close(fd1);
    printf("fd1 已关闭\n");

    // 再次以只读方式打开文件 b.txt
    fd3 = open("./b.txt", O_RDONLY);
    if (-1 == fd3) {
        PRINT_ERR("open file error");
        return -1;
    }
    printf("fd3 = %d\n", fd3);  // 打印文件描述符 fd3 的值

    return 0;  // 返回 0 表示程序成功结束
}

3.3. close函数

cpp 复制代码
#include <unistd.h>
int close(int fd);
功能:关闭文件
参数:
    fd:文件描述符
返回值:成功返回0,失败返回-1,置位错误码
cpp 复制代码
#include<my_head.h>

int main(int argc,const char* argv[])
{
    int var1 = 0;
    scanf("%d",&var1);
    printf("var1 = %d\n",var1);
    close(0);//关闭标准输入
    scanf("%d",&var1);
    printf("var1 = %d\n",var1);
    
    printf("111111111111\n");
    close(stdout->_fileno);//关闭标准输出
    printf("22222222222222\n");

    perror("msg");
    close(2); //关闭标准出错
    perror("msg");

    return 0;
}

3.4. read/write函数

cpp 复制代码
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
功能:从文件中读取count个字节到buf中
参数:
    fd:文件描述符
    buf:保存读取到的数据的首地址
    count:要读取的字节数
返回值:成功返回实际读取到的字节数,如果读取到文件结尾,返回0,失败返回-1.置位错误码
ssize_t write(int fd, const void *buf, size_t count);
功能:将buf中的数据,向文件中写入count个字节
参数:
    fd:文件描述符
    buf:保存我们要写入的数据
    count:想要写入的字节数
返回值:成功返回实际写入的字节数,失败返回-1,置位错误码

read使用实例

cpp 复制代码
#include <my_head.h>  // 包含自定义头文件

int main(int argc, const char* argv[])
{
    // 1. 打开文件
    // 以只读方式打开文件 a.txt,如果文件不存在则创建,权限设置为 0664
    int fd = open("./a.txt", O_RDONLY | O_CREAT, 0664);
    if (-1 == fd) {
        PRINT_ERR("open error");  // 如果打开文件失败,打印错误信息
        return -1;  // 返回 -1 表示程序失败
    }

    // 2. 从文件中读取字符串
    char buf[128] = {0};  // 定义一个长度为 128 的字符数组 buf,并初始化为 0
    int ret = 0;  // 定义一个整数变量 ret 用于存储 read 函数的返回值

    // 使用 read 函数从文件描述符 fd 中读取数据到 buf 中,最多读取 sizeof(buf) 字节
    ret = read(fd, buf, sizeof(buf));
    printf("buf = [%s]\n", buf);  // 打印读取到的字符串
    printf("ret = %d\n", ret);  // 打印 read 函数的返回值,表示实际读取的字节数

    // 再次从文件中读取数据
    // 由于文件指针已经到达文件末尾,再次读取会返回 0
    ret = read(fd, buf, sizeof(buf));
    printf("ret = %d\n", ret);  // 打印 read 函数的返回值,应为 0

    // 关闭文件描述符
    close(fd);
    return 0;  // 返回 0 表示程序成功结束
}

write函数使用实例

cpp 复制代码
#include <my_head.h>  // 包含自定义头文件
#include <string.h>   // 包含字符串处理函数的头文件

int main(int argc, const char* argv[])
{
    // 1. 打开文件
    // 以读写方式打开文件 a.txt
    int fd = open("./a.txt", O_RDWR);
    if (-1 == fd) {
        PRINT_ERR("open error");  // 如果打开文件失败,打印错误信息
        return -1;  // 返回 -1 表示程序失败
    }

    // 2. 向文件中写入内容, 写入整型
    int a = 100;
    // 使用 write 函数将整型变量 a 写入文件
    write(fd, &a, sizeof(a));

    // 3. 向文件中写入字符串
    char buf[128] = {"hello world"};
    // write(fd, buf, sizeof(buf));  // 这会将数组中所有的数据写进文件中,包括0
    // 使用 write 函数将字符串 buf 写入文件,只写入字符串的实际长度
    int ret = write(fd, buf, strlen(buf));
    printf("ret = %d\n", ret);  // 打印 write 函数的返回值,表示实际写入的字节数

    // 关闭文件描述符
    close(fd);
    return 0;  // 返回 0 表示程序成功结束
}

3.5. lseek函数

cpp 复制代码
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
功能:偏移文件光标
参数:
    fd:文件描述符
    offset:偏移量
        <0:向前偏移
        =0:不偏移
        >0:向后偏移
    whence:
        SEEK_SET:从文件开头开始偏移
        SEEK_CUR:从当前位置开始偏移
        SEEK_END:从文件结尾开始偏移
返回值:成功返回当前光标位置到文件开头的距离,失败返回(off_t) -1置位错误码

lseek使用实例

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

// 定义一个 RGB 结构体,用于表示颜色的 RGB 值
typedef struct RGB
{
    unsigned char b;  // 蓝色通道的值
    unsigned char g;  // 绿色通道的值
    unsigned char r;  // 红色通道的值
    // unsigned char a;//如果位深是32,需要用到这个成员,是透明度
} rgb_t;

/**
 * @brief 主函数,程序的入口点
 * 
 * 该函数用于打开一个 BMP 图片文件,读取其头部信息,
 * 并在图片的左上角绘制一个 500x300 的彩色矩形。
 * 
 * @param argc 命令行参数的数量
 * @param argv 命令行参数的数组
 * @return int 程序的退出状态码,0 表示正常退出
 */
int main(int argc, const char *argv[])
{
    // 1.打开图片文件
    int fd = open("./ddm.bmp", O_RDWR);  // 以读写模式打开指定的 BMP 文件
    if (-1 == fd)
        PRINT_ERR("fopen error");  // 如果打开文件失败,打印错误信息

    // 读取图片类型  BM
    char bftype[3] = {0};  // 用于存储 BMP 文件的类型信息,以字符串形式存储
    read(fd, bftype, 2);  // 从文件中读取 2 个字节的类型信息
    printf("bftype = %s\n", bftype);  // 打印 BMP 文件的类型信息

    // 读取图片大小  622054
    int bfSize = 0;  // 用于存储 BMP 文件的大小
    read(fd, &bfSize, 4);  // 从文件中读取 4 个字节的文件大小信息
    printf("bfsize = %d\n", bfSize);  // 打印 BMP 文件的大小

    // 读取图片头的大小,54
    int head_size = 0;  // 用于存储 BMP 文件头的大小
    lseek(fd, 0xA, SEEK_SET);  // 将文件指针移动到文件头中存储文件头大小的位置
    read(fd, &head_size, 4);  // 从文件中读取 4 个字节的文件头大小信息
    printf("head_size = %d\n", head_size);  // 打印 BMP 文件头的大小

    // 读取图片的宽度  1920
    int width = 0;  // 用于存储 BMP 图片的宽度
    lseek(fd, 0x12, SEEK_SET);  // 将文件指针移动到文件头中存储图片宽度的位置
    read(fd, &width, 4);  // 从文件中读取 4 个字节的图片宽度信息
    printf("width = %d\n", width);  // 打印 BMP 图片的宽度

    // 读取图片的高度  1080
    int high = 0;  // 用于存储 BMP 图片的高度
    read(fd, &high, 4);  // 从文件中读取 4 个字节的图片高度信息
    printf("high = %d\n", high);  // 打印 BMP 图片的高度

    // 读取图片的位深
    int bitcount = 0;  // 用于存储 BMP 图片的位深
    lseek(fd, 0x1C, SEEK_SET);  // 将文件指针移动到文件头中存储图片位深的位置
    read(fd, &bitcount, 4);  // 从文件中读取 4 个字节的图片位深信息
    printf("bitcount = %d\n", bitcount);  // 打印 BMP 图片的位深

    // 修改图像数据
    int ret = lseek(fd, 54, SEEK_SET);  // 偏移光标到像素数据之前
    printf("ret = %d\n", ret);  // 打印文件指针的偏移量

    rgb_t color = {227, 22, 233};  // 定义一个 RGB 颜色值,用于绘制矩形

    // 在图片的左上角绘制一个 500x300 的彩色矩形
    for (int j = 0; j < 300; j++)
    {
        for (int i = 0; i < 500; i++)
        {
            write(fd, &color, sizeof(rgb_t));  // 将指定的颜色值写入文件
        }
        lseek(fd, (1920 - 500) * 3, SEEK_CUR);  // 跳过当前行剩余的像素数据
    }

    close(fd);  // 关闭文件

    return 0;
}

3.6. stat函数

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
功能:获取文件信息
参数:
    pathname:文件的路径和名字
    statbuf:保存文件信息的结构体的首地址
返回值:成功返回0,失败返回-1,置位错误码
    struct stat {
    dev_t     st_dev;   /* 文件所在磁盘的设备号 */
    ino_t     st_ino;   /* 文件的inode号 */
    mode_t    st_mode;  /* 文件的类型和权限 */    
    nlink_t   st_nlink;  /* 硬连接数,对于目录文件来说,这个数值是目录下子目录的个数*/
    uid_t     st_uid;    /* 文件所属用户的用户id */
    gid_t     st_gid;    /* 文件所属组的组id */
    dev_t     st_rdev;   /* 如果文件是设备文件,那么该字段为设备文件的设备id */
    off_t     st_size;        /* 文件的大小 */
    blksize_t st_blksize;     /* 文件系统一页内存的大小*/
    blkcnt_t  st_blocks;      /* 文件所占块儿的数量*/
    struct timespec st_atim;  /* 上次访问文件的时间 */
    struct timespec st_mtim;  /* 上次修改文件的时间 */
    struct timespec st_ctim;  /*上次文件状态改变的时间 */
};

对于st_mode字段的解释:
mode_t    st_mode;
st_mode :12~15bit位表示的是文件的类型
st_mode: 0~8个bit位表示的是文件的权限
获取文件类型:
  S_IFMT     0170000   文件类型的掩码

   S_IFSOCK   0140000   套接字文件
   S_IFLNK    0120000   链接文件
   S_IFREG    0100000   普通文件
   S_IFBLK    0060000   块设备文件
   S_IFDIR    0040000   目录文件
   S_IFCHR    0020000   字符设备文件
   S_IFIFO    0010000   管道文件

    stat(pathname, &sb);
   if ((sb.st_mode & S_IFMT) == S_IFREG) {
       /* Handle regular file */
   }
   也可以通过如下宏定义去判断文件类型:
   如果文件类型匹配,下列的宏定义会返回1;
    S_ISREG(m)  is it a regular file?
    S_ISDIR(m)  directory?
    S_ISCHR(m)  character device?
    S_ISBLK(m)  block device?
    S_ISFIFO(m) FIFO (named pipe)?
    S_ISLNK(m)  symbolic link?  (Not in POSIX.1-1996.)
    S_ISSOCK(m) socket?  (Not in POSIX.1-1996.)
stat(pathname, &sb);
   if (S_ISREG(sb.st_mode)) {
       /* Handle regular file */
   }   
   
文件权限的获取:
    st_mode & 0777 即可获取到文件的所有权限信息
    如果想要查看系统提供的宏定义,可以 man 7 inode  
          
cpp 复制代码
#include <my_head.h>

/**
 * @brief 主函数,程序入口,用于获取并输出文件的属性信息
 * 
 * 该函数接收一个命令行参数作为文件名,通过 `stat` 函数获取文件的属性信息,
 * 然后输出文件的基本信息、访问/修改/状态改变时间、文件类型和权限。
 * 
 * @param argc 命令行参数的数量
 * @param argv 命令行参数数组,其中 argv[1] 应为要查询的文件名
 * @return int 程序的退出状态码,0 表示成功,-1 表示入参错误
 */
int main(int argc, const char *argv[])
{
    // 检查命令行参数数量是否正确
    if (argc != 2)
    {
        // 若参数数量不对,输出错误信息
        printf("入参错误\n");
        // 提示正确的使用方式
        printf("Usage:./a.out filename\n");
        return -1;
    }
    // 1.获取文件属性信息
    // 定义一个 stat 结构体变量,用于存储文件的属性信息
    struct stat st;
    // 调用 stat 函数获取指定文件的属性信息
    stat(argv[1], &st);

    // 2.输出文件信息
    // 输出文件的 i 节点号、用户 ID、文件大小、块大小、占用块数
    printf("ino:%ld,uid:%d,size:%ld,blksize:%ld,blks:%ld\n",
           st.st_ino, st.st_uid, st.st_size, st.st_blksize, st.st_blocks);
    // 输出文件上次被访问的时间
    printf("上次访问文件的时间%ld\n", st.st_atime);
    // 输出文件内容上次被修改的时间
    printf("上次修改文件的时间%ld\n", st.st_mtime);
    // 输出文件状态(如权限、所有者等)上次改变的时间
    printf("上次文件状态改变的时间%ld\n", st.st_ctime);
    // 3.输出文件的类型和权限
    // 根据文件的模式位判断文件类型
    switch (st.st_mode & __S_IFMT)
    {
    case __S_IFSOCK:
        // 如果是套接字文件,输出相应信息
        printf("是套接字文件\n");
        break;
    case __S_IFLNK:
        // 如果是符号链接文件,输出相应信息
        printf("是链接文件\n");
        break;
    case __S_IFREG:
        // 如果是普通文件,输出相应信息
        printf("是普通文件\n");
        break;
    case __S_IFBLK:
        // 如果是块设备文件,输出相应信息
        printf("是块设备文件\n");
        break;
    case __S_IFDIR:
        // 如果是目录文件,输出相应信息
        printf("是目录文件\n");
        break;
    case __S_IFCHR:
        // 如果是字符设备文件,输出相应信息
        printf("是字符设备文件\n");
        break;
    case __S_IFIFO:
        // 如果是命名管道文件,输出相应信息
        printf("是管道文件\n");
        break;
    }
    // 输出文件的权限,以八进制形式显示
    printf("mode = %#o\n", st.st_mode & 0777);
    return 0;
}

3.7. getpwuid,getgrgid函数的使用

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

int main(int argc, const char *argv[])
{
    // 1.获取用户的uid
    uid_t uid = getuid();
    // 2.根据uid获取用户信息
    struct passwd *usr_info = NULL;
    usr_info = getpwuid(uid);
    if(NULL == usr_info)
        PRINT_ERR("getpwuid error");

    printf("用户名:%s  用户密码:%s  uid:%d  gid:%d 用户描述:%s  家目录:%s   命令行解释器:%s \n",\
    usr_info->pw_name,usr_info->pw_passwd,usr_info->pw_uid,usr_info->pw_gid,\
    usr_info->pw_gecos,usr_info->pw_dir,usr_info->pw_shell);
    return 0;
}
cpp 复制代码
#include<my_head.h>

int main(int argc,const char* argv[])
{
    //1.获取组id
    gid_t gid = getgid();
    //2.通过组id获取组信息
    struct group * gr_info = NULL;
    gr_info = getgrgid(gid);
    if(NULL == gr_info)
        PRINT_ERR("getgrgid error");
    
    printf("组名:%s 组密码:%s 组id:%d \n",\
    gr_info->gr_name,gr_info->gr_passwd,gr_info->gr_gid);
    return 0;
}

3.8. 目录操作

cpp 复制代码
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
功能:打开目录
参数:
    name:目录的路径和名字
返回值:成功返回目录指针,失败返回NULL,置位错误码

#include <dirent.h>
struct dirent *readdir(DIR *dirp);
功能:从目录中读取内容,每读取一次,返回一个保存文件信息的结构体指针
参数:
    dirp:opendir返回的目录指针
返回值:成功返回一个结构体指针,这个结构体指针指向的结构体中包含了目录中某个文件的相关信息
    失败:返回NULL,置位错误码
    如果读取到文件结尾,返回NULL
struct dirent {
   ino_t      d_ino;   /* 文件的inode号*/
   off_t      d_off;  /* 不需要关注 */
   unsigned short d_reclen;  /* 这条记录的长度 */
   unsigned char  d_type;      /* 
文件的类型 */
   
DT_BLK      块儿设备文件.
DT_CHR      字符设备文件.
DT_DIR      目录文件.
DT_FIFO     管道文件.
DT_LNK      链接文件.
DT_REG      普通文件.
DT_SOCK     套接字文件.
DT_UNKNOWN  未知的文件类型
   char           d_name[256]; /* 文件的名字 */
           };

     
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
功能:关闭目录
参数:
    dirp:目录指针
返回值:成功返回0,失败返回-1,置位错误码

4.库的制作

4.1. 库的概念

库就是将.c文件编程生成的二进制文件。当我们需要使用的时候,直接将库文件链接到我们的可执行程序中即可。

windows:

  • xxx.lib -- 静态库
  • xxx.dll -- 动态库

linux:

  • libxxx.a -- 静态库
  • libxxx.so -- 动态库

4.2. 静态库

4.2.1. 静态库的概念

  • 静态库的使用,需要将整个库文件编译到我们的可执行程序中去。
  • 使用静态库的可执行程序,体积较大。
  • 使用静态库的可执行程序,执行效率更高。
  • 使用静态库的可执行程序,运行的时候,不需要依赖库文件。
  • 静态库的更新比较麻烦。

4.2.2. 静态库的制作

1.编译生成.o文件

gcc -c xxx1.c xxx2.c xxx3.c

2.将.o文件制作生成库文件

ar -cr libxxx.a xxx1.o xxx2.o xxx3.o

ar:制作静态库的命令

c:create

r:replace

上面的命令就是制作生成了静态库,将.o文件放入了库中

4.2.3. 静态库的使用

1.直接编译

gcc main.c libxxx.a -o main

2.更加标准的编译方式

gcc main.c -lxxx -L 库的路径

-l表示链接库,xxx表示库的名字

-L 指明库的路径

main.c

cpp 复制代码
#include<my_head.h> 
#include"add.h" 
int main(int argc,const char* argv[]) 
{ 
    int sum = 0; sum =add_func(10,20); 
    printf("sum = %d\n",sum); return 0; 
}

add.c

int add_func(int x,int y){ return x+y; }

add.h

#ifndef ADD_H #define ADD_H int add_func(int x,int y); #endif

编译和使用的命令

4.3. 动态库(共享库)

4.3.1. 动态库的概念

  • 动态库的使用,只会将库中函数的符号表编译到可执行程序中。
  • 使用动态库的可执行程序,体积较小。
  • 使用动态库的可执行程序执行效率会低一些。
  • 使用动态库的执行程序,运行的时候,需要依赖库文件。
  • 动态库的更新比较方便。

4.3.2.动态库的制作

方式1:

  • gcc -c -fPIC xxx.c -o xxx.o //编译生成.o文件
  • gcc -shared xxx.o -o libxxx.so
  • //-fPIC:表示忽略文件位置(赋予动态库共享的特性)
  • //-shared 制作动态库的命令

方式2:直接编译生成库文件

gcc -shared -fPIC xxx.c -o libxxx.so

4.3.3. 动态库的使用

  • gcc main.c -lxxx -L 库的路径
  • -l表示链接库,xxx表示库的名字
  • -L 指明库的路径
相关推荐
Tlog嵌入式11 分钟前
[项目]基于FreeRTOS的STM32四轴飞行器: 三.电源控制
c语言·单片机·mcu·iot
重生之成了二本看我逆天改命走向巅峰39 分钟前
从0搭建Tomcat第二天:深入理解Servlet容器与反射机制
java·开发语言·笔记·学习·servlet·tomcat·idea
rkmhr_sef41 分钟前
Java进阶:Dubbo
java·开发语言·dubbo
数维学长98643 分钟前
【2025rust笔记】超详细,小白,rust基本语法
开发语言·笔记·rust
不止会JS1 小时前
cursor使用经验分享(java后端服务开发向)
java·开发语言·经验分享·cursor
徐白11771 小时前
Rust WebAssembly 入门教程
开发语言·rust·wasm
solomonzw1 小时前
C++ 学习(八)(模板,可变参数模板,模板专业化(完整模板专业化,部分模板专业化),类型 Traits,SFINAE(替换失败不是错误),)
c语言·开发语言·c++·学习
阳洞洞1 小时前
“nullptr“ should be used to denote the null pointer
开发语言·c++
牛马baby2 小时前
Java高频面试之集合-03
java·开发语言·面试
用手手打人2 小时前
多线程&JUC(二)
java·开发语言