分析C語言的聲明

原文1鏈接:https://blog.csdn.net/paxhujing/article/details/77124453

原文2鏈接:https://www.cnblogs.com/monster-prince/p/6215769.html

文章1

讓我們先來看一些C語言的術語以及一些能組合成一個聲明的單獨語法成分。其中一個非常重要的成分就是聲明器(declarator)——它是所有聲明的核心。簡單地說,聲明器就是標識符以及與它組合在一起的任何指針、函數括號、數組下標等,如下表所示。爲了方便起見,我們把初始化內容(initializer)也放到裏面,並分類表示。

聲明器

注:上表中* const volatile、* volatile、* const、* volatile const,這裏的const和volatile是用於限定指針的。

一個聲明由下表所示的各個部分組成(並非所有的組合形式都是合法的,但這個表描述了我們進一步討論所要用到的詞彙)。聲明確定了變量的基本類型以及初始化值(如果有的話)。

聲明說明符

注:這裏的類型限定符const和volatile是用於限定類型說明符指定的類型,與上面* const volatie等是不一樣的。

注:上表中倒數第二行“零個或多個聲明器”的意思是,舉例來說就是一次聲明多個:static const int i,j,k,l,m.n;,這裏的j、k、l、m、n就屬於倒數第二行中提到的。而這裏的i就是倒數第三行中提到的。

注:const static int * const p(); 

const、static、int 屬於聲明說明符;* const、p()屬於聲明器

p是一個函數,它返回一個指針,這個指針是隻讀的,這個指針指向一個int類型的對象,並且該對象也是隻讀的,最後這個函數只能在聲明所在的文件內可見。

聲明說明符:聲明說明符是以嵌套形式組織的,以上爲例有三個聲明說明符const、static、int。

const是一個類型說明符,它後面的聲明說明符是static int;

static是一個存儲說明符,它後面的聲明說明符是int;

int是一個類型說明符,它後就就是聲明器,已經沒有嵌套了

對於聲明說明符的排列順序,C標準並沒有規定,誰嵌套誰都可以,所以static const int、int static const 、int const static 都是一個意思。

讓我們看一下如果你使用這些部件來構造一個聲明,情況能夠複雜到什麼程度。同時要記住,在合法的聲明中存在限制條件。你不可以像下面那樣做:

 

  • 函數的返回值不能是一個函數,所以像foo()()這樣是非法的
  • 函數的返回值不能是一個數組,所以像foo()[]這樣是非法的
  • 數組裏面不能有函數,所以像foo[]()這樣是非法的

 

但像下面這樣則是合法的:

 

  • 函數的返回值允許是一個函數指針,如: int (*fun())()
  • 函數的返回值允許是一個指向數組的指針,如:int (* foo())[]
  • 數組裏面允許有函數指針,如:int (* foo[])()
  • 數組裏面允許有其它數組,如:int foo[][]

 

擴展

C標準中的“右左法則”是用來辨識一個聲明的方法,其具體流程如下:

首先從未定義的標識符開始,然後往右看,再往左看。每當遇到圓括號時,就應該調轉閱讀方向。一旦解析完圓括號裏面所有東西,就跳出圓括號。重複這個過程直到整個聲明解析完畢。

例1:int (*func)(int *p);

首先找到未定義標識符func,它的外面有一對圓括號,而且它的左邊有一個*,所以func是一個指針;

跳出這個圓括號,看右邊,也是一個圓括號,說明(*func)是一個函數,而func是一個指向函數的指針,這類函數有一個int *類型的形參;

跳出這個圓括號,看左邊,是一個int,說明這類函數返回的是一個int類型的值

例2:int (*func)(int *p, int (*f)(int *));

未定義標識符func,它外面有一對圓括號,而且它的左邊有一個*,所以func是一個指針;

跳出這個圓括號,看右邊,也是一個圓括號,說明(*func)是一個函數,而func是一個指向函數的指針,這類函數具有int * 和 int (*)(int *)這樣的形參;

跳出這個圓括號,看左邊,是一個Int,說明這類函數返回的是一個int類型的值。

例3:int (*func[5])(int *p);

未定義標識符func,看右邊,有一對方括號,說明它是一個具有5個元素的數組;

看左邊,是一個*,說明數組的元素是指針;

跳出這個圓括號,看右邊,也是一對圓括號,說明數組中的元素是函數指針,這類函數具有int *這樣的形參;

跳出這個圓括號,看左邊,是一個int,說明這類函數返回的是一個int類型的值

例4:int (*(*func)[5])(int *p);

未定義標識符func,看左邊,是一個*,說明它是一個指針;

跳出這個圓括號,看右邊,是一對方括號,說明(*func)是一個數組,一個指向數組的指針;

看左邊,是一個*,數組的元素是指針;

跳出這個括號,看右邊,是一對圓括號,說明數組中的元素時函數指針,這類函數具有int * 這樣的形參;

跳出這個括號,看左邊,是一個int,說明這類函數返回的是一個Int類型的值

例5:int (*(*func)(int *p))[5];

未定義標識符func,看左邊,是一個*,說明它是一個指針;

跳出這個圓括號,看右邊,是一對圓括號,說明func是一個指向函數的指針,這類函數具有int* 這樣的參數;

看左邊,是一個*,說明這類函數返回的是一個指針;

跳出這個圓括號,看右邊,是一對方括號,說明這個返回的指針指向一個具有5個元素的數組;

看左邊,是一個int,說明數組的元素是Int類型

[非法] 例6:int func(void)[5]

未定義標識符func,看右邊,是一對括號,func是一個函數,沒有參數;

由於右邊是一對方括號,所以函數返回的是一個具有5個元素的數組;

這個數組的元素是int類型

文章2

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{
    char type;
    char string[MAXTOKENLEN];
};

int top = -1;
struct token stack[MAXTOKENS];
struct token thisToken;

#define pop stack[top--]
#define push(s)  stack[++top] = s

enum type_tag classify_string(void)
{
    char  *s = thisToken.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 IDENTIFIER;
}

void gettoken(void)
{
    char *p = thisToken.string;

    while((*p = getchar()) == ' '); //略過空白字符

    if(isalnum(*p)) //讀入標識符的首字符介於A-Z,0-9
    {
        while(isalnum(*++p = getchar()));
        ungetc(*p, stdin);
        *p = '\0';
        thisToken.type = classify_string();
        return;
    }

    if(*p == '*')
    {
        strcpy(thisToken.string, "pointer to");
        thisToken.type = '*';
        return;
    }
    thisToken.string[1] = '\0';
    thisToken.type = *p;
    return;
}

void read_to_first_identifer() //理解分析過程中的所有代碼段
{
    gettoken();
    while(thisToken.type != IDENTIFIER)
    {
        push(thisToken);
        gettoken();
    }
    printf("%s is ", thisToken.string);
    gettoken();
}

void deal_with_arrays()
{
    while(thisToken.type == '[')
    {
        printf("array ");
        gettoken(); //數字或']'
        if(isdigit(thisToken.string[0]))
        {
            printf("0..%d ", atoi(thisToken.string)-1);
            gettoken(); //讀取']'
        }
        gettoken(); //讀取']'之後的再一個標記
        printf("of ");
    }
}

void deal_with_function_args()
{
    while(thisToken.type != ')')
    {
        gettoken();
    }
    gettoken();
    printf("function returning ");
}

void deal_with_pointers()
{
    while(stack[top].type == '*')
    {
        printf("%s ", pop.string);
    }
}

void deal_with_declarator()
{
    switch(thisToken.type) //處理標識符之後可能存在的數組/函數
    {
        case '[' : deal_with_arrays(); break;
        case '(' : deal_with_function_args(); break;
    }

    deal_with_pointers();

    while(top >= 0)
    {
        if(stack[top].type == '(')
        {
            pop;
            gettoken(); //讀取')'之後的符號
            deal_with_declarator();
        }
        else
        {
            printf("%s ", pop.string);
        }
    }
}

TEST_F(Testcase, test)
{
    MuteOff();
    read_to_first_identifer(); //將標記壓入堆棧,直到遇見標識符
    deal_with_declarator();
    printf("\n");
}

實驗結果:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章