第 1 章 打好基础——预备知识和复习
1-1 C 语言是什么样的语言
1-1-1 C 语言的发展历程
众所周知,C 语言原本是为了开发 UNIX 操作系统而设计的语言*。
* 在如今,Linux 比 UNIX 更有名。Linux 是由林纳斯·托瓦兹(Linus B. Torvalds)重写的类 UNIX 操作系统。
如此说来,似乎 C 语言应该比 UNIX 更早问世,但实际上并非如此。最早期的 UNIX 并不是用 C 语言开发的,而是用汇编语言开发的。
汇编语言几乎可以说是与机器语言一一对应的语言。例如,对于从 1 加到 100 的程序,C 语言代码如代码清单 1-1 所示,而汇编语言代码则如代码清单 1-2 所示。
代码清单 1-1 assembly.c
int i;
int sum = 0;
for (i = 1; i <= 100; i++) {
sum += i;
}
代码清单 1-2 assembly.s(节选自以C语言代码为基础,在 x86-64 的 Linux 环境里通过 gcc 的
-S
选项输出的代码)
movl $0, -4(%rbp) <--将0赋给代表变量sum的内存空间的-4(%rbp)
movl $1, -8(%rbp) <--将1赋给代表变量i的内存空间的-8(%rbp)
jmp .L2 <--跳转到标签L2处
L3:
movl -8(%rbp), %eax <--将变量i的值赋给寄存器eax
addl %eax, -4(%rbp) <--将寄存器eax的值加上变量sum
addl $1, -8(%rbp) <--变量i加1
L2:
cmpl $100, -8(%rbp) <--比较变量i和100
jle .L3 <--当比较结果为i<100时,跳转到标签L3处
在汇编语言代码的旁边有简单的说明,不过现在没必要去理解它。看过代码清单 1-2 之后,就不难想象要用汇编语言写大型程序会多么地不容易。更何况,汇编语言还因 CPU 而异,不具备可移植性。
UNIX 之父肯·汤普森(Ken Thompson)考虑到不能再使用汇编语言来开发 UNIX 了,因而开发了一种称为 B 的语言。B 语言是剑桥大学的马丁·理查兹(Martin Richards)于 1967 年开发的 BCPL(Basic CPL)的精简版。BCPL 的前身是 1963 年剑桥大学与伦敦大学共同研究开发的 CPL(Combined Programming Language,组合编程语言)。
B 语言不直接生成机器码,而是先由编译器生成供栈式机使用的中间代码,然后由解释器来运行(类似于 Java 或者 UCSD Pascal)。因此,B 语言的运行十分耗时,最终人们放弃了在 UNIX 中使用它。
1971 年,肯·汤普森的同事丹尼斯·里奇(Dennis Ritchie)对 B 语言进行了改良,增加了 char
数据类型,并且使之能够直接输出 PDP-11 的机器码。B 语言曾在很短的一段时间内被称为 NB(New B)。
后来,NB 被改称为 C——C 语言诞生了。
当时大家都用汇编语言来编写操作系统这样的程序,早期的 UNIX 也是用汇编语言编写的,但如上所述,对于汇编语言,不论是编写还是维护,抑或是移植,都非常困难。因而,1973 年肯·汤普森用 C 语言几乎重写了整个 UNIX。
总的来说,C 语言是一线开发人员为满足自己的使用需求而创造出来的语言。之后 C 语言也主要是迎合使用 UNIX 的程序员的需求,一边接受各方的意见建议,一边顺其自然地不断扩展着各种功能。
然后,C 语言迎来爆炸式的普及,除了操作系统,还广泛应用于应用程序的开发。不过,希望大家牢记,C 语言一开始只是汇编器的替代品(至今还有人揶揄其为“结构化的汇编器”)。
补充 是汇编语言还是汇编器
上文中同时出现了“汇编语言”和“汇编器”两个词,这并非是作者和编辑粗心大意。
众所周知,计算机(CPU)只能执行机器语言。而因为机器语言只是单纯地罗列数字,所以人类是很难读写的*。在用机器语言写程序时,实际上是先编写跟机器语言一一对应的汇编语言代码,然后手工改写成机器语言(称为手工汇编),或者由被称为汇编器的程序改写成机器语言。也就是说,把汇编语言重写成机器语言这项工作称为汇编,而自动实现该功能的程序叫作汇编器。作为一种习惯,“用汇编语言写程序”就意味着“用汇编器来写”,所以也可以说成是“用汇编器写程序”。
* 不过以前也经常会有“大神”手工读写机器码。
对于汇编前的语言,现在通常称为“汇编语言”,有时也可以称为“汇编器语言”。从前,JIS*将它规定为“汇编器语言”,如今在信息处理工程师考试的大纲里依然保留着“汇编器语言”的称呼(2016年10月的版本)。因此,这两个词可以认为是同一个意思。
* JIS 是 Japanese Industrial Standards(日本工业标准)的简称。——译者注
补充 B 语言是什么样的语言
C 语言的入门书中经常会提到,C 语言是 B 语言的进化版,但几乎所有的书对 B 语言的介绍就仅此而已,并没有具体说明 B 语言究竟是一门什么样的语言。
如前所述,B 语言是在虚拟机上运行的解释型语言,但它并没有像 Java 那样追求“到处运行”(Run anywhere)的崇高目标,而是因为受到最初运行 UNIX 环境的 PDP-7 的硬件限制,只能使用解释器这样的实现方式。
B 语言是“没有类型”的语言。现在一提到没有类型的语言,人们就会想到 JavaScript、Python、Ruby 等“变量没有数据类型,什么类型的值都能进行赋值”的语言,但 B 语言并不是这样的,它只能使用
word
类型(即依赖于硬件种类来确定位数的整数类型。在 PDP-11 上是 16 位)。对于本书的主题——指针,在 B 语言中也是和整数一样处理的。指针,简而言之就是内存中的地址,因而在有的机器中也可以当作整数类型来处理(关于这一点,本章会详细介绍)。关于 B 语言的语法,可以参考论文“User's Reference to B”。看到该论文中的示例程序,你就会发现 B 语言中已经出现了像
adx = &x1
和x = *adx++
这类在现今的 C 语言里也能看到的写法。作为 B 语言的进化版,NB 是具有数据类型的语言。为了把指针和整数混为一谈的 B 移植到 NB,丹尼斯·里奇在指针的处理上下了很大功夫。C 语言的指针变得如此纷繁复杂,可能也有这方面的原因。
1-1-2 不完备和不统一的语法
C 语言是开发现场的人们根据自己的需求创造出来的,所以具备极强的实用性。但从人类工程学的角度来看,它就不是那么完美了。
比如,相信大家都犯过下面这样的错误。
if (a = 5) { <-- 把本该写成==的地方写成了=
在日语键盘上,“-
”和“=
”是同一个按键,因此经常会发生下面的问题。
for (i - 0; i < 100; i++) { <-- 忘记同时按下Shift键
即便是这种情况,编译器也往往并不报错。现在的编译器可能会给出警告,但是早期的编译器对这样的错误是全部无视的。
使用 switch case
时忘记写 break
也是易犯的错误。
幸运的是,对于易犯的语法错误,现在的编译器已经可以在很多地方给出警告了。因此,无视编译器的警告是不行的。我们应该尽可能提高编译器的警告级别,使编译器能够指出尽可能多的错误。
换句话说,在编译器给出错误和警告时,不要抱怨:“净给我找事儿,这个混蛋!”而是应该心怀感激地说一声:“谢谢您了,编译器先生。”然后认真地把 Bug 修改掉。
要点
尽可能调高编译器的警告级别。
不可以无视或者阻止编译器的警告。
1-1-3 C 语言“圣经”——K&R
被称为 C 语言“圣经”的 The C Programming Language[1] 第 1 版发行于 1978 年。
人们将布莱恩·柯林汉(Brian Kernighan)和丹尼斯·里奇(Dennis Ritchie)两位作者的英文名首字母合起来,称该书为 K&R。在后面提到的 ANSI 标准制定之前,该书一直被作为 C 语言语法的参考基准使用。
据说在最初发行该书时,出版方,即英国培生出版社曾预估在当时的 130 个 UNIX 站点里,平均每个站点可以卖出 9 本(摘自 Life with UNIX[2])。
结果,仅 K&R 第 1 版的销量就比培生出版社最初的预计多出了 3 位数*。原本只是为了满足自己的需求而开发的 C 语言,历经坎坷,最终成为全世界广泛使用的开发语言。
* 根据亚马逊上的本书试读章节,截至 2008 年 9 月 20 日,K&R 第 2 版仅日文版就已经印刷 321 次了!这个行业的图书能有这样的业绩,确实称得上现象级畅销书。
之后,在 1989 年,也就是 ANSI C 正式发布前不久,K&R 第 2 版问世,并成了 ANSI C 的依据。
在 ANSI C 尚未出现之时,K&R 是事实上的 C 语言标准,因此也有人将 ANSI C 之前的旧式 C 语言称为“K&R C”。不过考虑到目前在售的 K&R 是 ANSI C 的依据,这种叫法容易遭人误解。因此,在本书中,在提到 ANSI C 之前的 C 语言时,我们还是尊重事实,称之为“ANSI C 之前的 C 语言”。
另外,本书在下文提到 K&R 时,指的是日文版的第 2 版*。
* 中文版请参考《C 程序设计语言(第 2 版)》。——译者注
K&R 多年以来都被奉为 C 语言“圣经”。的确,书中附录 A 和附录 B 精心整理了 C 语言的规范和标准库,使用起来十分便利,但可能由于该书的定位是入门书,所以正文部分写得不够严谨,其中有些表述容易遭人误解或不够准确。特别是例题中的示例程序,以目前的眼光来看,我觉得相当不合适。
话虽如此,作为一名 C 程序员,就算是为了了解 C 语言的历史,也应该买一本放在书架上。但要是打算靠这一本书来理解 C 语言,即便说不上鲁莽,对大多数人来说也是低效的。更何况,这本书也不支持后面我们要讲的那些新标准(C95、C99 和 C11)。
1-1-4 ANSI C 之前的 C 语言
ANSI C 标准制定于 1989 年,其实已经非常陈旧了。或许有人会觉得,比这还要陈旧的 C 语言知识,学了可能也没什么用。实际上我也这么觉得,但老式的 C 语言规范对现在的 C 语言也是有一定影响的,所以我们还是耐着性子看一下吧。
在 ANSI C 制定之前,C 语言一直在不断扩展。
比如关于结构体的整体赋值,虽然 K&R 第 1 版里并没有介绍,但其实在 K&R 出版之前,这个功能就已经在丹尼斯·里奇的 C 编译器里实现了。从某种意义讲,K&R 第 1 版刚出版就已经过时了。不过,这在计算机图书界是常有的事。
在 ANSI C 里,变化比较大的是函数定义的语法和原型的声明。
在 ANSI C 制定之后,函数定义是下面这样的。
void func(int a, double b)
{
⋮
}
而在 ANSI C 之前的 C 语言中则是下面这样:
void func(a, b)
int a;
double b;
{
⋮
}
说起来,关于 C 语言中花括号 {}
的位置,有人是像下面这样,写在 if
等语句的右边(这是 K&R 里面的书写风格,所以这里称之为“K&R 风格”)。
if (a == 0) {
可是在函数定义的时候,他们又把 {
写在了代码行的开头,这让人很是困惑*。其实这是 ANSI C 之前的 C 语言写法的遗留问题(也因为有些工具以代码行开头的花括号作为判断函数起始的依据)。
* 毕竟,Java 等语言的方法定义也是把
{
写在右边的。
此外,老式的 C 语言里没有函数的原型声明。如果在 ANSI C 里写出如下原型声明,那么在调用该函数时,当参数的个数或类型发生错误时,编译器会报错。
void func(int a, double b);
但是,因为老式 C 语言里没有这个功能,所以正确指定参数的责任就落在了程序员身上。如今看来,这是一个非常危险的规则,但说到底,那时候的 C 语言只是汇编器的替代品,所以没人觉得不妥。
然而,对于有返回值的函数,假如不明确其返回值的类型,编译器就无法生成接收返回值的那部分的机器码。因此,比如对于三角函数 sin()
,就需要像下面这样,仅声明返回值的类型*。
* 如果不声明,该函数就会作为整体被当作返回
int
的函数处理。
double sin(); <-- 请注意,括号中是空的
在现代的 C 语言里,在声明没有参数的函数原型时,必须像下面这样在括号里写上 void
。
void func(void);
这是为了与老式 C 语言的函数声明兼容(为了区别到底是由于使用老式声明而不进行参数检查,还是在明确指出该函数不接收参数)*。
* C++ 放弃了兼容老式 C,所以不需要这个
void
。
1-1-5 ANSI C(C89/90)
如前所述,K&R 第 1 版里并没有记载在它出版后才实现的功能,其中的介绍也不一定就是严密的,因此程序的动作会因运行环境的不同而有所差异。
鉴于这些情况,经过一番争论,终于在 1989 年,美国国家标准学会(American National Standards Institute,ANSI)通过了 C 语言的标准规范。
顾名思义,美国国家标准学会是美国的标准。ANSI C 后来被国际标准化组织(International Organization for Standardization,ISO)采用,成为标准 ISO/IEC 9899:1990*。由于 ANSI C 发布于 1989 年,而 ISO 的标准发布于 1990 年,所以这个版本的 C 有时被称为 C89,有时被称为 C90。看上去有些混乱,其实内容都一样。
* 相应的中国国家标准为 GB/T 15272-1994。——译者注
由于 C89 和 C90 的称呼容易与后面将介绍的 C95 和 C99 混淆,所以本书将该版本的 C 叫作 ANSI C*。
* 事实上,C95、C99 和 C11 都是经过 ANSI 认可的标准,但通常在提到 ANSI C 时,指的是 C89 和 C90,所以本书也仿而效之。
随后,ISO/IEC 9899:1990 被日本的 JIS 标准采用,成为 JIS X3010:1993。
1-1-6 C95
ISO/IEC 9899:1990 于 1995 年增加了处理宽字符的库,成为 ISO/IEC 9899/AMD1:1995。所谓 AMD1,是指 Ammendment1,其中的 Ammendment 是“标准的修正”的意思。
这次修订增加了可以处理宽字符、实现宽字符和多字节字符的转换的函数集。
在 C 语言中,字符串基本上就是 char
的数组,而 char
类型的长度为 1 个字节(通常是 8 位)。对美国人来说,这就已经能够满足使用需求了,但因为我们使用汉字等多种字符,所以无法用 1 个字符对应 1 个字节来表示。
我们多使用 GB2312、GBK 或 UTF-8 这些字符编码,用多个字节构造中文的字符串。例如“"abc 一二三四五"
”这个字符串,如果用 GB2312 表示,那么“abc
”的部分是 1 个字符对应 1 个字节,而“一二三四五
”的部分是 1 个字符对应 2 个字节,这种表示方法就是多字节字符串。但是在使用这种方法的情况下,每个字符的长度都是可变的,因此在以字符为单位分割字符串时会很麻烦。例如我们要制作一款文字编辑器这样的程序,那么在前后移动光标时,就无法立刻辨别表示字符位置的变量到底是要移动 1 个字节,还是要前进 2 个字节。
因此,宽字符适时登场。如果使用宽字符,就可以用固定长度的 wchar_t
类型来定义单个字符。wchar_t
类型早在制定 ANSI C 时就已经被定义过了,但实际使用它的输入输出函数以及转换函数在 ISO/IEC 9899/AMD1:1995 中才被定义。
C95 并非主流称呼,而且后来的 C99 里也包含了这里增加的函数,所以无须区别对待 ISO/IEC 9899/AMD1:1995。只不过对于我们来说,不可避免地要处理汉字字符,所以这里简单介绍了一下。
长久以来,“C 的字符串就是 char
的数组”算是一个常识,但现在这一常识已经被打破了。不过,考虑到大部分读者是初学者,所以本书总体上还将基于“C 的字符串就是 char
的数组”来讲解。
1-1-7 C99
C99 是 1999 年 12 月 1 日由 ISO 制定的 C 语言标准,其正式名称为 ISO/ IEC 9899:1999。
在标准制定之前,C99 的代号为 C9x。之所以起这个代号,是因为当初预计该标准可以在 20 世纪 90 年代中期确定。可是,最终决策直到 1999 年 12 月才完成,真是一直争论到了最后一刻。不过到底是没白争论这么久,C99 最终增加了许多功能,如下所示。
- 以
//
开头的单行注释(C++ 从前就有这个写法) - 变量也可以不在代码块的开头声明(这也是 C++ 从前就有的功能)
- 预处理器的功能扩展、可变长参数等
- 增加了复数类型、
_Bool
类型 对类型定义更加严格。废除了如 1-1-4 节的注释里所说的“没有声明的函数就返回
int
”等规则** K&R 开头的程序“
hello, world.
”并没有在main()
中指定返回值的类型,这是违反 C99 的语法的。指定初始化器(6-3-11 节)
- 复合字面量(6-3-12 节)
- 可变长数组(Variable Length Array,VLA)
- 柔性数组成员
本书是关于数组和指针的,所以将重点讲解最后两项,即可变长数组和柔性数组成员。不过,由于并不是所有读者都使用 C99 的运行环境,所以对于 C99 特有的功能,我们会在编程时明确指出。
C99 也为 JIS 标准所采用,其标准号为 JIS X3010:2003。从日本标准协会的网页上可以购买其纸质版或 PDF 版。
在本书中,当“标准”一词单独出现时,特指 JIS X3010:2003,因为其标准文档是目前最容易获取的。
另外,虽然该标准文档的 PDF 版可以从网页下载,但为了防止不法之徒通过廉价出售大量复制的文件牟利,文档的每一页上都会嵌入购买者的姓名。说句题外话,个人希望除了 JIS 的标准文档,电子书也能尽量采取这种形式出售,以满足读者在其他终端上阅读或备份的需求。
1-1-8 C11
C11 是 2011 年 12 月 8 日制定的 C 语言标准,其正式名称为 ISO/IEC 9899:2011,这是截至 2017 年的最新版本*。
* 现行的 C 语言标准是ISO/IEC 9899:2018,发布于 2018 年 7 月。——编者注
C11 里增加了多线程支持、Unicode 支持以及无名联合体等功能,不过这些功能和本书的主题——数组和指针关系不大。关于库函数的部分内容,我们将在 6-1 节讲解。
C11 的标准文档目前还未经 JIS 处理,因而只有英文版。我们可以从 ISO 的 Web 站点购买 PDF 版,草案可以从开放标准网下载。
1-1-9 C 语言的理念
ANSI C 标准附有一份 Rationale(理论依据)文件资料(但它并非该标准的一部分)*。
* 对于以下引用部分,C99 版与当初的 ANSI C 是一样的。
其中提到了“Keep the spirit of C”(保持 C 的精神),关于“C 的精神”是这样介绍的:
- 相信程序员(Trust the programmer.)
- 不要阻止程序员做应该做的(Don't prevent the programmer from doing what needs to be done.)
- 保持语言的小巧和简单(Keep the language small and simple.)
- 对每种操作仅提供一种方法(Provide only one way to do an operation.)
- 即使损失可移植性,也要追求运行效率(Make it fast, even if it is not guaranteed to be portable.)
开头两点最重要——好吧,这么胡闹的事情还真能说出口。
C 是危险的语言。一着不慎全盘皆输的陷阱随处可见。
尤其是,可以说几乎所有的 C 语言实现都没有进行运行时检查。例如,在向超出数组范围的地址执行写入操作时,现在大部分语言可以当场报错,但 C 语言的大部分运行环境却是默默地执行写入操作,最终导致无关区域的内存数据遭到破坏。
C 语言是基于程序员无所不能的理念设计出来的。在设计 C 语言时,优先考虑的是:
- 如何才能简单地实现编译器(而不是让 C 语言的使用者能够简单地编程)
- 如何才能写出能够生成高效率的执行代码的程序(而不是优化编译器,使之生成高效率的执行代码)
而安全性的问题被完全忽略了。不管怎么说,C 语言原本就只是 UNIX 的开发者为了满足自己的使用需求而开发出来的。
幸运的是,如今的操作系统可以在程序明显出现问题时立刻终止程序运行。UNIX 会报出“段错误”(segmentation fault)或“总线错误”(bus error)这类错误提示,而 Windows 则会弹出“xx.exe 已停止工作”这样的消息。
同样,这时也不能抱怨“净给我找事儿,这个混蛋!”而应该心怀感激地说一声:“谢谢您了,操作系统先生!”然后认真地去调试。
话虽如此,靠操作系统来终止程序,说到底是沾了运气好的光。在程序明显是要访问奇怪的内存位置时,操作系统多半会替我们终止程序的运行。但麻烦的是那种在只越界几个字符的位置进行写入的情况。要追踪这类 Bug 非常困难,因为这些错误的症状很少会马上显现。
第 2 章将说明 C 语言具体是怎样使用内存的。理解了这一点,对解决此类 Bug 多少会有些帮助。
要点
操作系统帮助我们终止程序,这是运气好的情况。
麻烦的是操作系统无法终止的“一点点的”内存空间损坏。
1-1-10 C 语言的主体
这里先出个题目考考大家。
请从下面的单词中,选出 C 语言规定的关键字(保留字)。
if
printf
main
malloc sizeof
正确答案是 if
和 sizeof
。
“printf
和 malloc
不必多说,连 main
也不是 C 语言的关键字吗?”
有这样想法的读者,请拿起手头的书查一查。相信大部分 C 语言入门书会给出 C 语言的关键字列表。
C 语言之前的许多语言把输入输出作为语言自身功能的一部分。比如 Pascal 是用 write()
这样的标准过程* 来实现相当于 C 语言的 printf()
功能的。Pascal 的语法规则对它有特殊处理*。
* Pascal 中有函数(function)和过程(procedure)两种概念,
write()
属于标准过程。——译者注* 根据 JIS X3008 中 6.6.4.1 的备注内容,“标准过程及标准函数不一定遵从过程和函数的一般规则”。
与此相对,C 语言将 printf()
这样的输入输出功能从语言主体剥离,使之成为单纯的库函数。对于编译器来说,printf()
函数和一般程序员所写的函数并无差异*。
* 现在也有可以帮我们检查
printf()
的参数的编译器。
在程序员看来,输入输出只需调用一下 printf()
即可实现,但其实背后的处理相当复杂,需要向操作系统提出各种各样的请求等。C 语言并没有把这类复杂处理归拢在语言主体里,而是全都放到了库里。
很多编译型语言会将被称为“运行时例程”(run-time routine)的机器码“悄悄地”嵌入编译(链接)后的程序中。输入输出这样的功能就包含在运行时例程里。但 C 语言里几乎没有必须要“悄悄地”嵌入运行时例程的复杂功能*。由于稍微复杂一点的功能全都被封装到了库里,所以程序员只需显式地调用函数。
* 当初在 PDP-11 的运行环境上,似乎是悄悄地嵌入了 32 位的乘除运算,以及处理函数入口和出口的运行时例程。
这既是 C 语言的缺点,也是它的优点。正因为这个优点,C 语言程序开发和学习才变得容易了一些。
1-1-11 C 语言曾是只能使用标量的语言
对于标量(scalar)这个词,大家可能有些陌生。
简单地说,标量指的就是 char
、int
、double
、枚举型等算术类型以及指针。相对地,像数组、结构体和联合体这样由多个标量组合而成的类型,我们称为聚合类型(aggregate)。
早期的 C 语言能够一起处理的只有标量。
我经常听到初学者有以下疑问。
if (str == "abc")
写了这样的代码,但是无法运行。我确实已经把
abc
赋给str
了,但是条件表达式就是无法判为真。为什么呢?
对于这个疑问,可以给出这样的回答:“这个表达式并没有进行字符串的比较,只是比较了指针。”除此之外,我们也可以换一个角度来说明:
字符串其实就是
char
类型的数组,也就是说它不是标量,所以在 C 语言中不能用==
一下子对数组里的所有元素进行比较。
说到早期的 C 语言能够一起实现的功能,那就只有将标量这种“小巧的”类型从右边放到左边(赋值),或者标量之间的运算、标量之间的比较等。
C 就是这样一门语言,输入输出自不必说,就连数组和结构体,C 也放弃了通过语言自身的功能进行整合利用。
但是,得益于以下几个功能,ANSI C 中能够整合利用聚合类型了。
- 结构体的整体赋值
- 将结构体作为函数参数传递
- 将结构体作为函数返回值返回
auto
变量的初始化
这些当然都是非常便利的功能,虽然如今这些功能都可以积极地使用了(不如说是应该使用),但在早期的 C 语言里,根本没有这些功能。在理解 C 语言的基本原则时,以早期的 C 语言为基准来理解也不是什么坏事。
特别要指出来的是,别说 ANSI C 了,即便是 C99 和 C11,也还不能做到对数组进行整合利用。将数组赋值给另外一个数组,或者将数组作为参数传递给其他函数等做法在 C 语言中是不存在的。
但是,因为结构体是可以被整合利用的(但不能进行比较),所以在实际的编程中,应该积极地使用其可用的功能*。
* 话虽如此,在将结构体当作参数传递时,如果结构体的长度太长,在运行效率上可能会出现问题。