第6章 玩转指针
C程序设计新思维
他就是那个
喜欢我们所有歌曲的人,
他喜欢一起哼唱,
他喜欢边开枪边唱,
但是他不知道这歌的意义。
——选自Nirvana的歌曲“In Bloom(风华正茂)”
就像一首描述音乐的歌曲、一部刻画好莱坞的电影,指针就是一种描述其他数据的数据。我们很容易被指针搞崩溃,像引用的引用、别名、内存管理和malloc之类的东西,很容易把我们搞得天旋地转。但是,这些纷繁复杂的痛苦可以分解为独立的片段。例如,我们可以使用指针作为别名,这样就不需要再关注malloc,20世纪90年代的教科书常教导我们需要熟练掌握这个函数。一方面,C语法中星号的用法常常令人困惑。另一方面,C的语法也向我们提供了工具,用于处理那些格外复杂的指针细节,例如函数指针。
6.1 自动、静态和手工内存
C提供了3种基本的内存管理模式,比大多数语言要多上2种,事实上我们真正需要关注的也只是其中一种。但是,在本书第12章中,我还要向读者介绍2种额外的内存模型。
自动
我们在第一次使用一个变量时对它进行声明,当离开自己的作用域之后变量就会被销毁。如果不使用static关键字,在函数内部所声明的所有变量都是自动变量。一般的编程语言只具有自动类型的数据。
静态
静态变量在程序的整个生命期内一直存在。数组的长度在一开始就是固定的,但它所包含的值却可以改变(因此它并不是完全静态的)。数据是在main函数启动之前被初始化的,因此所有的初始化值都必须是常量,并且不需要计算。在函数的外部所声明的(属于文件作用域)和函数内部用static关键字声明的变量都是静态变量。最后,如果忘了对一个静态变量进行初始化,它会默认初始化为全零(或NULL)。
手工
手工类型的变量涉及malloc和free函数,这也是许多段错误的根源。这种内存模型是让许多C编程员欲哭无泪的罪魁祸首。另外,这也是唯一允许在声明之后改变数组长度的内存类型。
下面这张表显示了三种内存模型的区别所在。在接下来的几章中,我们将详细讨论这些区别。
表中有些特性适用于变量,例如改变长度和方便的初始化。有些特性是内存系统的技术性结果,例如是否可以在初始化时设置值。因此,如果我们想要一个不同的特性,例如能够在运行时改变长度,就不得不关注malloc函数和指针所指向的堆。如果我们可以抹掉这一切重新开始,我们就不会把这三组特性与相关联的技术性烦恼捆绑在一起。但是,我们必须面对这一切。
这些就是当我们把数据存放到内存时所浮现的相关问题。它与变量本身不同,可以产生另一层次的乐趣:
1.如果我们用static关键字在函数外部或者函数内部声明一个struct、char、int、double或其他类型变量,它就是静态的。否则,它们就是动态的。
2. 如果我们声明了一个指针,它本身具有一种内存类型,很可能如规则1所述的属于自动或静态类型。但是,这个指针可以指向三种数据类型的任何一种,包括指向由malloc函数所分配数据的静态指针和指向静态数据的自动指针。所有的组合都是可以成立的。
自己动手:检查一些现有的代码,研究变量的分类:哪些数据位于静态内存、自动内存或手动内存,哪些变量是指向手工内存的指针、指向静态值的自动指针等。如果手头上没有现成的代码,可以用例6-6为素材完成这个练习。
任何函数都在内存中占据一个空间,称为函数帧,用以保存与这个函数有关的信息,例如当函数执行完成之后返回到哪里,以及保存所有自动分配的变量的空间。
当一个函数(例如main)调用另一个函数时,第一个函数的函数帧中的活动就会暂停,并且一个新的函数被添加到这个堆栈帧中。当函数执行完成时,它的帧就从这个堆栈帧中弹出。在这个过程中,保存在这个函数帧中的所有变量都会消失。
遗憾的是,堆栈的长度限制要比一般内存小得多,大约是2~3M(本书写作时在Linux系统下大致如此)。这点空间对于保存莎士比亚的所有悲剧作品已经足够,因此不必担心分配一个包含10000个整数的数组会出现问题。但是,我们很可能会用到更大的数据集,因此需要使用malloc为它们在其它地方分配空间。
通过malloc所分配的内存并不位于堆栈中,而是在系统中称为堆的空间中。堆的大小可能有限制,也可能没有限制。在普通的PC机上,可以粗略地认为堆的大小就是所有可用内存的大小。