• 【文件I/O】(二)文件IO


    一、文件I/O基本概念

    1.什么是文件I/O?

    posix(可移植操作系统接口)定义的一组函数,不提供缓冲机制,每次读写操作都引起系统调用,核心概念是文件描述符,访问各类型文件,在Linux下,标准I/O基于文件I/O实现。

    2.文件描述符

    文件描述符(fd)是一个非负整数,Linux为程序中每个打开的文件分配一个文件描述符。文件描述符从0开始分配,依次递增,在ubuntu系统中一个正在执行的程序,文件描述符的范围是 0~1023(1024个)。每一个正在执行的程序都有自己的一套文件描述符,相互不干扰。

    一个正在执行的程序中0,1,2文件描述符已经被占用了,分别对应的是标准输入,标准输出,标准出错。

    #include 
    
    int main(int argc, const char* argv[])
    {
        printf("in  fd = %d\n", stdin->_fileno);  // 0
        printf("out fd = %d\n", stdout->_fileno); // 1
        printf("err fd = %d\n", stderr->_fileno); // 2
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ulimit -a 显示当前所有的资源限制等。
    ulimit -n 2048 修改文件描述符范围为2048。

    二、文件I/O函数

    head.h

    编写head.h的头文件
    将head.h放到/usr/include路径下
    修改head.h文件所属用户和组,sudo chown linux:linux /usr/include/head.h
    修改用户代码片段(c.json),将#include 改成#include

    #ifndef __HEAD_H__
    #define __HEAD_H__
    
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define PRINT_ERR(msg) \
        do                 \
        {                  \
            perror(msg);   \
            return -1;     \
        } while (0)
    
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    1.open、close(打开、关闭文件)

    1.1open、close函数API

    #include 
    #include 
    #include 
    
    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结合使用,在创建文件的时候加上这个选项,如果文件存在就返回EEXIST,如果文件不存在就会不返回错误
      		O_NOCTTY :使用本参数时,如文件为终端,那么终端不可作为调用open()系统调用的那个进程的控制终端。
    	 @mode:创建文件的权限,为8进制表示法
            实际创建出来的文件的权限 = (mode & ~umask)
            umask:文件的掩码:通过umask命令可以查看掩码的默认值是0002  
            ~umask:文件掩码的取反是在最大默认文件的权限的基础上取反的,最大默认文件的权限是0666
            实际创建出来的文件的权限 = (0666 & ~(0002)) =  (0666 & 0664) = 0664
    返回值:成功返回文件描述符,失败返回-1置位错误码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    #include 
    
    int close(int fd);
    功能:关闭文件
    参数:
        @fd:文件描述符
    返回值:成功返回0,失败返回-1置位错误码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    1.2文件I/O和标准I/O文件打开方式对比

    文件IO标准IO功能
    O_RDONLY“r”以只读的方式打开文件,将光标定位到开头
    O_RDWR“r+”以读写的方式打开文件,将光标定位到开头
    O_WRONLY | O_CREAT | O_TRUNC,0666“w”以只写的方式打开文件,如果文件不存在就创建,如果文件存在清空,光标在开头
    O_RDWR | O_CREAT | O_TRUNC,0666“w+”以读写的方式打开文件,如果文件不存在就创建,如果文件存在清空,光标在开头
    O_WRONLY | O_CREAT | O_APPEND,0666“a”以只写和追加的方式打开文件,如果文件不存在就创建,如果存在光标在结尾
    O_RDWR | O_CREAT | O_APPEND,0666“a+”以读写和追加的方式打开文件,如果文件不存在就创建,如果文件存在读光标在开头,写光标在结尾

    1.3open函数实例1(只读、只写)

    只读、只写方式打开文件。

    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define PRINT_ERR(msg) \
        do                 \
        {                  \
            perror(msg);   \
            return -1;     \
        } while (0)
    
    int main(int argc, const char *argv[])
    {
        int fd1, fd2;
    
        // 只读方式打开文件
        if (-1 == (fd1 = open("./1.txt", O_RDONLY)))
            PRINT_ERR("open error");
    
        // 只写方式打开文件
        if (-1 == (fd2 = open("./hello.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666)))
            PRINT_ERR("open error");
    
        close(fd1);
        close(fd2);
        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

    1.4open函数实例2(参数O_EXCL)

    如果文件不存在,创建文件,以只写方式打开文件。
    如果文件存在,以只读方式打开文件。

    #include 
    
    int main(int argc, const char *argv[])
    {
        int fd;
    
        // 如果文件不存在,创建文件,以只写方式打开文件
        // 如果文件存在,以只读方式打开文件
        if (-1 == (fd = open("./1.txt", O_WRONLY | O_CREAT | O_EXCL, 0666))) {
            if (errno = EEXIST) {
                if (-1 == (fd = open("./1.txt", O_RDONLY)))
                    PRINT_ERR("open error");
                    puts("文件存在,以只读方式打开文件");
            } else {
                PRINT_ERR("open error");
            }
        } else {
            puts("文件不存在,创建文件,以只写方式打开文件");
        }
    
        printf("fd = %d\n", fd);
        close(fd);
        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

    1.5close函数实例3(关闭标准输出)

    关闭标准输出(fd = 1),终端不会输出。

    #include 
    
    int main(int argc, const char *argv[])
    {
        puts("--------------start");
        close(1);
        puts("--------------end");  //关闭标准输出(fd = 1),终端不会输出这行
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2.read、write(读取、写入文件)

    2.1read、write函数API

    #include 
    
    ssize_t read(int fd, void *buf, size_t count);
    功能:读取文件中的内容
    参数:
        @fd:文件描述符
     	@buf:存储读取到数据的首地址
     	@count:想要读取的字节的个数
    返回值:成功返回读取到字节的个数,如果返回0代表读取到文件的结尾了
            失败返回-1置位错误码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    #include 
    
    ssize_t write(int fd, const void *buf, size_t count);
    功能:向文件中写数据
    参数:
        @fd:文件描述符
        @buf:想要写的数据的首地址
        @count:想要写的字节的个数
    返回值:成功返回写入的字节的个数,0表示不会写任何的数据
         失败返回-1置位错误码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2.2read函数实例1(读取整数、字符串、结构体)

    配合2.3wirte函数实例2使用

    #include 
    
    typedef struct stu {
        char name[20];
        int age;
        char sex;
    }stu_t;
    
    int main(int argc, const char *argv[])
    {
        int fd;
        if (-1 == (fd = open("./1.txt", O_RDONLY)))
            PRINT_ERR("open error");
     /*    
        // 1.验证读取整数
        int num;
        read(fd, &num, sizeof(num));
        printf("num = %d\n", num);
    
        // 2.验证读取字符串
        char buf[20] = {0};
        read(fd, buf, sizeof(buf));
        printf("buf = %s\n", buf);
     */
        // 3.验证读取结构体
        stu_t stu;
        read(fd, &stu, sizeof(stu));
        printf("name = %s, age = %d, sex = %c\n", stu.name, stu.age, stu.sex);
    
        close(fd);
        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

    2.3write函数实例2(写入整数、字符串、结构体)

    配合2.2read函数实例1使用

    #include 
    
    typedef struct stu {
        char name[20];
        int age;
        char sex;
    }stu_t;
    
    int main(int argc, const char *argv[])
    {
        int fd;
        if (-1 == (fd = (open("./1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666))))
            PRINT_ERR("open error");
    /* 
        // 1.验证写入整数
        int num = 12345;
        write(fd, &num, sizeof(num));
    
        // 2.验证写入字符串
        char buf[100] = "hello world!";
        write(fd, buf, strlen(buf));
     */
        // 3.验证写入结构体
       stu_t stu = {
            .name = "xiaoming",
            .age = 21,
            .sex = 'm'
        };
        write(fd, &stu, sizeof(stu_t));
    
        close(fd);
        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

    2.4write函数实例3(计算文件大小)

    #include 
    
    int main(int argc, const char *argv[])
    {
        if (2 != argc) {
            fprintf(stderr, "Usage: %s \n", argv[0]);
            return -1;
        }
    
        int fd;
        if (-1 == (fd = open(argv[1], O_RDONLY)))
            PRINT_ERR("open error");
    
        int total = 0;
        int ret;
        char buf[10] = {0};
        while (0 < (ret = read(fd, buf, sizeof(buf)))) {
            total += ret;
        }
    
        printf("%s size total is %d\n", argv[1], total);
    
        close(fd);
        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

    2.5read、write函数实例4(文件拷贝)

    #include 
    
    int main(int argc, const char *argv[])
    {
        // 1.校验用户输入的参数是否正确
        if (3 != argc) {
            fprintf(stderr, "Usage: %s  \n", argv[0]);
            return -1;
        }
    
        // 2.只读方式打开源文件,写的方式打开目标文件(创建)
        int sfd, dfd;
        if (-1 == (sfd = open(argv[1], O_RDONLY)))
            PRINT_ERR("open src_file error");
    
        if (-1 == (dfd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0666)))
            PRINT_ERR("open dest_file error");
    
        // 3.拷贝
        int ret;
        char buf[10] = {0};
        while (0 < (ret = read(sfd, buf, sizeof(buf)))) {
            write(dfd, buf, ret);
        }
    
        // 4.关闭文件
        close(sfd);
        close(dfd);
        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

    2.6fgets、write函数实例5(键盘输入内容到文件)

    #include 
    
    int main(int argc, const char *argv[])
    {
        if (2 != argc) {
            fprintf(stderr, "Usage: %s \n", argv[0]);
            return -1;
        }
    
        int fd;
        if (-1 == (fd = open(argv[1], O_RDWR | O_APPEND | O_CREAT, 0666)))
            PRINT_ERR("open error");
    
        char buf[10] = {0};
        while (NULL != fgets(buf, 10, stdin)) {
            if (0 == strcmp(buf, "quit\n")) break;  //终端输入quit退出
            write(fd, buf, strlen(buf));
        }
    
        close(fd);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    3.lseek(定位文件指针)

    3.1lseek函数API

    #include 
    #include 
    
    off_t lseek(int fd, off_t offset, int whence);
    功能:设置光标的位置
    参数:
        @fd:文件描述符
     	@offset:偏移量
            >0:向后偏移
            =0:不偏移
      		<0:向前偏移
    	@whence:从那个位置开始偏移
      		SEEK_SET:从文件的开头
            SEEK_CUR:从当前位置
      		SEEK_END:从文件的结尾
    返回值:成功返回光标到开头的字节数,失败返回-1置位错误码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    3.2lseek函数实例1

    #include 
    
    int main(int argc, const char *argv[])
    {
        // abcdefg1234567
        // 1.读写方式打开文件
        int fd;
        if (-1 == (fd = open("./1.txt", O_RDWR)))
            PRINT_ERR("open error");
    
        // 2.文件开头向后偏移5个字节
        lseek(fd, 5, SEEK_SET);
        char ch;
        read(fd, &ch, 1);
        printf("ch = %c\n", ch);  //读取到第6个字符,f
    
        ch = 'Q';
        write(fd, &ch, 1);  //写入到第7个字符,abcdefQ1234567
    
        close(fd);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    3.3lseek函数实例2

    如果使用读写和追加的方式打开文件,即使lseek偏移了文件指针,写指针永远是在结尾。

    #include 
    
    int main(int argc, const char *argv[])
    {
        // abcdefg1234567
        // 1.以读写和追加的方式打开文件
        int fd;
        if (-1 == (fd = open("./1.txt", O_RDWR | O_APPEND | O_CREAT, 0666)))
            PRINT_ERR("open error");
    
        // 2.文件开头向后偏移5个字节
        lseek(fd, 5, SEEK_SET);
        char ch;
        read(fd, &ch, 1);
        printf("ch = %c\n", ch);  //读取到第6个字符,f
    
        ch = 'T';
        write(fd, &ch, 1);  //T字符写到了文件的结尾,写永远是在结尾
    
        int ret;
        ch = 0;
        ret = read(fd, &ch, 1);  //光标已经在结尾了,这里读是读取不到内容的,read返回0(end of file)
        printf("ch = %c, ret = %d\n", ch, ret);  //ch = , ret = 0
    
        close(fd); 
        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

    4.stat(获取文件属性)

    4.1stat函数API

    #include 
    #include 
    #include 
    
    int stat(const char *pathname, struct stat *statbuf);
    //stat不能获取软连接文件,如果向测试软链接,使用lstat完成,用法和stat完全相同
    功能:获取文件的属性信息
    参数:
        @pathname:文件的路径及名字
     	@statbuf:获取的属性信息结构指针
             struct stat {
               dev_t     st_dev;         //磁盘设备号,底层驱动课程讲解
               ino_t     st_ino;         //文件的inode号,文件系统识别文件的唯一编号,通过ls -i查看
               mode_t    st_mode;        //文件的类型和权限
               
    			//1.文件的类型 文件的类型占4bit位,12-15就是文件的类型
                   S_IFMT     0170000   bit mask for the file type bit field
                   S_IFSOCK   0140000   socket           //套接字文件
                   S_IFLNK    0120000   symbolic link    //软连接文件
                   S_IFREG    0100000   regular file     //普通文件
                   S_IFBLK    0060000   block device     //块设备文件
                   S_IFDIR    0040000   directory        //目录
                   S_IFCHR    0020000   character device //字符设备文件
                   S_IFIFO    0010000   FIFO             //管道
                   eg:      
                       if((st_mode & S_IFMT) == S_IFREG){
                           printf("普通文件\n");
                       } 
    			//2.文件的权限 文件的权限占9bit位,0-8就是文件的权限 
                 文件的权限 = st_mode & 0777
                
               nlink_t   st_nlink;       //硬链接数
               uid_t     st_uid;         //用户的uid
               gid_t     st_gid;         //用户的gid
               dev_t     st_rdev;        //设备号,底层驱动课程讲解
               off_t     st_size;        //文件的大小,单位是字节
               blksize_t st_blksize;     //操作块设备的最小单位block,512字节
               blkcnt_t  st_blocks;      //文件的大小,单位是block
               struct timespec st_atim;  //最后一次访问这个文件的时间
               struct timespec st_mtim;  //最后一次修改这个文件的时间
               struct timespec st_ctim;  //最后一次状态改变的时间
           };
    返回值:成功返回0,失败返回-1置位错误码
    
    • 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

    4.2根据uid获取用户名,getpwuid函数

    #include 
    #include 
    
    struct passwd *getpwuid(uid_t uid);
    功能:根据uid获取用户信息结构体
    参数:
        @uid:用户的id
    返回值:成功返回用户信息结构指针,失败返回NULL,置位错误码
           struct passwd {
               char   *pw_name;       //用户名
               char   *pw_passwd;     //用户的密码
               uid_t   pw_uid;        //uid
               gid_t   pw_gid;        //gid
               char   *pw_gecos;      //用户的信息
               char   *pw_dir;        //用户的主目录
               char   *pw_shell;      //shell程序
           };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    4.3根据gid获取组名,getgrgid函数

    #include 
    #include 
    
    struct group *getgrgid(gid_t gid);
    功能:根据gid获取group结构体
    参数:
        @gid:组号
    返回值:成功返回结构体指针,失败返回NULL置位错误码
       struct group {
           char   *gr_name;        //组名
           char   *gr_passwd;      //组的密码
           gid_t   gr_gid;         //组号
       };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    4.4stat函数实例1(实现 ls -l)

    #include 
    
    void print_type(mode_t mode);
    void print_permission(mode_t mode);
    int print_time(struct timespec time);
    
    int main(int argc, const char *argv[])
    {
        // 1.校验参数输入是否正确
        if (2 != argc) {
            fprintf(stderr, "Usage: %s \n", argv[0]);
            return -1;
        }
    
        // 2.获取文件的属性
        struct stat st;
        if (stat(argv[1], &st))
            PRINT_ERR("stat get msg failed");
    
        // 3.将属性打印出来
        print_type(st.st_mode);  //打印文件类型
        print_permission(st.st_mode);  //打印文件权限
        printf(" %ld", st.st_nlink);  //打印硬链接个数
        printf(" %s", getpwuid(st.st_uid)->pw_name);  //打印用户名
        printf(" %s", getgrgid(st.st_gid)->gr_name);  //打印组名
        printf(" %ld", st.st_size);  //打印文件大小(字节)
        print_time(st.st_atim);  //打印时间戳
        printf(" %s", argv[1]);  //打印文件名
    	puts("");
    
        return 0;
    }
    
    // 打印文件类型
    void print_type(mode_t mode) {
        switch (mode & S_IFMT) {
            case S_IFSOCK:
                putchar('s'); break;
            case S_IFLNK:
                putchar('l'); break;
            case S_IFREG:
                putchar('-'); break;
            case S_IFBLK:
                putchar('b'); break;
            case S_IFDIR:
                putchar('d'); break;
            case S_IFCHR:
                putchar('c'); break;
            case S_IFIFO:
                putchar('p'); break;
        }
    }
    
    // 打印文件权限
    void print_permission(mode_t mode) {
        for (int n = 8; n >= 0; n--) {
            if ((mode & 0777) & (1 << n)) {
    	        switch (n % 3) {
    		        case 2:
    			        putchar('r'); break;
    			    case 1:
    			        putchar('w'); break;
    			    case 0:
    			        putchar('x'); break;
    		}
    	} else {
            putchar('-');
          }
       }
    }
    
    // 打印文件时间戳
    int print_time(struct timespec time) {
        struct tm* tm;
    
        if (NULL == (tm = localtime(&time.tv_sec)))
            PRINT_ERR("change time error");
        
        printf(" %02d月 %2d %02d:%02d", \
            tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min);
    }
    
    • 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
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
  • 相关阅读:
    C++(二)
    杰理之MIDI 解码方式共有 4 种,分别是【篇】
    冒烟测试的7个好处,你是否经常用到它?
    Linux操作文件的底层系统调用
    1000 - 熟悉一下Online Judge的环境
    CRM软件管理系统的基本功能
    LibreOffice编辑excel文档如何在单元格中输入手动换行符
    Linux操作命令和常用工具1
    Python150题day14
    Flutter笔记:使用相机
  • 原文地址:https://blog.csdn.net/weixin_50964793/article/details/128155908