溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

C語言程序環(huán)境中的預處理實例分析

發(fā)布時間:2022-02-28 10:06:34 來源:億速云 閱讀:159 作者:iii 欄目:開發(fā)技術

本篇內容介紹了“C語言程序環(huán)境中的預處理實例分析”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

    一、翻譯環(huán)境

    C語言程序環(huán)境中的預處理實例分析

    整個翻譯環(huán)境大致就可以畫成這樣一張圖。

    下列有幾點需要說明:

    1. 組成一個程序的每一個源文件通過編譯過程分別轉換成目標文件(在Linux中目標文件的后綴為.o;而在Windows中目標文件后綴為.obj)

    2. 每個目標文件由鏈接器(linker)捆綁在一起,形成一個單一而完整的可執(zhí)行程序

    3. 鏈接器同時也會引入標準C函數庫(鏈接庫)中任何被該程序所用到的函數,而且它可以搜索程序員個人的程序庫,將其需要的函數也鏈接到程序中

    接下來介紹每一步在Linux系統下整個翻譯環(huán)境的實現方法,以及每一個步驟的作用。

    編譯可分為三個部分:

    (1)預處理:輸入指令gcc -E test.c -o,就會將test.c文件變?yōu)閠est.i文件。這一步的作用是是對頭文件(#include)的包含、刪除注釋、#define定義符號的替換等文本操作(下文會對預處理這一個步驟展開詳細的介紹)

    (2)編譯:輸入指令gcc -S test.i,就會將test.i文件變?yōu)閠est.s文件,這一步主要作用是把C語言代碼轉換成匯編代碼,其中包含4步:1. 語法分析;2. 詞法分析;3. 語義分析;4. 符號匯總

    (3)匯編:輸入指令gcc -c test.s,就會將test.s文件變?yōu)閠est.o文件,這一步是把匯編代碼轉換成二進制的指令,這一步是會形成符號表,此時的符號表為接下來的鏈接操作做出了準備

    多個.c文件通過編譯過程后形成.o目標文件,在要執(zhí)行鏈接的時候,輸入指令gcc test.o add.o -o test,就會將.o文件變成可執(zhí)行文件,這其中的操作包括合并段表和符號表的合并和重定位,這一步主要就是將多個目標文件進行連接的時候通過符號表查看來自外部的符號是否真實存在,這樣就完成了整個翻譯環(huán)境的操作。

    二、執(zhí)行環(huán)境

    對于程序的執(zhí)行過程可分為以下幾個步驟:

    1. 程序必須載入內存中。在有操作系統的環(huán)境中:一般由操作系統完成。在獨立的環(huán)境中,程序的載入必須由手工安排,也可能是通過可執(zhí)行代碼置入只讀內存來完成

    2. 程序的執(zhí)行開始。之后就會調用main函數

    3. 開始執(zhí)行程序代碼。這個時候程序將使用一個運行時堆棧(stack),存儲函數的局部變量和返回地址;程序同時也可以使用靜態(tài)(static)內存,存儲于靜態(tài)內存中的變量在程序的整個執(zhí)行過程一直保留他們的值

    4. 終止程序。正在終止main函數,也有可能是意外終止的情況

    三、預處理

    1. 預處理符號

    在C語言中,有些預處理符號是語言內置的,就比如:

    __FILE__   //進行編譯的源文件
    __LINE__   //文件當前的行號
    __DATE__   //文件被編譯的日期
    __TIME__   //文件被編譯的時間
    __STDC__   //如果編譯器遵循ANSI C,其值為1,否則未定義

    2. #define定義標識符

    #define定義的標識符可以是常量、簡化關鍵字、一些符號等,例如:

    #define M 10   //定義常量
    #define reg register   //將關鍵字簡化
    #define do_forever for(;;)   //用形象的符號來替換一種實現
    #define CASE break;case   //在寫case語句的時候會自動地把break寫上

    對于#define定義標識符來說,如果定義的東西過長,還可以分幾行來寫,除最后一行外,其他每行都加上'\',例如:

    #define DEBUG_PRINT printf("file:%s\tline:%d\t \
    							date:%s\ttime:%s\n",\
    							__FILE__,__LINE__, \
    							__DATE__,__TIME__)

    3. #define定義宏

    在#define定義標識符外,#define還有一個規(guī)定,就是允許把參數替換到文本中,進而就形成了#define定義宏。聲明的方式如下:

    #define name(parament-list) stuff

    這里的parament-list是由一個逗號隔開的符號表,在實際的代碼中他們也會存在于stuff中。

    其中值得注意的是:

    1. 參數列表的左括號必須與name相鄰

    2. 如果parament-list與stuff兩者之間有任何空白存在,參數列表就會被注釋為stuff的一部分

    了解了#define定義宏是如何寫后,接下來就是#define定義宏的替換規(guī)則:

    1. 在調用宏的時候,首先對參數進行檢查,看看是否包含任何由#define定義的符號。如果是,它們首先被替換

    2. 替換文本隨后被插入到程序中原來的文本位置,參數名被它們的值所替換

    3. 最后,再次對結果文件進行掃描,看看它是否包含任何由#define定義的符號。如果是,就重復上述處理過程

    所以,總結以上規(guī)則后得出的結論就是:如果是#define定義宏用于對數值表達式進行求值的宏定義都應該加上括號,避免在使用宏時由于參數中的操作符或者鄰近操作符之間不可預料的相互作用。

    當然,對于#define的使用還有幾個注意的點:

    1. 宏參數和#define定義中可以出現其他#define定義的符號,但是對于宏,不能出現遞歸

    2. 當預處理器搜索#define定義的符號的時候,字符串常量的內容并不被搜索

    4. #和##

    對于一些想要把參數插入到字符串中的情況,我們會使用#來把一個宏參數變成對應的字符串,下面舉個例子:

    如果是直接打印出來的話,因為字符串是可以拼接的,所以就如這樣:

    #include <stdio.h>
    int main()
    {
    	int a = 10;
    	printf("the value of ""a"" is %d\n", a);
    	return 0;
    }

    那么,對于定義宏參數來說,就應該這樣:

    #include <stdio.h>
    #define PRINT(n) printf("the value of "#n" is %d\n", n)
    int main()
    {
    	int a = 10;
    	PRINT(a);
    	return 0;
    }

    這樣字符串中的n才會根據跟著宏參數的值變化而變化。

    而##的作用是可以把位于它兩邊的符號合成一個符號。它允許宏定義從分離的文本段創(chuàng)建標識符。但是這樣連接必須產生一個合法的標識符,否則會報錯說未定義標識符。

    5. 宏和函數的對比

    宏的優(yōu)勢:1. 在執(zhí)行一些小型計算工作的時候,定義宏比調用函數和從函數返回的代碼執(zhí)行所需要的時間會更短;2. 函數的參數必須聲明為特定的類型,二宏參數不用

    宏的劣勢:1. 每次使用宏的時候,一份宏定義的代碼將插入到程序中。除非宏比較短,否則可能大幅度增加程序的長度;2. 宏是無法進行調試的,而函數可以;3. 宏由于沒有進行類型定義,所以有時候就會不夠嚴謹;4. 宏可能會帶來運算符的優(yōu)先級的問題,導致程序容易出錯

    屬性#define定義宏函數
    代碼長度每次使用時&#xff0c;宏代碼都會被插入到程序中&#xff0c;除了非常小的宏以外&#xff0c;程序的長度會大幅度增長函數代碼只出現于一個地方&#xff1b;每次使用這個函數時&#xff0c;都調用那個地方的同一份代碼
    執(zhí)行速度更快存在函數的使用和返回的額外開銷&#xff0c;所以相對慢一些
    操作符優(yōu)先級宏參數的求值是在所有周圍表達式上下文環(huán)境里&#xff0c;除非加上括號&#xff0c;否則鄰近操作符的優(yōu)先級可能會產生不可預料的后果&#xff0c;所以建議宏在書寫的時候多些括號函數參數只在函數調用的時候求值一次&#xff0c;它的結果值傳給函數。表達式的求值結果更容易預測
    帶有副作用的參數參數可能被替換到宏體中的多個位置&#xff0c;所以帶有副作用的參數求值可能會產生不可預料的結果函數參數只在傳參的時候求值一次&#xff0c;結果更容易控制
    參數類型宏的參數與類型無關&#xff0c;只要參數的操作是合法的&#xff0c;它就可以使用于任何參數類型函數的參數是與類型有關的&#xff0c;如果參數類型不同&#xff0c;就需要不同的參數&#xff0c;即使他們執(zhí)行的任務的不同的
    調試宏是不方便調試的函數是可以逐語句調試的
    遞歸宏是不能遞歸的函數是可以遞歸的

    6. 條件編譯

    下面列舉一些編譯指令:

    1. #undef 該指令用于移除一個宏定義

    2. 該指令是判斷應該執(zhí)行哪一個語句塊

    #if 常量表達式
        執(zhí)行語塊
    #elif 常量表達式
        執(zhí)行語塊
    #else
        執(zhí)行語塊
    #endif

    3. 該指令是判斷是否被定義

    #if define(symbol)
        如果有定義,執(zhí)行此語句塊
    or
    #ifdef symbol
        如果有定義,執(zhí)行此語句塊
    or
    #if !define(symbol)
        如果沒有定義,執(zhí)行此語句塊
    or
    #ifndef symbol
        如果沒有定義,執(zhí)行此語句塊

    4. 對于條件編譯指令來說,其實還可以對其進行嵌套,稱為嵌套指令

    7. 文件包含

    我們在一些較大工程進行編譯的時候、在多人合作同一塊項目工程的時候,可能會出現頭文件重復包含的情況,如果真是這樣,則會導致整個代碼運行時的效率大大降低,所以對頭文件避免重復包含就顯得十分重要了。那么,如何避免呢?下面就有一段代碼可以用來避免這種情況:

    #ifndef __TEST_H__
    #define __TEST_H__
        寫頭文件內容
    #endif

    這段代碼就可以很好地解決了頭文件重復包含的問題,但是實際上,如果是在VS的環(huán)境下進行編譯,會自動在最開始的地方寫上:#pragma once,這句代碼一樣也是可以解決重復包含的問題。

    那么,解決完頭文件重復包含的問題后,就來介紹兩種頭文件包含的方式:

    1. 用引號包含的頭文件,例如:#include "test.h"。這種包含方式頭文件的查找策略是先在源文件所在的目錄下查找,如果該頭文件未被找到,編譯器就像查找?guī)旌瘮殿^文件一樣在標準位置查找頭文件,如果還找不到,則會直接報錯。

    2. 用尖括號包含頭文件,例如:#include 。這種包含方式則是未有第一步,直接進行第二步。

    但是不能說為了保證萬無一失,直接把全部頭文件的包含都用引號進行包含,這樣的話有些時候其實是用尖括號的情況而錯用引號導致程序的執(zhí)行速度下降、效率下降等。

    “C語言程序環(huán)境中的預處理實例分析”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!

    向AI問一下細節(jié)

    免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI