C語言中的程序環境與預處理詳情

1.程序的翻譯環境和執行環境

在ANSI C的任何一種實現中,存在兩個不同的環境

  • 第1種是翻譯環境,在這個環境中源代碼被轉換為可執行的機器指令,也就是從,c文件到.exe文件;
  • 第2種是執行環境,它用於實際執行代碼;

翻譯環境是由編譯器提供的,而執行環境是由操作系統提供的。

如MSVC,DEV C++,Codeblocks這些編譯軟件都是集成開發環境,也就是集成瞭編輯,編譯,鏈接和調試等功能。

2.詳解編譯和鏈接

2.1程序翻譯環境下的編譯和鏈接

從源文件到可執行程序可以分為編譯和鏈接兩步,在編譯階段源文件變成瞭目標文件,在鏈接階段目標文件變成瞭可執行程序。

組成程序的每個源文件通過編譯過程分別轉化成目標文件;

每個目標文件由鏈接器捆綁在一起,形成一個單一而完整的可執行程序;

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

圖解:

2.2深入編譯和鏈接過程

編譯本身可以分為預編譯(預處理),編譯和匯編。

預編譯:在預編譯階段會將#include引用的頭文件給輸入到文件裡面,進行#define定義的標識符的替換,以及將註釋給刪除,因為註釋是給程序員看的,不是給電腦看的;

編譯:在這個過程中會將C語言代碼翻譯成匯編代碼,編譯器會對代碼進行詞法分析,語法分析,語義分析,符號匯總;

匯編:會把在編譯階段形成的匯編代碼翻譯成二進制的指令,並將匯總的符號形成一個符號表;

在編譯完成之後,就會開始鏈接,鏈接過程會合成段表,也就是將目標文件捆綁在一起,以及將符號表合並並進行重定位,最後生成可執行程序。

2.3運行環境

程序執行的過程:

  • 1.程序必須載入內存中。在有操作系統的環境中,一般這個過程由操作系統完成,在獨立的環境中,程序的載入必須由手工安排,也可能是通過可執行代碼置入隻讀內存來完成。
  • 2.程序開始執行,並調用main函數。
  • 3.開始執行程序代碼,這個時候程序將使用一個運行時堆棧,存儲函數的局部變量和返回地址,程序同時也可以使用靜態內存,存儲於靜態內存中的變量在程序的整個執行過程一種保留它們的值。
  • 4.終止程序。正常終止main函數,也有可能是意外終止。

3.預處理詳解

3.1預定義符號

預定義符號都是語言內置的

__FILE__       //進行編譯的源文件

__LINE__      //當前代碼的行號

__DATE__    //文件被編譯時的日期

__TIME__     //文件被編譯時的時間

__STDC__    //如果編譯器遵循ANSI C,其值為1,否則未定義

預定義符號的使用:

int main()
{
	printf("file:%s\nline:%d\ndata:%s\ntime:%s\n", __FILE__, __LINE__, __DATE__, __TIME__);
	return 0;
}

3.2#define

3.2.1#define定義的標識符

#define name stuff

舉例:

#define MAX 1000
#define reg register      //為 register這個關鍵字,創建一個簡短的名字
#define do_forever for(;;)   //用更形象的符號來替換一種實現
#define CASE break;case     //在寫case語句的時候自動把 break寫上。
// 如果定義的 stuff過長,可以分成幾行寫,除瞭最後一行外,每行的後面都加一個反斜杠(續行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
             date:%s\ttime:%s\n" ,\
             __FILE__,__LINE__ ,    \
             __DATE__,__TIME__ )

在define定義標識符的時候,不要在最後加上;

如下面這種情況,會出現語法錯誤

#define NUM 100;
int main()
{
	int a = 0;
	if (1)
		a = NUM;
	else
		a = 0;
	return 0;
}

3.2.2#define定義宏

#define機制包括瞭一個規定,允許把參數替換到文本中,這種實現通常稱為宏或宏定義

宏的聲明方式如下:

#define name(parament-list) stuff

其中的parament-list是一個由逗號隔開的符號表,它們可能出現在stuff中

把name(parament-list)這個整體稱為宏

註意:

參數列表的左括號必須與name緊貼,如果兩者之間存在空格,參數列表就會被解釋為stuff的一部分,語法就是這麼規定的。

接下來是宏的使用:

比如用宏實現一個數的平方:

#define SQUARE(n) n * n
int main()
{
	SQUARE(6);
	return 0;
}

語句SQUARE(6)就會替換成6 * 6;

解釋:宏先是接受一個參數,SQRARE(n)中的n就變成瞭6,其後宏的內容也就由n * n變成瞭6 * 6,再將6 * 6替換到程序中使用宏的位置。

但是,這個宏這麼寫存在一個問題,如下代碼:

#define SQUARE(n) n * n
int main()
{
	printf("%d\n", SQUARE(1 + 3));
	return 0;
}

看上去似乎最後的結果是16,然而實際上參數n會被替換成1 + 3,這樣最終替換的內容是1 + 3 * 1 + 3,這條表達式最終的結果是7.

所以需要在n的左右兩邊加上一對括號,如下:

#define SQUARE(n) (n) * (n)

再看另外一個宏定義:

#define DOUBLE(n) (n) + (n)

代碼:

#define DOUBLE(n) (n) + (n)
int main()
{
	printf("%d\n", 5 * DOUBLE(3));
	return 0;
}

 看上去最終結果似乎是30,然而替換後語句實際上是

printf("%d\n", 5 * (3) + (3));

所以最終結果是18

所以為瞭保證獲得想要的結果,宏定義表達式兩邊還需要加上一對括號

#define DOUBLE ((n) + (n))

所以用於對數值表達式進行求值的宏定義都應該用這種方式加上括號,避免在使用宏時由於參數中
的操作符或鄰近操作符之間產生不可預料的相互作用。 

3.2.3#define替換規則

在程序中擴展#define定義符號和宏時,需要涉及幾個步驟

  • 1.在調用宏時,首先對宏括號中的參數進行檢查,看看是否包含由#define定義的符號,如果有,這些符號首先被替換。
  • 2.替換文本後,文本被插入到程序中原來文本的位置,對於宏,參數名被對應的值所替換。
  • 3.最後,再次對結果文件進行掃描,查看替換過後的內容是否還有#define定義的符號,如果有,則重復上述處理過程

註意:

  • 1.宏參數和#define定義中可以出現其他#define定義的符號,但是對於宏,不能實現遞歸。
  • 2.當預處理器搜索#define定義的符號時,字符串常量的內容並不被搜索。

比如:

#define a 123
int main()
{
	printf("%s", "a");
	return 0;
}

語句printf("%s", "a");中的a並不會被替換成123

3.3.4#和##

如何把參數插入到字符串中?

如下代碼:

int main()
{
	printf("abc""def");
	return 0;
}

輸出的結果是abcdef

發現字符串是有自動相連的特點的

看下面這個代碼:

#define PRINT(FORMAT, VALUE) printf("the value of "#VALUE " is "FORMAT"\n", VALUE)
int main()
{
	int a = 6;
	PRINT("%d", a);
	return 0;
}

最終輸出的結果是:

the value of a is 6

所以#VALUE會被預處理器在預處理階段預處理為"VALUE"

接下來看看##的作用:

##可以把位於它兩邊的符號合成一個符號,並且允許宏定義從分離的文本片段創建標識符。

如下代碼:

#define MAXMIN 6
#define MIDDLE MAX##MIN
int main()
{
	printf("%d\n", MIDDLE);
	return 0;
}

註意:連接之後產生的符號必須是已經定義的,否則結果就是非法的。

3.2.5帶副作用的宏參數

當宏參數在宏的定義中出現超過一次的時候,如果參數帶有副作用,那麼在使用這個宏的時候就可能會出現危險,導致不可預測的後果。副作用就是表達式求值的時候出現的永久性後果。

比如:

x + 1;//不帶有副作用

x++;  //帶有副作用

如下代碼可以證明副作用的宏參數帶來的問題

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	printf("x=%d y=%d z=%d\n", x, y, z);
	return 0;
}

 在代碼預處理之後

z = ( (x++) > (y++) ? (x++) : (y++));

所以最終結果是x=6 y=10 z=9

3.2.6宏和函數對比

宏通常被用於執行簡單的運算,比如在兩個數中找出較大的一個

#define MAX(a, b) ((a)>(b)?(a):(b))

對於為什麼不用函數來完成這個任務,有兩個原因:

  • 1.用於調用函數和從函數返回的代碼可能比實際執行這個小型計算工作所需要的時間多,所以此時宏在程序的規模和運算方面更勝一籌。
  • 2.函數的參數必須聲明為特定的類型,所以函數隻能在類型合適的表達式上使用,而宏可以適用於整型,浮點型,長整型,宏是類型無關的

當然宏也是有缺點的:

  • 1.每次使用宏的時候,會將宏定義的代碼插入到程序中,除非宏比較短,否則可能大幅度增加程序的長度;
  • 2.宏是沒法進行調試的,因為在預處理階段,宏定義的符號已經發生瞭替換,此時調試看到的代碼和實際上運行時的代碼是有所差異的;
  • 3.宏由於類型無關,也就不夠嚴謹瞭;
  • 4.宏可能會帶來運算級優先的問題,導致程序容易出錯; 

宏有時候也可以做到函數做不到的事情,比如宏的參數可以出現類型,但是函數不行

如下代碼:

#define MALLOC(num, type)\
(type *)malloc(num * sizeof(type))
...
//使用
MALLOC(10, int);//類型作為參數

 預處理替換後:

(int *)malloc(10 * sizeof(int));

會節省部分代碼。

總的來對比一下宏和函數的區別:

函數和宏的使用語法很相似,所以語言本身沒法幫助區分二者,所以平時的命名習慣是:

宏名全部大寫

函數名不要全部大寫

3.3#undef 

這條指令用於移除一個宏定義

語法:

#undef name

使用: 

#define MAX 5
int main()
{
	printf("%d\n", MAX);
#undef MAX
	return 0;
}

3.4命令行定義

許多C 的編譯器提供瞭一種能力,允許在命令行中定義符號。用於啟動編譯過程。
例如:當我們根據同一個源文件要編譯出不同的一個程序的不同版本的時候,這個特性有點用處。(假定某個程序中聲明瞭一個某個長度的數組,如果機器內存有限,我們需要一個很小的數組,但是另外一個機器內存大些,我們需要一個數組能夠大些。)

#include <stdio.h>
int main()
{
  int array [ARRAY_SIZE];
  int i = 0;
  for(i = 0; i< ARRAY_SIZE; i ++)
 {
    array[i] = i;
 }
  for(i = 0; i< ARRAY_SIZE; i ++)
 {
    printf("%d " ,array[i]);
 }
  printf("\n" );
  return 0;
}

3.5條件編譯

在編譯一個程序的時候如果要將一條語句或者一組語句編譯或者放棄掉是很方便的,因為有一個叫條件編譯的東西。

對於調試性的代碼,刪除比較可惜,保留又會礙事,所以可以選擇性的去編譯。

如下代碼:

#define __DEBUG__
int main()
{
	int arr[10] = { 0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		arr[i] = i;
#ifdef __DEBUG__
		printf("%d ", arr[i]);//為瞭觀察數組是否被賦值成功
#endif
	}
	return 0;
}

1.#if 常量表達式
        //…
#endif
//常量表達式由預處理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
        //..
#endif

2.多個分支的條件編譯
#if 常量表達式
        //…
#elif 常量表達式
        //…
#else
        //…
#endif

3.判斷是否被定義
#if defined(symbol)
#ifdef symbol

#if !defined(symbol)
#ifndef symbo

4.嵌套指令
#if defined(OS_UNIX)
        #ifdef OPTION1
                unix_version_option1();
        #endif
        #ifdef OPTION2
                unix_version_option2();
        #endif
#elif defined(OS_MSDOS)
        #ifdef OPTION2
                msdos_version_option2();
        #endif
#endif

3.6文件包含

#include指令可以使另一個文件被編譯,會讓被包含的頭文件出現在#include指令的地方

這種替換的方式很簡單,預處理器會先刪除這條指令,並用包含文件裡的內容進行替換,如果這個文  件被包含瞭10次,那實際上就會被編譯10次

3.6.1頭文件被包含的方式

本地文件包含:

#include "fliename.h"

查找方法:先在源文件的目錄下去查找,如果該頭文件未被找到,編譯器就會像去查找庫函數的頭文件一樣在標準位置去查找頭文件,如果還找不到就提示編譯錯誤。

庫文件包含:

#include <filename.h>

查找方法:直接在標準路徑下去查找,如果找不到就提示編譯錯誤。

雖然可以對庫文件也采用""的包含方式,但是當目錄下的文件非常多的時候,這樣查找起來的效率就會低一些瞭,而且也不容易去區分是庫文件還是頭文件瞭。

3.6.2嵌套文件包含

如圖:

  • common.h和common.c是公共模塊
  • test1.h和test1.c使用瞭公共模塊
  • test2.h和test2.c使用瞭公共模塊
  • test.h和test.c使用瞭test1模塊和test2模塊

這樣最終的程序中就會包含兩次common.h瞭,等於有2份common.h的內容,會造成代碼的重復。

對此可以采用條件編譯的方式來解決這個問題

在引用每個頭文件時在開頭寫上這麼一個內容:

#ifndef __STDIO_H__
#define __STDIO_H__
#include <stdio.h>
#endif

如果沒有定義標識符__STDIO_H__就定義__STDIO_H__並且去包含頭文件

如果下次還遇到包含頭文件的代碼,由於__STDUI_H__已經被定義過,所以也就不會進行第二次包含瞭

或者對於在頭文件的開頭也可以這麼寫:

#pragma once
#include <stdio.h>

也可以避免頭文件的重復引入

4.其他預處理指令

比如:

#error

#pragma

#pragma

#line

到此這篇關於C語言中的程序環境與預處理詳情的文章就介紹到這瞭,更多相關C語言預處理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: