• Linux C | Linux标准I/O编程


    1.Linux标准I/O编程

    1.1 linux系统调用和用户编程接口

    1.1.1 系统调用

    操作系统基本上都支持多任务,即同时可以运行多个程序。如果允许程序直接访问系统资源,肯定会带来很多问题。

    因此,所有软硬件资源的管理和分配都由操作系统负责。程序要获取资源(如内存分配、读写串口)必须通过操作系统来完成,及用户向操作系统发出服务请求,操作系统收到请求后执行相关的代码来处理。

    linux系统调用按照功能大致可分为进程控制进程间通信文件系统控制存储管理网络管理套接字控制用户管理等几类。

    1.1.2 用户编程接口

    为什么不直接使用系统调用接口?

    (1)系统调用接口功能非常简单,不能满足程序的需求;

    (2)不同操作系统的系统调用接口不兼容,程序移植时工作量大。

    1.2 linux标准I/O概述

    1.2.1 标准I/O的由来

    标准I/O指的是ANSI C中定义的用于I/O操作的一系列函数。

    只要操作系统中安装了C库,标准I/O函数就可以调用。换句话说,如果程序中使用的是标准I/O函数,那么源代码不需要修改就可以在其他操作系统下编译运行,具有更好的可移植性。

    除此之外,使用标准I/O可以减少系统调用的次数,提高系统效率。标准I/O函数在执行时也会用到系统调用。在执行系统调用时,Linux必须从用户态切换到内核态,处理相应的请求,然后再返回到用户态。如果频繁的执行系统调用会增加系统的开销。为了避免这种情况,标准I/O使用时在用户空间创建缓冲区,读写时先操作缓冲区,在合适的时机再通过系统调用访问实际的文件,从而减少了使用系统调用的次数。

    1.2.2 流的含义

    标准I/O的核心对象就是流。当用标准I/O打开一个文件时,就会创建一个FILE结构体描述该文件(或者理解为创建一个FILE结构体和实际打开的文件关联起来)。我们把这个FILE结构体形象的称为流。标准I/O函数都基于流进行各种操作。

    标准I/O中流的缓冲类型有以下三种:

    (1)全缓冲:;

    (2)行缓冲:;

    (3)无缓冲:;

    1.3 标准I/O编程

    1.3.1 流的打开

    #include 
    
    FILE *fopen(const char *path, const char *mode);
    
    FILE *fdopen(int fd, const char *mode);
    
    FILE *freopen(const char *path, const char *mode, FILE *stream);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    fopen函数语法要点:
    所需头文件#include
    函数原型FILE *fopen(const char *path, const char *mode);
    函数参数path:包含要打开的文件路径及文件名;mode:文件打开方式
    函数返回值成功:指向FILE的指针;失败:NULL
    mode的取值说明:
    r      Open text file for reading.  The stream is positioned at the beginning of the file.// 打开只读文件,该文件必须存在
    		
    r+     Open for reading and writing.  The stream is positioned at the beginning of the file.// 打开可读写文件,该文件必须存在
    		
    w      Truncate file to zero length or create text file for writing.  The stream is positioned at the beginning of the file.// 打开只写文件,若文件存在则文件长度为0,即会擦除写文件以前的内容;若文件不存在则建立文件
    
    w+     Open for reading and writing.  The file is created if it does not exist, otherwise it is truncated.  The stream is  positioned at the beginning of the file.// 打开可读写文件,若文件存在则文件长度为0,即会擦除写文件以前的内容;若文件不存在则建立文件
    
    a      Open  for  appending (writing at end of file).  The file is created if it does not exist.  The stream is positioned at the end of the file.//以附加的方式打开只写文件
    
    a+     Open for reading and appending (writing at end of file).  The file is created if it does not exist.  The initial file position for reading is at the beginning of the file, but output is always appended to the end of the file.//以附加的方式打开可读写的文件
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    更多说明,详见:[fly@fly-vm 01-linuxStdIO]$ man fopen

    1.3.2 流的关闭

    #include 
    
    int fclose(FILE *stream);
    
    • 1
    • 2
    • 3

    说明:函数返回值,成功返回0,失败返回EOF

    该函数将流的缓冲区内的数据全部写入文件中,并释放相关资源。

    1.3.3 错误处理

           #include 
    
           void perror(const char *s);
    
           #include 
    
           const char * const sys_errlist[];
           int sys_nerr;
           int errno;       /* Not really declared this way; see errno(3) */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    示例:
    /*******************************************************************
     *   > File Name: perror.c
     *   > Author: fly
     *   > Create Time: 2022年09月06日 星期二 23时13分12秒
     ******************************************************************/
    
    #include 
    
    int main(int argc, char* argv[])
    {
        FILE *fp;
    
        if((fp = fopen("test.txt", "r")) == NULL)
        {
            perror("file to open");
            return (-1);
        }
    
        fclose(fp);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    编译/运行:
    [fly@fly-vm 01-linuxStdIO]$ make
    gcc -o perror perror.c -g -Wall -lm
    [fly@fly-vm 01-linuxStdIO]$ ./perror
    file to open: No such file or directory
    
    • 1
    • 2
    • 3
    • 4

    // 返回错误码对应的错误信息字符串
    #include 
    
    char *strerror(int errnum);
    
    int strerror_r(int errnum, char *buf, size_t buflen);
    /* XSI-compliant */
    
    char *strerror_r(int errnum, char *buf, size_t buflen);
    /* GNU-specific */
    
    char *strerror_l(int errnum, locale_t locale);
    
    Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
    
    strerror_r():
    The XSI-compliant version is provided if:
    (_POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600) && ! _GNU_SOURCE
        Otherwise, the GNU-specific version is provided.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    示例:
    /*******************************************************************
     *   > File Name: strerror.c
     *   > Author: fly
     *   > Create Time: 2022年09月07日 星期三 22时42分48秒
     ******************************************************************/
    
    #include 
    #include 
    #include 
    
    int main(int argc, char* argv[])
    {
        FILE *fp;
        if((fp = fopen("test.txt", "r")) == NULL)
        {
            printf("fail to open: %s\n", strerror(errno));
            return (-1);
        }
    
        fclose(fp);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    编译、运行:
    [fly@fly-vm 01-linuxStdIO]$ make strerror
    gcc -o strerror strerror.c -g -Wall -lm
    [fly@fly-vm 01-linuxStdIO]$ ./strerror
    fail to open: No such file or directory
    
    • 1
    • 2
    • 3
    • 4

    1.3.4 流的读写

    1.按字符(字节)输入、输出
           #include 
    
           int fgetc(FILE *stream);//从指定流中读取一个字符
    
           int getc(FILE *stream);//从指定流中读取一个字符
    
           int getchar(void);//从stdin中读取一个字符
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

           #include 
    
           int fputc(int c, FILE *stream);//向指定流输出一个字符
    
           int putc(int c, FILE *stream);//向指定流输出一个字符
    
           int putchar(int c);//向stdout输出一个字符
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    示例:
    /*******************************************************************
     *   > File Name: fputs.c
     *   > Author: fly
     *   > Create Time: 2022年09月07日 星期三 23时01分01秒
     ******************************************************************/
    
    #include 
    
    int main(int argc, char* argv[])
    {
        int c;
    
        while(1){
            c = fgetc(stdin);
            if((c >= '0') && (c <= '9')) fputc(c, stdout);
            if(c == '\n') break;
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    编译、运行:
    [fly@fly-vm 01-linuxStdIO]$ make fputs
    gcc -o fputs fputs.c -g -Wall -lm
    [fly@fly-vm 01-linuxStdIO]$ ./fputs
    asdffg1233334
    1233334[fly@fly-vm 01-linuxStdIO]$
    
    • 1
    • 2
    • 3
    • 4
    • 5
    2.按行输入、输出
    #include 
    
    char *gets(char *s);
    
    char *fgets(char *s, int size, FILE *stream);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    gets函数容易造成缓冲区溢出,不推荐使用。

    fgets从指定流中读取一个字符串,当遇到\n或读取了size-1个字符后返回。注意fgets不保证每次都能读出一行。


           #include 
    
           int fputs(const char *s, FILE *stream);
    
           int puts(const char *s);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    示例:
    /*******************************************************************
     *   > File Name: fgets.c
     *   > Author: fly
     *   > Create Time: 2022年09月07日 星期三 23时21分33秒
     ******************************************************************/
    
    #include 
    #include 
    
    int main(int argc, char* argv[])
    {
        int line = 0;
        char buf[128];
        FILE *fp;
    
        if(argc < 2)
        {
            printf("Usage: %s \n", argv[0]);
            return -1;
        }
    
        if((fp = fopen(argv[1], "r")) == NULL)
        {
            perror("fail to fopen");
            return -1;
        }
    
    
        while(fgets(buf, 128, fp) != NULL)
        {
            if(buf[strlen(buf) - 1] == '\n') line ++;
        }
    
        printf("The line of %s is %d.\n", argv[1], line);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    编译、运行:
    [fly@fly-vm 01-linuxStdIO]$ make fgets
    gcc -o fgets fgets.c -g -Wall -lm
    [fly@fly-vm 01-linuxStdIO]$ ./fgets fgets.c
    The line of fgets.c is 38.
    
    • 1
    • 2
    • 3
    • 4
    3.以指定大小为单位读写文件
    #include 
    
    size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
    
    size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    1.3.5 流的定位

    每次打开的流的内部都有一个当前读写位置。流在打开时,当前读写位置为0,表示文件的开始位置。

    每读写一次后,当前读写位置自动增加实际的读写的大小。在读写流之间可先对流进行定位,即移动到指定的位置再操作。


    #include 
    
    int fseek(FILE *stream, long offset, int whence);
    
    long ftell(FILE *stream);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    示例:
    /*******************************************************************
     *   > File Name: ftell.c
     *   > Author: fly
     *   > Create Time: 2022年09月07日 星期三 23时45分49秒
     ******************************************************************/
    
    #include 
    
    int main(int argc, char* argv[])
    {
        FILE *fp;
    
        if(argc < 2)
        {
            printf("Usage: %s \n", argv[0]);
            return -1;
        }
    
        if((fp = fopen(argv[1], "r")) == NULL)
        {
            perror("fail to open");
            return -1;
        }
    
        fseek(fp, 0, SEEK_END);
        printf("The size of %s is %ld.\n", argv[0], ftell(fp));
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    编译、运行:
    [fly@fly-vm 01-linuxStdIO]$ make ftell
    gcc -o ftell ftell.c -g -Wall -lm
    [fly@fly-vm 01-linuxStdIO]$ ./ftell ftell.c
    The size of ./ftell is 656.
    
    • 1
    • 2
    • 3
    • 4

    1.3.6 格式化输入、输出

           #include 
    
           int scanf(const char *format, ...);
           int fscanf(FILE *stream, const char *format, ...);
           int sscanf(const char *str, const char *format, ...);
    
           #include 
    
           int vscanf(const char *format, va_list ap);
           int vsscanf(const char *str, const char *format, va_list ap);
           int vfscanf(FILE *stream, const char *format, va_list ap);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

           #include 
    
           int printf(const char *format, ...);
           int fprintf(FILE *stream, const char *format, ...);
           int dprintf(int fd, const char *format, ...);
           int sprintf(char *str, const char *format, ...);
           int snprintf(char *str, size_t size, const char *format, ...);
    
           #include 
    
           int vprintf(const char *format, va_list ap);
           int vfprintf(FILE *stream, const char *format, va_list ap);
           int vdprintf(int fd, const char *format, va_list ap);
           int vsprintf(char *str, const char *format, va_list ap);
           int vsnprintf(char *str, size_t size, const char *format, va_list ap);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    1.4 示例

    1.4.1 文件的复制

    检查参数-》打开源文件-》打开目标文件-》循环读写文件-》关闭文件。


    代码1(需要调试):
    /*******************************************************************
     *   > File Name: mycopy.c
     *   > Author: fly
     *   > Create Time: 2022年09月07日 星期三 23时56分22秒
     ******************************************************************/
    
    #include 
    #include 
    #include 
    #include 
    
    #define N 64
    
    int main(int argc, char* argv[])
    {
        int n;
        char buf[N];
        FILE *fps, *fpd;
    
        if(argc < 3)
        {
            printf("Usage: %s  \n", argv[0]);
            return -1;
        }
    
        if((fps = fopen(argv[1], "r")) == NULL){
            fprintf(stderr, "fail to fopen %s : %s\n", argv[1], strerror(errno));
            exit(-1);
        }
    
        if((fpd = fopen(argv[2], "w")) == NULL)
        {
            fprintf(stderr, "fail to fopen %s : %s\n", argv[2], strerror(errno));
            fclose(fps);
            exit(-1);
        }
    
        while((n = fread(buf, 1, N, fps)) >= 0)
        {
            fwrite(buf, 1, N, fpd);
        }
    
        fclose(fps);
        fclose(fpd);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    编译、运行:
    [fly@fly-vm 01-linuxStdIO]$ make mycopy
    gcc -o mycopy mycopy.c -g -Wall -lm
    [fly@fly-vm 01-linuxStdIO]$ ./mycopy mycopy.c test1.txt
    ^C
    
    • 1
    • 2
    • 3
    • 4

    运行发现程序一直没有结束,也就几百字节的数据,复制需要这么久?最开始,怀疑复制大文件,BUF设置小,读取效率慢,导致复制时间久。于是换了个小文件,发现也是如此。

    于是打开复制后的文件查看:

    [fly@fly-vm 01-linuxStdIO]$ vim test1.txt

    发现了如下问题,复制目标文件一直在死循环写入如下:

    fpd);
     }
    
     fclose(fps);
     fclose(fpd);
    
     return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    检查代码,发现问题出在这一句“ while((n = fread(buf, 1, N, fps)) >= 0)”上面;当fread读取完源文件后,没有读到新的数据,就一直返回0,也就会一直死循环向目标文件写最后一次BUF里面的数据;

    做如下修改:

    while((n = fread(buf, 1, N, fps)) > 0)
    
    • 1
    重新编译、运行:
    [fly@fly-vm 01-linuxStdIO]$ make mycopy
    gcc -o mycopy mycopy.c -g -Wall -lm
    [fly@fly-vm 01-linuxStdIO]$ ./mycopy mycopy.c test.txt
    [fly@fly-vm 01-linuxStdIO]$ diff mycopy.c test.txt
    48a49,55
    > fpd);
    >     }
    >
    >     fclose(fps);
    >     fclose(fpd);
    >
    >     ret
    \ No newline at end of file
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    发现会多复制一些东西,怀疑最后一次复制,BUF不干净,做了如下修改;

     while((n = fread(buf, 1, N, fps)) > 0)
     {
         fwrite(buf, 1, N, fpd);
         bzero(buf, N);
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    编译、运行,还是存在问题,如下:

    [fly@fly-vm 01-linuxStdIO]$ make mycopy
    gcc -o mycopy mycopy.c -g -Wall -lm
    [fly@fly-vm 01-linuxStdIO]$ ./mycopy mycopy.c t2.txt
    [fly@fly-vm 01-linuxStdIO]$ diff mycopy.c t2.txt
    Binary files mycopy.c and t2.txt differ
    [fly@fly-vm 01-linuxStdIO]$ vim -O mycopy.c  t2.txt
    2 files to edit
    [fly@fly-vm 01-linuxStdIO]$ ls -l mycopy.c t2.txt
    -rw-rw-r-- 1 fly fly 1056 99 23:00 mycopy.c
    -rw-rw-r-- 1 fly fly 1088 99 23:15 t2.txt
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    最终修改如下:

    buf读到多少字节,最终就向目标文件写入多少字节,也就不用每次写完后把buf清零;及读到n个字节就写入n个字节。

        while((n = fread(buf, 1, N, fps)) > 0)
        {
            fwrite(buf, 1, n, fpd);
        }
    
    • 1
    • 2
    • 3
    • 4

    [fly@fly-vm 01-linuxStdIO]$ make mycopy
    gcc -o mycopy mycopy.c -g -Wall -lm
    [fly@fly-vm 01-linuxStdIO]$ ./mycopy mycopy.c t3.txt
    [fly@fly-vm 01-linuxStdIO]$ diff mycopy.c t3.txt
    [fly@fly-vm 01-linuxStdIO]$ ls -l mycopy.c  t3.txt
    -rw-rw-r-- 1 fly fly 1033 99 23:18 mycopy.c
    -rw-rw-r-- 1 fly fly 1033 99 23:18 t3.txt
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    代码2(最终版):
    /*******************************************************************
     *   > File Name: mycopy.c
     *   > Author: fly
     *   > Create Time: 2022年09月07日 星期三 23时56分22秒
     ******************************************************************/
    
    #include 
    #include 
    #include 
    #include 
    
    #define N 64
    
    int main(int argc, char* argv[])
    {
        int n;
        char buf[N];
        FILE *fps, *fpd;
    
        if(argc < 3)
        {
            printf("Usage: %s  \n", argv[0]);
            return -1;
        }
    
        if((fps = fopen(argv[1], "r")) == NULL){
            fprintf(stderr, "fail to fopen %s : %s\n", argv[1], strerror(errno));
            exit(-1);
        }
    
        if((fpd = fopen(argv[2], "w")) == NULL)
        {
            fprintf(stderr, "fail to fopen %s : %s\n", argv[2], strerror(errno));
            fclose(fps);
            exit(-1);
        }
    
        while((n = fread(buf, 1, N, fps)) > 0)
        {
            fwrite(buf, 1, n, fpd);
        }
    
        fclose(fps);
        fclose(fpd);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    1.4.2 循环记录系统时间

    打开文件-》获取系统时间-》写入文件-》延时1S-》返回第2步(获取系统时间)。


    代码3:
    /*******************************************************************
     *   > File Name: mytime.c
     *   > Author: fly
     *   > Create Time: 2022年09月09日 星期五 23时54分20秒
     ******************************************************************/
    
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char* argv[])
    {
        FILE *fp;
        time_t t;
    
        if(argc != 2)
        {
            printf("Usage: %s \n", argv[0]);
            exit(-1);
        }
    
        if((fp = fopen(argv[1], "w")) == NULL)
        {
            perror(" fail to fopen ");
            exit(-1);
        }
    
        while(1)
        {
            time(&t);
            fprintf(fp, "%s", ctime(&t));
            fflush(fp);
            sleep(1);
        }
    
        fclose(fp);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    编译/运行:
    [fly@fly-vm 01-linuxStdIO]$ make mytime
    gcc -o mytime mytime.c -g -Wall -lm
    [fly@fly-vm 01-linuxStdIO]$ ./mytime time.txt
    ^C
    [fly@fly-vm 01-linuxStdIO]$ cat time.txt
    Fri Sep  9 23:55:19 2022
    Fri Sep  9 23:55:20 2022
    Fri Sep  9 23:55:21 2022
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  • 相关阅读:
    【Java八股文总结】之类
    YOLO DNF辅助教程完结
    弱监督点云分割(论文解读:CVPR2020)
    linux 应用中offsetof ()是个啥?
    C++设计模式之适配器模式
    片内总线在cpu扮演什么角色?他为什么能实现高效,不同的CPU为什么采用不同的总线协议?
    使用旭日X3派做跟随车(一)——开箱篇
    Linux知识点 -- 网络基础 -- 数据链路层
    (万文)最全、最细前端面试问题总结(答题思路分析、答案解析)
    国际播客日 · 森海塞尔精选播客设备满足各类音频需求
  • 原文地址:https://blog.csdn.net/I_feige/article/details/126791507