本文已收录至:C语言——梦想系列

更多知识尽在此专栏中!

欢迎点赞收藏关注


目录

前言

正文

关于文件

什么是文件?

文件有什么用?

文件的格式是什么?

使用文件

文件指针

文件的打开和关闭

顺序读写

输入、输出流

fputc 与 fgetc

fputs 与 fgets

fprintf 与 fscanf

fwrite 与 fread

随机读写

fseek

ftell

rewind

fseek、ftell、rewind 三合一

文本文件与二进制文件

文本文件

二进制文件

注意

文件使用注意事项

被错误使用的feof

文件读取结束原因判断

文件缓冲区

总结


前言

文件——是我们生活中必不可缺的一部分,优秀的文件管理能使我们工作效率更高,比如上学时的点名册、平时记账的手账本、电脑中存储数据的各种文件夹等。数据构成文件,文件成就数据,因此我们需要学习C语言中的各种文件操作,使数据能够做到持久化存储。

图片来源:百度图片

正文

文件操作涉及到的内容还是比较多的,大致可分为三个问题:是什么? 怎么用? 要注意什么? 从这三个问题可以衍生出很多问题,其中怎么用是内容最丰富的版块,让我们直接进入正题吧。

关于文件

什么是文件?

如上图所示,这就是文件,不过这是传统的纸质文件。在我们电脑中的都是电子文件夹,存储的都是电子文件,比如数字、图片、文档等,这些数据都是存储在我们电脑的硬盘上的,只要硬盘没坏,那么这些数据随时随地都能找到,而且方便检索。在程序设计中,我们一般将文件分为两种:程序文件数据文件(从文件功能的角度分类),本文主要介绍的是数据文件。

程序文件

包括源程序文件,比如我们的 .c 文件;目标文件,经过预编译、编译、汇编后生成的目标文件,后缀为 .o ,对其进行链接后,就能生成可执行程序;当然最后一种就是可执行程序文件,后缀为.exe

数据文件

就像上图一样,主要存储的是各种数据信息,数据文件的职能是能让程序读取到数据,以及能够对其写入数据,这些数据是能够持久化存储的。

文件有什么用?

电脑C盘中存储的各种信息

文件可以保存数据,使数据能做到持久化存储。文件可以使我们的操作更为合理,比如现在写的这篇博客,本质就是一个文件,不过是存储在服务器上的文件(数据)。电子文件的最大特点就是易于检索了,这也正是电脑的优点之一。至于C语言中的文件可以用于保存程序运行所产生的数据,比如通讯录系统,可以将联系人信息保存到文件中,现在的程序设计数据一般都是存储在数据库中,毕竟本地文件夹安全性还是比较低。

文件的格式是什么?

所有文件都有唯一的标识符,标识符可以分为三部分:文件路径+文件名主干+文件后缀,比如存储在我电脑中的VS文件标识符为:

C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE

为了方便称呼,我们一般将其称为文件名,比如 devenv.exe 就是一个典型的程序文件

使用文件

文件指针

文件是一个庞大的集合体,类似于结构体,不过更为复杂,因此在C语言中有一个专门的指针 文件类型指针,简称为 文件指针 用来指向文件首地址。系统会将文件规范化,当使用文件时,系统会在内存中开辟一个对应的文件信息区,这个信息区中包括了文件的各种信息(文件名、文件状态、文件位置等),如果对应信息缺失,系统会自动补齐。前面说过,文件类似于结构体,因此整个文件信息是保存在一个庞大的结构体中的,为了与传统结构体区分开,专门创建了 FILE* 这种特殊的指针,即文件指针。

因为VS2019将其分的太细了,这里不好演示,但知道 FILE这个东西本质是个结构体就行了

文件的打开和关闭

文件得先打开,才能关闭,最好跟动态内存管理一样,有申请就要有释放,成对出现更为安全。先来说说打开吧!

文件打开

文件打开用的是 fopen 这个函数,fopen 的作用是从一个文件中以某种方式打开文件,返回类型是 FILE* 即打开文件的起始地址,因此我们需要用一个 FILE* 类型的指针来接收。

注意:文件打开后,要对文件指针进行判断,如果指针为空,说明文件打开失败,此时要报错,并终止后续操作

if (NULL == fp){//报错函数,说明此文件打开失败perror("fopen::test.txt!");return 1;//错误结束}

目标文件

有两种形式,一种是绝对地址,另一种是相对地址

绝对地址

即唯一路径,使用绝对地址访问文件时,文件可以在电脑中的任意位置,前提是地址要合法。绝对位置的文件标识符必须全,即文件路径+文件名主干+文件名后缀

比如 C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE

注意:

使用绝对路径时,需要在每个 \ 前额外加一个 \原因很简单,单个 \ 与其他字符结合,可能会变成一个转义字符,因此需要两个 \\ 来表示一个 \

如果是 Mac 就不需要担心这个问题,因为它用的是 /

//绝对,指此地址是唯一的,能通过这个地址找到唯一的文件FILE* fp = fopen(" C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\Common7\\IDE", "w");

相对位置

此时的路径是固定的,一般和当前源文件处于同一位置,相对嘛,就是相对于当前程序文件。相对位置只需要文件名主干+文件后缀就行了。

比如 devenv.exe ,此时存储位置相对于上面的绝对地址,位于同一目录下

//相对,指在当前工程文件内的文件FILE* fp = fopen("test.txt", "w");

打开方式

文件打开方式有很多种,比如只读、只写、读+、二进制写等……

值得注意的是当我们通过的方式打开文件时,如果目标文件不存在,那么打开就会失败;但如果是通过的方式打开文件时,如果文件不存在,会自动创建一个目标文件

下面来演示下用写的方式打开文件,然后文件不存在,自动创建文件的情况:

注意:这种是文件的标准使用方式,即先打开,然后判断是否打开失败,如果失败就报错,否则就可以使用文件,最后再关闭文件

//文件创建,通过程序创建int main(){//可以利用只读的特性:没有就会自己创建//这里是相对路径FILE* fp = fopen("test.txt", "w");//打开if (NULL == fp){//报错函数,说明此文件打开失败perror("fopen::test.txt!");return 1;//错误结束}//进行文件操作……//fclose(fp);//关闭fp = NULL;//置空return 0;}

注意:

  • 这个特点很好用,但也很致命,因为每次写文件,都相当于在覆盖文件,假如我们想对原文进行追加,就需要创建原来的数据,再创建新数据,然后一起写入文件中。其实面对这种场景,C语言还提供了另一种文件打开方式 追加 “a” ,下面就是各种打开指令的集合表。
文件打开指令含义如果目标文件不存在
“r”只读打开一个文件,只能对这个文件进行读取操作打开失败,报错
“w”只写打开一个文件,只能对这个文件进行写入操作建立目标文件
“a”追加向文件末尾处添加数据建立目标文件
“rb”只读打开二进制文件,只能对其进行二进制读取打开失败,报错
“wb”只写打开二进制文件,只能对其进行二进制写入建立目标文件
“ab”追加向二进制文件末尾处添加数据追加失败,报错
“r+”读写打开一个文件,可以进行读取和写入操作打开失败,报错
“w+”读写打开一个文件,可以进行读取和写入操作建立目标文件
“a+”读写对文件末尾处进行读取和写入操作建立目标文件
“rb+”读写打开二进制文件,可以进行二进制读取和写入打开失败,报错
“wb+”读写打开二进制文件,可以进行二进制读取和写入建立目标文件
“ab+”读写对二进制文件末尾处进行读取和写入操作建立目标文件

文件关闭

文件关闭用到的是 fclose 这个函数,fclose 就比较简单了,只需要一个参数—文件指针(FILE*),然后就能关闭这个文件指针所指向的文件,感觉有点像 free,功能强大,使用方便。同 free 一样,fclose 关闭文件后,也需要将指针(文件指针)置空,避免出现野指针

fclose(fp);//关闭fp = NULL;//置空

顺序读写

输入、输出流

在介绍文件读写操作前,需要先说明一下C语言中“流”(format)的概念。假设将数据看作水流,那么它就有两个关键部分:从哪里流出(源头)、流入哪里(终点),其中流出可以看作输出流入可以看作输入。C语言中有三种流:标准输入输出流、文件输入输出流、二进制输入输出流(实际使用时用前两种流,第三种的目标流一般为文件)。

标准输入输出流

标准输入输出流(I/O)包括标准输入流(stdin)—从键盘输入标准输出流(stdout)—从屏幕输出标准错误流(strerr)—从屏幕输出,任何一个C程序,只要运行起来都会默认打开以上三个流,比如我们常用的 scanf、printf就是基于标准输入输出流而运行的,这也正是二者需要键盘、屏幕的原因。

文件输入输出流

顾名思义,文件输入输出流所依赖的载体为文件,无论是输入还是输出数据,都是在文件上进行的,因此它的对象类型为 FILE* ,文件输入输出流可以使用所有输入输出流函数,比如fputc、fprintf、fscanf等,使用时只需要加上目标流类型就行了。

二进制输入输出流

二进制输入输出流主要适用于文件操作,对文件进行二进制数据的读取和写入,所以二进制输入输出一般用在文件操作中。二进制只有0、1这两个数,因此如果我们使用二进制输出流对某个文件进行写入,文件中存储的信息就变成了一串二进制数(可以使用二进制文件查看器观察),如果用普通文本的形式查看此文件,会得到一串乱码。二进制输入输出流有fwrite、fread这两个函数。

注意:

  • printf、scanf、gets等这种不需要指定目标流的函数,设计时就已经规定好了,它们是标准输入输出流函数。而fprintf、fscanf、fgets等这些面向所有输入输出流的函数更为原始,需要用户使用时根据具体情况选择目标流,所以这些函数也能实现标准输入输出流函数的功能,只需要把目标流写成 stdin(输入)、stdout(输出)就行了。下面是各种输入输出函数的集合表:
功能函数名适用于(目标流)
进行单字符的输入fgetc所有输入流
进行单字符的输出fputc所有输出流
文本行输入函数(读取一行数据)fgets所有输入流
文本行输出函数(写入一行数据)fputs所有输出流
格式化输入函数fscanf所有输入流
格式化输出函数fprintf所有输出流
二进制输入函数fread文件输入流
二进制输出函数fwrite文件输出流

注意:为了方便函数的介绍,接下来会先介绍写入(输出),再介绍读取(输入)函数

fputc 与 fgetc

fputc 对文件进行单字符的写入fgetc 读取文件中的单字符

fputc

//文件读写之逐字符写//因为没有数据,所以我们先写再读int main(){//打开FILE* fp = fopen("test.txt", "w");if (NULL == fp){perror("fopen::test.txt!");return 1;}//进行操作char* pc = "abcdef123";//逐字符写入while (*pc){fputc(*pc, fp);//逐字符放pc++;}//关闭fclose(fp);fp = NULL;return 0;}

fgetc

//文件读写之逐字符读int main(){FILE* fp = fopen("test.txt", "r");if (NULL == fp){perror("fopen::test.txt!");return 1;}//逐字符读取int ch = 0;//需要用整型,因为EOF是-1while ((ch = fgetc(fp)) != EOF){//逐字符读取后,赋给字符变量ch,然后打印printf("%c", ch);}//关闭fclose(fp);fp = NULL;return 0;}

注意:

  • 在读取或写入字符串时,可以通过特定的条件结束读写。比如写入:可以通过字符串自带的结束标志 \0 结束写入读取:可以通过fgetc的返回值进行判读,如果返回 -1(EOF) 就说明数据已经读取完了
  • 单纯写文本数据时,要使用指令 “w” 单纯读数据时,要使用指令 “r” 指令与操作一定要匹配上,不然就会发生意想不到的错误

fputs 与 fgets

fputs 对文件进行一行数据的写入fgets读取文件中的一行数据

fputs

//文件读写之行写int main(){FILE* fp = fopen("test.txt", "w");if (NULL == fp){perror("fopen::test.txt");return 1;}char* pc = "这是由标准输入输出流写入的数据";fputs(pc, fp);//行写入fclose(fp);fp = NULL;return 0;}

fgets

//文件读写之行读int main(){FILE* fp = fopen("test.txt", "r");if (NULL == fp){perror("fopen::test.txt");return 1;}char tmp[30] = "0";fgets(tmp, sizeof(tmp), fp);//行读取printf("%s\n", tmp);fclose(fp);fp = NULL;return 0;}

注意:

  • 行写入时,要确保写入的是字符串数据,传参数时要传地址;行读取时,需要设定待读取数据的数量,一般是跟待存储空间大小相匹配。如果行读取结束,有两种情况:1、因无法读取数据而结束 2、因读取到文件末尾而结束
  • 单纯文本数据时,要使用指令 “w” ;单纯数据时,要使用指令 “r”

fprintf 与 fscanf

fprintf 是对文本进行格式化数据的写入fscanf 是将文本中的数据进行格式化读取

fprintf

//按照文件流格式化写入struct S{char name[20];int age;float score;}a = { "张三",20,88.8f };int main(){FILE* fp = fopen("test.txt", "w");if (NULL == fp){perror("fopen::test.txt");return 1;}fprintf(fp, "%s %d %.2f", a.name, a.age, a.score);//适用于所有流的格式化输入输出函数fclose(fp);fp = NULL;return 0;}

fscanf

//按照文件流格式化读取struct S{char name[20];int age;float score;}tmp;int main(){FILE* fp = fopen("test.txt", "r");if (NULL == fp){perror("fopen::test.txt");return 1;}fscanf(fp, "%s %d %f", tmp.name, &(tmp.age), &(tmp.score));//适用于所有流的格式化输入输出函数printf("%s %d %.2f\n", tmp.name, tmp.age, tmp.score);fclose(fp);fp = NULL;return 0;}

延申:sprintf 和 scanf

除了 fprintf / fscanf printf / scanf 这两组格式化输入输出外,还存在另一组格式化输入输出函数:sprintf / sscanf

简单介绍一下,sprintf 是把格式化的数据按照一定的格式转换为字符串,相反的,sscanf 就是从字符串中按照一定格式读取出格式化的数据

sprintf sscanf 可以把结构体中的数据打包成一个字符串,也可以对某个字符串进行拆分。这个东西在我们生活中有应用,比如当我们登录账号时,会把账号、密码这个结构体打包成一串字符串,交给后端处理,当然有个更高级的名词:序列化与反序列化

注意:

  • printf 输出家族返回的是实际写入(输出)的字符总数(包括转义字符),而 scanf 输入家族返回的是实际读取(输入)的元素个数。举个栗子,字符串 abc ,输出返回 3,输入返回 1,因为此时的字符串视为一个元素。
  • 单纯文本数据时,要使用指令 “w” ;单纯数据时,要使用指令 “r”

fwrite 与 fread

fwrite 是对文件进行二进制数据的写入fread 是以二进制的形式读取文件中的数据

fwrite

//文件读写之二进制写入struct S{char name[20];int age;float score;}a = { "张三",20,88.8f };int main(){//把a中的数据写到文件中FILE* fp = fopen("test.txt", "wb");if (NULL == fp){perror("fopen::test.txt");return 1;}//二进制的写文件fwrite(&a, sizeof(a), 1, fp);fclose(fp);fp = NULL;return 0;}

fread

//文件读写之二进制读取struct S{char name[20];int age;float score;}tmp;int main(){//把文件中的数据读取到tmp中FILE* fp = fopen("test.txt", "rb");if (NULL == fp){perror("fopen::test.txt");return 1;}fread(&tmp, sizeof(tmp), 1, fp);printf("%s %d %.2f\n", tmp.name, tmp.age, tmp.score);fclose(fp);fp = NULL;return 0;}

注意:

  • 当我们使用二进制写入数据到文件时,如果是以文本的方式打开,只能看懂字符串部分,数字部分是看不懂的,我们可以通过VS中的二进制编辑器,来观察其中的数据。
  • 单纯二进制数据时,要使用指令 “wb” ;单纯二进制数据时,要使用指令 “rb”

随机读写

随机读写函数,需要配合上面的输入输出函数使用,所谓的随机读写,是指通过改变文件指针的偏移量,来写入或读取数据。介绍三个和随机读取有关的函数:fseek 改变文件指针偏移量ftell 查看当前文件指针的偏移量rewind 使文件指针复原至起始位置

fseek

//fseek,文件指针偏移量int main(){FILE* fp = fopen("test.txt", "w");if (NULL == fp){perror("fopen::test.txt");return 1;}char* pc = "abc";fseek(fp, 20, SEEK_SET);//从起点往后偏移fputs(pc, fp);fclose(fp);fp = NULL;return 0;}

ftell

//ftell,返回当前文件指针偏移信息int main(){FILE* fp = fopen("test.txt", "r");if (NULL == fp){perror(fp);return 1;}printf("当前文件指针偏移量为:%d\n", ftell(fp));fseek(fp, 20, SEEK_SET);//向后偏移20printf("经过fseek设置后的文件指针偏移量为:%d\n", ftell(fp));fclose(fp);fp = NULL;return 0;}

rewind

//rewind,使文件指针恢复至原位置int main(){FILE* fp = fopen("test.txt", "r");if (NULL == fp){perror("fopen::test.txt");return 1;}fseek(fp, 20, SEEK_SET);//先让文件指针向后偏移20printf("当前文件指针偏移量为:%d\n", ftell(fp));rewind(fp);//使文件指针恢复至起始位置printf("经过恢复后的文件指针偏移量为:%d\n", ftell(fp));return 0;}

fseek、ftell、rewind 三合一

//fseek、ftell、rewind三合一//假设文件中存储数据为abcdefint main(){FILE* fp = fopen("test.txt", "r");if (NULL == fp){perror("fopen::test.txt");return 1;}printf("现在文件中内容为abcdef,我们要依次取出e、b、d\n");fseek(fp, -2, SEEK_END);//从后往前偏移printf("先取出字符%c\n", fgetc(fp));rewind(fp);//还原至起始位置fseek(fp, 1, SEEK_SET);//从前往后偏移printf("再取出字符%c\n", fgetc(fp));fseek(fp, 1, SEEK_CUR);//从当前位置向后偏移printf("最后再取出字符%c\n", fgetc(fp));fclose(fp);fp = NULL;return 0;}

注意:

  • 每进行一次文件输入输出操作,文件指针都会向后移动一位。比如上面的三合一,当我们读取到字符 ‘b’ 后,文件指针向后移动一位,指向字符 ‘c’ ,此时只需要把文件指针向后偏移一位,就能愉快的读取到字符 ‘d’ 了。

文本文件与二进制文件

文本文件

文本文件指以ASCII码(文本方式)存储的数据,原始数据机器能直接看懂,将内存中的数据对应ASCII码解码存储后,我们人类也能看懂,举个栗子,在记事本中写的文本,就是文本文件

二进制文件

二进制文件是将数据编译后转成二进制形式,然后直接存储的文件,这种文件机器能秒懂,读取效率很高(因为不需要转译),但二进制一般人是看不懂的,部分二进制数据也无法通过ASCII码解码为正确的数据,因此强行输出二进制文件,极有可能会得到乱码。比如将上面的那段话通过二进制形式写入文件中,可以看到除字符类型数外,其他类型的数据变成了乱码。

下图为上面的二进制文件在内存中以二进制形式存储的样子,显示为十六进制(节省空间),实际为二进制

注意

  • 如果待读取的文件中存储的是二进制数据,就需要使用 二进制读取 “rb” 的形式读取数据;反之如果想写入二进制数据,就需要用 二进制写入 “wb” ,无论是二进制还是普通文本,计算机都能读懂,只是我们看不得罢了。小技巧:可以使用二进制存储重要数据,这样外行人一时半会也理解不了。

文件使用注意事项

被错误使用的feof

很多人在写C语言课设的时候(学生信息管理系统、通讯录系统等),会通过 feof 来判断文件是否读取结束,这是一种错误的用法,因为 feof 的作用是判断当前文件读取结束原因的,如果是因为读取到了末尾而结束,feof(fp) 就为真;除了这个以外,还有另一个文件读取结算原因判断函数,ferror ferror(fp) 为真时,说明此时发生了读取异常,并非正常结束,我们可以通过这两个报错函数来判断文件读取结束的真正原因。

char arr[100] = "0";fgets(arr, sizeof(arr), fp);printf("%s\n", arr);int n = 0;if ((n = feof(fp)))printf("End by EOF\n");if (ferror(fp))printf("End by IO Error\n");

文件读取结束原因判断

既然 feof 不是用来判断读取是否结束的,那说明存在其他判断方法,其实答案就是函数设计中,前辈在设计函数时已经考虑好了,比如 fgetc 没有读取到数据会返回EOF,fgets 没有读取到数据会返回NULL,fscanf 可以通过其返回的实际读取元素个数进行判断,fread 可以通过返回值与指定读取的元素数比较。每种读取函数都有属于的自己的判断方法,比如下面这两个例子:

对文本数据进行读取

//读取错误信息判断//1.文本文件版,假设文件内已有信息,为abcdefint main(){FILE* fp = fopen("test.txt", "r");if (NULL == fp){perror("fopen::test.txt");return 1;}int ch = 0;//接收读取的数据,要用整型,因为EOF为-1while ((ch = fgetc(fp)) != EOF){printf("%c", ch);}//判断是为何结束if (feof(fp))printf("\nEnd by EOF(因读到文件末尾而结束)\n");else if (ferror(fp))printf("\nEnd by IO(因中途读取失败而结束)\n");fclose(fp);fp = NULL;return 0;}

对二进制文件进行读取

//2.二进制文件版enum { SIZE = 5 };//相当于宏定义int main(){double a[SIZE] = { 1.1,2.2,3.3,4.4,5.5 };//首先把五个浮点数以二进制的形式,写入文件中FILE* fp = fopen("testFoBin.txt", "wb");if (NULL == fp){perror(fp);return 1;}fwrite(&a, sizeof(*a), SIZE, fp);fclose(fp);//写入完成,关闭文件double b[SIZE] = { 0.0 };//现在以二进制的形式读取数据fp = fopen("testFoBin.txt", "rb");int size = fread(&b, sizeof(*b), SIZE, fp);if (size == SIZE){printf("No Error!\n");int i = 0;while (i < size)printf("%.2lf ", b[i++]);}else{if (feof(fp))printf("\nError by EOF\n");else if(ferror(fp))printf("\nError by IO\n");}fclose(fp);fp = NULL;return 0;}

文件缓冲区

ANSIC 标准定义了“缓冲文件系统”这个概念,所谓缓冲文件系统是指系统自动地在内存中为程序
中每一个正在使用的文件开辟一块文件缓冲区。无论是读取还是写入数据时,都会先将数据送入文件缓冲区,等文件缓冲区装满遇到刷新指令后,数据才会被读取(写入)到目标空间中。文件缓冲区的大小是由编译器决定的。

验证文件缓冲区是否存在

我们可以利用睡眠函数 Sleep 来使程序暂停,此时数据还没有被写入文件中,仍然位于缓冲区;之后再手动刷新缓冲区,数据此时会被推送至文件中

//文件缓冲区#includeint main(){//打开文件FILE* fp = fopen("test.txt", "w");if (NULL == fp){perror(fp);return 1;}char* ps = "测试文件缓冲区";fputs(ps, fp);//先将数据写到缓冲区中printf("数据现在已经在缓冲区里面了,但还没有推送到文件中\n");printf("程序睡眠10秒,10秒后刷新缓冲区\n");Sleep(10000);//睡眠函数,单位是毫秒fflush(fp);printf("现在缓冲区已经刷新,数据已经写入文件中了\n");Sleep(10000);//关闭文件,当文件关闭时,缓冲区也会被刷新fclose(fp);fp = NULL;return 0;}

可以看到文件缓冲区是真实存在的。

注意:

  • fclose 关闭文件后,会自动刷新缓冲区,数据能够推送至文件
  • 当程序运行结束后,缓冲区也会被自动刷新
  • scanf 遇到 \n 也会触发缓冲区刷新,另外如果其在读取字符型数据时,遇到空白字符(空格TAB键)也会触发缓冲区的刷新

总结

以上就是C语言文件操作的所有内容了,从文件的打开到文件的关闭,中间可以进行多种操作,构造出巧妙的数据。当然前提是我们得学会文件的相关操作,可以巧记为单字符读写、行读写、格式化读写和二进制读写,无论是那种操作,都需要和对应的文件操作指令匹配上;关于随机读写,记住那三个偏移量函数就行了;最后需要对文件缓冲区有一定的理解,确保数据能成功推送至文件内。总之,文件操作的学习可以宣布毕业了。

如果你觉得本文写的还不错的话,期待留下一个小小的赞,你的支持是我分享的最大动力!

如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正

相关文章推荐

C语言进阶——动态内存管理

C语言进阶——自定义类型

C语言进阶——指针进阶试题讲解(万字长文详解)