首先看看以下一個聲明:node
int* ( *( *fun )( int* ) )[10];
這是一個會讓初學者感到頭暈目眩、感到恐懼的函數指針聲明。在熟練掌握C/C++的聲明語法以前,不學習必定的規則,想理解好這類複雜聲明是比較困難的。編程
C/C++全部複雜的聲明結構,都是由各類聲明嵌套構成的。如何解讀複雜指針聲明?右左法則是一個很著名、頗有效的方法。不過,右左法則其實並非C/C++標準裏面的內容,它是從C/C++標準的聲明規定中概括出來的方法。C/C++標準的聲明規則,是用來解決如何建立聲明的,而右左法則是用來解決如何辯識一個聲明的,從嵌套的角度看,二者能夠說是一個相反的過程。右左法則的英文原文是這樣說的:數組
The right-left rule: Start reading the declaration from the innermost parentheses, go right, and then go left. When you encounter parentheses, the direction should be reversed. Once everything in the parentheses has been parsed, jump out of it. Continue till the whole declaration has been parsed.ide
這段英文的翻譯以下:函數
右左法則:首先從最裏面的圓括號看起,而後往右看,再往左看。每當遇到圓括號時,就應該掉轉閱讀方向。一旦解析完圓括號裏面全部的東西,就跳出圓括號。重複這個過程直到整個聲明解析完畢。學習
筆者要對這個法則進行一個小小的修正,應該是從未定義的標識符開始閱讀,而不是從括號讀起,之因此是未定義的標識符,是由於一個聲明裏面可能有多個標識符,但未定義的標識符只會有一個。翻譯
如今經過一些例子來討論右左法則的應用,先從最簡單的開始,逐步加深:設計
int (*func)(int *p);指針
首先找到那個未定義的標識符,就是func,它的外面有一對圓括號,並且左邊是一個*號,這說明func是一個指針,而後跳出這個圓括號,先看右邊,也是一個圓括號,這說明(*func)是一個函數,而func是一個指向這類函數的指針,就是一個函數指針,這類函數具備int*類型的形參,返回值類型是int。code
int (*func)(int *p, int (*f)(int*));
func被一對括號包含,且左邊有一個*號,說明func是一個指針,跳出括號,右邊也有個括號,那麼func是一個指向函數的指針,這類函數具備int *和int (*)(int*)這樣的形參,返回值爲int類型。再來看一看func的形參int (*f)(int*),相似前面的解釋,f也是一個函數指針,指向的函數具備int*類型的形參,返回值爲int。
int (*func[5])(int *p);
func右邊是一個[]運算符,說明func是一個具備5個元素的數組,func的左邊有一個*,說明func的元素是指針,要注意這裏的*不是修飾func的,而是修飾func[5]的,緣由是[]運算符優先級比*高,func先跟[]結合,所以*修飾的是func[5]。跳出這個括號,看右邊,也是一對圓括號,說明func數組的元素是函數類型的指針,它所指向的函數具備int*類型的形參,返回值類型爲int。
int (*(*func)[5])(int *p);
func被一個圓括號包含,左邊又有一個*,那麼func是一個指針,跳出括號,右邊是一個[]運算符號,說明func是一個指向數組的指針,如今往左看,左邊有一個*號,說明這個數組的元素是指針,再跳出括號,右邊又有一個括號,說明這個數組的元素是指向函數的指針。總結一下,就是:func是一個指向數組的指針,這個數組的元素是函數指針,這些指針指向具備int*形參,返回值爲int類型的函數。
int (*(*func)(int *p))[5];
func是一個函數指針,這類函數具備int*類型的形參,返回值是指向數組的指針,所指向的數組的元素是具備5個int元素的數組。
要注意有些複雜指針聲明是非法的,例如:
int func(void) [5];
func是一個返回值爲具備5個int元素的數組的函數。但C語言的函數返回值不能爲數組,這是由於若是容許函數返回值爲數組,那麼接收這個數組的內容的東西,也必須是一個數組,但C/C++語言的數組名是一個不可修改的左值,它不能直接被另外一個數組的內容修改,所以函數返回值不能爲數組。
int func[5](void);
func是一個具備5個元素的數組,這個數組的元素都是函數。這也是非法的,由於數組的元素必須是對象,但函數不是對象,不能做爲數組的元素。
實際編程當中,須要聲明一個複雜指針時,若是把整個聲明寫成上面所示這些形式,將對可讀性帶來必定的損害,應該用typedef來對聲明逐層分解,加強可讀性。
typedef是一種聲明,但它聲明的不是變量,也沒有建立新類型,而是某種類型的別名。typedef有很大的用途,對一個複雜聲明進行分解以加強可讀性是其做用之一。例如對於聲明:
int (*(*func)(int *p))[5];
能夠這樣分解:
typedef int (*PARA)[5];
typedef PARA (*func)(int *);
這樣就容易看得多了。
typedef的另外一個做用,是做爲基於對象編程的高層抽象手段。在ADT中,它能夠用來在C/C++和現實世界的物件間創建關聯,將這些物件抽象成C/C++的類型系統。在設計ADT的時候,咱們經常聲明某個指針的別名,例如:
typedef struct node * list;
從ADT的角度看,這個聲明是再天然不過的事情,能夠用list來定義一個列表。但從C/C++語法的角度來看,它實際上是不符合C/C++聲明語法的邏輯的,它暴力地將指針聲明符從指針聲明器中分離出來,這會形成一些異於人們閱讀習慣的現象,考慮下面代碼:
const struct node *p1;
typedef struct node *list;
const list p2;
p1類型是const struct node*,那麼p2呢?若是你覺得就是把list簡單「代入」p2,而後得出p2類型也是const struct node*的結果,就大錯特錯了。p2的類型實際上是struct node * const p2,那個const限定的是p2,不是node。形成這一奇異現象的緣由是指針聲明器被分割,標準中規定:
6.7.5.1 Pointer declarators
Semantics
If in the declaration ‘‘T D1’’, D1 has the form
* type-qualifier-listopt D
and the type specified for ident in the declaration ‘‘T D’’ is
‘‘derived-declarator-type-list T’’
then the type specified for ident is
‘‘derived-declarator-type-list type-qualifier-list pointer to T’’
For each type qualifier in the list, ident is a so-qualified pointer.
指針的聲明器由指針聲明符*、可選的類型限定詞type-qualifier-listopt和標識符D組成,這三者在邏輯上是一個總體,構成一個完整的指針聲明器。這也是多個變量同列定義時指針聲明符必須緊跟標識符的緣由,例如:
int *p, q, *k;
p和k都是指針,但q不是,這是由於*p、*k是一個總體指針聲明器,以表示聲明的是一個指針。編譯器會把指針聲明符左邊的類型包括其限定詞做爲指針指向的實體的類型,右邊的限定詞限定被聲明的標識符。但如今typedef struct node *list硬生生把*從整個指針聲明器中分離出來,編譯器找不到*,會認爲const list p2中的const是限定p2的,正因如此,p2的類型是node * const而不是const node*。
雖然typedef struct node* list不符合聲明語法的邏輯,但基於typedef在ADT中的重要做用以及信息隱藏的要求,咱們應該讓用戶這樣使用list A,而不是list *A,所以在ADT的設計中仍應使用上述typedef語法,但須要注意其帶來的不利影響。