在本文中,我們將介紹在c、java這三種編程語言中如何在把函數(shù)作為一個參數(shù)傳入另一個函數(shù)中,以及分析這些語言函數(shù)傳參內(nèi)部的原理。
- c語言 (函數(shù)指針)
- Java (lambda表達式)
一、c語言與函數(shù)指針
- 指針函數(shù)和函數(shù)指針
在介紹函數(shù)指針的使用之前我們先要分清楚和它長得特別想的一個概念——指針函數(shù)
來人啊,上定義!
函數(shù)指針
指針函數(shù)是指帶指針的函數(shù),即本質(zhì)是一個函數(shù),函數(shù)返回類型是某一類型的指針。
比如說:
int * add(int a,int b){
int c = a+b;
return &c;
}
這就是一個指針函數(shù),它的接受兩個int值的參數(shù),返回一個int類型的指針。
指針函數(shù)
函數(shù)指針是指向函數(shù)的指針變量,即本質(zhì)是一個指針變量
函數(shù)指針的定義形式如下:
函數(shù)返回值類型 (* 指針變量名) (函數(shù)參數(shù)列表);
我們可以這樣去使用一個函數(shù)指針——
int add(int a,int b){
return a+b;
}
int main(){
int (*fun)(int,int);
fun = &add;
int c = (*fun)(1,2);
printf("%d\n",c);
return 0;
}
上面代碼main函數(shù)中的第一行就定義了一個函數(shù)指針int (*fun)(int,int),這個函數(shù)的參數(shù)列表中有兩個int類型的參數(shù),返回值類型是,并將add函數(shù)的首地址賦給了這個指針fun,在后面我們就像調(diào)用了這個函數(shù),結(jié)果是顯而易見的,打印出來的c值為3。
這個過程是不是有些熟悉?想想我們平常所用的變量指針,我們會寫如下的代碼:
int main(){
int a = 0;
int * p;
p = &a;
printf("%p\n",p);
}
與之前的代碼進行比較,其區(qū)別在于指針指向的函數(shù)而不是變量,而我們調(diào)用這個函數(shù)指針所指向的函數(shù),得到了返回值并打印。
-
c語言的函數(shù)回調(diào)
首先先說一下什么是回調(diào),回調(diào)函數(shù)就是一個通過函數(shù)指針調(diào)用的函數(shù)。emmm其實還蠻好理解的嘛,就是把函數(shù)作為參數(shù)傳進去嘛,這么機智的你一定想到了可以在函數(shù)的參數(shù)列表中接收一個函數(shù)指針類型的參數(shù)~沒錯就是醬紫?° (?′?`?):int add(int a,int b){ return a+b; } void dosome(int (*callback)(int a,int b)) { printf("%d\n",(*callback)(1,2)); } int main(){ dosome(&add); }我們把add函數(shù)作為參數(shù)傳給了dosome,在dosome函數(shù)中調(diào)用了這個callback函數(shù)(也就是我們傳入的add函數(shù)),并把返回值打印了出來,輸出結(jié)果顯而易見也是3。是不是寫一個回調(diào)特別的簡單呢~當然我們可以定義不同參數(shù)列表和返回值的函數(shù)指針來滿足我們不同的需求。
-
函數(shù)指針內(nèi)部原理
在講內(nèi)部原理之前,先需要預(yù)備一些匯編的知識。計算機并不能識別我們平時寫的c語言程序,真正物理機器可以識別的,就是一串串二進制01流。我們用c語言寫的程序,到最后都會被編譯成二進制指令。在函數(shù)被執(zhí)行時,由eip指向下一條要被執(zhí)行的指令,CPU 會取出這一條指令并且執(zhí)行。就比如之前的add函數(shù),編譯成對應(yīng)的匯編語言就是://c語言函數(shù) int add(int a,int b){ return a+b; } //對應(yīng)匯編 add: pushl %ebp movl %esp,%ebp subl $16,%esp movl $3, -4(%ebp) movl -4(%ebp),%eax leave ret注:匯編語言其實就是機器指令的助記符,為方便展示,此處使用匯編語言
而這些代表程序代碼的01流被存放在了內(nèi)存區(qū)域中的Text段。之前我們提到過,函數(shù)指針是一個指針,因此,它和別的指針并沒有什么太大的差異,只不過其它指針一般指向的是Heap、Stack和Data區(qū)中的變量,而函數(shù)指針如果指向函數(shù)的話,指針指向的是存放在Text段中,這個函數(shù)編譯得到的二進制串的首地址。
在我們調(diào)用這個函數(shù)時,就把這個地址賦值給了eip寄存器,然后,CPU讀取到地址中存放的指令,開始執(zhí)行這一段函數(shù)。而給函數(shù)指針賦不同的地址值,其指向的函數(shù)(二進制代碼)也就不同,我們可以通過一個函數(shù)指針賦不同的值來調(diào)用不同的函數(shù)。
-
來點騷操作?
之前我們說函數(shù)指針如果指向函數(shù)的話,指針指向的是存放在Text段中,這個函數(shù)編譯得到的二進制串的首地址。但是我們一定要指向一個函數(shù)嗎?答案是否定的,我們可以自己把一個函數(shù)的二進制代字符流存在一個char數(shù)組中,并讓函數(shù)指向這個數(shù)組。//add函數(shù)對應(yīng)的二進制代碼 const unsigned char code[]="\x55\x89\xe5\x8b\x45\x0c\x8b\x55\x08\x01\xd0\x5d\xc3"; int main(){ int (*fun)(int,int); fun = (void*)code; r = fun(1,2); printf("%d\n",r); }在本實例中,定義了一個全局數(shù)組 code,code數(shù)組里保存的若干字符就是機器指令,這些制定與我們之前定義的add函數(shù)作用一樣,在main函數(shù)中通過
int (*fun)(int,int)定義了一個指針函數(shù)fun,接著通過fun = (void*)code將該指針函數(shù)指向一個內(nèi)存地址,也就是code數(shù)組的首地址,最后通過r = fun(1,2);就可以調(diào)用這個指針函數(shù)了。當這段c程序執(zhí)行到r = fun(1,2)這條指令時,就會將CS:IP 段寄存器指向了code首地址,從而code數(shù)組所在的一塊連續(xù)內(nèi)存區(qū)域被當作代碼來執(zhí)行。不出意外這段代碼的運行結(jié)果,依舊是3。
值得一提的是,這也是JVM中函數(shù)執(zhí)行的奧秘所在,JVM會將由解釋器或者即時編譯的生成的二進制文件存放在內(nèi)存的某一個區(qū)域,在需要執(zhí)行的時候,將函數(shù)指針指向想要執(zhí)行的二進制代碼的首地址,并執(zhí)行這個函數(shù)。
二、Java與 lambda表達式
- java 回調(diào)
在jdk1.8之前還沒有l(wèi)ambda表達式的時候,我們一般會定義一個接口,然后接收這個接口類的對象,調(diào)用這個對象的方法:
當然我們可以用匿名內(nèi)部類來簡化這種寫法://OnClickListener.java interface OnClickListener{ void onClick(); } //OnClickListenerImpl.java class OnClickListenerImpl implements OnClickListener{ @Override void onClick(){ System.out.println("clicked"); } } //Test.java class Test{ void static onClicked(OnClickedListener listener){ listener.onClick(); } public static void main(String []args){ onClicked(new OnClickListenerImpl()); } }//OnClickListener.java interface OnClickListener{ void onClick(); } //Test.java class Test{ void static onClicked(OnClickedListener listener){ listener.onClick(); } public static void main(String []args){ onClicked(new OnClickListener(){ @Override void onClick(){ System.out.println("clicked"); } }); } }
2.為什么要需要傳一個接口的實現(xiàn)?
從上面的代碼我們可以看出,如果不用lambda表達式的話,其實Java寫回調(diào)似乎是要比c語言的函數(shù)指針要麻煩一些的。我們需要實現(xiàn)一個接口,并把這個接口的實例傳進去。
- java是一門純粹的面向?qū)ο笳Z言,不允許函數(shù)脫離類而存在。
- 在jdk1.7以前,java又沒有動態(tài)語言支持,不可以隨便一個類中的同參數(shù)列表、同返回值的函數(shù)就可以被函數(shù)調(diào)用。
- 在Java中不允許多繼承
所以,最好的解決辦法就是使用接口作為函數(shù)和參數(shù)之間的橋梁。
函數(shù)句柄
那在Java中有沒有類似函數(shù)指針的概念呢?筆者個人認為,在jdk1.7中出現(xiàn)的java.lang.invoke包中的MethodHandle就是類似的概念。
MethodHandle如何使用呢?請參考 http://www.itdecent.cn/p/a9cecf8ba5d9
這篇文章-
lambda表達式:
Lambda表達式是java8的重要更新,可以替代傳統(tǒng)的匿名內(nèi)部類去實現(xiàn)參數(shù)的回調(diào),比如之前的代碼可以簡化如下//OnClickListener.java interface OnClickListener{ void onClick(); } //Test.java class Test{ void static onClicked(OnClickedListener listener){ listener.onClick(); } public static void main(String []args){ onClicked(()->System.out.println("clicked")); } }lambda表達式的寫法為
(參數(shù)列表)->{ //程序代碼 }除此之外,lambda表達式還有著一定的限制,就是lambda表達式所表示的接口中只能有一個非default方法。
-
lambda表達式的內(nèi)部原理
看完如此簡潔的lambda表達式,我們應(yīng)當去一探其內(nèi)部的實現(xiàn)原理。
其實我們也可以猜測一下,我們
在java語言層面我們自然是看不出來什么東西了啦,不如去探究一下lambda表達式編譯后的字節(jié)碼,于是編寫demo如下:interface Add{ void add(int a,int b); } class Test { public static void main(String []args){ Add add = (int a,int b) -> a+b; } }
Test類的編譯后的字節(jié)碼如下:
// class version 52.0 (52)
// access flags 0x21
public class com/example/Test {
// compiled from: Test.java
// access flags 0x19
public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup
// access flags 0x1
public <init>()V
L0
LINENUMBER 7 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/example/Test; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 9 L0
INVOKEDYNAMIC add()Lcom/example/Add; [
// handle kind 0x6 : INVOKESTATIC
java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
// arguments:
(II)I,
// handle kind 0x6 : INVOKESTATIC
com/example/Test.lambda$main$0(II)I,
(II)I
]
ASTORE 1
L1
LINENUMBER 10 L1
ALOAD 1
ICONST_1
ICONST_2
INVOKEINTERFACE com/example/Add.add (II)I
ISTORE 2
L2
LINENUMBER 11 L2
RETURN
L3
LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
LOCALVARIABLE add Lcom/example/Add; L1 L3 1
LOCALVARIABLE c I L2 L3 2
MAXSTACK = 3
MAXLOCALS = 3
// access flags 0x100A
private static synthetic lambda$main$0(II)I
L0
LINENUMBER 9 L0
ILOAD 0
ILOAD 1
IADD
IRETURN
L1
LOCALVARIABLE a I L0 L1 0
LOCALVARIABLE b I L0 L1 1
MAXSTACK = 2
MAXLOCALS = 2
}
其中<init>是編譯器幫我們生成的無參構(gòu)造函數(shù)。
而lambdamain0是編譯器根據(jù)我們寫的lambda表達式生成的一個靜態(tài)函數(shù)
reference