教程 - 软件 - 文章 - 论坛

::白话C++::

基础-程序与内存(二)

程序就是用一些“指令”去处理一些“数据”。

“指令”其实也是数据,它存在“代码段”中。数据呢,上一节说了,被区分处理,分别放在“数据段”,“堆”内存段,“栈”内存段。根据什么来区分哪种数据放在哪个内存段中呢?

全局数据

假设“人”是一段程序,那么有一些数据是生俱来的,我们永远切割不了的。比如年龄、身高、体重、性别等等。无论你喜欢不喜欢,也无论你关心不关心,这些数据其实一直存在。我知道我女儿出生时是6斤,但我不知道30多年前那个风雨交加的夜晚,我在一个小山村出生时,我是几斤几两——虽然我不知道“我出生时的体重”,但我知道“我出生时的体重”这个数据,它是客观存在的。

这一类“与生俱来”,并且显然会陪伴程序整个运行周期的数据,称为“静态数据”。它们被存放在“数据段”。注意,“静态”是指这些数据将在程序运行期间一直存在,而不是说这些数据不会改变。就像一个人的“体重”,会在一生中不断变化。当然,我们是可以规定一些数据不允许改变,比如如果你反对变性,那可以指定“性别”是一个在出生以后,就不能被改变的数据项,这是后话了。静态数据通常也称为“全局数据”。注意,这里的“全局”是指它的生存期。

局部数据

还有一类数据,或者不是每个人一定都会拥有的,或者不是一个人一生都会拥有,相反,它是你生活中临时出现的数据,这些数据也很多。举一个夸张的。某年某月某日的某个早上,你在上班的路上突然买了一张彩票,并且中了大奖。“中了什么大奖”、“奖额是多少”,这些数据虽然每个人都可能有,但往往只有少数人会真正拥有。因些对于多数,这是一些平常根本不需要的数据。所以这些数据如果被当成“静态数据”而一直放在内存中的话,就是浪费内存空间了。

在某一局部范围内才需要有的数据,在人的一生其实很多,比中奖现实得的多的局部数据比如:你去坐长途汽车,上了车你就有多拥有了一个数据:座号,你必须按号入座,但这个号码,你到达目的地后,一下车,它就失去了继续存在的意义。

这一类需要时才拥有的数据,我们可以称为“局部数据”,如果你觉得不好理解,不如就把它当成是“临时数据”的另一种称呼。在程序中,“局部数据”一般就放在“堆”,或“栈”里。顾名思义,“临时数据”我们可以在需要时才创建出它们,在不需要时,就干掉它们。因此它们的“生死”的就变得重要了。而事实上,“栈”与“堆”里数据的区别,也正是在这一点上。

让我先回忆一上,数据的“生”与“死”分别代表什么:“生”就是占用了内存,“死”就是归还了内存。

栈数据

栈数据的典型的特点是“自生自灭”。

生:栈数据在被定义时,就自动“生”了,即该数据自动占用了它所需要的内存。

死:对应地,当程序执行时,出了指定的范围,该数据自动归还它所占用的内存。

堆数据

通常,堆数据不是单独存在,而是先结合一个栈数据或静态数据存在。典型过程如下:

首先定义一个有特殊意义的栈数据(或静态数据,为了方便,下面仅提栈数据)。

然后在需要时,再通过这个栈数据,通过代码创建一个堆数据。即,此时才有堆数据的“生”,即:它占用了一定的内存空间,并且它占用了以后,除非我们再主动写特定代码去释放它。否则它就永远占用着了,直到程序退出。

之后,堆数据一直占着内存。除非我们再通过它所结合的那个“栈”数据去干掉它。因此,堆数据的典型特点是:要操作这“堆”数据,包括“生”它和“杀”它,都需要间接地通过一个特殊的“栈”数据来操作。这个特殊数据既然是“栈”数据,所以它当然也是自生自灭的。我们需要特别地管理好这个栈数据,因为如果丢了它,就会无法去操作那“堆”数据了。

听起来好复杂,是吧?还是来说说“彩票”的事吧。

前面所说的“彩票”似乎可以拿来做堆数据的例子:其中那个“特殊的栈数据”,就是“彩票”那张纸。而真正的堆数据,则是指中奖后的彩票所对应的着那“堆”钱。

首先,因为你并不是嘴里衔着彩票出生的,相反,你是头脑一热时才去买了一张彩票。所以彩票是一个栈数据。它是自生自灭的:在下一期开票时,这张过期的票就自然失效了。

然后就是激动人心的大奖了。对你的人生来说,本来是没有那笔500万的——虽然此时那张薄薄的彩票占用你上衣口袋的一点点空间,但也并不意味着一定会中奖。然而,幸运女神注意到了你,你中奖了,领奖需要你持有彩票,这就是通过那个“栈”数据去申请得到真实的“堆”数据的过程。

生活中与类“堆”数据有一定相似性的真实数据,有不少。比如“存折”和“存款”,又如农民工从雇主手里拿来的“白条”工资等等。不过比喻总是苍白的,因为程序虽然是现实问题的映射,但它总是要映射得简单一些、纯粹一些——如果越映射越复杂,越映射越暧昧,我们要电脑程序做什么——所以,程序中的数据间的关系,往往无法在现实得到一个完像。我们还是来真实地讲讲内存中的数据吧。

内存地址

数据是有地址的,由于数据是存在内存的,所以数据地址其实就是内存的地址。

不过,什么叫“地址”呢?现实生活中,往往就是“中国福建省厦门市湖里区XXX街YYY号几零几室”这样的内容。其实地址的功能就是为了找到某个指定的对象,像门牌号。内存地址长什么样子?猜都猜得出肯定是数字编号。确实数字编号最直观简便,邮政也不推编码嘛。

无论是栈还是堆中的内存,都有地址(事实上数据段和代码段的内存也有地址)。假设有一个堆数据,这个堆数据的值上“5000000”。并且假设它是存放在内存地址为1310600的堆内存中,则图示如下:

堆数据

(一个堆数据和它的地址)

在图中,我们用一个格子表示堆内存的某一片段,该内存片段中存着一个数据:500万;并且,这段内存的地址是1310600。不是说堆数据一般需要结合了另外一个特殊的栈数据吗?那个栈数据在哪里呢?让我们继续画:

堆,栈对应

(一个栈内存中,存着一个堆内存地址)

看到了什么变化?我们看到了“有一块栈内存的空间中,存放着一块堆内存的地址”。栈内存本身当然也有地址,如上图示意的话,它是9319830,它类似于彩票自身的唯一编号,然而决定你是否中奖的,是彩票的选号。这里类似就是1310600这个数字。只有通过1310600这个数字,我们才能找到5000000这个存放在堆空间中的值。就像只有通过彩票的号码,我们才能最终拿到500万大奖;或者只有通过你的帐号,才能操作银行里的存款。

实际代码

基础

数据存在内存中,当程序要操作某一数据时,其实,是通过内存地址来操作数据的。内存地址是一串无特定意义的数字,假设我们想在在内存地址为1250780的位置存入一个数据:20,表示某人的年龄,那总不能这样写吧:

1250780 = 20;

这样太别扭了,并且,我们也没办法在程序运行之前,就知道数据到底会存在哪段内存上。解决方法是为数据取一个名字,一来,名字可以表示具体的意义,二来名字可以在编译时,确定它的将来映射到内存时的位置。 前例结果如下:

age = 20;

我们可以理解为age就代表了一个内存地址。

另外,C++要求为数据指定类型。最常见的就是“整数”类型。下面的例子中采用“整数”来表达人的年龄和体重等数据。你可能会说,这二者都可能带小数啊?确实如此,今天你可能是25.6岁,并且体重64.5公斤……因为只是示例,我们就用最常见的整数来表达吧。更多的类型我们以后才学习。

在C++程序中,定义一个整数数据,句法如下:

int age;

这行代码,定义了age这个数据,并且指出它是一个整数。

如果要修改它的值,则通过赋值语句,类似我们学的代数:

age = 20;

在程序运行到这一句时,相当于让age代表的内存地址中,放入了20这个数。

上这方法,可以直接用在“静态数据”或“栈”数据上。

如此,一个“静态数据”或“栈”数据产生、赋值、释放过程是:

int age;
age = 20;

看不到特别的“生、死”操作。这是栈数据或静态数据的特性。

对于堆数据。通常它需要结合一个“特殊”的栈数据(或者静态数据,语法完全一致),然后再通过手工写的语句,来产生一个堆数据,并且将堆数据的内存地址放在前面产生的栈数据中:

int *weight;

这里定义了weight这个栈数据,特殊的是名字之前,类型之后,多了一个星号:*。注意,此时weight就是所说的特殊的栈数据,并且它的名字就叫weight,而不是*weight。星号在这里是指示了它是一个“特殊”数据,也就是“指针”数据。

然后我们要通过代码,产生一个堆数据,并且将该堆数据所在的内存地址放在weight中,语法如下:

weight = new int;

现在,我们要在设置产生的堆数据值为60的话,应该如何呢?

错误做法:

weight = 60; <---错了!因为weight是其实是那个特殊栈数据。

正确做法:

*weigth = 60;

当我们不再需要堆数据时,还必须写代码完成释放,这时用的是“delete”这个词。

delete weight;

注意,delete时和new时一样,没有用到星号。

如此,一个有关堆数据产生、赋值、释放的过程是:

int *weight;
weight = new int;
*weight = 60;
delete weight;

除了注意到有专门的“new”和“delete”对应于生死操作之外,我们还需要注意到如何使用“*”这个符号的用法。

代码实例

新建一个控制台程序,在主文件里代码如下:

//---------------------------------------------------------------------------

#pragma hdrstop
#include <iostream>
#include <cstdlib>
//---------------------------------------------------------------------------

using namespace std;

////////////////////////////////////////
//这里是两个“全局变量”
int age;		//年龄
int weight = 3;	//体重(单位:公斤)
////////////////////////////////////////

#pragma argsused
int main(int argc, char* argv[])
{
	cout << "出生时,您的年龄是: " << age << "岁" << endl;
	cout << "出生时,您的体重是: " << weight << "公斤" << endl;

	//...25年过去了...

	age = 25;
	weight = 64;

	cout << "现在,您的年龄是: " << age << "岁" << endl;
	cout << "现在,您的体重是: " << weight << "公斤" << endl;


	{
		//假设您现在要坐长途车,那么您需要一个座号:
		int number_of_the_seat = 12;
		cout << "您的座号是: " << number_of_the_seat << endl;
	}

	//假设您买了一张彩票:
	int * lottery;
	cout << "您的彩票编号是: " << (int)(&lottery) << endl;

	//抽奖:
	lottery = new int;
	cout << "通过机选,您的彩票选号为:" << (int)(lottery) << endl;

	//中奖金额:
	cout << "您的彩票中奖了,奖额为:" << *lottery 
		<< ", (这个数应该是个天文数字,有些不可置信吧?)" << endl;

	*lottery = 5000000;
	cout << "现在是500万大奖: " << *lottery << endl;

	system("pause");

	return 0;
}
//---------------------------------------------------------------------------

下面,我们逐段分析上面的代码。重点在于哪些是“静态数据”,哪些是“堆数据”或“栈数据”。

静态数据

代码片段一——静态数据定义

(代码片段1——静态数据定义)

age 和 weight 是两个“全局变量”,属于“静态数据”。和其它几个变量,例如: number_of_the_seat lott的明显区别,就是age和weight两个数据没有被定义在main()函数内。

weight (体重)被初始化为3,用于表示出生时3公斤。那么出生是几岁了?表面上看来,age没有被赋值,但是静态数据会被自动初始化为:0。请继续看下面的代码:

代码片段2——静态数据的初始值

(代码片段2——静态数据的初始值)

现在进入主函数了。通过cout(念:C-OUT,或者C、O、U、T),我们在屏幕上分两行输出了age和weight的当前值,如前所述,此时是0岁和3公斤。

代码片段2的输出结果

(age和weight的初始化值)

静态数据在程序运行期间“一直存在”,然后,和其它数据一样,它们的值可以一直被改变。下面的代码片段验证了这一点。

代码片段3——改变静态数据的值

(代码片段3——改变静态数据的值)

虽然后面的代码中,我们没有再用到age和weight;但事实上这二者将一直存在着,存在着,直到程序的退出……

栈数据

接下来是“栈数据”:

代码片段4——栈数据

(代码片段4——栈数据)

这段代码中,我们特意加了一对花括号{...},用于建立出一段短暂的“时空”。数据number_of_the_seat生存在这个短暂的时空内。就像我们曾经的比喻:“生如夏花”一样,它的生存期仅限于图中红线所标的范围。

堆数据

我们用“彩票”与“堆数据”进行类比;彩票至少用三个数据:

1、彩票的编号。假设第一张彩票是1号,第二张彩票是2号、由此类推,编号是不可重复的。

2、彩票的选号。可以是机选,也可以手选。就是一个“幸运数字”。选号可能重复,如果你和我选的号一样,要是都中了500万,结局就是多人平分。

3、彩票的中奖额。是500万?还是5元钱?

编号对应于数据的内存地址。有关内存地址我们下一节还要做更全面的讲解。现在可以形像地理解为,第一个数据放在内存的第一个“格子”里,第二个数据放在内存的第二个“格子”里……这样每个数据就都有了一个唯一的编号,即内存地址。

数据的值,可以对应成彩票的中奖额。不过对于“堆”数据,它的值又是一个内存格子的编号,这里我们比喻于彩票的“选号”。一张彩票的真正的价值,得看它的选号所对应的值,——就是它的中奖额。

代码片段5——堆数据的定义

(代码片段5——堆数据的定义)

平常我们买彩票,可能很少关心它还有一个“编号”。我们把“堆”数据比喻成“彩票”,那么,“栈”数据可以比喻成人民币 :人民币也是有编号——别告诉我你注意过,但我们同样很少关心这个编号。不过在一些特殊时刻,这个编号会有很重要的意义,其原因就在它的唯一性。程序数据也是这样。不管是静态数据,还是堆或栈数据,都具备编号。这就是它们的内存地址。平常我们关心的得不多,但在某些时候它的作用却很大(哪些时候?以后再说)。如何看到一个数据的内存地址呢?在C/C++语言里,用的&这个操作符,我们称&操作符为“取址”符。

int age = 3;

std::cout << age << ", " << (int)&age << std::endl;

请大家思考这两行代码的输出内容是什么?

接下来,我们重点谈谈“堆”数据的特殊之处。和人民币可以从其面额直接表示出它的价值不同,彩票的价值,我们更关心的是它的中奖额;而中奖额是多少决定于彩票的“选号”。如何决定选号,可以“手选”,也可以“机选”。下一小节我们会讲到“手选”,今天的例子只有“机选”:

代码片段6——分配堆内存

(代码片段6——分配堆内存)

通过new操作符,程序会自动分配一段堆内存。“分配一段堆内存”似乎听着有些高深。没房的兄弟们经常在夜里流着口水做梦“分配房子”。老板给你分房子,当然不是抱着一间三室二厅的房让你接过去,而是扔一串钥匙给你,告诉你“A座12橦808室”归你啦!分配内存也如此,通过new操作,在“堆”这块地上找到一块没有占用的内存空间,然后把这块内存空间的地址赋给指定数据,本例这个数据叫“lottery”。因此,我们用cout输出lottery的值,其实就是输出那串地址。内存的地址也是一串整数,而不是“A座12橦808室”。回到“彩票”的比喻上来,通过new操作,就相于机器为你选了一个号,并且打印到既定编号的那张彩票上。

如果你是房奴,那么你现在一定在想“A座12橦808室”是一套什么样的房子呢?“楼中楼”?面积200平方米的3房2厅?如果是彩迷,那么你现在一定想手头的这个号,会中什么奖?500万乎?20万乎……lottery是一个程序数据,它没有什么梦想,程序员创建了它,理应赋给它一个确定的值。不过在C/C++的世界里,有一个有趣的现象:当你分配一段内存,那么在你主动为这个内存赋值之前,这块内存中存放的值是不确定的;基于这一点,有了我们这样一段超无聊的代码:

代码片段7——堆数据不确定的初始值

(代码片段7——堆数据不确定的初始值)

直接“西奥特”或“西鸥优踢”lottery,我们看到的是另一个“内存地址”。要看到一个“堆”数据,C/C++规定必须在一个“指针”数据前面加*操作符,才可以那个“内存地址”内所放的值。因此*操作符,称为“取值操作符”,它所的事和“取值操作符”正好相反。图例中的代码,输出一个莫名其妙的数字,为什么?

在编程世界,操作任何“不确定”的数据,都是一件危险的事。彩票买多了,容易赔;曾经,老板发给我一套房子,我兴匆匆地跑过去时,发现是一块坟地。

下面我们为lottery赋值——严格地讲,lottery已经有值了,它的值就是另一块内存的地址——下面,让我们为“lottery所存的那个内存地址所对应的那块内存”赋值吧!

代码片段8——为堆数据赋值

(代码片段8——为堆数据赋值)

现在,我们中了500万大奖。我的地盘我做主。我是程序员,我写个代码假装自己中了500万还不行吗?——这就是YY。传说中程序员最擅长的本事之一。

让我们以这段程序的完整输出作为结束:

完整输出

(完整输出)

下一节《程序与内存(三)》见。

版权所有 谢绝复制。作者:南郁(nanyu) www.d2school.com