博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
C语言中闭包的探究及比较
阅读量:4106 次
发布时间:2019-05-25

本文共 10816 字,大约阅读时间需要 36 分钟。

 

转自

http://blog.csdn.net/jasonblog/article/details/8077340

这是一篇技术分享,并且之前发表在酷壳上,,现在发布到自己的博客上。

下文是直接从酷客复制过来的,这里偷了个懒,没有再次对格式做很仔细的整理,只有稍微整理。汗。

这里主要讨论的是C语言的扩展特性。该特性是Apple为C、C++、Objective-C增加的扩展,让这些语言可以用类Lambda表达式的语法来创建。前段时间,在对CoreData存取进行封装时(让开发人员可以更简洁快速地写相关代码),我对block机制有了进一步了解,觉得可以和C++ 11中的Lambda表达式相互印证,所以最近重新做了下整理,分享给大家。

0. 简单创建匿名函数

下面两段代码的作用都是创建匿名函数并调用,输出Hello, World语句。分别使用Objective-C和C++ 11:

[cpp] 
  1. ^{printf("Hello, World!\n"); } ();  
[cpp] 
  1. [] { cout << "Hello, World" << endl; } ();  

Lambda表达式的一个好处就是让开发人员可以在需要的时候临时创建函数,便捷。

在创建闭包(或者说Lambda函数)的语法上,Objective-C采用的是上尖号^,而C++ 11采用的是配对的方括号[]

不过“匿名函数”一词是针对程序员而言的,编译器还是采取了一定的命名规则。

比如下面Objective-C代码中的3个block,

[cpp] 
  1. #import <Foundation/Foundation.h>  
  2.    
  3. int(^maxBlk)(intint) = ^(int m, int n){ return m > n ? m : n; };  
  4.    
  5. int main(int argc, const char * argv[])  
  6. {  
  7.     ^{printf("Hello, World!\n"); } ();  
  8.    
  9.     int i = 1024;  
  10.     void(^blk)(void) = ^{ printf("%d\n", i); };  
  11.     blk();  
  12.    
  13.     return 0;  
  14. }  
会产生对应的3个函数:

[cpp] 
  1. __maxBlk_block_func_0  
  2. __main_block_func_0  
  3. __main_block_func_1  

可见函数的命名规则为:__{$Scope}_block_func_{$index}。其中{$Scope}为block所在函数,如果{$Scope}为全局就取block本身的名称;{$index}表示该block在{$Scope}作用域内出现的顺序(第几个block)。

1. 从语法上看如何捕获外部变量

在上面的代码中,已经看到“匿名函数”可以直接访问外围作用域的变量i:

[cpp] 
  1. int i = 1024;  
  2. void(^blk)(void) = ^{ printf("%d\n", i); };  
  3. blk();  

当匿名函数和non-local变量结合起来,就形成了闭包(个人看法)。

这一段代码可以成功输出i的值。

我们把一样的逻辑搬到C++上:

[cpp] 
  1. inti = 1024;  
  2. auto func = [] { printf("%d\n", i); };  
  3. func();  

GCC会输出:错误:‘i’未被捕获。可见在C++中无法直接捕获外围作用域的变量。

以BNF来表示Lambda表达式的上下文无关文法,存在:

[cpp] 
  1. lambda-expression : lambda-introducer lambda-parameter-declarationopt compound-statement  
  2. lambda-introducer : [ lambda-captureopt ]  
因此,方括号中还可以加入一些选项:
[cpp] 
  1. []        Capture nothing (or, a scorched earth strategy?)  
  2. [&]       Capture any referenced variable by reference  
  3. [=]       Capture any referenced variable by making a copy  
  4. [=, &foo] Capture any referenced variable by making a copy, but capture variable foo by reference  
  5. [bar]     Capture bar by making a copy; don't copy anything else  
  6. [this]    Capture the thispointer of the enclosing class  
根据文法,对代码加以修改,使其能够成功运行:
[cpp] 
  1. bash-3.2# vi testLambda.cpp  
  2. bash-3.2# g++-4.7 -std=c++11 testLambda.cpp -o testLambda  
  3. bash-3.2# ./testLambda  
  4. 1024  
  5. bash-3.2# cat testLambda.cpp  
  6. #include <iostream>  
  7.    
  8. using namespace std;  
  9.    
  10. int main()  
  11. {  
  12.      int i = 1024;  
  13.      auto func = [=] { printf("%d\n", i); };  
  14.      func();  
  15.    
  16.      return 0;  
  17. }  
  18. bash-3.2#  

2. 从语法上看如何修改外部变量

上面代码中使用了符号=,通过拷贝方式捕获了外部变量i。

但是如果尝试在Lambda表达式中修改变量i:

[cpp] 
  1. auto func = [=] { i = 0; printf("%d\n", i); };  
会得到错误:
[cpp] 
  1. testLambda.cpp: 在 lambda 函数中:  
  2. testLambda.cpp:9:24: 错误:向只读变量‘i’赋值  

可见通过拷贝方式捕获的外部变量是只读的。Python中也有一个类似的经典case,个人觉得有相通之处:

[cpp] 
  1. x=10  
  2. def foo():  
  3.     print(x)  
  4.     x+=1  
  5. foo()  

这段代码会抛出UnboundLocalError错误,原因可以参见。

在C++的闭包语法中,如果需要对外部变量的写权限,可以使用符号&,通过引用方式捕获:

[cpp] 
  1. int i = 1024;  
  2. auto func = [&] { i = 0; printf("%d\n", i); };  
  3. func();  

反过来,将修改外部变量的逻辑放到Objective-C代码中:

[cpp] 
  1. int i = 1024;  
  2. void(^blk)(void) = ^{ i = 0; printf("%d\n", i); };  
  3. blk();  

会得到如下错误:

[cpp] 
  1. main.m:14:29: error: variable is not assignable (missing __block type specifier)  
  2.     void(^blk)(void) = ^{ i++; printf("%d\n", i); };  
  3.                            ~^  
  4. 1 error generated.  

可见在block的语法中,默认捕获的外部变量也是只读的,如果要修改外部变量,需要使用__block类型指示符进行修饰。

为什么呢?请继续往下看 :)

3. 从实现上看如何捕获外部变量

闭包对于编程语言来说是一种语法糖,包括Block和Lambda,是为了方便程序员开发而引入的。因此,对Block特性的支持会落地在编译器前端,中间代码将会是C语言。

先看如下代码会产生怎样的中间代码。

[cpp] 
  1. int main(int argc, const char * argv[])  
  2. {  
  3.     int i = 1024;  
  4.     void(^blk)(void) = ^{ printf("%d\n", i); };  
  5.     blk();  
  6.    
  7.     return 0;  
  8. }  

首先是block结构体的实现:

[cpp] 
  1. #ifndef BLOCK_IMPL  
  2. #define BLOCK_IMPL  
  3. struct__block_impl {  
  4.     void *isa;  
  5.     int Flags;  
  6.     int Reserved;  
  7.     void *FuncPtr;  
  8. };  
  9. // 省略部分代码  
  10.    
  11. #endif  

第一个成员isa指针用来表示该结构体的类型,使其仍然处于Cocoa的对象体系中,类似Python对象系统中的PyObject。

第二、三个成员是标志位和保留位。

第四个成员是对应的“匿名函数”,在这个例子中对应函数:

[cpp] 
  1. static void __main_block_func_0(struct __main_block_impl_0 *__cself) {  
  2.     inti = __cself->i; // bound by copy  
  3.     printf("%d\n", i);  
  4. }  

函数__main_block_func_0引入了参数__cself,为struct __main_block_impl_0 *类型,从参数名称就可以看出它的功能类似于C++中的this指针或者Objective-C的self。

而struct __main_block_impl_0的结构如下:

[cpp] 
  1. struct __main_block_impl_0 {  
  2.     struct __block_impl impl;  
  3.     struct __main_block_desc_0* Desc;  
  4.     int i;  
  5.     __main_block_impl_0(void*fp, struct__main_block_desc_0 *desc, int_i, intflags=0) : i(_i) {  
  6.         impl.isa = &_NSConcreteStackBlock;  
  7.         impl.Flags = flags;  
  8.         impl.FuncPtr = fp;  
  9.         Desc = desc;  
  10.     }  
  11. };  

从__main_block_impl_0这个名称可以看出该结构体是为main函数中第零个block服务的,即示例代码中的blk;也可以猜到不同场景下的block对应的结构体不同,但本质上第一个成员一定是struct __block_impl impl,因为这个成员是block实现的基石。

结构体__main_block_impl_0又引入了一个新的结构体,也是中间代码里最后一个结构体:

[cpp] 
  1. static struct __main_block_desc_0 {  
  2.     unsigned long reserved;  
  3.     unsigned long Block_size;  
  4. } __main_block_desc_0_DATA = { 0, sizeof(struct__main_block_impl_0)};  

可以看出,这个描述性质的结构体包含的价值信息就是struct __main_block_impl_0的大小。

最后剩下main函数对应的中间代码:

[cpp] 
  1. int main(int argc, const char * argv[])  
  2. {  
  3.     int i = 1024;  
  4.     void(*blk)(void) = (void(*)(void))&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, i);  
  5.     ((void(*)(struct__block_impl *))((struct__block_impl *)blk)->FuncPtr)((struct__block_impl *)blk);  
  6.    
  7.     return 0;  
  8. }  

从main函数对应的中间代码可以看出执行block的本质就是以block结构体自身作为__cself参数,这里对应__main_block_impl_0,通过结构体成员FuncPtr函数指针调用对应的函数,这里对应__main_block_func_0。

其中,局部变量i是以值传递的方式拷贝一份,作为__main_block_impl_0的构造函数的参数,并以初始化列表的形式赋值给其成员变量i。所以,基于这样的实现,不允许直接修改外部变量是合理的——因为按值传递根本改不到外部变量。

4. 从实现上看如何修改外部变量(__block类型指示符)

如果想要修改外部变量,则需要用__block来修饰:

[cpp] 
  1. int main(int argc, const char * argv[])  
  2. {  
  3.     __block int i = 1024;  
  4.     void(^blk)(void) = ^{ i = 0; printf("%d\n", i); };  
  5.     blk();  
  6.    
  7.     return 0;  
  8. }  
此时再看中间代码,发现多了一个结构体:
[cpp] 
  1. struct __Block_byref_i_0 {  
  2.     void *__isa;  
  3.     __Block_byref_i_0 *__forwarding;  
  4.     int __flags;  
  5.     int __size;  
  6.     int i;  
  7. };  

于是,用__block修饰的int变量i化身为__Block_byref_i_0结构体的最后一个成员变量

代码中blk对应的结构体也发生了变化:

[cpp] 
  1. struct __main_block_impl_0 {  
  2.     struct __block_impl impl;  
  3.     struct __main_block_desc_0* Desc;  
  4.     __Block_byref_i_0 *i; // by ref  
  5.     __main_block_impl_0(void*fp, struct__main_block_desc_0 *desc, __Block_byref_i_0 *_i, intflags=0) : i(_i->__forwarding) {  
  6.         impl.isa = &_NSConcreteStackBlock;  
  7.         impl.Flags = flags;  
  8.         impl.FuncPtr = fp;  
  9.         Desc = desc;  
  10.     }  
  11. };  
__main_block_impl_0发生的变化就是int类型的成员变量i换成了__Block_byref_i_0 *类型,从名称可以看出现在要通过引用方式来捕获了。

对应的函数也不同了:

[cpp] 
  1. static void __main_block_func_0(struct __main_block_impl_0 *__cself) {  
  2.     __Block_byref_i_0 *i = __cself->i; // bound by ref  
  3.     (i->__forwarding->i) = 0; // 看起来很厉害的样子  
  4.     printf("%d\n", (i->__forwarding->i));  
  5. }  
main函数也有了变动:
[cpp] 
  1. int main(int argc, const char * argv[])  
  2. {  
  3.     __block __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024};  
  4.     void(*blk)(void) = (void(*)(void))&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, (struct__Block_byref_i_0 *)&i, 570425344);  
  5.     ((void(*)(struct__block_impl *))((struct__block_impl *)blk)->FuncPtr)((struct__block_impl *)blk);  
  6.    
  7.     return 0;  
  8. }  
前两行代码创建了两个关键结构体,特地高亮显示。

这里没有看__main_block_desc_0发生的变化,放到后面讨论

使用__block类型指示符的本质就是引入了__Block_byref_{$var_name}_{$index}结构体,而被__block关键字修饰的变量就被放到这个结构体中。另外,block结构体通过引入__Block_byref_{$var_name}_{$index}指针类型的成员,得以间接访问到外部变量。

通过这样的设计,我们就可以修改外部作用域的变量了,再一次应了那句话:

There is no problem in computer science that can’t be solved by adding another level of indirection.

指针是我们最经常使用的间接手段,而这里的本质也是通过指针来间接访问,为什么要特地引入__Block_byref_{$var_name}_{$index}结构体,而不是直接使用int *来访问外部变量i呢?

另外,__Block_byref_{$var_name}_{$index}结构体中的__forwarding指针成员有何作用?

请继续往下看 :)

5. 背后的内存管理动作

在Objective-C中,block特性的引入是为了让程序员可以更简洁优雅地编写并发代码(配合看起来像敏感词的GCD)。比较常见的就是将block作为函数参数传递,以供后续回调执行。

先看一段完整的、可执行的代码:

[cpp] 
  1. #import <Foundation/Foundation.h>  
  2. #include <pthread.h>  
  3.    
  4. typedef void (^DemoBlock)(void);  
  5.    
  6. void test();  
  7. void *testBlock(void *blk);  
  8.    
  9. int main(int argc, const char * argv[])  
  10. {  
  11.     printf("Before test()\n");  
  12.     test();  
  13.     printf("After test()\n");  
  14.    
  15.     sleep(5);  
  16.     return 0;  
  17. }  
  18.    
  19. void test()  
  20. {  
  21.     __block int i = 1024;  
  22.     void(^blk)(void) = ^{ i = 2048; printf("%d\n", i); };  
  23.    
  24.     pthread_tthread;  
  25.     int ret = pthread_create(&thread, NULL, testBlock, (void*)blk);  
  26.     printf("thread returns : %d\n", ret);  
  27.    
  28.     sleep(3);// 这里睡眠1s的话,程序会崩溃  
  29. }  
  30.    
  31. void *testBlock(void *blk)  
  32. {  
  33.     sleep(2);  
  34.    
  35.     printf("testBlock : Begin to exec blk.\n");  
  36.     DemoBlock demoBlk = (DemoBlock)blk;  
  37.     demoBlk();  
  38.    
  39.     returnNULL;  
  40. }  
在这个示例中,位于test()函数的block类型的变量blk就作为函数参数传递给testBlock。

正常情况下,这段代码可以成功运行,输出:

[cpp] 
  1. Before test()  
  2. threadreturns : 0  
  3. testBlock : Begin to exec blk.  
  4. 2048  
  5. After test()  

如果按照注释,将test()函数最后一行改为休眠1s的话,正常情况下程序会在输出如下结果后崩溃:

[cpp] 
  1. Before test()  
  2. threadreturns : 0  
  3. After test()  
  4. testBlock : Begin to exec blk.  

从输出可以看出,当要执行blk的时候,test()已经执行完毕回到main函数中,对应的函数栈也已经展开,此时栈上的变量已经不存在了,继续访问导致崩溃——这也是不用int *直接访问外部变量i的原因。

5.1 拷贝block结构体

上文提到block结构体__block_impl的第一个成员是isa指针,使其成为NSObject的子类,所以我们可以通过相应的内存管理机制将其拷贝到堆上:

[cpp] 
  1. void test()  
  2. {  
  3.     __block int i = 1024;  
  4.     void(^blk)(void) = ^{ i = 2048; printf("%d\n", i); };  
  5.    
  6.     pthread_tthread;  
  7.     intret = pthread_create(&thread, NULL, testBlock, (void*)[blk copy]);  
  8.     printf("thread returns : %d\n", ret);  
  9.    
  10.     sleep(1);  
  11. }  
  12.    
  13. void*testBlock(void*blk)  
  14. {  
  15.     sleep(2);  
  16.    
  17.     printf("testBlock : Begin to exec blk.\n");  
  18.     DemoBlock demoBlk = (DemoBlock)blk;  
  19.     demoBlk();  
  20.     [demoBlk release];  
  21.    
  22.     return NULL;  
  23. }  

再次执行,得到输出:

[cpp] 
  1. Before test()  
  2. threadreturns : 0  
  3. After test()  
  4. testBlock : Begin to exec blk.  
  5. 2048  

可以看出,在test()函数栈展开后,demoBlk仍然可以成功执行,这是由于blk对应的block结构体__main_block_impl_0已经在堆上了。不过这还不够——

5.2 拷贝捕获的变量(__block变量)

在拷贝block结构体的同时,还会将捕获的__block变量,即结构体__Block_byref_i_0,复制到堆上。这个任务落在前面没有讨论的__main_block_desc_0结构体身上:

[cpp] 
  1. static void __main_block_copy_0(struct__main_block_impl_0*dst, struct__main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}  
  2.    
  3. static void __main_block_dispose_0(struct__main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}  
  4.    
  5. static struct __main_block_desc_0 {  
  6.     unsignedlongreserved;  
  7.     unsignedlongBlock_size;  
  8.     void(*copy)(struct__main_block_impl_0*, struct__main_block_impl_0*);  
  9.     void(*dispose)(struct__main_block_impl_0*);  
  10. } __main_block_desc_0_DATA = { 0, sizeof(struct__main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};  
栈上的__main_block_impl_0结构体为src,堆上的__main_block_impl_0结构体为dst,当发生复制动作时,__main_block_copy_0函数会得到调用,将src的成员变量i,即__Block_byref_i_0结构体,也
复制到堆上
5.3 __forwarding指针的作用

当复制动作完成后,栈上和堆上都存在着__main_block_impl_0结构体。如果栈上、堆上的block结构体都对捕获的外部变量进行操作,会如何?

下面是一段示例代码:

[cpp] 
  1. void test()  
  2. {  
  3.     __block int i = 1024;  
  4.     void(^blk)(void) = ^{ i++; printf("%d\n", i); };  
  5.    
  6.     pthread_tthread;  
  7.     intret = pthread_create(&thread, NULL, testBlock, (void*)[blk copy]);  
  8.     printf("thread returns : %d\n", ret);  
  9.    
  10.     sleep(1);  
  11.     blk();  
  12. }  
  13.    
  14. void *testBlock(void*blk)  
  15. {  
  16.     sleep(2);  
  17.    
  18.     printf("testBlock : Begin to exec blk.\n");  
  19.     DemoBlock demoBlk = (DemoBlock)blk;  
  20.     demoBlk();  
  21.     [demoBlk release];  
  22.    
  23.     returnNULL;  
  24. }  
在test()函数中调用pthread_create创建线程时,
blk被复制了一份到堆上作为testBlock函数的参数。
  1. test()函数中的blk结构体位于栈中,在休眠1s后被执行,对i进行自增动作。
  2. testBlock函数在休眠2s后,执行位于堆上的block结构体,这里为demoBlk。

上述代码执行后输出:

[cpp] 
  1. Beforetest()  
  2. thread returns : 0  
  3. 1025  
  4. Aftertest()  
  5. testBlock : Begin to execblk.  
  6. 1026  

可见无论是栈上的还是堆上的block结构体,修改的都是同一个__block变量

这就是前面提到的__forwarding指针成员的作用了:

起初,栈上的__block变量的成员指针__forwarding指向__block变量本身,即栈上的__Block_byref_i_0结构体。

当__block变量被复制到堆上后,栈上的__block变量的__forwarding成员会指向堆上的那一份拷贝,从而保持一致。

参考资料:

你可能感兴趣的文章
CSS语法简单入门
查看>>
javascript中的对象查找
查看>>
Google 面试中的古怪问题
查看>>
HTML5标准学习 – 文档结构
查看>>
告别wordpress,拥抱jekyll
查看>>
企业网站用户体验度的5点小建议
查看>>
别告诉我你懂Javascript
查看>>
为什么CoffeeScript这么美?
查看>>
Node入门
查看>>
什么是Node.js
查看>>
开发者最容易犯的13个JavaScript错误
查看>>
现在就使用HTML5的十大原因
查看>>
写了10年Javascript未必全了解的连续赋值运算
查看>>
关于html5的7个传说
查看>>
javascript程序编码规范
查看>>
10 个技巧让你的 RESTful Web 服务更加实用
查看>>
为什么《七周七语言》选中的是这几种语言?
查看>>
最佳 Web 中文默认字体
查看>>
默认Web字体样式
查看>>
浏览器如何渲染文本
查看>>