(19)Linux系统下的文件操作 文件系统接口

前言:本篇主要讲解底层文件系统接口,详细介绍 open 接口和它的 flags 参数 (即系统传递标记位),重点讲解 O_RDWR, O_RDONLY, O_WRONLY, O_CREAT 和 O_APPEND 这些操作模式。

一、先来段代码回顾C文件接口 

hello.c写文件 

#include <stdio.h>
#include <string.h>
int main()
{FILE *fp = fopen("myfile", "w");if(!fp){printf("fopen error!\n");}const char *msg = "hello bit!\n";int count = 5;while(count--){fwrite(msg, strlen(msg), 1, fp);}fclose(fp);return 0;
}

 hello.c读文件

#include <stdio.h>
#include <string.h>
int main()
{FILE *fp = fopen("myfile", "r");if(!fp){printf("fopen error!\n");}char buf[1024];const char *msg = "hello bit!\n";while(1){//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明ssize_t s = fread(buf, 1, strlen(msg), fp);if(s > 0){buf[s] = 0;printf("%s", buf);}if(feof(fp)){break;}}fclose(fp);return 0;
}

 输出信息到显示器,你有哪些方法

#include <stdio.h>
#include <string.h>
int main()
{const char *msg = "hello fwrite\n";fwrite(msg, strlen(msg), 1, stdout);printf("hello printf\n");fprintf(stdout, "hello fprintf\n");return 0;
}

1、 关于文件操作的思考

我们曾经讲过:文件 = 文件内容 + 文件属性  

文件属性也是数据!这意味着,即便你创建一个空文件,也要占据磁盘空间!所以:

                                ② 文件操作 = 文件内容的操作 + 文件属性的操作 

 因此,在操作文件的过程中,既改变内容又改变属性的情况很正常,不要把它们割裂开来!

那么,所谓的 "打开" 文件,究竟在做什么? ③ 

"打开文件不是目的,访问文件才是目的!" 

访问文件时,都是要通过 fread,fwrite,fgets... 这样的代码来完成对文件的操作的,

如果通过这些方式,那么 "打开" 文件就需要 将文件的属性或内容加载到内存 (memory) 中!

因为这是由冯诺依曼体系结构决定的,将来 \textrm{CPU} 要执行 fread,fwrite 来对文件进行读写的。

 既然如此……  是不是所有的文件都会处于被打开的状态呢?并不是! 

那没有被打开的文件在哪里?

对于文件的理解,在宏观上我们可以区分成 内存文件 (打开的文件) 和 磁盘文件。  (存储在磁盘中) 

通常我们打开文件、访问文件和关闭文件,是谁在进行相关操作?

运行起来的时候,才会执行对应的代码,然后才是真正的对文件进行相关的操作。
实际上是  进程在对文件进行操作! 在系统角度理解是我们曾经写的代码变成了进程。

进程执行调度对应的代码到了 fopen,write 这样的接口,然后才完成了对文件的操作。
当我执行 fopen 时,对应地就把文件打开了,所以文件操作和进程之间是撇不开关系。

🔺 结论:学习文件操作,实际上就是学习 "进程" 与 "打开文件" 的关系。 ⑦
 

2、当前路径(Current Path)

文件的本质实际上是进程与打开文件之间的关系。

 因此文件操作和进程有关系,我们写一下我们的代码,获取进程 \textrm{pid},查一下进程信息:

#include <stdio.h>
#include <unistd.h>int main(void)
{FILE* pf = fopen("log.txt", "w"); // 写入if (pf == NULL) {perror("fopen");return 1;}/* 获取进程 pid */printf("Mypid: %d\n", getpid());/* 打开文件后,等一等,方便查询 */while (1) {sleep(1);}const char* msg = "hello!";int count = 1;while (count <= 10) {fprintf(pf, "%s: %d\n", msg, count++);}fclose(pf);
}

getpid 拿到进程 \textrm{pid} 后,得益于 "昏睡指令" while(1){sleep(1);) 

我们的进程就一直的跑着,再打开一个窗口,通过 $ls proc 指令检视该进程信息:

 

我们重点关注 \textrm{exe} 和 \textrm{cwd}\textrm{exe} 后面链接指向的是可执行程序 mytest,即 路径 + 程序名。

而 \textrm{cwd} (current working directory),即 当前工作目录,记录着当前进程所处的路径!

每个进程都有一个工作路径,所以我们上一节实现的简单 \textrm{shell} 程序可以用 chdir 更改路径。

创建文件时,如果文件在当前目录下不存在,fopen 会默认在当前路径下自动创建文件。

 默认创建在当前路径,和源代码、可执行程序在同一个路径下,因为这取决于 \textrm{cwd}

cwd -> /home/ayf/lesson1

所以,当前路径更准确的说法应该是:在当前进程所处的工作路径。

" 当前路径指的是在当前进程所处的工作路径 "

只不过默认情况下 (默认路径) ,一个进程的工作路径在它当前所处的路径而已,这是可以改的。

所以我们在写文件操作代码时,不带路径默认是源代码所在的路径,注意是默认!而已!

3、文件操作模式(File Operation Mode) 

由文件操作符 (mode) 参数来指定,常用的模式包括:

man fopen

r:只读模式,打开一个已存在的文本文件,允许读取文件。r+:读写模式,打开一个已存在的文本文件,允许读写文件。w:只写模式,打开一个文本文件并清除其内容,如果文件不存在,则创建一个新文件。w+:读写模式,打开一个文本文件并清除其内容,如果文件不存在,则创建一个新文件。a:追加模式,打开一个文本文件并将数据追加到文件末尾,如果文件不存在,则创建一个新文件。a+:读写模式,打开一个文本文件并将数据追加到文件末尾,如果文件不存在,则创建一个新文件。

 这里我们重点讲一下 a 和 a+

a 对应的是 appending 的首字母,意为 "追加" 。属于写入操作,不会覆盖源文件内容。

代码演示:测试追加效果

每次运行都会在 test.txt 里追加,我们多试几次看看:

a(append) 追加写入,可以不断地将文件中新增内容。(这让我联想到了追加重定向)

不同于 w,当我们以 w 方式打开文件准备写入的时候,其实文件已经先被清空了。

4、文件的读取(File Read) 

我们复习一下文本行输入函数 fgets,它可以从特定的文件流中,以行为单位读取特定的数据: 

char* fgets(char* s, int size, FILE* stream);_

 代码演示:fgets()

#include <stdio.h>int main(void)
{FILE* pf = fopen("log.txt", "r"); // 读if (pf == NULL) {perror("fopen");//显示错误信息return 1;}char buffer[64];  // 用来存储while (fgets(buffer, sizeof(buffer), pf) != NULL) {// 打印读取到的内容printf("echo: %s", buffer);}fclose(pf);
}

运行结果如下:

 我们下面再来实现一个类似 $cat 的功能,输入文件名打印对应文件内容。

代码演示:实现一个自己的 cat

#include <stdio.h>/* 读什么就打什么   mycat */
int main(int argc, char* argv[]) 
{if (argc != 2) {printf("Usage: %s filename\n", argv[0]);return 1;}FILE* pf = fopen(argv[1], "r");    // 读取if (pf == NULL) {perror("fopen");return 1;}char buffer[64];while (fgets(buffer, sizeof(buffer), pf) != NULL) {printf("%s", buffer);}fclose(pf);
}

读到什么就打印身边。

如果再把可执行程序 mytest 改名成 cat,$mv mytest cat ,

 我们就实现了一个自己的 cat 代码。

二、文件系统接口(Basic File IO)

操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问, 先来直接以代码的形式,实现和上面一模一样的代码: 

写文件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{umask(0);int fd = open("myfile", O_WRONLY|O_CREAT, 0644);if(fd < 0){perror("open");return 1;}int count = 5;const char *msg = "hello world!\n";int len = strlen(msg);while(count--){write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数
据。 返回值:实际写了多少字节数据}close(fd);return 0;
}

 读文件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}const char *msg = "hello world!\n";char buf[1024];while(1){ssize_t s = read(fd, buf, strlen(msg));//类比writeif(s > 0){printf("%s", buf);}else{break;}}close(fd);return 0;
}

1、系统调用与封装(Syscall and Wrapper) 

当我们向文件写入时,最终是不是向磁盘写入?是!磁盘是硬件吗?就是硬件。

当我们像文件写入时,最后是向磁盘写入。磁盘是硬件,那么谁有资格向磁盘写入呢?

只能是操作系统!

既然是操作系统在写入,那我们自然不能绕开操作系统对磁盘硬件进行访问。

因为操作系统是对软硬件资源进行管理的大手子,你的任何操作都不能越过操作系统!

 所有的上层访问文件的操作,都必须贯穿操作系统。

想要被上层使用,必须使用操作系统的相关的 系统调用 (syscall) !

回顾:: 如何理解 printf?我们怎么从来没有见过这些系统调用接口呢?

显示器是硬件,我们 printf 的消息打印到了硬件上,是你自己调用的 printf 打印到硬件上的,

但并不是你的程序显示到了显示器上,因为显示器是硬件,它的管理者只能是操作系统,

你不能绕过操作系统,而必须使用对应的接口来访问显示器,我们看到的 printf 一打,

内容就出现在屏幕上,实际上在函数的内部,一定是 调用了系统调用接口 的。

 结论:printf 函数内部一定封装了系统调用接口。

所有的语言提供的接口,之所以你没有见到系统调用,因为所有的语言都被系统接口做了 封装

所以你看不到对应的底层的系统接口的差别。为什么要封装?原生系统接口,使用成本比较高。

系统接口是 OS 提供的,就会带来一个问题:如果使用原生接口,你的代码只能在一个平台上跑。

直接使用原生系统接口,必然导致语言不具备 跨平台性 (Cross-platform) !

如果语言直接使用操作系统接口,那么它就不具备跨平台性,可是为什么采用封装就能解决?

封装是如何解决跨平台问题的呢?很简单:

                                " 穷举所有的底层接口 + 条件编译 "

结论:我们学习的接口,C 库提供的文件访问接口,系统调用。它们两具有上下层关系,C 底层一定会调用这些系统调用接口。

  • 解释:不同的语言,有不同的文件访问接口。
  • 系统调用:这就是我什么我们必须学习文件级别的系统接口!

 2、文件打开:open()

打开文件,在 C 语言上是 fopen,在系统层面上是 open。

open 接口是我们要学习的系统接口中最重要的一个,没有之一!所以我们放到前面来讲。

#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);

我们看到,这个 open 接口一个是两参数的,一个是三参数的,这个我们放到后面解释。

  •  open 接口的 pathname 参数表示要打开的文件名,和 C 语言的 fopen 一样,是要带路径的。
  •  flags 参数是打开文件要传递的选项,即 系统传递标记位,我们下面会重点讲解。
  •  mode 参数,就是 "文件操作模式" 了。
#if (defined _CRT_DECLARE_NONSTDC_NAMES && _CRT_DECLARE_NONSTDC_NAMES) || (!defined _CRT_DECLARE_NONSTDC_NAMES && !__STDC__)#define O_RDONLY     _O_RDONLY#define O_WRONLY     _O_WRONLY#define O_RDWR       _O_RDWR#define O_APPEND     _O_APPEND#define O_CREAT      _O_CREAT#define O_TRUNC      _O_TRUNC#define O_EXCL       _O_EXCL#define O_TEXT       _O_TEXT#define O_BINARY     _O_BINARY#define O_RAW        _O_BINARY#define O_TEMPORARY  _O_TEMPORARY#define O_NOINHERIT  _O_NOINHERIT#define O_SEQUENTIAL _O_SEQUENTIAL#define O_RANDOM     _O_RANDOM
#endif

思考:在 Linux 下,C 语言中文件不存在,就直接创建它,创建是不是需要权限?

当然是需要的,我们需要给文件设置初始权限,这个 mode 参数就是干这个活的。

我们再来看看这个接口的返回值,居然是个 int,而不是我们 fopen 的 FILE* 

  •  open 的返回值是个 int,返回 -1 表示 error,并设置 errno。

3、flags 系统传递标记位

我们可以输入 man 2 open  看看如何设置 flags 参数,实际上就是设置文件操作模式的。

我们重点关注下面这几个文件操作模式,它们被定义在 <fcntl.h> 头文件中:

 O 实际上就是 Open 的意思,它们的用途通过名字不难猜:

  • O_RDONLY: 只读打开
  • O_WRONLY: 只写打开
  • O_RDWR : 读,写打开 这三个常量,必须指定一个且只能指定一个
  • O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
  • O_APPEND: 追加写

返回值:

  • 成功:新打开的文件描述符
  • 失败:-1 
int open(const char* pathname, int flags);

我们称 flags 标记位,并且它是个整数类型(C99 标准之前没有 bool 类型) 

标记位实际上我们造就用过了,比如定义 flag 变量,设 flag=0,设 flag=1,传的都是单个的。

 思考:但如果我想一次传递多个标志位呢?定义多个标记位?flag1, flag2, flag3...

方案:系统传递标记位是通过 位图 来进行传递的。

想必大家已经对位图不陌生了,在前几章我们讲解 waitpid 的 status 参数时就介绍过了:

status 参数也是整型,也是被当作一个 "位图结构" 看待的,这里的 flags 也是如此!

当成位图,就是一串整数0000\, 0000

我们可以让不同的位表示,是否只读,是否只写,是否读写…… 等等等等: 

每个 宏标记一般只需要满足有一个比特位是 1,并且和其它宏对应的值不重叠 即可。 

代码演示:我们创建一个 test.c 

#include <stdio.h>#define PRINT_A   0x1     // 0000 0001
#define PRINT_B   0x2     // 0000 0010
#define PRINT_C   0x4     // 0000 0100
#define PRINT_D   0x8     // 0000 1000
#define PRINT_DFL 0x0// open
void Show (int flags  /* 传递标志位 */)
{if (flags & PRINT_A)     printf("Hello A\n");if (flags & PRINT_B)     printf("Hello B\n");if (flags & PRINT_C)     printf("Hello C\n");if (flags & PRINT_D)     printf("Hello D\n");if (flags == PRINT_DFL)  printf("Hello Default\n");
}int main(void)
{/* 我想打谁,只需要传对应的标记位即可 */printf("# PRINT_DFL: \n");Show(PRINT_DFL);printf("# PRINT_A: \n");Show(PRINT_A);printf("# PRINT_B: \n");Show(PRINT_B);printf("# PRINT_A AND PRINT_B: \n");Show(PRINT_A | PRINT_B);printf("# PRINT_C AND PRINT_D: \n");Show(PRINT_C | PRINT_D);printf("# PRINT_A AND PRINT_B AND PRINT_C AND PRINT_D: \n");Show(PRINT_A | PRINT_B | PRINT_C | PRINT_D);return 0;
}

4、open 接口用法演示  

讲完了 flags 标记位,现在我们可以演示 open 接口的用法了。

int open(const char* pathname, int flags);

 代码演示:是用 open() 打开 log.txt 文件没有就创建。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main(void)
{int fd = open("log.txt", O_WRONLY | O_CREAT);if (fd < 0) {         // 打开失败perror("open"); return 1;}printf("fd: %d\n", fd);   // 把 fd 打出来看看return 0;
}

此时,我们的log.txt是原本就存在的,

如果你要创建这个文件,该文件是要受到Linux 权限的约束的!

创建一个文件,你需要告诉操作系统默认权限是什么。

当我们要打开一个曾经不存在的文件,不能使用两个参数的 open,而要使用三个参数的 open!

也就是带 mode_t mode 的 open,这里的 mode 代表创建文件的权限:

int open(const char* pathname, int flags, mode_t mode);  

代码演示:

int main()
{int fd = open("log.txt", O_APPEND | O_CREAT, 0666);  // 八进制表示if (fd < 0) {perror("open");return 1;}printf("fd: %d\n", fd);return 0;
}

因为你要创建的文件,所以要听操作系统!我们来看看 umask: 

你要 666,操作系统不能给你,因为 umask 是 0002,所以最多只能给你 664。 

我们现在就是要 666,我们只需要调用 umask(),然后传 0:umask(0) 

就可以让权限掩码暂时不听按操作系统的默认权限掩码,而用你设置的!

 

 此时权限就变成了我们的666.

实际上,umask 命令就是调用这个接口的。

umask 设为 0,可以让我们以确定的权限打开文件,比如服务器要打开一个日志文件,权限就必须要按照它对应的权限设置好,不要采用系统的默认权限,可能会出问题。

5、文件关闭:close() 

在 C 语言中,关闭文件可以调用 fclose,在系统接口中我们可以调用 close 来关闭: 

#include <unistd.h>int close(int fd);

 该接口相对 open 相对来说比较简单,只有一个 fd 参数,我们直接看代码:

  1 #include <stdio.h>  2 #include <sys/types.h>  3 #include <sys/stat.h>  4 #include <fcntl.h>  5 #include <unistd.h>  // 需引入头文件                                  6 int main()  7 {                                                                   8       umask(0);   // umask现在就为0,听我的,别听操作系统的umask了   9     int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);  // 八进制表示 10 if (fd < 0) {  11         perror("open");  12         return 1;        13     }              14        15     printf("fd: %d\n", fd);  16     close(fd);//关闭文件     17                                              18     return 0;                                19 }  

 

没啥问题,就。。。。。没了。

6、文件写入:write()

文件打开和文件关闭都有了,我们总要干点事吧?现在我们来做文件写入!

在 C 语言中我们用的是 fprintf, fputs, fwrite 等接口,而在系统中,我们可以调用 write 接口:

#include <unistd.h>
ssize_t write(int fd, const void* buf, size_t count);

write 接口有三个参数:

  • fd:文件描述符
  • buf:要写入的缓冲区的起始地址(如果是字符串,那么就是字符串的起始地址)
  • count:要写入的缓冲区的大小

代码演示:向文件写入 6行信息

1 #include <stdio.h>  2 #include<string.h>3 #include <sys/types.h>4 #include <sys/stat.h>5 #include <fcntl.h>6 #include <unistd.h>  // 需引入头文件                                  7 int main()                             8 {                                                                   9       umask(0);   // umask现在就为0,听我的,别听操作系统的umask了   10     int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);  // 八进制表示 11     if (fd < 0) {                      12         perror("open");                13         return 1;                      14     }                                  15     int cnt=6;                         16     const char* str="666666\n";        17     while(cnt--){                      18       write(fd,str,strlen(str));       19     }                                  20                                        21     close(fd);//关闭文件               22                                        23     return 0;                          24 }                                      

 运行结果:

 > 文件名 ,前面什么都不写,直接重定向 + 文件名: 

$ > log.txt

 

这算是一个小技巧吧

感谢阅读!!!!!!!!!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/329938.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

1329:【例8.2】细胞 广度优先搜索

1329&#xff1a;【例8.2】细胞 时间限制: 1000 ms 内存限制: 65536 KB 【题目描述】 一矩形阵列由数字0 到9组成,数字1到9 代表细胞,细胞的定义为沿细胞数字上下左右还是细胞数字则为同一细胞,求给定矩形阵列的细胞个数。如: 4 10 0234500067 1034560500 2045600671 00000000…

Linux查找命令(which命令、find命令)

1.which命令 Linux命令&#xff0c;其实它们的本体就是一个个的二进制可执行程序&#xff0c;和Windows系统中的.exe文件&#xff0c;是一个意思。我们可以通过which命令&#xff0c;查看所使用的一系列命令的程序文件存放在哪里 功能&#xff1a;查看命令的程序本体文件路径…

linux反汇编工具: ida pro、rizinorg/cutter; ubuntu 22 flameshot延迟截图 以应对下拉菜单

rizinorg/cutter rizinorg/cutter 是 命令行反汇编工具 rizinorg/rizin 的图形化界面, 这比 ida pro跑在kvm虚拟机中方便多了, ubuntu22.04下直接下载Cutter-v2.3.2-Linux-x86_64.AppImage后即可运行,如下图: 注意 有个同名的报废品: radare2/Cutter 即 radare2的图形化界…

第二百五十二回

文章目录 概念介绍实现方法示例代码 我们在上一章回中介绍了如何在页面中添加图片相关的内容&#xff0c;本章回中将介绍如何给组件添加阴影.闲话休提&#xff0c;让我们一起Talk Flutter吧。 概念介绍 我们在本章回中介绍的阴影类似影子&#xff0c;只是它不像影子那么明显&a…

【unity】基于Obi的绳/杆蓝图、绳杆区别及其创建方法

绳索 是通过使用距离和弯曲约束将粒子连接起来而形成的。由于规则粒子没有方向(只有位置)&#xff0c;因此无法模拟扭转效应(维基百科)&#xff0c;绳子也无法保持其静止形状。然而&#xff0c;与杆不同的是&#xff0c;绳索可以被撕裂/劈开&#xff0c;并且可以在运行时改变其…

前端学习笔记 3:Vue 工程

前端学习笔记 3&#xff1a;Vue 工程 上一篇文章介绍了如何在单一 Html 页面中使用 Vue&#xff0c;本文介绍如何从头开始用 Vue 构建一个前端工程项目。 1.环境准备 Vue 框架代码的创建依赖于 Node.js&#xff0c;因此需要先安装 Node.js。 2.创建和启动 2.1.创建 通过以…

ARMv8-AArch64 的异常处理模型详解之异常等级、执行状态以及安全状态

ARMv8-AArch64 的异常处理模型详解 一&#xff0c;特权和异常等级1.1 异常等级 Exception levels 二&#xff0c;特权的类型2.1 内存特权2.2 访问寄存器的特权 三&#xff0c;执行状态和安全状态3.1 执行状态 Execution states3.2 执行状态切换 3.3 安全状态 Security states3.…

C#,深度优先搜索(DFS)、广度优先搜索(BFS)算法的源代码与数据可视化

概述 下载源代码&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1sLxMT78LVg2dWyXXFvM--w?pwd2kwl 提取码&#xff1a;2kwl --来自百度网盘超级会员V5的分享https://pan.baidu.com/s/1sLxMT78LVg2dWyXXFvM--w?pwd2kwl 深度优先搜索&#xff08;亦称深度优先遍历&a…

c++ / day06

1. 利用模板类完成顺序表(两天时间&#xff0c;今天至少写出大致框架) 代码 //implement template in sqlist #include <iostream> #include <cstring>#define MAXSIZE 100using namespace std;template <typename T> class Sqlist {unsigned int len 0;T…

HTTP打怪升级之路

新手村 上个世纪80年代末&#xff0c;有一天&#xff0c;Tim Berners-Lee正在工作&#xff0c;他需要与另一台计算机上的同事共享一个文件。他尝试使用电子邮件&#xff0c;但发现电子邮件不能发送二进制文件。Tim Berners-Lee意识到&#xff0c;他需要一种新的协议来共享二进制…

【Python排序算法系列】—— 希尔排序

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 &#x1f4ab;个人格言:"没有罗马,那就自己创造罗马~" 目录 希尔排序 &#xff08;ShellSort&#xff09; 由来和特点 理解 过程演示 Step1&#xff1a;…

【C++】- 类和对象(构造函数!析构函数!拷贝构造函数!详解)

类和对象② 类的6个默认成员函数构造函数析构函数拷贝构造函数 类的6个默认成员函数 上一篇详细介绍了类。如果一个类中什么成员都没有&#xff0c;简称为空类。 那么空类中真的什么都没有吗&#xff1f; 并不是&#xff0c;当类在什么都不写时&#xff0c;编译器会自动生成…