- 浏览: 3059824 次
- 性别:
- 来自: 海外
-
文章分类
- 全部博客 (430)
- Programming Languages (23)
- Compiler (20)
- Virtual Machine (57)
- Garbage Collection (4)
- HotSpot VM (26)
- Mono (2)
- SSCLI Rotor (1)
- Harmony (0)
- DLR (19)
- Ruby (28)
- C# (38)
- F# (3)
- Haskell (0)
- Scheme (1)
- Regular Expression (5)
- Python (4)
- ECMAScript (2)
- JavaScript (18)
- ActionScript (7)
- Squirrel (2)
- C (6)
- C++ (10)
- D (2)
- .NET (13)
- Java (86)
- Scala (1)
- Groovy (3)
- Optimization (6)
- Data Structure and Algorithm (3)
- Books (4)
- WPF (1)
- Game Engines (7)
- 吉里吉里 (12)
- UML (1)
- Reverse Engineering (11)
- NSIS (4)
- Utilities (3)
- Design Patterns (1)
- Visual Studio (9)
- Windows 7 (3)
- x86 Assembler (1)
- Android (2)
- School Assignment / Test (6)
- Anti-virus (1)
- REST (1)
- Profiling (1)
- misc (39)
- NetOA (12)
- rant (6)
- anime (5)
- Links (12)
- CLR (7)
- GC (1)
- OpenJDK (2)
- JVM (4)
- KVM (0)
- Rhino (1)
- LINQ (2)
- JScript (0)
- Nashorn (0)
- Dalvik (1)
- DTrace (0)
- LLVM (0)
- MSIL (0)
最新评论
-
mldxs:
虽然很多还是看不懂,写的很好!
虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩 -
HanyuKing:
Java的多维数组 -
funnyone:
Java 8的default method与method resolution -
ljs_nogard:
Xamarin workbook - .Net Core 中不 ...
LINQ的恶搞…… -
txm119161336:
allocatestlye1 顺序为 // Fields o ...
最近做的两次Java/JVM分享的概要
(本来是想详细点写的……不过这坑似乎太大了。这个就算是预演好了。主要是写了不发怕坑掉,发了不完整又どうかと思って)
在程序设计语言的语境下,一个“闭包”到底是什么?这还是得从一些别的基本概念说起。
=======================================================================================
基本概念解释:
在程序设计语言中,变量可以分为自由变量(free variable)与约束变量(bound variable)两种。简单来说,一个函数里局部变量和参数都被认为是约束变量;而不是约束变量的则是自由变量。
在冯·诺依曼(von Neumann)计算机体系结构的影响下,命令式语言(imperative language)里一个变量的属性可以看作一个六元组:(名字,地址,值,类型,生命期,作用域)。
名字:顾名思义。
地址:变量所关联的存储器地址。这个属性有明显的冯·诺依曼体系结构的色彩。
值:变量所关联的存储器单元的内容。
类型:规定了变量可以取的值得范围,以及该类型的值可以进行的操作。根据类型的值的可赋值状况,可以把类型分为三类:
- 1、一级的(first class)。该等级类型的值可以传给子程序作为参数,可以从子程序里返回,可以赋给变量。大多数程序设计语言里,整型、字符类型等简单类型都是一级的。
- 2、二级的(second class)。该等级类型的值可以传给子程序作为参数,但是不能从子程序里返回,也不能赋给变量。
- 3、三级的(third class)。该等级类型的值连作为参数传递也不行。
生命期:变量与一个特定的存储区地址相绑定的过程。一个变量的生命期从它与一个特定的存储器地址相绑定开始,到它与存储器解除绑定为止。根据生命期分类,变量可以被分为四类:
- 1、静态(static)。该种变量的存储器分配在程序开始运行之前就决定,并且在程序运行过程中一直不变,直到程序结束为止。
- 2、栈动态(stack-dynamic)。该种变量的存储器分配在声明它的语句被执行到的时候才决定,但变量类型是静态决定的。顾名思义,空间是在运行时栈上分配的。
- 3、显式堆动态(explicit heap-dynamic)。该种变量由程序员显式使用运行时指令(或运算符)来指定在堆上空间的分配(和/或回收)。典型的例子是使用new和delete运算符。
- 4、隐式堆动态(implicit heap-dynamic)。该种变量每次被赋值时都会重新在堆上分配空间,不需要特别的运行时指令(或运算符)来指定。
作用域:变量在语句中可见的范围。如果在某个语句中可以引用某个变量,则该变量在该语句中可见。根据作用域的特征分类,程序设计语言中所支持的作用域可以被分为:
- 1、静态作用域。在静态作用域的规定下,变量的作用域与其在代码中所处的位置相关;因为代码可以静态决定(运行前就可以决定),所以变量的作用域也可以被静态决定,所以这种规定被称为静态作用域。
- 2、动态作用域。相对的,在动态作用域的规定下,变量的作用域与代码的执行顺序相关;执行顺序只有在程序运行时才能被决定,所以这种规定被称为动态作用域。
某种意义上来说,这两种规定是统一的:静态作用域由空间(代码字面上的空间)范围决定,动态作用域由时间(程序的执行)顺序决定。但由于作用域本身是个空间概念,所以一般而言静态作用域更容易被人理解。
变量的生命期与作用域并不一样,一个是时间概念,一个是空间(源代码字面上的)概念。但支持静态作用域,并且支持栈动态变量的程序设计语言中,这两个概念会有一些联系,例如:一种支持静态作用域与栈动态变量的语言(例如C)中,一个原始类型(或者在某些语言中,值类型)的局部变量会在栈上分配空间,它的生命期从其所在的函数被调用的时候开始,到调用结束的时候为止;如此生命期就与作用域联系在了一起。
在允许在全局层次定义变量的语言里,全局变量是个特例。全局变量的存储器空间分配与局部变量不一样,一般是静态决定其分配的地址(在存储器的全局空间部分),所以它的生命期涵盖程序的整个执行过程。因此,虽然对于所有函数来说全局变量都是自由变量,但下面的讨论中我们不需要对其做讨论(因为它不涉及动态的存储器空间分配)。某些语言里的static关键字也可以对局部变量指定使用静态存储器空间分配,这样它们虽然仍遵守静态作用域,生命期却与一般的栈动态局部变量不同。
在静态作用域中,也可以分为两类:可以嵌套定义子程序的与不可以的。基于C的语言(指标准C、C++、Java)都无法在一个函数里再嵌套定义函数;而其它的一些,像是Scheme、Pascal、Ada、JavaScript、C#等则允许嵌套的函数定义。
虽然基于C的语言不允许嵌套定义函数,但这类语言都支持块结构。通过代码块,我们可以在一个静态作用域里新建一个嵌套的静态作用域。这样就可以有效的控制变量的作用域,使其尽量的小,便于减少变量名冲突的问题。
(不过等C++0x正式成为标准后,基于C的语言里也将允许这种嵌套的函数定义)
=======================================================================================
让我们来看一个支持静态作用域,同时支持栈动态局部变量的例子:(C++)
上面的代码所对应的作用域状况是:
很正常对吧,学过C/C++的人都知道这个。
在第一行声明的global变量是一个全局变量。它的作用域是它所在的整个源文件,在它声明了之后的部分。
paramOfM是函数m()的形式参数。它的作用域覆盖整个m()的范围。
localOfM是函数m()之内的一个局部变量。它的作用于从它声明的位置开始,到m()结束的地方为止。
localToFor是m()中的一个for循环中的一个控制变量。它是一个局部于for语句的变量,作用域覆盖了整个for语句中的部分,直到离开for语句块为止。注意到早期的C++并不是这样规定的;早期的C++规定,for语句开头的括号中声明的变量的作用域一直到包含这个for循环的代码块结束的位置为止。因此用过Visual C++ 6.0的人肯定都有过痛苦的经历:
这个问题也不能怪VC++6不标准,VS6出的时候C++98还没什么编译器能完全正确的实现出来吧。
回到主题。前面代码里,localToForBody是在for语句的循环体里定义的一个局部变量。它的作用域从它声明的位置开始,到for的循环体结束的位置为止。值得注意的是,由于for语句开头的括号在for的循环体之前出现,所以循环体里定义的局部变量在开头的括号里都是不可见的。
后面是函数n()的作用域。可以看到m()与n()的作用域相互没有影响——它们相互没有交集。
=======================================================================================
让我们来看看,在现实程序中一个函数调用是如何实现的。下面的代码来自AQUAPLUS的ToHeart2。
my_inc2/MM_std.cpp, line 766:
其中CREATE_READ_FILE是一个宏,在my_inc2/MM_std.h定义:
于是通过宏展开,上面的函数变为:
然后在程序启动时会调用上述函数,在WIN_Init()中,ScriptEngine/src/Winmain.cpp, line 1844:
调用关系是:
WIN_Init()
- STD_CheckFile()
-- CreateFile()
让我们仔细观察一下这个调用是如何进行的。注意到这里都采用C调用约定(cdecl),所以参数从右向左压入栈中,由调用者负责清理栈。
调用者的汇编代码是:
运行到这个call时,相应的运行时栈的内容是:
这个地方就是当前栈顶了。
跟进这个调用,看到栈的内容发生了变化:
栈顶变为0012FD2C。可以看到,call指令的执行导致函数的返回地址被压入栈顶。
然后来看看被调用者(STD_CheckFile())的汇编代码:
该函数的第一行将调用时传入的第一个参数读入了EAX。对应C的源代码看,源代码的第一行声明了一个局部变量。这个变量跑哪儿去了呢?
其实是被编译器优化掉了。如果编译器没有优化的话,这个变量很可能位于当前栈顶的“上面”(低地址方向)。于是这个例子里我们不会在栈上看到属于STD_CheckFile()的局部变量。它在\CreateFileA()返回后被放置在EAX里了。
继续运行到对CreateFileA的call那句时,栈的状况是:
留意到现在从栈顶开始7个存储器单元(每个单元是一个双字,DWORD,32位)都是对CreateFileA()调用而传入的参数。
同样,跟进这个调用,看看栈的变化:
跟前一个调用一样,call指令使返回地址被压入栈顶。
在CreatFileA()运行完毕,返回到STD_CheckFile()之后,栈的内容是:
可以看到,跟调用CreatFileA()之前一样。retn指令会从栈顶弹出一个值作为返回地址,然后把EIP(指令指针)设置到那个值上,完成返回跳转。
程序从STD_CheckFile()返回到WIN_Init()之后,栈顶是:
由于C调用约定是由调用者来清理栈,而碰巧WIN_Init()接下来还要用到这个值,所以编译器优化让这个值保持在了栈顶。否则这个值也应该被抛弃掉了。
上面的例子主要是想说明C语言中的函数调用在现实中运行的情况,以便说明其语义。
每个函数被调用时,都会有一个活动记录(activation record)伴随产生。像C这样的语言,活动记录是在运行时栈上分配空间的,所以也成为栈帧(stack frame)。一个函数的活动记录以及该函数能访问到的任何“东西”(变量之类)的总和,被称为这个函数拥有的引用环境(referencing environment)。
一般来说,一个函数的参数和返回地址都会放置在栈帧里,在未经优化的时候局部变量也会放在栈帧里。这种运行方式使得一个函数的参数和局部变量都只在其运行当中才被分配空间;函数的运行一结束,分配的空间就解除了与相应变量的绑定(约束)。由于参数与局部变量的状况都是可以在程序运行前确定的,所以它们在栈帧中的位置(偏移量)可以静态确定。栈帧本身的存在于否只能到运行的时候才能知道,有动态性,所以这种生命期得名“栈动态”。
这样,变量的生命期就与静态作用域的规定产生了联系。
那么假如允许嵌套定义函数,并且允许内层函数访问外围函数里的变量会怎样呢?
可以观察到,内层函数只能为自己的参数和局部变量分配空间,而无法为外围函数中的变量分配空间;但同时,内层函数需要能够访问到外围函数里的局部变量,意味着它的活动记录里必须有某种手段去访问到外围函数的活动记录,否则内层函数的“引用环境”就不包含外围函数的活动记录也就无法访问外围函数里的局部变量。
(本来是想画图来表示的……时间不足,只好直接贴文字。下次补完吧……)
=======================================================================================
在接着讨论之前,先得说明一点:静态作用域并不总是意味着被嵌套的作用域(nested scope)中能访问包围它的外围作用域(enclosing scope)中的变量。
如果一种语言中,被嵌套的作用域总是能访问其外围作用域中的变量(或者说名字),则该语言被认为支持词法作用域(lexical scope)。反之则不算支持词法作用域。
(有些资料上将“静态作用域”与“词法作用域”写为同一个概念,或许这里还有值得商榷的地方吧。)
从一般意义的词法作用域概念来说,Python虽然支持静态作用域,但并不是一般的词法作用域。虽然有些Python用户可能会争论这点,但Python的词法作用域很明显与“一般的”不一样:
Python里只能对函数内的局部变量或者全局变量赋值,而不能对非局部非全局的变量赋值;可以读取非局部非全局变量,但是一旦尝试赋值,Python就会自动创建一个新的同名的局部变量。根据词法作用域,上面的例子表明Python中嵌套的内部函数可以读取外围函数里的变量:对inner()来说x是一个自由变量,非局部非全局,并且被inner()外围的闭包所捕获因而可以访问。但下面的例子却会失败:
+=运算符包含了三个操作,先计算运算符左边的表达式,再将运算符右边的表达式的值加上左边表达式原本的值,最后赋值回到左边的表达式对应的变量上。由于包含了赋值操作,Python在查找变量x时只在局部或全局范围内查找,而此时符合这样条件的局部变量x还不存在(或者说还未被赋值过),所以出现了错误。这是设计者为了安全而做的设计取舍,不过无论如何它与一般意义上的词法作用域不一样。
最典型的地方是Python里成员方法都必须以self为第一个参数的做法(名字不一定要是self,不过约定上都叫这个名字)……不熟悉Python的话算了,这个不用深究。
=======================================================================================
允许嵌套定义子程序的语言也可以被分为三类:允许将函数作为参数传递的与不允许的,还有中间比较奇怪的(=_=)。如果一种语言允许将函数作为参数传递,意味着这种语言里的函数至少是二级的类型。
先从不能将函数作为参数传递的开始讨论。
(待补完)
------------
然后是允许将函数作为参数传递的。
标准Pascal(ISO 7185:1990):
引用Wikipedia上的例子:
留意到在F()里,外围的E()中的x是如何被“捕获”到当前作用域的。对于F()来说,y是一个形式参数,因而是一个“约束变量”;x既不是局部变量或者参数,也不是全局变量,因而是一个“自由变量”。F()的执行依赖于外部环境提供的“x”变量才可以进行。
Pascal是支持栈动态变量的语言。上面的例子中,x和y的存储器空间都是在运行时栈上分配的。
ISO 7185:1990,6.6.3 Parameters部分说明了将过程或者函数作为参数传递的一些规定。
------------
GNU C:
GNU C算是个比较特殊的实现吧。上面Pascal的例子在GNU C里也可以写出来:
注意:只是GNU C支持嵌套定义函数,GNU C++是不支持的。
C里的函数指针可以传递函数的代码,却无法传递函数的环境。因此,当允许嵌套定义函数时,内层函数只有在外围函数活动的时候才能捕获到其引用环境。下次再详细说……
接下来让我们看看JavaScript的例子。
(待补完)
=======================================================================================
闭包与对象的等价性
(待补完)
=======================================================================================
闭包的应用意义:
1、状态隐藏
2、算法封装
3、自定义控制流
...
(待补完)
=======================================================================================
Lambda Expressions and Closures: Wording for Monomorphic Lambdas (Revision 4)
在C++0x之前,我们经常会使用functor来解决一些问题:
The constructor call between(min_salary, 1.1 * min_salary) creates a function object, which is comparable to
what, e.g., in the context of functional programming languages is known as a closure. A closure stores the
environment, that is the values of the local variables, in which a function is defined. Here, the environment
stored in the between function object are the values low and high, which are computed from the value of the
local variable min_salary.
但是这样挺麻烦的。明明只是需要一个函数,却需要写一整个类出来。而有了C++0x的lambda表达式之后,就可以在函数内直接定义匿名的嵌套函数了。
这段代码里的作用域:
(哎呀,图做完了才发觉for_each之前漏了std::……算了懒得重新做图,凑合吧)
不过这个例子在C++0x里也未必要用到lambda表达式了,直接用新增加的for-each循环就行:
如果在elem前面加上&来修饰,则对elem的赋值也会反映到容器内。真好。
照这么说,C++0x里的闭包也是D 2.0早期所定义的“动态闭包”了。关于D的动态闭包,可以看看我去年写的这帖,靠下面的部分。
(待补完)
=======================================================================================
相关链接:
Wikipedia:
Closure (computer science)
Lambda Calculus
Free variables and bound variables
Nested Function
C++0x
Cunningham & Cunningham, Inc.:
Lexical Closure
在程序设计语言的语境下,一个“闭包”到底是什么?这还是得从一些别的基本概念说起。
=======================================================================================
基本概念解释:
在程序设计语言中,变量可以分为自由变量(free variable)与约束变量(bound variable)两种。简单来说,一个函数里局部变量和参数都被认为是约束变量;而不是约束变量的则是自由变量。
在冯·诺依曼(von Neumann)计算机体系结构的影响下,命令式语言(imperative language)里一个变量的属性可以看作一个六元组:(名字,地址,值,类型,生命期,作用域)。
名字:顾名思义。
地址:变量所关联的存储器地址。这个属性有明显的冯·诺依曼体系结构的色彩。
值:变量所关联的存储器单元的内容。
类型:规定了变量可以取的值得范围,以及该类型的值可以进行的操作。根据类型的值的可赋值状况,可以把类型分为三类:
- 1、一级的(first class)。该等级类型的值可以传给子程序作为参数,可以从子程序里返回,可以赋给变量。大多数程序设计语言里,整型、字符类型等简单类型都是一级的。
- 2、二级的(second class)。该等级类型的值可以传给子程序作为参数,但是不能从子程序里返回,也不能赋给变量。
- 3、三级的(third class)。该等级类型的值连作为参数传递也不行。
生命期:变量与一个特定的存储区地址相绑定的过程。一个变量的生命期从它与一个特定的存储器地址相绑定开始,到它与存储器解除绑定为止。根据生命期分类,变量可以被分为四类:
- 1、静态(static)。该种变量的存储器分配在程序开始运行之前就决定,并且在程序运行过程中一直不变,直到程序结束为止。
- 2、栈动态(stack-dynamic)。该种变量的存储器分配在声明它的语句被执行到的时候才决定,但变量类型是静态决定的。顾名思义,空间是在运行时栈上分配的。
- 3、显式堆动态(explicit heap-dynamic)。该种变量由程序员显式使用运行时指令(或运算符)来指定在堆上空间的分配(和/或回收)。典型的例子是使用new和delete运算符。
- 4、隐式堆动态(implicit heap-dynamic)。该种变量每次被赋值时都会重新在堆上分配空间,不需要特别的运行时指令(或运算符)来指定。
作用域:变量在语句中可见的范围。如果在某个语句中可以引用某个变量,则该变量在该语句中可见。根据作用域的特征分类,程序设计语言中所支持的作用域可以被分为:
- 1、静态作用域。在静态作用域的规定下,变量的作用域与其在代码中所处的位置相关;因为代码可以静态决定(运行前就可以决定),所以变量的作用域也可以被静态决定,所以这种规定被称为静态作用域。
- 2、动态作用域。相对的,在动态作用域的规定下,变量的作用域与代码的执行顺序相关;执行顺序只有在程序运行时才能被决定,所以这种规定被称为动态作用域。
某种意义上来说,这两种规定是统一的:静态作用域由空间(代码字面上的空间)范围决定,动态作用域由时间(程序的执行)顺序决定。但由于作用域本身是个空间概念,所以一般而言静态作用域更容易被人理解。
变量的生命期与作用域并不一样,一个是时间概念,一个是空间(源代码字面上的)概念。但支持静态作用域,并且支持栈动态变量的程序设计语言中,这两个概念会有一些联系,例如:一种支持静态作用域与栈动态变量的语言(例如C)中,一个原始类型(或者在某些语言中,值类型)的局部变量会在栈上分配空间,它的生命期从其所在的函数被调用的时候开始,到调用结束的时候为止;如此生命期就与作用域联系在了一起。
在允许在全局层次定义变量的语言里,全局变量是个特例。全局变量的存储器空间分配与局部变量不一样,一般是静态决定其分配的地址(在存储器的全局空间部分),所以它的生命期涵盖程序的整个执行过程。因此,虽然对于所有函数来说全局变量都是自由变量,但下面的讨论中我们不需要对其做讨论(因为它不涉及动态的存储器空间分配)。某些语言里的static关键字也可以对局部变量指定使用静态存储器空间分配,这样它们虽然仍遵守静态作用域,生命期却与一般的栈动态局部变量不同。
在静态作用域中,也可以分为两类:可以嵌套定义子程序的与不可以的。基于C的语言(指标准C、C++、Java)都无法在一个函数里再嵌套定义函数;而其它的一些,像是Scheme、Pascal、Ada、JavaScript、C#等则允许嵌套的函数定义。
虽然基于C的语言不允许嵌套定义函数,但这类语言都支持块结构。通过代码块,我们可以在一个静态作用域里新建一个嵌套的静态作用域。这样就可以有效的控制变量的作用域,使其尽量的小,便于减少变量名冲突的问题。
(不过等C++0x正式成为标准后,基于C的语言里也将允许这种嵌套的函数定义)
=======================================================================================
让我们来看一个支持静态作用域,同时支持栈动态局部变量的例子:(C++)
int global; // ... void m( int paramOfM ) { int localOfM; // ... for ( int localToFor = 0; localToFor < 10; ++localToFor ) { int localToForBody; // ... } } void n( int paramOfN ) { int localOfN; // ... }
上面的代码所对应的作用域状况是:

很正常对吧,学过C/C++的人都知道这个。
在第一行声明的global变量是一个全局变量。它的作用域是它所在的整个源文件,在它声明了之后的部分。
paramOfM是函数m()的形式参数。它的作用域覆盖整个m()的范围。
localOfM是函数m()之内的一个局部变量。它的作用于从它声明的位置开始,到m()结束的地方为止。
localToFor是m()中的一个for循环中的一个控制变量。它是一个局部于for语句的变量,作用域覆盖了整个for语句中的部分,直到离开for语句块为止。注意到早期的C++并不是这样规定的;早期的C++规定,for语句开头的括号中声明的变量的作用域一直到包含这个for循环的代码块结束的位置为止。因此用过Visual C++ 6.0的人肯定都有过痛苦的经历:
for ( int i = 0; i < 10; ++i) { // ... } for ( int i = 0; i < 10; ++i) { // error: variable i is already defined // ... }
这个问题也不能怪VC++6不标准,VS6出的时候C++98还没什么编译器能完全正确的实现出来吧。
回到主题。前面代码里,localToForBody是在for语句的循环体里定义的一个局部变量。它的作用域从它声明的位置开始,到for的循环体结束的位置为止。值得注意的是,由于for语句开头的括号在for的循环体之前出现,所以循环体里定义的局部变量在开头的括号里都是不可见的。
后面是函数n()的作用域。可以看到m()与n()的作用域相互没有影响——它们相互没有交集。
=======================================================================================
让我们来看看,在现实程序中一个函数调用是如何实现的。下面的代码来自AQUAPLUS的ToHeart2。
my_inc2/MM_std.cpp, line 766:
BOOL STD_CheckFile( char *fname ) { HANDLE fh; fh = CREATE_READ_FILE( fname ); if( fh == INVALID_HANDLE_VALUE ) return FALSE; CloseHandle( fh ); return TRUE; }
其中CREATE_READ_FILE是一个宏,在my_inc2/MM_std.h定义:
#define CREATE_READ_FILE(FNAME) CreateFile( FNAME, GENERIC_READ , FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)
于是通过宏展开,上面的函数变为:
BOOL STD_CheckFile( char *fname ) { HANDLE fh; fh = CreateFile( fname, GENERIC_READ , FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if( fh == INVALID_HANDLE_VALUE ) return FALSE; CloseHandle( fh ); return TRUE; }
然后在程序启动时会调用上述函数,在WIN_Init()中,ScriptEngine/src/Winmain.cpp, line 1844:
InitBootFlag = !STD_CheckFile( "Sys.sav" );
调用关系是:
WIN_Init()
- STD_CheckFile()
-- CreateFile()
让我们仔细观察一下这个调用是如何进行的。注意到这里都采用C调用约定(cdecl),所以参数从右向左压入栈中,由调用者负责清理栈。
调用者的汇编代码是:
004B9025 push ToHeart2.00542C18 ; ASCII "Sys.sav" 004B902A call ToHeart2.004A6BA0
运行到这个call时,相应的运行时栈的内容是:
0012FD30 00542C18 ASCII "Sys.sav"
这个地方就是当前栈顶了。
跟进这个调用,看到栈的内容发生了变化:
0012FD2C 004B902F 返回到 ToHeart2.004B902F 来自 ToHeart2.004A6BA0 0012FD30 00542C18 ASCII "Sys.sav"
栈顶变为0012FD2C。可以看到,call指令的执行导致函数的返回地址被压入栈顶。
然后来看看被调用者(STD_CheckFile())的汇编代码:
004A6BA0 mov eax,dword ptr ss:[esp+4] ; 将第一个参数读入EAX寄存器中 004A6BA4 push 0 ; /hTemplateFile = NULL 004A6BA6 push 80 ; |Attributes = NORMAL 004A6BAB push 3 ; |Mode = OPEN_EXISTING 004A6BAD push 0 ; |pSecurity = NULL 004A6BAF push 1 ; |ShareMode = FILE_SHARE_READ 004A6BB1 push 80000000 ; |Access = GENERIC_READ 004A6BB6 push eax ; |FileName 004A6BB7 call dword ptr ds:[<&KERNEL32.CreateFileA>] ; \CreateFileA <- 调用CreatFileA()。上面是压入参数的过程 004A6BBD cmp eax,-1 ; 将返回值与INVALID_HANDLE_VALUE比对 004A6BC0 jnz short ToHeart2.004A6BC5 ; 若返回值是INVALID_HANDLE_VALUE则跳转到004A6BC5 004A6BC2 xor eax,eax ; 将EAX清零 004A6BC4 retn ; 正常返回 004A6BC5 push eax ; /hObject 004A6BC6 call dword ptr ds:[<&KERNEL32.CloseHandle>] ; \CloseHandle 004A6BCC mov eax,1 ; 将EAX设置为1 004A6BD1 retn ; 无法打开文件,出错返回
该函数的第一行将调用时传入的第一个参数读入了EAX。对应C的源代码看,源代码的第一行声明了一个局部变量。这个变量跑哪儿去了呢?
其实是被编译器优化掉了。如果编译器没有优化的话,这个变量很可能位于当前栈顶的“上面”(低地址方向)。于是这个例子里我们不会在栈上看到属于STD_CheckFile()的局部变量。它在\CreateFileA()返回后被放置在EAX里了。
继续运行到对CreateFileA的call那句时,栈的状况是:
0012FD10 00542C18 |FileName = "Sys.sav" 0012FD14 80000000 |Access = GENERIC_READ 0012FD18 00000001 |ShareMode = FILE_SHARE_READ 0012FD1C 00000000 |pSecurity = NULL 0012FD20 00000003 |Mode = OPEN_EXISTING 0012FD24 00000080 |Attributes = NORMAL 0012FD28 00000000 \hTemplateFile = NULL 0012FD2C 004B902F 返回到 ToHeart2.004B902F 来自 ToHeart2.004A6BA0 0012FD30 00542C18 ASCII "Sys.sav"
留意到现在从栈顶开始7个存储器单元(每个单元是一个双字,DWORD,32位)都是对CreateFileA()调用而传入的参数。
同样,跟进这个调用,看看栈的变化:
0012FD0C 004A6BBD /CALL 到 CreateFileA 来自 ToHeart2.004A6BB7 0012FD10 00542C18 |FileName = "Sys.sav" 0012FD14 80000000 |Access = GENERIC_READ 0012FD18 00000001 |ShareMode = FILE_SHARE_READ 0012FD1C 00000000 |pSecurity = NULL 0012FD20 00000003 |Mode = OPEN_EXISTING 0012FD24 00000080 |Attributes = NORMAL 0012FD28 00000000 \hTemplateFile = NULL 0012FD2C 004B902F 返回到 ToHeart2.004B902F 来自 ToHeart2.004A6BA0 0012FD30 00542C18 ASCII "Sys.sav"
跟前一个调用一样,call指令使返回地址被压入栈顶。
在CreatFileA()运行完毕,返回到STD_CheckFile()之后,栈的内容是:
0012FD2C 004B902F 返回到 ToHeart2.004B902F 来自 ToHeart2.004A6BA0 0012FD30 00542C18 ASCII "Sys.sav"
可以看到,跟调用CreatFileA()之前一样。retn指令会从栈顶弹出一个值作为返回地址,然后把EIP(指令指针)设置到那个值上,完成返回跳转。
程序从STD_CheckFile()返回到WIN_Init()之后,栈顶是:
0012FD30 00542C18 ASCII "Sys.sav"
由于C调用约定是由调用者来清理栈,而碰巧WIN_Init()接下来还要用到这个值,所以编译器优化让这个值保持在了栈顶。否则这个值也应该被抛弃掉了。
上面的例子主要是想说明C语言中的函数调用在现实中运行的情况,以便说明其语义。
每个函数被调用时,都会有一个活动记录(activation record)伴随产生。像C这样的语言,活动记录是在运行时栈上分配空间的,所以也成为栈帧(stack frame)。一个函数的活动记录以及该函数能访问到的任何“东西”(变量之类)的总和,被称为这个函数拥有的引用环境(referencing environment)。
一般来说,一个函数的参数和返回地址都会放置在栈帧里,在未经优化的时候局部变量也会放在栈帧里。这种运行方式使得一个函数的参数和局部变量都只在其运行当中才被分配空间;函数的运行一结束,分配的空间就解除了与相应变量的绑定(约束)。由于参数与局部变量的状况都是可以在程序运行前确定的,所以它们在栈帧中的位置(偏移量)可以静态确定。栈帧本身的存在于否只能到运行的时候才能知道,有动态性,所以这种生命期得名“栈动态”。
这样,变量的生命期就与静态作用域的规定产生了联系。
那么假如允许嵌套定义函数,并且允许内层函数访问外围函数里的变量会怎样呢?
可以观察到,内层函数只能为自己的参数和局部变量分配空间,而无法为外围函数中的变量分配空间;但同时,内层函数需要能够访问到外围函数里的局部变量,意味着它的活动记录里必须有某种手段去访问到外围函数的活动记录,否则内层函数的“引用环境”就不包含外围函数的活动记录也就无法访问外围函数里的局部变量。
(本来是想画图来表示的……时间不足,只好直接贴文字。下次补完吧……)
=======================================================================================
在接着讨论之前,先得说明一点:静态作用域并不总是意味着被嵌套的作用域(nested scope)中能访问包围它的外围作用域(enclosing scope)中的变量。
如果一种语言中,被嵌套的作用域总是能访问其外围作用域中的变量(或者说名字),则该语言被认为支持词法作用域(lexical scope)。反之则不算支持词法作用域。
(有些资料上将“静态作用域”与“词法作用域”写为同一个概念,或许这里还有值得商榷的地方吧。)
从一般意义的词法作用域概念来说,Python虽然支持静态作用域,但并不是一般的词法作用域。虽然有些Python用户可能会争论这点,但Python的词法作用域很明显与“一般的”不一样:
引用
If a name is assigned to anywhere in a code block (even in unreachable code), and is not mentioned in a global statement in that code block, then it refers to a local name throughout that code block.
C:\Python25>python ActivePython 2.5.1.1 (ActiveState Software Inc.) based on Python 2.5.1 (r251:54863, May 1 2007, 17:47:05) [MSC v.1310 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> def outer(x): ... def inner(): ... print x ... inner() ... inner() ... >>> outer(3) 3 3
Python里只能对函数内的局部变量或者全局变量赋值,而不能对非局部非全局的变量赋值;可以读取非局部非全局变量,但是一旦尝试赋值,Python就会自动创建一个新的同名的局部变量。根据词法作用域,上面的例子表明Python中嵌套的内部函数可以读取外围函数里的变量:对inner()来说x是一个自由变量,非局部非全局,并且被inner()外围的闭包所捕获因而可以访问。但下面的例子却会失败:
C:\Python25>python ActivePython 2.5.1.1 (ActiveState Software Inc.) based on Python 2.5.1 (r251:54863, May 1 2007, 17:47:05) [MSC v.1310 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> def outer(x): ... def inner(): ... x += 1 ... print x ... inner() ... inner() ... >>> outer(2) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 5, in outer File "<stdin>", line 3, in inner UnboundLocalError: local variable 'x' referenced before assignment
+=运算符包含了三个操作,先计算运算符左边的表达式,再将运算符右边的表达式的值加上左边表达式原本的值,最后赋值回到左边的表达式对应的变量上。由于包含了赋值操作,Python在查找变量x时只在局部或全局范围内查找,而此时符合这样条件的局部变量x还不存在(或者说还未被赋值过),所以出现了错误。这是设计者为了安全而做的设计取舍,不过无论如何它与一般意义上的词法作用域不一样。
最典型的地方是Python里成员方法都必须以self为第一个参数的做法(名字不一定要是self,不过约定上都叫这个名字)……不熟悉Python的话算了,这个不用深究。
=======================================================================================
允许嵌套定义子程序的语言也可以被分为三类:允许将函数作为参数传递的与不允许的,还有中间比较奇怪的(=_=)。如果一种语言允许将函数作为参数传递,意味着这种语言里的函数至少是二级的类型。
先从不能将函数作为参数传递的开始讨论。
(待补完)
------------
然后是允许将函数作为参数传递的。
标准Pascal(ISO 7185:1990):
引用Wikipedia上的例子:
function E(x: real): real function F(y: real): real begin F := x + y end begin E := F(3) end
留意到在F()里,外围的E()中的x是如何被“捕获”到当前作用域的。对于F()来说,y是一个形式参数,因而是一个“约束变量”;x既不是局部变量或者参数,也不是全局变量,因而是一个“自由变量”。F()的执行依赖于外部环境提供的“x”变量才可以进行。
Pascal是支持栈动态变量的语言。上面的例子中,x和y的存储器空间都是在运行时栈上分配的。
ISO 7185:1990,6.6.3 Parameters部分说明了将过程或者函数作为参数传递的一些规定。
------------
GNU C:
GNU C算是个比较特殊的实现吧。上面Pascal的例子在GNU C里也可以写出来:
float E(float x) { float F(float y) { return x + y; } return F(3); }
注意:只是GNU C支持嵌套定义函数,GNU C++是不支持的。
C里的函数指针可以传递函数的代码,却无法传递函数的环境。因此,当允许嵌套定义函数时,内层函数只有在外围函数活动的时候才能捕获到其引用环境。下次再详细说……
接下来让我们看看JavaScript的例子。

(待补完)
=======================================================================================
闭包与对象的等价性
(待补完)
=======================================================================================
闭包的应用意义:
1、状态隐藏
2、算法封装
3、自定义控制流
...
(待补完)
=======================================================================================
Lambda Expressions and Closures: Wording for Monomorphic Lambdas (Revision 4)
在C++0x之前,我们经常会使用functor来解决一些问题:
引用
class between { double low, high; public: between(double l, double u) : low(l), high(u) { } bool operator()(const employee& e) { return e.salary() >= low && e.salary() < high; } } .... double min_salary; .... std::find_if(employees.begin(), employees.end(), between(min_salary, 1.1 * min_salary));
The constructor call between(min_salary, 1.1 * min_salary) creates a function object, which is comparable to
what, e.g., in the context of functional programming languages is known as a closure. A closure stores the
environment, that is the values of the local variables, in which a function is defined. Here, the environment
stored in the between function object are the values low and high, which are computed from the value of the
local variable min_salary.
但是这样挺麻烦的。明明只是需要一个函数,却需要写一整个类出来。而有了C++0x的lambda表达式之后,就可以在函数内直接定义匿名的嵌套函数了。
double min_salary = .... .... double u_limit = 1.1 * min_salary; std::find_if(employees.begin(), employees.end(), [&](const employee& e) { return e.salary() >= min_salary && e.salary() < u_limit; });
#include <algorithm> #include <vector> void outer( ) { std::vector<int> v; int i; // ... std::for_each( v.begin(), v.end(), [ &i ]( int& elem ) { std::cout << elem << ' '; }); // ... }
这段代码里的作用域:

(哎呀,图做完了才发觉for_each之前漏了std::……算了懒得重新做图,凑合吧)
不过这个例子在C++0x里也未必要用到lambda表达式了,直接用新增加的for-each循环就行:
std::vector<int> v; int i; // ... for ( int elem : v ) { cout << elem << ' '; ++i; }
如果在elem前面加上&来修饰,则对elem的赋值也会反映到容器内。真好。
引用
If every name in the effective capture set is preceded by &, F is publicly derived from std::reference_closure<
R(P)> (20.5.17), where R is the return type and P is the parameter-type-list of the lambda expression. Converting
an object of type F to type std::reference_closure<R(P)> and invoking its function call operator shall
have the same effect as invoking the function call operator of F. [ Note: This requirement effectively means that such
F must be implemented using a pair of a function pointer and a static scope pointer. —end note ]
R(P)> (20.5.17), where R is the return type and P is the parameter-type-list of the lambda expression. Converting
an object of type F to type std::reference_closure<R(P)> and invoking its function call operator shall
have the same effect as invoking the function call operator of F. [ Note: This requirement effectively means that such
F must be implemented using a pair of a function pointer and a static scope pointer. —end note ]
照这么说,C++0x里的闭包也是D 2.0早期所定义的“动态闭包”了。关于D的动态闭包,可以看看我去年写的这帖,靠下面的部分。
(待补完)
=======================================================================================
相关链接:
Wikipedia:
Closure (computer science)
Lambda Calculus
Free variables and bound variables
Nested Function
C++0x
Cunningham & Cunningham, Inc.:
Lexical Closure
发表评论
-
Sun JDK1.4.2_28有TieredCompilation
2014-05-12 08:48 0原来以前Sun的JDK 1.4.2 update 28就已经有 ... -
IBM JVM notes (2014 ver)
2014-05-11 07:16 0Sovereign JIT http://publib.bou ... -
Java 8的lambda表达式在OpenJDK8中的实现
2014-02-04 12:08 0三月份JDK8就要发布首发了,现在JDK8 release c ... -
基于LLVM实现VM的JIT的一些痛点
2014-01-07 17:25 0同事Philip Reames Sanjoy Das http ... -
tailcall notes
2013-12-27 07:42 0http://blogs.msdn.com/b/clrcode ... -
《自制编程语言》的一些笔记
2013-11-24 00:20 0http://kmaebashi.com/programmer ... -
字符串的一般封装方式的内存布局 (1): 元数据与字符串内容,整体还是分离?
2013-11-07 17:44 22433(Disclaimer:未经许可请 ... -
字符串的一般封装方式的内存布局 (0): 拿在手上的是什么
2013-11-04 18:22 21532(Disclaimer:未经许可请 ... -
字符串的一般封装方式的内存布局
2013-11-01 12:55 0(Disclaimer:未经许可请 ... -
关于string,内存布局,C++ std::string,CoW
2013-10-30 20:45 0(Disclaimer:未经许可请 ... -
Function.prototype.bind
2013-09-24 18:07 0polyfill http://stackoverflow. ... -
Java的instanceof是如何实现的
2013-09-22 16:57 0Java语言规范,Java SE 7版 http://docs ... -
struct做参数不能从寄存器传?
2013-08-28 23:33 0test test test struct Foo { i ... -
也谈类型: 数据, 类型, 标签
2013-08-18 01:59 0numeric tower http://en.wikiped ... -
SDCC 2012上做的JVM分享
2012-10-17 16:35 32686刚把在SDCC 2012做的JVM分享的演示稿上传了。 演示 ... -
运行时支持系统的“重量”
2012-10-12 16:08 0运行时支持系统的“重量” 好久没写博客了,可写的话题已经堆积 ... -
class?metaclass?meta-what?
2011-04-05 19:43 0http://en.wikipedia.org/wiki/Me ... -
“代码模式”与抽象
2010-10-28 15:21 0嗯,我是说“代码模式”,不是“设计模式”;这里指的是在给定的场 ... -
lvalue与rvalue
2010-09-03 00:40 0http://en.wikipedia.org/wiki/Va ... -
动态链接的“依据”
2010-02-09 09:54 0动态链接,意味着在生成的“东西”里留有符号信息,等到运行时再r ...
相关推荐
Lambda表达式的捕获列表可以捕获当前函数作用域的零个或多个变量,变量之间用逗号分隔;这些变量可以在Lambda表达式中被访问和修改。捕获方式有三种,分别是值捕获、引用捕获和混合捕获。 值捕获:将外部变量以...
Lambda表达式可以使用与其相同范围scope内的变量,这个引入符的作用就是表明,其后的lambda表达式以何种方式使用(正式的术语是“捕获”)这些变量(这些变量能够在lambda表达式中被捕获,其实就是构成了一个闭包)...
**C++ Lambda表达式**是C++编程语言中一个强大的特性,从C++11标准开始引入,到C++20标准进一步增强。Lambda表达式允许程序员在代码中定义匿名函数,即没有名称的函数,这极大地提高了代码的灵活性和可读性。本书...
* lambda 表达式的闭包:lambda 表达式可以捕获外部变量,并将其作为闭包的一部分。 * 递归算法的优点:递归算法可以将复杂的问题分解成更小的子问题,从而使得问题变得更易解决。 * 递归算法的缺点:递归算法可能会...
查Lambda表达式资料时很容易被函数闭包、Lambda演算、形式系统这些深奥名词淹没而放弃学习,其实Lambda表达式就是匿名函数(annoymous function)——允许我们使用一个函数,但不需要给这个函数起名字。还是有点难懂...
虽然Lambda表达式非常灵活,但它们也有一些限制,比如不能包含控制流语句(如`if`、`for`或`while`)以及无法访问局部变量(除非是闭包)。 总之,C# Lambda表达式提供了一种简洁的方式来定义函数,尤其在处理数据...
Lambda 表达式,也被称作闭包,它使得 Java 语言变得更加灵活和具有函数式编程特性。在 Java 8 之前,如果要将一个行为(例如方法)作为参数传递给另一个方法,通常需要通过定义一个接口和一个实现该接口的匿名类。...
Lambda表达式可以捕获其定义范围内的变量,这种特性称为闭包。这些变量在其生命周期内保持有效,即使Lambda表达式已经定义在该范围之外。 ### 9. Lambda表达式与异步编程 在C#中,`async`和`await`关键字可以与...
实际上是一个闭包(closure),类似于一个匿名函数,拥有捕获所在作用域中变量的能力,能够将函数做为对象一样使用,通常用来实现回调函数、代理等功能。Lambda表达式是函数式编程的基础,C++11引入了Lambda则弥补了...
1. **闭包**:能够捕获外部作用域的变量,形成闭包。 ```cpp auto add = [x = 5](int y) { return x + y; }; ``` 2. **类型推导**:使用`auto`关键字可以自动推导出lambda表达式的类型。 ```cpp auto lambda...
3. **捕获机制**:通过捕获列表,Lambda 可以访问并操作外部作用域的变量,提供了闭包的能力。 **注意事项与陷阱:** 1. **按值捕获**:捕获的变量是按值复制的,所以任何对原变量的修改不会影响到Lambda内的副本。...
此外,Lambda表达式也可以与`QVector`、`QList`等容器结合,用于高效地迭代和操作数据集合,如`std::transform`和`std::for_each`算法: ```cpp QVector<int> numbers = {1, 2, 3, 4, 5}; std::transform(numbers....
1. **捕获列表(Capture Clause)**:定义Lambda函数如何访问外部作用域中的变量。 2. **参数列表(Parameter List)**:定义传递给Lambda函数的参数。 3. **返回类型(Return Type)**:定义Lambda函数返回值的类型。 4. ...
闭包是JavaScript中一个高级概念,它允许一个函数记住并访问它外部作用域中的变量,即使该函数在其外部作用域之外执行也是如此。 ##### 1. 闭包的定义 闭包是由函数和与其相关的引用环境组合而成的实体,这个环境...
当Lambda表达式引用了一个局部变量时,即使该变量在Lambda表达式执行时已经超出作用域,它仍然可以访问。但是要注意,这种访问方式创建的是副本,而非对原始变量的引用,因此对于可变类型(如引用类型)会有不同的...
Python 中的变量作用域可以分为四个层次:局部作用域(Local scope)、外部作用域(Enclosing scope)、全局作用域(Global scope)和内置作用域(Built-in scope)。 局部作用域是指当前函数或代码块中的变量作用...
3. 闭包支持:Lambda表达式支持闭包,允许函数访问直接词法作用域之外的变量。 4. 类型推论支持:Lambda表达式支持类型推论,能够自动推断出类型,简化代码编写。 Lambda表达式在Java8中的应用 在Java8中,Lambda...
Python 的变量名解析机制也称为 LEGB 法则,具体如下:当在函数中使用未确定的变量名时,Python 搜索四个作用域:本地作用域(L),之后是上一层嵌套结构中 def 或 lambda 的本地作用域(E),之后是全局作用域(G)...
闭包是一种可以记住并访问其定义时作用域内变量的函数,即使这些变量在其调用时可能已经不存在。通过捕获列表,lambda表达式能够捕获并存储外部变量的状态,这使得闭包成为处理异步操作、事件驱动编程等场景的强大...
Lambda表达式起源于数学中的λ演算,这是一种用变量和代换规则来表示计算的逻辑系统。在编程领域,Lambda表达式允许我们定义轻量级的匿名函数,无需预先声明委托类型,可以直接在需要的地方使用。C# 3.0引入Lambda...