【C語言】第十四章-程序的編譯

第十四章 程序的編譯

程序的編譯過程

  在我們使用ide或者gcc的時候,編譯器總是自動幫我們直接生成了可執行文件,但是在編一個過程那種還可細分爲幾個步驟,這幾個步驟的說明則是這一章的重點。

預處理

  預處理是編譯器對我們的代碼進行的第一道處理 ,在這個過程中編譯器會做以下幾件事情。
  1、拷貝頭文件。
  2、去掉註釋。
  3、對宏展開。
  4、處理條件編譯。
  我們在Linux中可以通過指令讓gcc生成預處理後的文件。這裏假設我有一個hello.c文件。
        gcc -E hello.c -o hello.i
  之後就會生成一個預處理後的文件,我們可以用vim打開看看其中的內容,會發現代碼一下多了好幾百行,這就是因爲編譯器將頭文件中的內容全部拷貝了進來所導致的,同時會發現編譯器在預處理期間確實就做了上述的4件事。

編譯

  在預處理結束後編譯器才真正開始進行編譯,編譯器會對整合的代碼進行語義和語法分析處理,使我們的源碼變成彙編代碼。當然我們也有語句可以讓gcc生成編譯過後的文件。
        gcc -S hello.i -o hello.s
  編譯後打開.s文件會發現我們的代碼會轉換爲彙編語言,但是計算機也並不直接看得懂彙編語言,於是就有了下一步編譯。

彙編

  在這一步裏編譯器會進行彙編將生成的彙編文件轉換爲機器碼也就是二進制指令,這樣計算機就看得懂了,我們可以用以下這段代碼生成彙編結束的文件。
        gcc -c hello.s -o hello.o
  彙編結束後會發現代碼我們就真的一個字符都看不懂了,這就是因爲文件已經變成了二進制的指令,但是到此編譯過程還沒有結束,我們還差最後一步生成可執行文件。

鏈接

  在最後一步中會進行各個文件的鏈接,因爲我們的各個文件都是分開進行編譯的,就需要最後一步來將他們合併,因此最後一步往往有兩個步驟,合併段表,合併符號表和符號表的重定位,具體可以理解爲將項目中各個源文件進行合併統一管理。之後就可以生成可執行文件了。
        gcc hello.o -o Hello
  至此編譯過程就結束了。

預處理詳解

預處理符號

  在程序編寫中有一些語言自帶的在預處理中會進行處理的特殊的宏供我們使用,這裏列舉出一些。

#include <stdio.h>
int main()
{
  printf("當前的文件:%s\n", __FILE__);//__FILE__表示當前的文件
  printf("當前的行號:%d\n", __LINE__);//__LINE__表示當前的行號
  printf("文件被編譯的日期:%s\n", __DATE__);//打印文件被編譯的日期
  printf("文件被編譯的時間:%s\n", __TIME__);//打印文件被編譯的時間
  printf("是否遵循ANSI C標準:%s\n", __STDC__ != 0 ? "是" : "否");//如果編譯器遵循ANSI C標準,它就是個非零值
}         



[misaki@localhost 程序的編譯]$ ./Main
當前的文件:main.c
當前的行號:5
文件被編譯的日期:Mar 18 2019
文件被編譯的時間:00:28:03
是否遵循ANSI C標準:是

  這些符號在預處理期間就會進行替換從而可以使用在各個場景下。

define詳解

  宏定義是在預處理期間就被處理的宏,用提十分廣泛,不光是定義常量,定義函數,定義別名都有着不可小覷的作用。
  但是在使用宏定義的過程中我們始終要記住宏就是文本替換,不過是將上面的代碼替換了下來罷了,由這一特性宏既有優勢也有劣勢。

#include <stdio.h>
/* 續行符 \  */
//用宏定義函數想要換行的話再行尾+'\'
#define CHECK(fp) if (fp == NULL) \
{ \
  printf("fopen failed! %s:%d\n", __FILE__, __LINE__); \
}
void Check(FILE* fp)
{
  if(fp == NULL)
  {
    printf("fopen failed! %s:%d\n", __FILE__, __LINE__);
  }
}
int main()
{
  FILE* fp1 = fopen("./test.txt", "r");
  CHECK(fp1);
  Check(fp1);
  FILE* fp2 = fopen("./test.txt", "r");
  CHECK(fp2);
  Check(fp2);
}


[misaki@localhost 程序的編譯]$ ./a.out
fopen failed! hello.c:42
fopen failed! hello.c:17
fopen failed! hello.c:45
fopen failed! hello.c:17

  這個例子可以看出用宏定義寫的函數會明確的將代碼出錯的行號準確的返回,讓我們知道是哪個位置出現了錯誤,但是用普通的函數卻做不到這一點,只會返回函數定義的地方,這也是得益於宏定義的特性。同時我們還要注意宏定義是沒有參數檢查的,因此我們再宏定義中的fp參數其實可以是任意類型的。
  同樣宏定義也有着不少的缺點。

#include <stdio.h>
#define ADD(x, y) x + y
#define MUL(x, y) x * y
int main()
{
  int a = ADD(10, 20) * 10 + 20;
  printf("a = %d\n", a);//純文本替換這裏就會出錯
  int a2 = MUL(10, 10 + 10);//也會出錯          
  printf("a = %d\n", a2);
}



[misaki@localhost 程序的編譯]$ ./a.out
a = 230
a = 110

  以上這兩個例子我們都沒有的到預期想要的結果,都是因爲宏定義直接將文本拷貝至此因此沒有優先順序,導致計算沒有按照預期進行,爲此我們不得不多加幾個括號。
  綜上所述宏定義有利有弊,我們總結一下哪些情況一定要使用宏定義。
  1、打印日誌的行號和文件。
  2、沒有參數類型檢查。
  3、要求開銷更小。

‘#‘和’##’

‘#’

  當我們使用printf()打印時不同的字符串是可以拼接的,我們使用以下語句printf("Hello""Misaki")會出現HelloMisaki的結果,我們利用這一點可以使用宏定義模擬實現printf()。

#include <stdio.h>
#define PRINT(FORMAT, VALUE) \
  printf("the value = " FORMAT " is format\n", VALUE);
int main()
{
    int i = 10;
    PRINT("%d", i + 1);
}



[misaki@localhost 程序的編譯]$ ./a.out
the value = 11 is format

  這樣自然可以,但是這樣只有當參數是字符串的時候纔可以完成拼接,呢有沒有什麼方式可以將不是字符串的參數在宏中轉換爲字符串呢?

#include <stdio.h>
#define PRINT(FORMAT, VALUE) \
  printf("the value = " #VALUE " is format\n", VALUE);
int main()
{
    int i = 10; 
    PRINT("%d", i + 1);
    //宏的參數 # 能把參數變成一個字符串,然後這個字符串就可以再代碼中進行文本拼接
}

  這樣即可將不是字符串的參數轉換爲字符串。

‘##’

  ##的作用是符號拼接,這個操作符十分強大,甚至允許拼接變量。

#include <stdio.h>
#define ADD_TO_NUM(num , value) \
  sum##num += value;//## 拼接可以拼接變量,這裏變成了sum(num) => sum1
int main()
{
    int sum1 = 10;
    ADD_TO_NUM(1, 10);//等於給num1 + 10
    printf("%d\n", sum1);
}

其他預處理指令

undef

  undef用於移除一個已經有了的宏定義。

#define SIZE 10
#undef SIZE//清除原來的宏定義
#define SIZE 5               

條件編譯

  當滿足某個條件的時候進行編譯,否則不編譯。

#include <stdio.h>
int main()
{
#if 0 //條件爲真就編譯,爲假就不編譯
  printf("hehe\n");
#else                               
  printf("haha\n");
#endif
}



[misaki@localhost 程序的編譯]$ ./a.out
haha

  另一種條件編譯。

#ifdef SIZE//如果SIZE被宏定義就編譯以下代碼
  printf("haha\n");  
#endif  

pragma once

  頭文件在預處理階段會進行問唄拷貝合併到一個文件中,因此如果一個頭文件多次引用就會導致重複定義從而報錯,因此我們往往想要一個頭文件只編譯一次,因此我們就會使用pragma once告訴編譯器這個文件只編譯一次。我們還可以使用以下的代碼替代它,不過效果並不如它,在一些特定的情況下也會出錯。

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