C語言宣告語句
設計理念:
C語言的一個設計理念就是宣告變數和使用變數的形式應該是一致的
優點:宣告變數和使用變數時的運算子優先順序是相同的
缺點:運算子的優先順序是C語言過度解析的部分之一
術語:
變數宣告中使用到的符號的術語:(並不是所有的組合是合法的)
數量 | 名稱 | 舉例 |
0或更多 | 指標(pointer) | * |
一個 | 說明符(declarator) | identifier identifier[size] identifier(args) (declarator) |
0個或一個 | 初始化器(initializer) | = initial_value |
至少一個型別限定符 | 型別說明符 儲存型別 型別修飾符 |
void char short extern static register const volatile |
0個或一個 | 更多的說明符 | ,declarator |
一個 | 分號 | ; |
關於struct和union:
- 型別說明符是:struct {stuff...}
- 宣告形式:struct {stuff...} s;
- tag:為了簡寫,可以在struct後面加上結構體tag:struct struct_tag {stuff...},這樣就聲明瞭struct_tag代表具體的型別集合{stuff...},之後的宣告就可以使用struct struct_tag s;
關於引數傳遞的兩點說明:
- 某些書中會說"引數傳遞到呼叫函式的時候是從右到左壓到棧中",這種說法是不對的。引數傳遞的時候會盡可能使用暫存器,所以一個整數和一個只含有一個整數的結構體的傳遞方法是完全不同的,一個整數可能通過暫存器傳遞,結構體會通過棧傳遞
- 通常一個數組是不能直接通過賦值來傳遞整個陣列的,或者被一個函式返回,但是通過把陣列作為結構體的唯一一個成員就可以實現
雖然union具有和結構體類似的結構,但是它們有完全不同的儲存方式
- 結構體將每個成員儲存到其前一個成員後面
- union的所有成員都儲存在相同的起始地址,所以不同的成員是相互覆蓋的,同時只能有一個成員可以被儲存
union的一個明顯的問題就是儲存了一種型別但是用另一種型別取的型別安全問題,Ada語言主張在記錄中儲存說明欄位來解決這個問題,但是C語言依賴於程式設計師能夠記住存了什麼而不採取任何措施
union有兩個用途:
- 節約空間
- 所有的成員有相同的儲存空間大小的時候,就可以用不同的方式解析相同的二進位制資料而不用顯式的進行型別轉換
宣告語句的解析:
C語言可以有非常複雜的宣告語句而讓人無法輕易的搞清楚到底定義了什麼東西
有兩種解析方式:
方式一:優先順序法則
- 宣告的解析從名稱開始,然後按照優先順序規則繼續執行
- 優先順序從高到低:
- 將宣告的各個部分組合在一起的括號
- 字尾操作符:指明一個函式的"()"和指明陣列的"[]"
- 字首操作符:指明是"指向..."的星號
- 如果"const"或"volatile"關鍵字和一個型別說明符相鄰,就應用到這個型別說明符;否則,如果應用到左邊緊鄰的"*"
方式二:狀態機規則
- 從最左側的識別符號開始,"identifier是" "identifier is"
- 如果右側是"[]"就獲取,"一個...的陣列" "array of"
- 如果右側是"()"就獲取,"引數為...返回值為...的函式" "function returning"
- 如果左側是"("就獲取整個括號中的內容,這個括號包含的是已經處理過的宣告,回到步驟2
- 如果左側是"const""volatile""*"就獲取,持續讀取左側的符號直到不再是這三個之中的,之後返回步驟4
- "const":"只讀的" "read only"
- "volatile":"volatile" "volatile"
- "*":"指向..." "pointer to"
- 餘下的就是基本資料型別
舉例:char *(*c[10])(int **p);
- 按照優先順序規則解析:
- c是一個...陣列---c[10]
- c是一個指向...的指標的陣列---*c[10]
- c是一個指向引數為...的返回值為...函式的指標的陣列---(*c[10])()
- c是一個指向引數為整數的指標的指標的返回值為...函式的指標的陣列---(*c[10])(int **p)
- c是一個指向引數為整數的指標的指標的返回值為指向...的指標函式的指標的陣列---*(*c[10])(int **p)
- c是一個指向引數為整數的指標的指標的返回值為指向char的指標函式的指標的陣列---char *(*c[10])(int **p)
- 按照狀態機規則解析:
- c是...---c---1->2
- c是一個...的陣列---c[10]---2->3
- c是一個指向...的指標的陣列---*c[10]---3,4,5->4
- c是一個指向...的指標的陣列---(*c[10])---4->2
- c是一個指向引數為int的指標的指標返回值為...的函式的指標的陣列---(*c[10])(int **p)---2,3->4
- c是一個指向引數為int的指標的指標返回值為...的函式的指標的陣列---(*c[10])(int **p)---4->5
- c是一個指向引數為int的指標的指標返回值為指向...的指標的函式的指標的陣列---*(*c[10])(int **p)---5->6
- c是一個指向引數為int的指標的指標返回值為指向char的指標的函式的指標的陣列---*(*c[10])(int **p)---5->6
實現程式:
狀態機可以實現為自動翻譯程式:
https://github.com/biaoJM/translate-C-declaration-statement
typedef和#define:
1.巨集定義的型別名和其他型別說明符一起執行定義,但是typedef只能使用它本身
#define peach int
unsigned peach i; /* works fine */
typedef int banana;
unsigned banana i; /* Bzzzt! illegal */
2.typedef的型別會實施到每個說明符,但是巨集定義不會
#define int_ptr int *
int_ptr chalk, cheese;
// 結果為:
int * chalk, cheese;
導致chalk是int的指標型別,而cheese是int型別
typedef char * char_ptr;
char_ptr Bentley, Rolls_Royce;
Bentley和Rolls_Royce都是char指標型別
名稱空間:
C語言的名稱空間
- 標籤名,所有的標籤名的名稱空間
- tags,對於所有的結構體、列舉類和聯合體的tag具有的名稱空間
- 成員名稱,對每個結構體、列舉類或聯合體都有自己的成員名稱空間
- 其他,其他名稱的名稱空間
所以對宣告:
typedef struct baz {int baz;} baz;
這樣的定義是合法的:
struct baz variable_1; /*這裡baz是定義的型別名*/
baz variable_2; /*這裡baz是tag*/
對於這樣的定義:
struct foo {int foo;int foo2;} foo;
第一個foo是這個結構體的tag,第二個foo是一個結構體變數
sizeof(foo)的結果是變數foo的大小,所以如果宣告時這樣的:
struct foo {int foo;int foo2;} *foo;
sizeof(foo)返回的就是4而不是8,如果想要用tag獲取結構體的大小:sizeof(struct foo)——tag只有和struct關鍵字一起才起作用
而如果這樣定義:
typedef struct foo {int foo;int foo2;} foo;
那麼就不能再用foo作為變數名,因為此時foo不再是tag而和變數有相同的名稱空間
參考:
《expert C programming:deep C secrets》
Chapter 3. Unscrambling Declarations in C