当前位置:编程学习 > C/C++ >>

C语言中闭包的探究及比较

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

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

0. 简单创建匿名函数

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

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

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

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

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

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

[cpp] 
#import <Foundation/Foundation.h> 
  
int(^maxBlk)(int, int) = ^(intm, intn){ returnm > n ? m : n; }; 
  
int main(intargc, constchar * argv[]) 

    ^{printf("Hello, World!\n"); } (); 
  
    int i = 1024; 
    void(^blk)(void) = ^{ printf("%d\n", i); }; 
    blk(); 
  
    return 0; 

会产生对应的3个函数:

[cpp] 
__maxBlk_block_func_0 
__main_block_func_0 
__main_block_func_1 

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

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

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

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

当匿名函数和non-local变量结合起来,就形成了闭包(个人看法)。
这一段代码可以成功输出i的值。

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

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

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

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

[cpp] 
lambda-expression : lambda-introducer lambda-parameter-declarationopt compound-statement 
lambda-introducer : [ lambda-captureopt ] 

因此,方括号中还可以加入一些选项:
[cpp] 
[]        Capture nothing (or, a scorched earth strategy?) 
[&]       Capture any referenced variable by reference 
[=]       Capture any referenced variable by 易做图 a copy 
[=, &foo] Capture any referenced variable by 易做图 a copy, but capture variable foo by reference 
[bar]     Capture bar by 易做图 a copy; don't copy anything else 
[this]    Capture the thispointer of the enclosing class 

根据文法,对代码加以修改,使其能够成功运行:
[cpp] 
bash-3.2# vi testLambda.cpp 
bash-3.2# g++-4.7 -std=c++11 testLambda.cpp -o testLambda 
bash-3.2# ./testLambda 
1024 
bash-3.2# cat testLambda.cpp 
#include <iostream> 
  
using namespace std; 
  
int main() 

     int i = 1024; 
     auto func = [=] { printf("%d\n", i); }; 
     func(); 
  
     return 0; 

bash-3.2# 


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

上面代码中使用了符号=,通过拷贝方式捕获了外部变量i。
但是如果尝试在Lambda表达式中修改变量i:

[cpp] 
auto func = [=] { i = 0; printf("%d\n", i); }; 

会得到错误:
[cpp] 
testLambda.cpp: 在 lambda 函数中: 
testLambda.cpp:9:24: 错误:向只读变量‘i’赋值 

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

[cpp] 
x=10 
def foo(): 
    print(x) 
    x+=1 
foo() 

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

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

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

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

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

会得到如下错误:

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

可见在block的语法中,默认捕获的外部变量也是只读的,如果要修改外部变量,需要使用__block类型指示符进行修饰。
为什么呢?请继续往下看 :)

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

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

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

[cpp] 
int main(intargc, constchar * argv[]) 

    int i = 1024; 
    void(^blk)(void) = ^{ printf("%d\n", i); }; 
    blk(); 
  
    return 0; 

首先是block结构体的实现:

[cpp]
#ifndef BLOCK_IMPL 
#define BLOCK_IMPL 
struct__block_impl { 
    void *isa; 
    int Flags; 
    int Reserved; 
    void *FuncPtr; 
}; 
// 省略部分代码 
  
#endif 

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

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

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

[cpp] 
static void __main_block_func_0(struct__main_block_impl_0 *__cself) { 
    inti = __cself->i; // bound by copy 
    printf("%d\n", i); 

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

[cpp] 
struct __main_block_impl_0 { 
    struct __block_impl impl; 
    struct __main_block_desc_0* Desc; 
    int i; 
    __main_block_impl_0(void*fp, struct__main_block_desc_0 *desc, int_i, intflags=0) : i(_i) { 
        impl.isa = &_NSConcreteStackBlock; 
    

补充:软件开发 , C语言 ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,