在以前咱們把抽象定義爲一種過程,程序員能夠經過它將一個名字與一段可能很複雜的程序片斷關聯起來。抽象最大的意義就在於,咱們能夠從功能和用途的角度來考慮它,而不是實現。java
在大多數程序設計語言中,子程序是最主要的控制抽象的方法。大多數子程序都是參數化的,即經過傳遞一些參數來影響子程序的行爲。程序員
當一個子程序被調用的時候,在棧的頂部將給它一個新的棧幀或稱爲活動記錄。這個棧幀可能包含實際參數和/或返回值、簿記信息(包含返回地址和保存的寄存器)、局部變量和/或各類臨時量。當子程序返回時,棧幀從棧中彈出。數組
若是某個對象的大小在編譯時位置,那麼就將它放在棧幀的頂部大小可變的區域,並將它的地址和內情向量保存在棧幀的某個部分,放在相對於棧指針的一個靜態可知的偏移處。安全
在那些容許嵌套子程序和靜態做用域的語言,對象有可能出如今外圍的子程序中,經過維護一個靜態鏈就能夠找到這些既非局部也非全局的對象。每一個棧幀都包含一個對詞法上位於其外圍的幀的引用閉包
維護子程序調用棧是調用序列的責任。所謂調用序列就是由調用方緊接着子程序子程序調用和以後執行的代碼。併發
在進入自稱的過程當中須要完成不少工做,包括出艾迪參數,保存返回地址,修改程序計數器,修改棧指針以分配空間,保存那些維護着重要的值可是可能被子程序改寫的寄存器等等等異步
許多處理器調用序列都是將並不是爲特殊用途而保留的寄存器分爲數目差很少的兩組,其中一組由調用方負責,另外一組由被調用方負責。函數
在有嵌套子程序的語言中,至少有一部分靜態鏈維護工做必須由調用方完成,而不能由被調用方完成佈局
被調用直接嵌套在調用方內,在這種狀況下,被調用方的靜態鏈應該直接引用調用方的棧幀操作系統
被調用方在k>=0做用域以外,更接近詞法嵌套的外層,在這種狀況下,全部圍繞着被調用方的做用也圍繞着調用方。這時候調用方就對靜態鏈作k次間接引用,將結果送給被調用方作靜態鏈
通常的調用序列調用方能夠按以下的方式操做:
被調用方的前序操做則是:
在子程序完成以後的後序操做:
最後調用方則能夠:
做爲基於棧的調用方式的一種替代,許多語言實現中還容許將特定子程序在調用的位置內聯展開。被調用子程序的副本成爲調用方的一部分;沒有任何實際子程序調用發生。
在C中能夠由程序員來指示是否建議將某些子程序內聯化
inline int max(int a, int b) { return a > b ? a : b;}
可是與真正的子程序調用相比,內聯展開的一個明顯缺點就是增長了代碼量
大多數子程序都是參數化的,它們將獲得一些參數,這些參數或控制着子程序行爲的某些特定方面,或指定子程序須要來操做的數據。
以前提了一下實參傳遞,以及明確實參與形參關係的語義規則。有些語言定義了惟一一組規則,適用於全部參數,這樣的語言包括了C 、Fortran和Lisp,其它一些語言則提供了兩組或更多組不一樣的規則
對於f(x)咱們有兩種實現方式,
這兩種最基本的參數傳遞模式分別稱爲值調用和引用調用,它們的設計反映了它們的實現方式
值調用和引用調用在使用值模型的語言的語言中最有意義。在使用引用模型的語言中,變量自己已是對象的引用,這兩種模型實際上都沒有意義。
在Java中,內部類型使用值調用,而用戶定義類型使用引用模型。相對的是C#中使用的是值調用,可是能夠經過顯式的關鍵字來使用引用傳遞。
閉包(對一個子程序的引用,再加上該子程序的引用環境)也會由於某些緣由須要做爲參數傳遞。最明顯的緣由就是當參數被聲明爲子程序時。
在子函數式語言中,子程序每每是做爲參數傳遞的,並做爲結果返回
在面嚮對象語言中,雖然沒有嵌套子程序,可是也能夠模仿子程序閉包的行爲,方法是將與一個方法和它的環境打包在一個顯示的對象裏,
C#的代理擴展了對象閉包的概念,代理不只能夠用特殊的對象方法來實例化,也能夠用靜態函數或者匿名嵌套代理或lambda表達式來實例化。
在不一樣語言中,數組維數和邊界的約束時間也不大相同,可推遲到運行時再肯定形狀的形式數組參數稱爲類似數組參數或開放數組參數,例如C中的多維數組。
默認參數就是調用方能夠不提供的參數,若是沒有給出就使用預先設置的默認值
實現方式也是直截了當的,調用時若是缺乏了某個實際參數,編譯器就認爲提供的是相應的默認值
在至今爲止的討論中,咱們一直假定參數按位置相互對應:第一個實參對應於第一個形參,以此類推。實際上,在一些語言中,如Lisp和Python,這些語言都容許對參數進行命名,命名參數與默認參數結合時特別有用。
命名參數不只可使參數以任意順序描述,還能夠起到說明參數用途的做用
在Lisp、Python和C及其後羿的一個不尋常之處,是它容許用戶定義一類子程序,這種子程序的參數個數能夠變化
在C中,printf能夠按以下方式聲明:
int printf(char *format, ...)
C中經過內置的函數來獲取省略參數
在Java中則是將省略參數包裝成一個數組
static void print_lines(String foo, String...lines)
對於函數指定返回值的語法,各語言之間區別很大,在Lisp和ML這種不區分表達式和語句的語言中,函數的值就是函數體的值,而函數體自己就是一個表達式
而如今的許多命令式語言都引入了顯示的return語句
return expr
子程序爲在許多不一樣的對象值(參數)上執行某個操做提供了一種很天然的方式。在大型程序中,也經常須要在許多不一樣的對象類型上作某個操做。
在以前有一篇講到隱式參數多態性繞過了這個問題,它使咱們能夠聲明一種子程序,其參數類型式沒有徹底描述的,但仍然是類型安全。可是這種方式,須要將全部的類型檢查推遲到類型檢查時纔來作。
還有一種顯式多態性的泛型機制,使一組相似的子程序或模塊能夠經過惟一一段源代碼建立出來。
泛型特徵能夠經過多種方式實現。在C++的大多數實現中,它們是一種純粹的靜態機制,建立和使用泛型代碼多個實例的全部工做都在編譯時完成。在一般狀況下,編譯器爲每一個實例建立一個獨立代碼副本。可是在C++中,爲這樣每一個實例安排獨立的類型檢查
而在Java中使用一種類型擦除的機制,從效果上看,若是T是Java中的一個泛型類型參數,那麼類T的對象將被看成標準基類Object的實例對待,但程序員不須要在將它們用做T類的對象以前插入顯式的類型強制,並且編譯器能夠保證這樣的省略的強制不會發生失敗。
由於泛型也是一種抽象,其接聲明的頭部應該爲抽象的用戶提供使用它須要知道的所有信息
在Java和C#中,利用了面向對象和繼承的能力來實現。它能夠要求某個泛型參數必須支持一組特定的方法
例如在Java中:
public static <T extends Comparable<T>> void sort(T A[]) { }
異常能夠定義爲程序執行過程當中出現了沒有預料的狀況,或者至少是不尋常的狀況,而這種狀況很難在局部上下文中處理。異常狀況多是由語言實現自動檢查的,或者是由程序自己顯式引起的。
在許多語言中,動態語義錯誤會自動產生程序可捕獲的異常。程序員還能夠定義其它特定於具體應用的異常
在大多數面嚮對象語言中,異常是某個與風衣或用戶定義的類類型的一個實例。
一般使用嵌入在If語句中的throw語句或raise語句來在運行時引起異常。若是一個子程序引起了異常,可是其內部沒有捕獲,那麼它就可能以某種非預期的方式返回。在Java和C++中,在子程序頭部包含了一個表,在其中列出可能傳播到子程序以外的異常。
在大多數語言中,一個代碼塊能夠由一組異常處理程序,在C++中:
try { } catch(end_if_file) { } catch(io_error_r) { }
在出現異常時,處理程序將出現的順序檢查,控制傳入第一個與異常匹配的處理程序。
在像Lisp一類的面向表達式語言中,異常處理程序被附着於表達式上,而不是語句上。在發生異常時,因爲處理程序的執行將代替被保護代碼中還沒有結束的那一部分,所以附在表達式上的處理程序還必須爲表達式提供一個值
val foo = (f(a) * b) handle Overflow => max_int
異常的最明顯實現方式是維護一個處理程序的連接表棧。當控制進入一個受保護塊時,將做用於這個塊的處理程序被加到表的頭部。當某個異常被引起時,語言運行時系統就彈出表中最內層的處理程序而且調用它。
在一種內部並無提供異常的語言中,有時也能夠模擬異常機制。
Scheme提供了一個名爲call-with-current-continuation的通用函數。這個函數帶有一個參數f,該參數自己也是函數。它調用f並將一個繼續c(閉包)傳給它做爲參數。這個閉包包含當前的程序計數器和引用環境。在將來的任什麼時候刻,f能夠經過調用c來從新創建起所保存的環境。若是之前作過嵌套調用,控制機制就會彈出它們,就像異常所作的那樣。
C的大多數版本提供了一對庫例程setjmp和longjmp。setjmp以一個緩衝區做爲參數,它將程序當前狀態以某種形式存入其中。隨後咱們能夠將這個緩衝區傳給longjmp,要求恢復所保存的狀態。
有了對運行時棧的佈局的理解後,咱們能夠考慮更通常的控制抽象的實現問題,協程。與繼續同樣,協程也須要用閉包表示,能夠經過非局部的goto跳進來,關於協程的這種特定操做被稱爲transfer。這兩種抽象之間的主要不一樣點在於:繼續是一個常量,一旦建立以後就不會改變了,而協程在每次運行中都會變化。
從效果上看,一組協程在一些同時存在的上下文中執行,但在每一個時刻只有一個正在執行,控制將經過命名方式在它們之間轉移。協程能夠用於實現迭代器和線程
因爲不一樣協程是併發的,所以它們不能共享同一個棧,由於做爲一個總體看,它們的子程序調用和返回並非按後進先出的順序進行的。若是每一個協程都放在詞法嵌套的最外層聲明處,那麼它們的棧就是互不相交的
最簡單的解決方案是給每一個協程一塊固定大小的靜態分配的棧空間
在從一個協程轉移到另外一個協程時,運行系統必須修改程序計數器、棧和處理器寄存器的內容。這些修改都被封裝在transfer操做中。
對於棧的修改,最多見的方式就是簡單的修改棧指針寄存器,避免在transfer中使用幀指針。在transfer開始,咱們將返回地址和全部其它被調用所保存的寄存器壓入當前棧,而後修改sp,由新棧中彈出新的指令地址和其它寄存器內容,而後返回
事件就是在程序外部發生,出現的時間不可預測,可是須要運行中的程序相應某種狀況。最多見的事件就是圖形用戶界面系統的輸入:按鍵、鼠標活動。
傳統上,順序程序設計語言中事件處理程序是做爲自發的子程序調用實現的,通常會使用語言以外由操做系統定義和實現的機制。爲了準備好經過這種機制接受事件,一個程序將調用一個setup_handler庫例程,在事件發生時將但願調用的子程序做爲參數傳遞
在硬件層上,在P的執行期間異步設備的活動將觸發一箇中斷機制,保持在P的寄存器,切換到一個不一樣的棧,並跳轉到OS內核中的一個預先定義的地址上。相似的,若是另外一個過程Q在中斷髮生時正在運行,則內核將在本身最後的時間段結束時,保存P的狀態。
當一箇中斷髮生時,主程序可能處於代碼的任何位置,內核將保存狀態,並經過正常的調用序列調用事件處理程序,最後恢復狀態。
這一篇集中關注控制抽象的問題,特別是子程序有關的問題。首先咱們先了解了子程序調用棧的管理問題和維護棧的調用序列。在以後討論了有關參數的問題,各類參數傳遞模型等。最後考察了異常處理機制、協程和事件。