基础-程序与内存(二)
程序就是用一些“指令”去处理一些“数据”。
“指令”其实也是数据,它存在“代码段”中。数据呢,上一节说了,被区分处理,分别放在“数据段”,“堆”内存段,“栈”内存段。根据什么来区分哪种数据放在哪个内存段中呢?
全局数据
假设“人”是一段程序,那么有一些数据是生俱来的,我们永远切割不了的。比如年龄、身高、体重、性别等等。无论你喜欢不喜欢,也无论你关心不关心,这些数据其实一直存在。我知道我女儿出生时是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 *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——静态数据的初始值)
现在进入主函数了。通过cout(念:C-OUT,或者C、O、U、T),我们在屏幕上分两行输出了age和weight的当前值,如前所述,此时是0岁和3公斤。

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

(代码片段3——改变静态数据的值)
虽然后面的代码中,我们没有再用到age和weight;但事实上这二者将一直存在着,存在着,直到程序的退出……
栈数据
接下来是“栈数据”:

(代码片段4——栈数据)
这段代码中,我们特意加了一对花括号{...},用于建立出一段短暂的“时空”。数据number_of_the_seat生存在这个短暂的时空内。就像我们曾经的比喻:“生如夏花”一样,它的生存期仅限于图中红线所标的范围。
堆数据
我们用“彩票”与“堆数据”进行类比;彩票至少用三个数据:
1、彩票的编号。假设第一张彩票是1号,第二张彩票是2号、由此类推,编号是不可重复的。
2、彩票的选号。可以是机选,也可以手选。就是一个“幸运数字”。选号可能重复,如果你和我选的号一样,要是都中了500万,结局就是多人平分。
3、彩票的中奖额。是500万?还是5元钱?
编号对应于数据的内存地址。有关内存地址我们下一节还要做更全面的讲解。现在可以形像地理解为,第一个数据放在内存的第一个“格子”里,第二个数据放在内存的第二个“格子”里……这样每个数据就都有了一个唯一的编号,即内存地址。
数据的值,可以对应成彩票的中奖额。不过对于“堆”数据,它的值又是一个内存格子的编号,这里我们比喻于彩票的“选号”。一张彩票的真正的价值,得看它的选号所对应的值,——就是它的中奖额。

(代码片段5——堆数据的定义)
平常我们买彩票,可能很少关心它还有一个“编号”。我们把“堆”数据比喻成“彩票”,那么,“栈”数据可以比喻成人民币 :人民币也是有编号——别告诉我你注意过,但我们同样很少关心这个编号。不过在一些特殊时刻,这个编号会有很重要的意义,其原因就在它的唯一性。程序数据也是这样。不管是静态数据,还是堆或栈数据,都具备编号。这就是它们的内存地址。平常我们关心的得不多,但在某些时候它的作用却很大(哪些时候?以后再说)。如何看到一个数据的内存地址呢?在C/C++语言里,用的&这个操作符,我们称&操作符为“取址”符。
int age = 3;
std::cout << age << ", " << (int)&age << std::endl;
请大家思考这两行代码的输出内容是什么?
接下来,我们重点谈谈“堆”数据的特殊之处。和人民币可以从其面额直接表示出它的价值不同,彩票的价值,我们更关心的是它的中奖额;而中奖额是多少决定于彩票的“选号”。如何决定选号,可以“手选”,也可以“机选”。下一小节我们会讲到“手选”,今天的例子只有“机选”:

(代码片段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——堆数据不确定的初始值)
直接“西奥特”或“西鸥优踢”lottery,我们看到的是另一个“内存地址”。要看到一个“堆”数据,C/C++规定必须在一个“指针”数据前面加*操作符,才可以那个“内存地址”内所放的值。因此*操作符,称为“取值操作符”,它所的事和“取值操作符”正好相反。图例中的代码,输出一个莫名其妙的数字,为什么?
在编程世界,操作任何“不确定”的数据,都是一件危险的事。彩票买多了,容易赔;曾经,老板发给我一套房子,我兴匆匆地跑过去时,发现是一块坟地。
下面我们为lottery赋值——严格地讲,lottery已经有值了,它的值就是另一块内存的地址——下面,让我们为“lottery所存的那个内存地址所对应的那块内存”赋值吧!

(代码片段8——为堆数据赋值)
现在,我们中了500万大奖。我的地盘我做主。我是程序员,我写个代码假装自己中了500万还不行吗?——这就是YY。传说中程序员最擅长的本事之一。
让我们以这段程序的完整输出作为结束:
(完整输出)
下一节《程序与内存(三)》见。
|