我曾經和一些人聊過天,他們那時在書寫在一個小型的微處理器上單機啟動並執行C程式。當這台機器的開關開啟的時候,硬體會調用地址為0處的子程式。
為了模仿電源開啟的情形,我們要設計一條C語句來顯式地調用這個子程式。經過一些思考,我們寫出了下面的語句:
(*(void(*)())0)();
這樣的運算式會令C程式員心驚膽戰。但是,並不需要這樣,因為他們可以在一個簡單的規則的協助下很容易地構造它:以你使用的方式聲明它。
每個C變數聲明都具有兩個部分:一個類型和一組具有特定格式的期望用來對該類型求值的運算式。最簡單的運算式就是一個變數:
float f, g;
說明運算式f和g——在求值的時候——具有類型float。由於待求值的時運算式,因此可以自由地使用圓括弧:
float ((f));
則表示((f))求值為float並且因此,通過推斷,f也是一個float。
同樣的邏輯用在函數和指標類型。例如:
float ff();
表示運算式ff()是一個float,因此ff是一個返回一個float的函數。類似地,
float *pf;表示*pf是一個float並且因此pf是一個指向一個float的指標。
這些形式的組合聲明對錶達式是一樣的。因此
float *g(), (*h)();
表示*g()和(*h)()都是float運算式。由於()比*綁定得更緊密,*g()和*(g())表示同樣的東西:g是一個返回指float指標的函數,而h是一個指向返回float的函數的指標。
當我們知道如何聲明一個給定類型的變數以後,就能夠很容易地寫出一個類型的模型(cast):只要刪除變數名和分號並將所有的東西包圍在一對圓括弧中即可。因此,由於
float *g();聲明g是一個返回float指標的函數,所以(float *())就是它的模型。
有了這些知識的武裝,我們現在可以準備解決(*(void(*)())0)()了。 我們可以將它分為兩個部分進行分析。首先,假設我們有一個變數fp,它包含了一個函數指標,並且我們希望調用fp所指向的函數。可以這樣寫:
(*fp)();
如果fp是一個指向函數的指標,則*fp就是函數本身,因此(*fp)()是調用它的一種方法。(*fp)中的括弧是必須的,否則這個運算式將會被分析為*(fp())。我們現在要找一個適當的運算式來替換fp。
這個問題就是我們的第二步分析。如果C可以讀入並理解類型,我們可以寫
(*0)();
但這樣並不行,因為*運算子要求必須有一個指標作為他的運算元。另外,這個運算元必須是一個指向函數的指標,以保證*的結果可以被調用。因此,我們需要將0轉換為一個可以描述“指向一個返回void的函數的指標”的類型。
如果fp是一個指向返回void的函數的指標,則(*fp)()是一個void值,並且它的聲明將會是這樣的
void (*fp)();
因此,我們需要寫
void (*fp)();
(*fp)();
來聲明一個啞變數。一旦我們知道了如何聲明該變數,我們也就知道了如何將一個常數轉換為該類型:只要從變數的聲明中去掉名字即可。因此,我們像下面這樣將0轉換為一個“指向返回void的函數的指標”
(void(*)())0
接下來,我們用(void(*)())0來替換fp:
(*(void(*)())0)();
結尾處的分號用於將這個運算式轉換為一個語句。
在這裡,我們就解決了這個問題時沒有使用typedef聲明。通過使用它,我們可以更清晰地解決這個問題:
typedef void (*funcptr)();
(*(funcptr)0)();