[你必须知道的.NET]第十八回:对象创建始末(上)

博客园CLR基础研究团队|CLR团队精品系列|Anytao技术博客

 

[你必须知道的.NET]第十八回:对象创建始末(上)

发布日期:2007.12.3 作者:Anytao
© 2007 Anytao.com ,原创作品,转贴请注明作者和出处。

 

本文将介绍以下内容:

  • 对象的创建过程
  • 内存分配分析
  • 内存布局研究

   

1. 引言

了解.NET的内存管理机制,首先应该从内存分配开始,也就是对象的创建环节。对象的创建,是个复杂的过程,主要包括内存分配和初始化两个环节。例如,对象的创建过程可以表示为:

            FileStream fs = new FileStream(@"C:"temp.txt", FileMode.Create);

通过new关键字操作,即完成了对FileStream类型对象的创建过程,这一看似简单的操作背后,却经历着相当复杂的过程和周折。

本篇全文,正是对这一操作背后过程的详细讨论,从中了解.NET的内存分配是如何实现的?

2. 内存分配

关于内存的分配,首先应该了解分配在哪里的问题。CLR管理内存的区域,主要有三块,分别为:

·       线程的堆栈,用于分配值类型实例。堆栈主要由操作系统管理,而不受垃圾收集器的控制,当值类型实例所在方法结束时,其存储单位自动释放。栈的执行效率高,但存储容量有限。

·       GC堆,用于分配小对象实例。如果引用类型对象的实例大小小于85000字节,实例将被分配在GC堆上,当有内存分配或者回收时,垃圾收集器可能会对GC堆进行压缩,详情见后文讲述。

·       LOHLarge Object Heap)堆,用于分配大对象实例。如果引用类型对象的实例大小不小于85000字节时,该实例将被分配到LOH堆上,而LOH堆不会被压缩,而且只在完全GC回收时被回收。

本文讨论的重点是.NET的内存分配机制,因此下文将不加说明的以GC堆上的分配为例来展开。关于值类型和引用类型的论述,请参见[第八回:品味类型---值类型与引用类型(上)-内存有理]

了解了内存分配的区域,接着我们看看有哪些操作将导致对象创建和内存分配的发生,关于实例创建有多个IL指令解析,主要包括:

·       newobj,用于创建引用类型对象。

·       ldstr,用于创建string类型对象。

·       newarr,用于分配新的数组对象。

·       box,在值类型转换为引用类型对象时,将值类型字段拷贝到托管堆上发生的内存分配。

    在上述论述的基础上,下面从堆栈的内存分配和托管堆的内存分配两个方面来分别论述.NET的内存分配机制。

2.1 堆栈的内存分配机制

对于值类型来说,一般创建在线程的堆栈上。但并非所有的值类型都创建在线程的堆栈上,例如作为类的字段时,值类型作为实例成员的一部分也被创建在托管堆上;装箱发生时,值类型字段也会拷贝在托管堆上。

对于分配在堆栈上的局部变量来说,操作系统维护着一个堆栈指针来指向下一个自由空间的地址,并且堆栈的内存地址是由高位到低位向下填充。以下例而言:

        public static void Main()
        {
            
int x = 100;
            
char c = 'A';
        }

   假设线程栈的初始化地址为50000,因此堆栈指针首先指向50000地址空间。代码由入口函数Main开始执行,首先进入作用域的是整型局部变量x,它将在栈上分配4Byte的内存空间,因此堆栈指针向下移动4个字节,则值100将保存在49997~50000单位,而堆栈指针表示的下一个自由空间地址为49996,如图所示:

接着进入下一行代码,将为字符型变量c分配2Byte的内存空间,堆栈指针向下移动2个字节至49994单位,值’A’会保存在49995~49996单位,地址的分配如图:

最后,执行到Main方法的右括号,方法体执行结束,变量xc的作用域也随之结束,需要删除变量xc在堆栈内存中的值,其释放过程和分配过程刚好相反:首先删除c的内存,堆栈指针向上递增2个字节,然后删除x的内存,堆栈指针继续向上递增4个字节,程序执行结束,此时的内存状况为:

    其他较复杂的分配过程,可能在作用域和分配大小上有所不同,但是基本过程大同小异。栈上的内存分配,效率较高,但是内存容量不大,同时变量的生存周期随着方法的结束而消亡。

未完待续:托管堆的内存分配机制必要的补充说明,近期发布,敬请关注。 

 

参考文献

(USA)Joe Duffy, Professinal .NET Framework 2.0

(USA)Don Box, Essiential .NET

(MSDN)Hanu Kommalapati and Tom Christian, Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects, http://msdn.microsoft.com/msdnmag/issues/05/05/JITCompiler/default.aspx 
 

温故知新

[开篇有益]
[第一回:恩怨情仇:is和as]
[第二回:对抽象编程:接口和抽象类]
[第三回:历史纠葛:特性和属性]
[第四回:后来居上:class和struct]
[第五回:深入浅出关键字---把new说透]
[第六回:深入浅出关键字---base和this]
[第七回:品味类型---从通用类型系统开始]
[第八回:品味类型---值类型与引用类型(上)-内存有理]
[第九回:品味类型---值类型与引用类型(中)-规则无边]
[第十回:品味类型---值类型与引用类型(下)-应用征途]
[第十一回:参数之惑---传递的艺术(上)]
[第十二回:参数之惑---传递的艺术(下)]
[第十三回:从Hello, world开始认识IL]
[第十四回:认识IL代码---从开始到现在]
[第十五回:继承本质论]
[第十六回:深入浅出关键字---using全接触]
[第十七回:貌合神离:覆写和重载]

© 2007 Anytao.com

原创作品,转贴请注明作者和出处,留此信息。

本文以“现状”提供且没有任何担保,同时也没有授予任何权利。
This posting is provided "AS IS" with no warranties, and confers no rights.

posted @ 2007-12-03 02:31 Anytao 阅读(6807) 评论(72)  编辑 收藏 网摘 所属分类: 01 [你必须知道的.NET]

  回复  引用  查看    
#1楼2007-12-03 08:41 | 戏水      
呜呼,大哥又发强2文,大早起 套老牛 拉破车 前来捧场 。
您的文章让我收益良多。真是开卷有益啊 。感谢感谢。

  回复  引用    
#2楼2007-12-03 09:14 | icesigil[未注册用户]
未完待续:托管堆的内存分配机制和必要的补充说明,近期发布,敬请关注
这话实在是调人胃口,这篇文章其实没说什么实质性的。 作者估计是想调人胃口 嘿嘿,真是佩服作者的无私奉献精神!看了作者的好多美文 受益匪浅!

  回复  引用  查看    
#3楼2007-12-03 09:14 | 1-2-3      
详细看完,好文啊,期待下篇。
  回复  引用    
#4楼2007-12-03 09:36 | lost2[未注册用户]
好文,lz辛苦了
  回复  引用  查看    
#5楼2007-12-03 09:41 | 周银辉      
好文好文.....
  回复  引用  查看    
#6楼2007-12-03 09:57 | Anders Liu      
嗯!你太猛了!我已经开始崇拜你了! ;D
等你的(下)写完我再来看过~

  回复  引用    
#7楼2007-12-03 09:57 | zzz[未注册用户]
非常好,又清晰了许多
  回复  引用  查看    
#8楼2007-12-03 10:06 | idior      
内容太少,建议上下合并发表。
  回复  引用  查看    
#9楼2007-12-03 10:33 | Anders Liu      
@idior
内容不在多,精品就行嘛~

  回复  引用  查看    
#10楼[楼主]2007-12-03 11:15 | Anytao      
@戏水
谢谢啦:-)

  回复  引用  查看    
#11楼[楼主]2007-12-03 11:16 | Anytao      
@icesigil
嘿嘿,并非掉胃口,主要是下半部分还需雕琢,另外在篇幅上分割不均,所以上篇略显单薄:-)

  回复  引用  查看    
#12楼[楼主]2007-12-03 11:17 | Anytao      
@1-2-3
近期发布,下篇还需修改修改,事情太多,有时不够迅速。

  回复  引用  查看    
#13楼[楼主]2007-12-03 11:17 | Anytao      
@lost2
:-)

  回复  引用  查看    
#14楼[楼主]2007-12-03 11:18 | Anytao      
@周银辉
呵呵,你也来了,过段时间也好好向你学习学习WPF:-)

  回复  引用  查看    
#15楼[楼主]2007-12-03 11:20 | Anytao      
@Anders Liu
哈哈,言重了,从你那里学习了不少:-)
idoir的建议也不错,就是下篇还没定稿,只能忍痛两篇了
:-)

  回复  引用  查看    
#16楼[楼主]2007-12-03 11:26 | Anytao      
@zzz
呵呵,有用就行,其实希望就某些问题达成讨论,这样最好。

  回复  引用  查看    
#17楼[楼主]2007-12-03 11:28 | Anytao      
@idior
所言甚是,下篇在篇幅上比较大一点儿,而且有些地方还需修改再发,所以就本文就略显单薄:-)

  回复  引用  查看    
#18楼2007-12-03 11:46 | 装配脑袋      
栈帧应该是一次分配的,函数返回后一次弹出。不是运行到一个变量分配一个变量的说。
  回复  引用  查看    
#19楼[楼主]2007-12-03 12:22 | Anytao      
@装配脑袋
终于等来脑袋光顾:-)
就执行来说,栈的分配是一次性完成的,这里关注的是其分配的过程,主要表达的是堆栈指针在分配时向低地址扩展的细化。
另外补充,变量的弹出不一定是函数返回后一次弹出,仍然有弹出的顺序和作用域之分,例如:
public void Method()
{
int a;
for(int i; i < 10; i ++)
{
}
}

a和i的作用域是不同的,其弹出的时间和顺序也不同:-)

  回复  引用  查看    
#20楼2007-12-03 12:51 | 坐断东南 笑煞之!!      
期待下一期。

必定精彩

  回复  引用  查看    
#21楼2007-12-03 13:24 | 装配脑袋      
@Anytao

不管函数内的变量定义在哪里,同一帧都是一次分配一次弹出,绝没有根据作用域先弹出后弹出的。正因为这样,值类型的分配与解除分配效率才能高于引用类型。

至于是否按照书写代码的顺序进行分配我正在考证中……

  回复  引用  查看    
#22楼2007-12-03 13:31 | 墙外行人      
需要了解
  回复  引用  查看    
#23楼[楼主]2007-12-03 13:56 | Anytao      
@坐断东南 笑煞之!!
:-)
有压力了,嘿嘿

  回复  引用  查看    
#24楼[楼主]2007-12-03 14:03 | Anytao      
@装配脑袋
我也再考证考证,需要进一步了解一下细节,
谢了:-),有结果通知一下

  回复  引用  查看    
#25楼[楼主]2007-12-03 14:03 | Anytao      
@墙外行人
:-)

  回复  引用  查看    
#26楼2007-12-03 15:16 | Anders Liu      
是这样的,在执行时,堆栈是一次性分配的,而在方法调用完毕后,也是所有局部变量一起弹出的。
至于你所说的这个“顺序”,其实是语言层面上的,编译器去处理的。比如
{
int a = 10;
{
int a = 20;
}
}
其实堆栈中有两个局部变量,但编译器会将两个a分别绑定到各自的变量上。


--引用--------------------------------------------------
Anytao: @装配脑袋
终于等来脑袋光顾:-)
就执行来说,栈的分配是一次性完成的,这里关注的是其分配的过程,主要表达的是堆栈指针在分配时向低地址扩展的细化。
另外补充,变量的弹出不一定是函数返回后一次弹出,仍然有弹出的顺序和作用域之分,例如:
public void Method()
{
int a;
for(int i; i &lt; 10; i ++)
{
}
}

a和i的作用域是不同的,其弹出的时间和顺序也不同:-)
--------------------------------------------------------

  回复  引用  查看    
#27楼2007-12-03 15:28 | Betree Xing      
又学到了一点, 多谢楼主分享.
  回复  引用  查看    
#28楼2007-12-03 16:57 | 装配脑袋      
所谓局部变量的作用域,只是在语法层面做的限制。CLR在创建一个Method Call Frame的时候会一次将所有参数、局部变量布局在栈帧上,然后在函数返回的时候将这一帧弹出。在方法执行期间没有任何局部变量在栈上的分配和解除分配动作,更不要说作用域的影响了。
  回复  引用    
#29楼2007-12-03 18:12 | 注册公司[未注册用户]
又学到了一点, 多谢楼主分享
  回复  引用  查看    
#30楼[楼主]2007-12-04 02:56 | Anytao      
@Anders Liu
@装配脑袋

脑袋、Anders Liu:

您好,看了二位的留言,可能有些地方还是值得思考,脑袋给了我不错的思考和提示,我想关于栈上分配的理解应该是:

每次调用方法时,都会在栈中创建一个活动记录(也就是包含了参数、返回值地址和局部变量),并分配相应的内存空间,这种分配是一次性完成的。而方法运行结束返回时,活动记录清空,也是一次性的。

分配和解除分配都是一次性完成的,而数据的压栈和出栈是有顺序的,栈内肯定是先进后出(FILO)的形式。具体的操作:首先入栈的是返回地址(函数执行之后的下一条可执行语句的地址),用于方法返回之后的继续执行;然后是参数,以由右向左的顺序入栈;最后是局部变量,依次入栈。方法执行之后,出栈的顺序正好相反,首先是局部变量,再是参数,最后就是那个记录指针。

  回复  引用  查看    
#31楼[楼主]2007-12-04 02:58 | Anytao      
@Anders Liu
@装配脑袋

还是举个例子好了:
public static void Main()
{
int i = 1;
int j = 2;
}


上述代码对应的IL为:
.method public hidebysig static void Main() cil managed
{
.entrypoint
// 代码大小 6 (0x6)
.maxstack 1
.locals init ([0] int32 i,
[1] int32 j)
IL_0000: nop
IL_0001: ldc.i4.1 //压入Evaluatioin Stack
IL_0002: stloc.0 //存入Call Stack
IL_0003: ldc.i4.2 //压入Evaluation Stack
IL_0004: stloc.1 //存入Call Stack
IL_0005: ret //结束方法调用
} // end of method GenericInterface::Main

分析这段IL的执行过程,正是局部变量数据入栈的简单过程。

本文其实意图表达的正是上述过程的大致模式(当然比实际的情况简单的多),只是有些理解偏差:-)

因此,在程序执行过程中,栈将随方法调用而动态的增长或者收缩,基址指针和堆栈指针将不断变化,伴随着活动目录的创建和销毁。

不知我的理解是否有误,望脑袋和Anders Liu兄斧正:-)


  回复  引用  查看    
#32楼[楼主]2007-12-04 02:58 | Anytao      
@Betree Xing
:-)

  回复  引用  查看    
#33楼2007-12-04 08:51 | 装配脑袋      
@Anytao

IL的运算栈和线程栈关系不大吧,运行的时候运算栈是不存在的。像这条指令
stloc.0 不会在栈上分配任何空间,只是把刚刚的运算的结果放在已经分配好的0位置上而已。应该只有stackalloc能够动态分配栈上的空间

  回复  引用  查看    
#34楼2007-12-04 10:53 | Anders Liu      
@Anytao

千万搞清一个概念,对于一个程序来说,堆栈可能物理上只有一个,但逻辑上是分成两部分的——调用栈和运算栈。(这从你的例子中也可以看出来,不知你注意了没)。

其中调用栈的作用是在每次调用时,向其中压入堆栈帧(返回地址、参数列表、局部变量等信息)。而运算栈才是放置运算数和运算结果的地方。

对于运算栈来说,各方面讨论的比较多,我想Anytao兄弟也很清楚。而调用栈似乎讨论话题不是很多。

对于调用栈来说,堆栈帧是一个整体被压入堆栈的,包括返回地址和实参列表。弹出的时候也一样,其实弹出很简单,只要将栈顶指针放到堆栈帧的起始位置上就可以了,下次调用时就会覆盖掉这些内容。 一句话,对于调用栈来说,堆栈帧是一个整体,参数是其中不可分割的一部分(不能单独压入或弹出)。

参数和局部变量一旦压入堆栈,无需弹出就能使用——可以通过(调用)栈指针+偏移量来访问。但运算必需把运算数压入运算栈中才能进行。在你的例子里你也说了,“压入Evaluatioin Stack”、“//存入Call Stack”。玄机就在这些细节文字上了,运算栈是要“压入”的,这是典型的堆栈访问;而调用栈不用“压入”只要“存入”,这其实是一种随机访问。

也就是说,在程序执行过程中,参数、局部变量的出入栈时机只有方法调用和方法返回时,在方法体的执行过程中,参数、局部变量才用随机访问,不用出入栈。

-----

昨天加班比较晚,现在脑袋沉沉的,也不知说清没。

  回复  引用  查看    
#35楼[楼主]2007-12-04 12:26 | Anytao      
@装配脑袋
线程栈和运算栈,还是了解,只是又拿了一个不恰当的例子,嘿嘿:_)
对于局部变量,肯定是静态分配,所以不存在stackalloc动态分配,其实对于活动记录一次性分配,一次性清空已经清除了,没有异议。
我的问题是,在程序执行过程中,可以通过栈指针和偏移量来访问局部变量,那么参数、局部变量和返回地址的布局必然有一定的顺序,而这个顺序是如何保证呢,又是在什么时机保证呢?

  回复  引用  查看    
#36楼[楼主]2007-12-04 12:59 | Anytao      
@Anders Liu
是比较清楚了,我还是关心参数、局部变量和返回地址,在线程栈中的顺序问题,其布局肯定是有顺序的,那么这个顺序是怎么保证的呢?
继续研究。。。

  回复  引用  查看    
#37楼2007-12-04 16:51 | jillzhang      
@装配脑袋
不明白怎么个一次性
一次性的话,各个变量之间的先后顺序靠什么来维护?
还不是要用到临时栈么?
一次性装入的说法是否有依据?依据出在哪?敬请指教

  回复  引用  查看    
#38楼2007-12-04 16:53 | jillzhang      
下面这样的一个函数调用,
void a()
{
..
}

void b()
{
...
}

void ab()
{
..
a();
..
b();
}
如何分配?ab也是一次性么?

  回复  引用  查看    
#39楼2007-12-04 16:58 | jillzhang      
我的想法呀,如果说函数的堆栈分配在效果上的确是一次性的,但深究其内部分配原则,应该为博主所说,一次性的是现象,而楼主说的是本质。装配脑袋是不是给层次没划清呢?自己猜想呀,希望脑袋兄出来解释一下
  回复  引用  查看    
#40楼2007-12-04 17:43 | 兴百放      
期待楼主的下一篇力作
学到很多东西
也谢谢楼主的分享

  回复  引用  查看    
#41楼[楼主]2007-12-04 17:45 | Anytao      
@jillzhang
呵呵,讨论越发有意思了。
对于栈上活动记录的一次性装入和一次性弹出,是确定的。

以方法ab为例,当调用到ab时,将创建其本身的记录框架,也就是为其在栈上分配了相应的空间,随着方法的执行到方法a()时,将在栈上创建a()的活动记录(应该是近邻着为ab分配的空间,并且向低地址扩展),执行完a(),将按照a()的活动记录中的Return Address返回到主调函数,并且丢弃a()的活动记录;然后继续执行,直到ab()执行结束。

以上是我的理解,所以方法执行过程中,物理栈是动态增加或收缩的,伴随着活动目录的创建和销毁(例如,a()和b()的活动目录)。
而对于活动目录中包含的参数、局部变量和return address,一次入栈,其顺序是如何来实现的,是这里关心的话题了。

有请脑袋兄了:-)

  回复  引用  查看    
#42楼[楼主]2007-12-04 17:47 | Anytao      
@兴百放
欢迎光临:-)

  回复  引用  查看    
#43楼2007-12-04 19:40 | 装配脑袋      
如果想知道local变量怎么分配的,看IL显然是不够的了,应该从SSCLI中找答案。在Fjit.cpp中FJit::jitCompile我们看到一句:
unsigned int localWords = (localsFrameSize+sizeof(void*)-1)/ sizeof(void*);

emit_prolog(localWords);

而emit_prolog宏的x86实现中包含将第一个参数存入ECX,第二参数存入EDX,本地变量所需的字节依次入栈的代码。这里的localsFrameSize是通过FJit::computeLocalOffsets() 计算出来的,里面已经计算了所有参数和本地变量的尺寸。所以从内存分配的角度,无法区分哪个栈对象先初始化,哪个后初始化。至于本地变量什么时候置0值我还没有弄清楚,但是不会影响“所有栈对象一起初始化”这个结论吧……

  回复  引用  查看    
#44楼2007-12-04 20:02 | 玉血      
关注....
  回复  引用  查看    
#45楼[楼主]2007-12-04 21:35 | Anytao      
@装配脑袋
继续跟踪,很多细节原来是不那么清晰的,和你讨论很有收获。
有了新收获,我会及时交流。
:-)

  回复  引用  查看    
#46楼[楼主]2007-12-04 21:35 | Anytao      
@玉血
:-)

  回复  引用  查看    
#47楼2007-12-07 14:42 | mythzz      
收获啊 老大 太感谢了
  回复  引用  查看    
#48楼[楼主]2007-12-07 14:54 | Anytao      
@mythzz
:-)
建议看看评论,脑袋有很多指正

  回复  引用  查看    
#49楼[楼主]2007-12-10 01:42 | Anytao      
@shanghai escort
I will delete the advertisement information without any notification. 3x

  回复  引用    
#50楼2007-12-11 09:32 | 游客[未注册用户]
请问,这个是你自己研究的还是翻译来的
  回复  引用  查看    
#51楼[楼主]2007-12-11 11:30 | Anytao      
@游客
本博客和本系列,如果没用特别说明,则全文原创作品,你必须知道的.NET是我的原创系列,本文也是。

:-)

  回复  引用    
#52楼2007-12-12 14:51 | 冰之印记[未注册用户]
关键是不知道他们的研究方法!现在只能看他们说 我们听听了!
  回复  引用  查看    
#53楼[楼主]2007-12-12 16:02 | Anytao      
@冰之印记
了解IL,分析SSCLI,使用某些工具例如WinDBgg等,还是有很多的方法来了解,当然最重要的还是看书和相关资料,借用别人的研究岂不是更爽,当然必要的时候做些探索也很重要,关键就是方法和心力。
:-)

  回复  引用  查看    
#54楼2007-12-13 20:51 | 冬虫草      
意犹未尽啊!!
哈哈

  回复  引用  查看    
#55楼[楼主]2007-12-13 23:59 | Anytao      
@冬虫草
呵呵,3x,莫大的鼓励:-)

  回复  引用  查看    
#56楼2007-12-14 19:52 | Silent Void      
MSIL中的栈跟OS的线程栈/运算栈是不同的概念
  回复  引用  查看    
#57楼[楼主]2007-12-15 12:00 | Anytao      
@Silent Void
当然不同了,一个是基于物理的,一个是基于CLR的。

  回复  引用    
#58楼2008-02-10 17:39 | 减速机[未注册用户]
MSIL中的栈跟OS的线程栈?
  回复  引用  查看    
#59楼[楼主]2008-02-14 19:32 | Anytao      
@减速机
?
评论中有所澄清:-)

  回复  引用  查看    
#60楼2008-04-27 09:23 | beyondme      
收益良多,3Q
  回复  引用  查看    
#61楼[楼主]2008-04-27 22:03 | Anytao      
@beyondme
常来常新,我会继续努力呈现更多:-)

  回复  引用    
#62楼2008-04-29 13:24 | 文祥[未注册用户]
在你的书《你必须知道的.NET》给出的样章里面,这一部分的堆栈图100和'A'的位置是反的,搞的我想了很久不明白,原来就是错的!相当误导人啊,赶紧改了吧。
  回复  引用  查看    
#63楼[楼主]2008-04-29 13:47 | Anytao      
@文祥
已经发现这个问题,我会尽快在勘误中说明,谢谢提醒:-)

  回复  引用  查看    
#64楼2008-05-29 23:09 | 不规则几何图形      
最近看到你的 《你必须知道的.NET》收益不少,期待精彩继续:)
  回复  引用  查看    
#65楼[楼主]2008-06-01 22:48 | Anytao      
@不规则几何图形
呵呵,一定继续努力,这是我的兴趣:-)

  回复  引用  查看    
#66楼2008-06-03 10:14 | landylee      
看了你的《你必须知道的.NET》,堆棧的內存分配機制有點問題,和我原來看C#高級編程中看到的不太一致,堆棧的地址空間有高地址向低地址分配,入棧時是棧頂向低地址擴展,出棧時是低地址向棧頂回退,也就是說先入棧的應該在棧頂部分位置,然后依順序一次向下分配,出棧時是低地址的內存先釋放,然后按順序向上,直到棧頂!是嗎?
哎,我快要暈了!

  回复  引用  查看    
#67楼[楼主]2008-06-03 21:34 | Anytao      
@landylee
很抱歉,该错误已经提交勘误表,详见勘误P176页勘误信息。给你阅读造成的不便,深表歉意:-)

  回复  引用  查看    
#68楼2008-11-27 01:28 | 徐培华      
呵呵,和汇编里的堆栈差不多 的
要是学了汇编再了看这个很容易懂噢,
不过不学相信也很快,呵呵,楼主强,

  回复  引用  查看    
#69楼[楼主]2008-11-27 10:08 | Anytao      
@徐培华
呵呵,所言甚是,理解更底层的知识,对于高层应用是大有裨益的:-)




发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 980402




相关文章:

相关链接: