《C专家编程》第三章——分析C语言的声明
前面一章我們已經說過C語言存在的一些問題和它晦澀的地方,讓我們對這門神奇的語言有了更深的了解。現在這一章則集中精力來討論C語言的聲明,分為三塊,首先是說明C語言聲明晦澀難懂的原因和聲明是如何形成的,其次就是學習怎樣對C語言的聲明進行分析,另外本文將詳細來探討一個分析C語言聲明的工具——cdecl,分析和編寫它的源代碼。
C語言的聲明晦澀難懂這一點應該是名不虛傳的,比如說下面這個聲明:
void (*signal(int sig, void(*func) (int)))(int);
這可不是嚇人的,熟悉C語言的人會發現,這原來就是ANSI C標準中的信號的信號處理函數的函數原型,如果你沒有聽說過,那么你確實應該好好補補你的C語言了。那么這個函數原型是什么意思呢?后面會說明,在這里提出就是證明在C語言中,的確存在這種晦澀難懂的聲明。
為什么在C語言中會存在這種晦澀難懂的聲明呢?這里有幾個原因。首先,在設計C語言的時候,由于人們對于“類型模型”尚屬陌生,而且C語言進化而來的BCPL語言也是無類型語言,所以C語言先天有缺。然后出現了一種C語言設計哲學——要求對象的聲明形式與它的使用形式盡可能相似,這種做法的好處是各種不同操作符的優先級在“聲明”和“使用”時是一樣的。比如說:
聲明一個int型變量時:int n;
使用這個int型變量時:n
可以看出聲明形式和使用形式非常相似。不過它也有缺點,它的缺點在于操作符的優先級是C語言中另外一個設計不當的地方。也就是說,C語言之前存在的操作符優先級的問題在這里又繼續影響它的聲明和定義,這就導致程序員需要記住特殊規則才能推測出一些稍微復雜的聲明,當然之前也說過,C語言并不是為程序員設計的,它只是為編譯器設計的。在C++中,這一點倒是有所改善,比如說int &p;就是聲明p是一個只想整形地址的數也就是指針。C語言的聲明存在的最大問題是你無法以一種人們所習慣的自然方式從左向右閱讀一個聲明,在ANSI C引入volatile和const關鍵字之后,情況就更糟糕了。由于這些關鍵字只能出現在聲明中,這就使得聲明形式與使用形式完全對得上號的越來越少了。我相信有很多學習C語言的人都搞不太清楚const與指針之間的聲明關系,請看下面的例子:
const int * grape;
int const * grape;
int * const grape;
const int * const grape;
int const * const grape;
怎么樣?如果你能正確的分析它們的含義,那么說明你的C語言學得不錯,如果你已經暈了,那也不怪你,畢竟這種情況只會在C語言里出現。不過,還是讓我們來解決這幾個例題,首先我們要明白const關鍵字,它的名字經常誤導人們,導致讓人覺得它就是個常量,在這里有個更合適的詞適合它,我們把它叫做”只讀“,它是個變量,不過你只有讀取它的權限,不能對它進行任何修改。我是這么分析這種const聲明的:只要const出現在"*"這個符號之前,可能是int const *,也可能是const int *,總之,它出現在”*"之前,那么就說明它指向的對象是只讀的。如果它在”*"這個符號之后,也就是說它靠近變量名,那么就說明這個指針是只讀的。換句話也可以這么說,如果它出現在"*"之前,說明它修飾的是標識符int或者其他類型名,那么說明這個int的值是只讀的,說明它指向的對象是常量;如果它出現在“*"之后,說明它修飾的是變量名grape,那么說明這個指針本身是只讀的,說明這個指針為常量。這樣再來看上面兩個例題就很簡單了,第一個和第二個的const均出現在"*"符號之前,而"*"之后沒有const變量,那么說明這兩個都是常量指針,也就是說指向的int值是只讀的;第三個const則出現在"*"之后,而”*"之前沒有,說明第三個是一個指針常量,這個指針是只讀的;第四個和第五個const出現在“*"之前和之后,就說明它既是指針常量也是常量指針,指針本身和指針所指向的int值都是只讀的。
看到這里,相信大家已經對C語言這種晦澀的聲明語法有所體會了,這樣看來,正常人都不是很喜歡這種晦澀的語法,可能只有編譯器才會喜歡了吧!
下面我們來看看聲明是如何形成的:
首先要了解的東西叫做聲明器——是所有聲明的核心。聲明器是標識符以及與它組合在一起的任何指針、函數括號、數組下標等。下面我列出一個聲明器的組成部分,首先它可以有零個或多個指針,這些指針是否有const或是volatile關鍵字都沒有關系,其次,一個聲明器里有且只有一個直接聲明器,這個直接聲明器可以是只有一個標識符,或者是標識符[下標],或者是標識符(參數),或者是(聲明器)。書中給出的表格可能有些困難,所以把它總結下來就是這么一個公式:
聲明器 = 直接聲明器( 標識符 or 標識符[下標] or 標識符(參數) or (聲明器) ) + (零個或多個指針)
這個式子已經相當簡潔了,不過早些時候提到過,()操作符在C語言中代表的意思太多了,在這里就體現了出來,它既表示函數,又表示聲明器,還表示括號優先級。為了讓大家更好的理解,我來舉出一些例子給予說明:
有一個直接聲明器,并且這個聲明器為標識符:n
有一個直接聲明器為標識符,還有一個指針: * n
有一個直接聲明器為標識符[下標],還有一個指針: * n[10]
有一個直接聲明器為標識符(參數): n(int x)
這些聲明器看上去跟我們平時的聲明很相似,但是好像又不完整,別著急,因為聲明器只是聲明的一個部分,下面我們來看一條聲明的組成部分:C語言中的聲明至少由一個類型說明符和一個聲明器以及零個或多個其他聲明器和一個分號組成。下面我們一一來介紹這每個部分:
首先類型說明符有這些:void、char、short、int、long、signed、unsigned、float、double以及結構說明符、枚舉說明符、聯合說明符。然后我們知道C語言的變量存儲類型有auto、static、register,鏈接類型有extern、static,還有類型限定符const、volatile,這些都是C語言常見的關鍵字和各種類型,那么一個聲明中至少要有一個類型說明符,這個很好理解,因為這個類型說明符告訴計算機我們要存儲的數據類型。
聲明器的部分見上面,我已經把它說得比較清楚了。
關于其他聲明器,我舉出一個例子大家就明白了:
char i, j;
看到它同時聲明了兩個變量,其中j就是其他聲明器,這表示同一條聲明語句中可以同時聲明多個變量。
最后一個分號,是C語言中一條語句結束的標志。
至此,C語言的聲明就已經很清楚了,不過要注意在聲明的時候還是有一些其他規則,比如說函數的返回值不能是一個函數或數組,數組里面也不能含有函數。
了解了C語言聲明的詳細內容之后,我們再來看看如何分析C語言的聲明:
接下來我們要做的事情就是,用通俗的語言把聲明分解開來,分別解釋各個組成部分。
在這里提一句,關于分析C語言的聲明部分,在《C與指針》的第13章——高級指針話題中也有詳細的描述,會一步一步從簡單的聲明到復雜的聲明,再介紹一些高級指針的用法。而在本書中,我們將著重建立一個模型來分析所有的聲明。
先來理解C語言聲明的優先級規則:
A 聲明從它的名字開始讀取,然后按照優先級順序依次讀取
B 優先級從高到低依次是:
1. 聲明中被括號括起來的部分
2. 后綴操作符:括號()表示這是一個函數;[]則表示這是一個數組
3. 前綴操作符:星號* 表示"指向...的指針“
C 如果const或volatile關鍵字存在,那么按我在前面所說的辦法判斷它們修飾標識符還是修飾類型
下面,還是給出一個例子來幫助理解:
char * const *(*next) ();
A 首先,變量名是next
B next被一個括號括住,而括號的優先級最高,所以”next是一個指向...的指針“
然后考慮括號外面的后綴操作符為(),所以”next是一個函數指針,指向一個返回值為...的函數“
然后考慮前綴操作符,從而得出”next是一個函數指針,指向一個返回值為...的指針的函數“
C 最后,char * const是一個指向字符的常量指針
所以,我們可以得出結論:next是一個函數指針,該函數返回一個指針,這個指針指向一個類型為char的常量指針。
當然,如果不想自己分析這些復雜的聲明,你還有一個好的選擇,就是用一個工具來幫助你分析;或者你想知道自己的分析對不對,也可以用到這個工具——cdecl,它是一個C語言聲明的分析器,可以解釋一個現存的C語言聲明。下面我簡述它的安裝和使用過程,同樣是在Linux上:
首先,安裝命令
sudo apt install cdecl然后直接輸入應用程序名進入程序
cdecl然后直接輸入,來檢測一下我們剛剛分析的例子
cdecl> explain char * const *(*next) (); declare next as pointer to function returning pointer to const pointer to char噢,看起來很不錯嘛,我們分析得對,這個程序也解釋得很棒,怎么樣,是不是對這個程序感到好奇,下面我們來嘗試自己實現這個程序:
首先我們想辦法用一個圖來表示分析聲明的整個過程,上面給出的步驟很有用,但是還是不夠直觀,在書中作者給出了一個解碼環的圖來描述這個步驟,下面我把這個圖大致的描述出來,有的地方可能加上我自己的理解和修改:
?
要注意的事項我已經把它們都標志出來了,現在讓我們用這個流程圖來分析一個實例:
char * const *(*next) ();
分析過程中另外有一點需要特別注意,那就是需要逐漸把已經處理過的片段“去掉”,這樣便能知道還需要分析多少內容。
上面的表格就是這個表達式根據前面給出的流程圖分析聲明的全部過程,從表格中第一列可以看出這個表達式被處理過的部分在一步一步的去掉,這個程序的處理過程現在已經講得非常清楚了,接下來,給出實現這個過程的具體代碼,為了簡單起見,暫且忽略錯誤處理部分,以及在處理結構、枚舉、聯合時只簡單的用“struct”,“enum“和”union“來代表,假定函數的括號內沒有參數列表,否則事情就變得復雜多了!這個程序可能要用到一些數據結構,比如說堆棧,像這種需要一個一個按序列讀取的程序總是免不了要用到堆棧的,在表達式求值等其他應用也經常見到。
我們把用結構來包括每一種標記,首先定義數據結構
#include <stdio.h> #include <string.h> #include <ctype.h> #include <stdlib.h>#define MAXTOKENS 100 //一條聲明中的標記數量 #define MAXTOKENLEN 64 //每個標記最長的長度 enum type_tag {IDENTIFIER, QUALIFIER, TYPE}; //定義類型標簽,包括標識符、限定符、類型符 struct token{ //聲明一個標記結構,其中type就是枚舉變量中的類型 char type; char string[MAXTOKENLEN]; //類型的名字,例如int }; int top = -1; //棧頂 struct token stack[MAXTOKENS]; //棧的內存為MAXTOKENS struct token this; //剛剛讀入的那個標記 #define pop stack[top--] //用宏定義兩個函數,一個是push,一個是pop #define push(s) stack[++top] = s? 定義好數據結構之后,main函數要做的第一步是找出標識符,然后按照上面的流程表執行就行
main() {read_to_first_identifier(); //找出第一個標識符deal_with_declarator(); //進入處理標識的函數printf("\n"); return 0; }下面來介紹單個函數,首先是兩個基本工具函數,一個是用來判斷當前讀取的標記的類型,另一個用來讀取下一個標記到”this“
enum type_tag classify_string(void) //判斷當前讀取的標記是什么類型,返回值為枚舉中的一個值 {char *s = this.string; //如果是const,則把這個標記的string成員改為"只讀“ if(!strcmp(s, "const")){ strcpy(s, "read-only"); return QUALIFIER; } if(!strcmp(s, "volatile")) return QUALIFIER; if(!strcmp(s, "void")) return TYPE; if(!strcmp(s, "char")) return TYPE; if(!strcmp(s, "signed")) return TYPE; if(!strcmp(s, "unsigned")) return TYPE; if(!strcmp(s, "short")) return TYPE; if(!strcmp(s, "int")) return TYPE; if(!strcmp(s, "long")) return TYPE; if(!strcmp(s, "float")) return TYPE; if(!strcmp(s, "double")) return TYPE; if(!strcmp(s, "struct")) return TYPE; if(!strcmp(s, "union")) return TYPE; if(!strcmp(s, "enum")) return TYPE; return INDENTIFIER; //如果標記既不是限定符也不是類型符,那就是標識符 } void gettoken(void) //讀取下一個標記到”this“ { char *p = this.string; while((*p = getchar()) == ' '); //跳過所有的空白字符 if(isalnum(*p)){ //如果遇到標記(標記必須以字母或數字開頭) while(isalnum(*++p = getchar())); ungetc(*p, stdin); *p = '\0'; this.type = classify_string(); return ; } if(*p == '*'){ //如果遇到指針 strcpy(this.string, "pointer to"); this.type = '*'; return ; } this.string[1] = '\0'; this.type = *p; return ; }接下來看讀取第一個標識符,并且同時從左至右掃描聲明,遇到標記則壓入棧中
read_to_first_identifer() //[流程中的第一步] {gettoken();while(this.type != IDENTIFIER){push(this); gettoken(); } printf("%s is ",this.string); gettoken(); }找到標識符的同時,標記的左半部分我們也已經壓入了棧中,接下來處理標識符之后可能存在的數組、函數
deal_with_declarator() {switch(this.type){ //查看當前標記的類型case '[': deal_with_arrays(); //如果是數組,那么執行對數組的處理[流程中的第二步] break; case '(': //如果是函數,那么執行對函數的處理[流程中的第三步] deal_witch_function_args(); } deal_with_pointers(); //處理指針,流程中的第五步 while(top >= 0){ if(stack[top].type == '('){ //判斷是否是左括號,流程中的第四步 pop; gettoken(); deal_with_declaration(); }else{ printf("%s ",pop.string); } } }接下來就是編寫這些處理函數
deal_with_arrays() {while(this.type == '['){ printf("array "); gettoken(); if(isdigit(this.string[0])){ printf("0..%d ",atoi(this.string) - 1); gettoken(); } gettoken(); printf("of "); } } deal_with_fucntion_args() { while(this.type != ')'){ gettoken(); } gettoken(); printf("function returning "); } deal_with_pointers() { while(stack[top].type == '*'){ printf("%s ", pop.string); } }以上就是整個程序簡單版的代碼,為了增加一些趣味,有興趣的朋友可以用昨天的lint檢查程序對上面的程序做出檢查,從而完善這個簡單版的程序。
轉載于:https://www.cnblogs.com/monster-prince/p/6215769.html
總結
以上是生活随笔為你收集整理的《C专家编程》第三章——分析C语言的声明的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第一次冲刺
- 下一篇: python之eval函数,map函数,