程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> 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 making a copy 
[=, &foo] Capture any referenced variable by making a copy, but capture variable foo by reference 
[bar]     Capture bar by making 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; 
        impl.Flags = flags; 
        impl.FuncPtr = fp; 
        Desc = desc; 
    } 
}; 
從__main_block_impl_0這個名稱可以看出該結構體是為main函數中第零個block服務的,即示例代碼中的blk;也可以猜到不同場景下的block對應的結構體不同,但本質上第一個成員一定是struct __block_impl impl,因為這個成員是block實現的基石。

結構體__main_block_impl_0又引入了一個新的結構體,也是中間代碼裡最後一個結構體:

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

可以看出,這個描述性質的結構體包含的價值信息就是struct __main_block_impl_0的大小。

最後剩下main函數對應的中間代碼:

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

    int i = 1024; 
    void(*blk)(void) = (void(*)(void))&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, i); 
    ((void(*)(struct__block_impl *))((struct__block_impl *)blk)->FuncPtr)((struct__block_impl *)blk); 
  
    return 0; 

從main函數對應的中間代碼可以看出執行block的本質就是以block結構體自身作為__cself參數,這裡對應__main_block_impl_0,通過結構體成員FuncPtr函數指針調用對應的函數,這裡對應__main_block_func_0。

其中,局部變量i是以值傳遞的方式拷貝一份,作為__main_block_impl_0的構造函數的參數,並以初始化列表的形式賦值給其成員變量i。所以,基於這樣的實現,不允許直接修改外部變量是合理的——因為按值傳遞根本改不到外部變量。

4. 從實現上看如何修改外部變量(__block類型指示符)

如果想要修改外部變量,則需要用__block來修飾:

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

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

此時再看中間代碼,發現多了一個結構體:
[cpp]
struct __Block_byref_i_0 { 
    void *__isa; 
    __Block_byref_i_0 *__forwarding; 
    int __flags; 
    int __size; 
    int i; 
}; 

於是,用__block修飾的int變量i化身為__Block_byref_i_0結構體的最後一個成員變量。

代碼中blk對應的結構體也發生了變化:

[cpp] 
struct __main_block_impl_0 { 
    struct __block_impl impl; 
    struct __main_block_desc_0* Desc; 
    __Block_byref_i_0 *i; // by ref 
    __main_block_impl_0(void*fp, struct__main_block_desc_0 *desc, __Block_byref_i_0 *_i, intflags=0) : i(_i->__forwarding) { 
        impl.isa = &_NSConcreteStackBlock; 
        impl.Flags = flags; 
        impl.FuncPtr = fp; 
        Desc = desc; 
    } 
}; 

__main_block_impl_0發生的變化就是int類型的成員變量i換成了__Block_byref_i_0 *類型,從名稱可以看出現在要通過引用方式來捕獲了。
對應的函數也不同了:

[cpp] 
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { 
    __Block_byref_i_0 *i = __cself->i; // bound by ref 
    (i->__forwarding->i) = 0; // 看起來很厲害的樣子 
    printf("%d\n", (i->__forwarding->i)); 

main函數也有了變動:
[cpp] 
int main(intargc, constchar * argv[]) 

    __block __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024}; 
    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); 
    ((void(*)(struct__block_impl *))((struct__block_impl *)blk)->FuncPtr)((struct__block_impl *)blk); 
  
    return 0; 

前兩行代碼創建了兩個關鍵結構體,特地高亮顯示。
這裡沒有看__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] 
#import <Foundation/Foundation.h> 
#include <pthread.h> 
  
typedef void (^DemoBlock)(void); 
  
void test(); 
void *testBlock(void*blk); 
  
int main(int argc, const char * argv[]) 

    printf("Before test()\n"); 
    test(); 
    printf("After test()\n"); 
  
    sleep(5); 
    return 0; 

  
void test() 

    __block int i = 1024; 
    void(^blk)(void) = ^{ i = 2048; printf("%d\n", i); }; 
  
    pthread_tthread; 
    int ret = pthread_create(&thread, NULL, testBlock, (void*)blk); 
    printf("thread returns : %d\n", ret); 
  
    sleep(3);// 這裡睡眠1s的話,程序會崩潰 

  
void *testBlock(void*blk) 

    sleep(2); 
  
    printf("testBlock : Begin to exec blk.\n"); 
    DemoBlock demoBlk = (DemoBlock)blk; 
    demoBlk(); 
  
    returnNULL; 

在這個示例中,位於test()函數的block類型的變量blk就作為函數參數傳遞給testBlock。
正常情況下,這段代碼可以成功運行,輸出:

[cpp]
Before test() 
threadreturns : 0 
testBlock : Begin to exec blk. 
2048 
After test() 

如果按照注釋,將test()函數最後一行改為休眠1s的話,正常情況下程序會在輸出如下結果後崩潰:

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

從輸出可以看出,當要執行blk的時候,test()已經執行完畢回到main函數中,對應的函數棧也已經展開,此時棧上的變量已經不存在了,繼續訪問導致崩潰——這也是不用int *直接訪問外部變量i的原因。

5.1 拷貝block結構體
上文提到block結構體__block_impl的第一個成員是isa指針,使其成為NSObject的子類,所以我們可以通過相應的內存管理機制將其拷貝到堆上:

[cpp] 
void test() 

    __block int i = 1024; 
    void(^blk)(void) = ^{ i = 2048; printf("%d\n", i); }; 
  
    pthread_tthread; 
    intret = pthread_create(&thread, NULL, testBlock, (void*)[blk copy]); 
    printf("thread returns : %d\n", ret); 
  
    sleep(1); 

  
void*testBlock(void*blk) 

    sleep(2); 
  
    printf("testBlock : Begin to exec blk.\n"); 
    DemoBlock demoBlk = (DemoBlock)blk; 
    demoBlk(); 
    [demoBlk release]; 
  
    return NULL; 

再次執行,得到輸出:

[cpp] 
Before test() 
threadreturns : 0 
After test() 
testBlock : Begin to exec blk. 
2048 

可以看出,在test()函數棧展開後,demoBlk仍然可以成功執行,這是由於blk對應的block結構體__main_block_impl_0已經在堆上了。不過這還不夠——

5.2 拷貝捕獲的變量(__block變量)
在拷貝block結構體的同時,還會將捕獲的__block變量,即結構體__Block_byref_i_0,復制到堆上。這個任務落在前面沒有討論的__main_block_desc_0結構體身上:

[cpp] 
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*/);} 
  
static void __main_block_dispose_0(struct__main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);} 
  
static struct __main_block_desc_0 { 
    unsignedlongreserved; 
    unsignedlongBlock_size; 
    void(*copy)(struct__main_block_impl_0*, struct__main_block_impl_0*); 
    void(*dispose)(struct__main_block_impl_0*); 
} __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] 
void test() 

    __block int i = 1024; 
    void(^blk)(void) = ^{ i++; printf("%d\n", i); }; 
  
    pthread_tthread; 
    intret = pthread_create(&thread, NULL, testBlock, (void*)[blk copy]); 
    printf("thread returns : %d\n", ret); 
  
    sleep(1); 
    blk(); 

  
void *testBlock(void*blk) 

    sleep(2); 
  
    printf("testBlock : Begin to exec blk.\n"); 
    DemoBlock demoBlk = (DemoBlock)blk; 
    demoBlk(); 
    [demoBlk release]; 
  
    returnNULL; 

在test()函數中調用pthread_create創建線程時,blk被復制了一份到堆上作為testBlock函數的參數。
test()函數中的blk結構體位於棧中,在休眠1s後被執行,對i進行自增動作。
testBlock函數在休眠2s後,執行位於堆上的block結構體,這裡為demoBlk。
上述代碼執行後輸出:

[cpp]
Beforetest() 
thread returns : 0 
1025 
Aftertest() 
testBlock : Begin to execblk. 
1026 

可見無論是棧上的還是堆上的block結構體,修改的都是同一個__block變量。

這就是前面提到的__forwarding指針成員的作用了:

起初,棧上的__block變量的成員指針__forwarding指向__block變量本身,即棧上的__Block_byref_i_0結構體。

當__block變量被復制到堆上後,棧上的__block變量的__forwarding成員會指向堆上的那一份拷貝,從而保持一致。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved