02 January 2008

Stack and heaps

Very good article on stack and heaps from CSDN blog.
 
堆和栈的区别

一、预备知识—程序的内存分配
一个由c/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
二、例子程序
这是一个前辈写的,非常详细
//main.cpp
int a = 0; 全局初始化区
char *p1; 全局未初始化区
main()
{
int b; 栈
char s[] = "abc"; 栈
char *p2; 栈
char *p3 = "123456"; 123456\0在常量区,p3在栈上。
static int c =0; 全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}


二、堆和栈的理论知识
2.1申请方式
stack:
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大小,在c中malloc函数
如p1 = (char *)malloc(10);
在C++中用new运算符
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在栈中的。


2.2
申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会 遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内 存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大 小,系统会自动的将多余的那部分重新放入空闲链表中。

2.3申请大小的限制
栈:在Windows下,栈是向低地址扩展的数 据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总 之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。


2.4申请效率的比较:
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。

2.5堆和栈中的存储内容
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

2.6存取效率的比较

char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:
#include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
对应的汇编代码
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。


2.7小结:
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。 (经典!)

CS truth and illussion

From CSDN :

幻象与真实--计算机的本质 

一、计算机的本质
计算机的本质是一个复杂的数字电路,数字电路只能处理和输出高低两种电位组成的信号,所以计算机只认识高低两种电位状态!
任 何人类的观念(比如数字,字符,图像等),在计算机里都被转换成这两种状态,再通过硬件电路或者程序的方式,使对这些状态的输出,符合这些观念本身应有的 规律和形式,因为人看不到内部的转换和处理,只看到外部输出的样子,于是,在人看来,计算机可以完成多种多样的任务,相当神奇!
计算机能够完成多种任务的两大基石:
1.编码:用计算机的高低电位对应外部事物的各种状态。
2.输出:以外部事物的样子把编码处理后的结果展示出来。(可以通过硬件电路也可以通过软件程序)
编码的概念:
用少量简单基本的符号,选用一定的组合规则,以表示出大量复杂多样的信号。基本符号的种类和这些符号的组合则是一切信息编码的两大要素,例如用10个阿拉伯数码表示数字,用26个英文字母表示英文词汇等,这就是编码的典型案例。计算机中,广泛采用01两个基本符号组成的基2,或称二进制码。(一般用高低两个电位表示10
--《计算机组成与结构》
二、具体事例
1.字符
编码:以ASCII字符集编码为例,将数字、大小写字母、标点符号、控制字符等128个符号,对应到 0x0 0x7e 上。也就是说用 0x0 - 0x7e 128个状态编码了128个符号。
输出:早期的计算机通过符号显示器这一硬件来将字符显示出来。比如当接收到计算机发过来的一个字节 0x58 时,crt将根据0x58指向的字符rom中的高低电位控制电子束的开关,在屏幕上组成图形“X”。
2.数值
编码:数值是表示数量多少的数据。对数值数据的编码,计算机中有:原码,补码,反码。
处理,计算机的运算电路,根据数值编码方案设计,使输入状态经过电路处理后得到的输出状态符合加减乘除等运算规则。比如,输入 ▂ 和 ▂ 输出 █ █ ,符合 10 + 01 = 11
输出:将结果数字转换成ascii编码,送字符显示设备。比如将 5 )转换成ascii字符 5 ▂ ▂ █ █ ▂ █ ▂ █),然后送显卡画图。
3.图像
编码:将点的位置和颜色等信息,对应到一定的高低电位序列,实现编码。
输出:显示设备根据点的信息,在显示器上输出一个点,大量的点按一定规则排列,形成人们观念中的图像。
三、一个重要理念:计算机是一部状态机,不是数字机。
计算机不认识数字,只认识高低两个状态。
数字只表示高低电位的排列状态。比如,0xb8 ,它并不代表数量,只代表电位排列状态,化成二进制为:10111000 ,就是代表: █ █ █ ▂ 这一状态。
自然界中数字的概念:
表示事物个数的符号。
进制的概念:
数学上的进位制本来是人们为了计数和运算的方便而约定的,约定逢二进一,就是二进制,约定逢三进一,就是三进制,依次类推。不同的进位制,除了繁简的差异外,没有任何本质上的区别。也就是说,不同的进制,只是自身的形式不同,其表示数目的本质是一样的,比如二进制100与十六进制4 都表示一样的数目,他们本质是一样的,仅仅形式不同。以计算机举例,内存中有 █ █ ,你可以用二进制的概念叫他为 1011 ,亦可以以十六进制叫他为 b ,也可以以十进制叫他为 11 。无论怎么叫,他内存中的状态始终不变。
因为计算机里只有高低两种状态,所以数字无论是二进制还是十六进制,只是这种状态排列的一种表示方式。在这里数字已经失去了它本来的表示数量功能的意义了,取而代之,这里表示电位排列状态。
我比较习惯把高低电位用符号表示,而不用01.因为用数字0 1 来表示高低电位,潜在的意思是:这个东西是数字,而数字是符合运算规则的。其实,计算机内部很多东西都不是数字,比如mov指令: █ █ █ ▂ ,它并不代表一个数值,只是一个cpu能识别的电位序列信号。如果用0 1 来表示就是,10111000 ,看上去就是一个数字。还有,是否符合运算规则是内部电路来决定的,如果电路设计成 输入 ▂ 和 ▂ 输出 █ █ ,那么就符合加法运算,但如果设计成输出 ▂ ▂ 呢,再表示成输入:10 01 ,输出:00 就比较别扭了。
如果克服这些障碍,把 0 1 仅仅看成是计算机的两种状态,那么表示起来还是很方便的,因为这时不仅可以用 0 1 ,还可以转化成更方便的十六进制,象上面那条 mov 指令就可以表示成 10111000 或者 十六进制 b8 。但要记住,10111000 也好 b8 也好,只是内存中高低电位状态分布的一种表示,并不表示数量。
知道这些,就容易理解:汇编程序在汇编过程中,直接查表把ASCII 码组成的 mov 汇编成 0xb8,这里如果把 0xb8 看成一个数字,就容易迷惑,把它想象成一个高低电位分布的状态,就容易理解了。
四、ASCII 里的数字字符,与真正的数字,有时让人迷惑。
以无符号整数5为例,在计算机屏幕上看到的5,与计算机内部以原码表示的数值5是不同的。在计算机屏幕上显示的5是一个字符,它以ASCII编码,在计算机内部的电位序列为: ▂ ▂ █ █ (二进制 0011 0101 十六进制0x 35)。
而计算机内部原码表示的数字 5 电位序列为:▂ ▂ ▂ ▂ ▂ (二进制 0000 0101 十六进制0x 5)。
就通常的输入手段来说,通过键盘,在编辑器里输入,不能手工输入机器数 5 ,因为编辑器程序把键盘输入的扫描码转换成了ascii编码,但老式计算机可以通过开关,打孔等输入方式输入机器数。
要想得到机器数5 ,必须借助编译器等第三方程序。比如汇编语句“ db 5 ,这里的 5 仍然是一个ASCII 码,通过汇编程序处理后,才在内存中得到真正的机器数 5
c语句“int x=5这里的5也是ASCII ,编译后才是真正的5
计算机语言的源程序,都是ASCII码组成的,尽管如此,他们还是会提供一些语法,以便确定编译后,你到底需要ASCII还是真实的数字,比如c char x = 5;与 char x = 5’;
总之,人眼可以看到的数字,就是以ASCII形式编码的字符,而真正数字的机器码,人眼是看不到的。一般计算机语言都会提供一种语法,来确定最后编译得到的是哪种形式的编码。
五、汇编与反汇编
汇编过程:
语句“ mov ax 5 ”在机器内部的表示为一堆ASCII编码:
符号形式 二进制 十六进制
m▂ █ █ ▂ █ █ ▂ █ 01101101 6d
o▂ █ █ ▂ █ █ █ █ 01101111 6f
v▂ █ █ █ ▂ █ █ ▂ 01110110 76
空格( ▂ ▂ █ ▂ ▂ ▂ ▂ ▂ 00100000 20
a▂ █ █ ▂ ▂ ▂ ▂ █ 01100001 61
x▂ █ █ █ █ ▂ ▂ ▂ 01111000 78
,( ▂ ▂ █ ▂ █ █ ▂ ▂ 00101100 2c
5▂ ▂ █ █ ▂ █ ▂ █ 00110101 35
经过汇编程序得到cpu可以运行的真实的指令:
符号形式: █ █ █
二进制形式:101110000000010100000000
十六进制形式:0x b80500
反汇编过程,这里只反汇编为16进制数表示的结果:
还是这个语句:
█ █ █
以半字节为单位,转换为16进制数的ASCII
█ █ 0x b 转换为ASCII 编码的 “b0x62 为:█ █
0x 8 转换为ASCII 编码的 “8 0x38为: █ █ █ ▂ ▂
0x 0 转换为ASCII 编码的 “00x30为:▂ ▂ █ █
█ 0x 5 转换为ASCII 编码的 “50x35为:▂ ▂ █ █
0x 0 转换为ASCII 编码的 “00x30为:▂ ▂ █ █
0x 0 转换为ASCII 编码的 “00x30为:▂ ▂ █ █
把这些反汇编后的数据送到显存,屏幕上就显示出ASCII b80500 了。

CS Abstraction

一.万事万物皆抽象

1. 人的智力是有限的,世界是越来越复杂的,人之所以能够用有限的智力建设并操控越来越复杂的世界靠的是层层抽象。

2. 原理:(建造大楼为例)

a. 最原始的状态:人的所有精力都放在研究制造砖头上,此时没有也不可能顾及如何造大楼(因为人的智力是有限的)

b. 砖头造出来后,人就不再考虑砖头,把砖头当作原料进行更高一层的抽象,开始把所有精力放在用砖头造墙上。

c. 墙造出来后,人就不再考虑墙,把墙当作原料进行更高一层的抽象,开始把所有精力放在用墙造房间上。

………………

最后造成大楼。



计 算机科学,层的概念特别突出(与其自身复杂性有关),例如:汇编层、高级语言层…,操作系统层、网络层…,每一层都有自己关心的问题、对象及处理方法,并 对低一层的结论成果等直接拿来用,而不考虑其构成(那是低一层次要做的事)。面向过程编程中的自上而下层层分解的方法,也属于此范围。

每一个单独的层面如果仍十分巨大的话,还可以按逻辑功能把这一层分成一个个独立的模块,使思维规模进一步缩小。比如在操作系统层面,可以分成:引导模块、中断模块、进程处理模块。。。

不按照每一层面每一模块上特定的规则处理问题,企图越过层面和模块办事,必然会引起混乱。

进一步说明:在做操作系统时,主要考虑的是机器的组成,工作方式,指令集,在操作时也主要使用汇编和c这样的底层、细微的工具。高层面的概念不用考虑。

而在操作系统的基础上做应用开发时,主要考虑的是系统提供的功能调用,问题对象的描述等。使用的工具也是更高级的,如object pascal,c++,basic,python等等。低层次的概念如寄存器、内存地址等概念不需要考虑(不一个层级,属于低层)。

问题的粒度不同,使用的知识与工具也不相同。

不可纠缠于细节,应该在一个层次上熟练以后,向更高级的层面延伸。如在汇编层面掌握以后,应该用汇编的知识开发操作系统和高级语言编译器(如c),然后向操作系统和高级语言层迈进,在这个层面熟练以后,应该开发组件和解释型语言,然后向这个层面进军。




当你被各种新奇的概念、瑰丽的界面和无休止的名词弄的筋疲力尽的时候,闭上眼睛,想象一下内存中机械的、冰冷的一条条高高低低的电位指令,想想地址、寄存器、callmovinout指令,你会发现:所有概念都是人自己创造的,真实的世界原来是那么的单纯明了。

·高级语言如:cc++java,都是假的。高级的概念如:段、对象、组件都是假的。他们最终都要变成内存中一个个机器指令(电信号)。

·闭上眼睛,想象下操作系统、dll模块、编译器、链接器、载入器、各个api函数、在内存中都是一块一块的指令集合。你编的程序进行编译、链接、装载、系统调用时,其实就是在这些指令块里面callret去,就是跟这些地址打交道。看看c函数编译出来的汇编吧,就是push,然后call

·当你试图弄清一个层面的问题时,往往需要借助更深层面的知识。比如c语言的函数其实就是汇编里的几个pushcall

·程序的本质在“序”。几个简单的有限的东西通过不同的排列(序),可以构成复杂而强大的东西。比如26个字母构成英语、简单的机器指令构成缤纷的软件。(这种思想是朴实而自然的,比如简单的砖头和泥灰可以盖成各式各异的建筑,简单的原子构成复杂的世界。古代有“道生一,一生二,二生三,三生万物”的说法,可见,图灵的通用计算机思想也没有什么新奇之处。)

·计算机学科的基石。

思想方面:

1. 协议。就是大家都遵从的一种约定,这样才能把力量用在一处。数据结构也是协议。

2. 模块化。把能复用的东西,抽出来,避免重复劳动。子程序(过程,函数),就是最好的例子。

·人应该做有创造性的工作,单调重复乏味的事情让计算机去做。