原始碼變成可執行程式的深入了解
- 原始碼變成可執行程式的流程
- 大概流程
- 具體流程
- 預處理
- 頭檔案的復制
- 注釋的清除
- #define定義的替換
- 條件編譯
- 編譯
- 匯編
- 鏈接
- 預處理詳解
- define
- #define定義識別符號
- #define定義宏
- 宏
- 宏的易錯點
- 宏與函式對比
- define替換規則
- #和##的作用
- #的作用
- ##的作用
- 條件編譯
- 頭檔案包含
- 兩中頭檔案包含的區別
- 如何避免頭檔案被重復包含
原始碼變成可執行程式的流程
大概流程
當我們在vs下編譯了一個工程的時候,我們會發現,在我們的debug中每個.c檔案都會對應的生成一個.obj檔案(目標檔案),如下圖


那么這是為什么呢?
我們就要大概來說一說關于程式編譯的流程了,如下圖,每個原始碼檔案通過編譯器生成對應的目標檔案,然后再通過聯結器與鏈接庫連接,最終生成我們的可執行程式,大概了解了之后就請繼續往下看以更深入的了解它們,

具體流程
預處理
預處理有四大作用:
1.頭檔案的復制
2.注釋的清除
3.#define定義的替換
4.條件編譯
因為我們的vs是IDE(集成開發環境),會將原始碼到程式的步驟一步走完,不便理解,我們可以在Linux環境下進行一步一步的詳解,
頭檔案的復制
話不多說,我們打開Linux虛擬機,執行預處理后我們先打開test.c,再打開test.i進行對比

這是test.c 一段很簡單的代碼

然后我們再打開test.i,我們發現原來只有九行的代碼現在已經變成了八百多行,相對應的#include包含的頭檔案這句代碼消失了,
這就是頭檔案的復制,在預處理階段,會將你所包含的頭檔案的內容拷貝一份進你的代碼中,就會讓你的代碼變得很大

注釋的清除
同樣的我們在源代碼中增加注釋,如下圖

我們會發現對應的注釋也沒了,所以預處理還有清除注釋的功能

#define定義的替換
同上,我們在再來做個小實驗,定義一個宏

我們會發現宏所在的地方被替換了

條件編譯
編譯
話不多說,我們依然是從實驗中得出我們的結論

我們會發現,編譯這個步驟的作用是將源代碼轉換成匯編代碼

匯編
匯編這個流程也是同樣的,我們先執行完匯編這個步驟,再打開,

當我們打開了test.o這個檔案的時候,我們發現里面是一堆我們看不懂的符號,但是呢這是機器看得懂的符號,也就是二進制代碼,
所以說,我們可以知道,匯編這個階段的作用就是將會匯編代碼轉換成我們的二進制指令,也就是計算機能識別的代碼

此外,我們也可以使用一個叫readelf這個指令來讀這個二進制代碼(因為在Linux環境下,.o檔案是以elf這個的形式來組織的)
-s的話是一個選項,表示顯示symple,即匯總的符號

到這,我們發現了一個叫做符號匯總的東西,那么具體有啥用呢這個符號匯總,我們接著往下看,
先注意一點:符號匯總 是編譯的時候產生的,但是在匯編的時候有一個形成符號表的功能,所以我們需要在.o檔案中查看形成的符號表
還有一點,上文我們說過,每個源檔案都會對應的生成自己的目標檔案,當然,每個目標檔案里面也有自己對應的符號表,至于這個有啥用呢,我們就要繼續談到鏈接了,
鏈接
至于鏈接的操作,就接著上面,如下圖,會之間生成可執行程式

在這里,鏈接主要有兩個作用
第一:合并段表
第二:符號表的合并和符號表的重定位
之前我們說過,原始碼檔案在編譯的時候進行符號合并,在匯編的時候生成符號表,以及每個原始碼檔案都會生成對應的目標檔案,
因此,如果我們有多個檔案,比如在一個.c檔案里面有函式的實作,在另一個.c檔案里面又有函式的呼叫,它們就會有符號的重合,因此,在最后一步鏈接的時候進行各個目標檔案里面的符號表的合并和重定位,最終讓我們的各個檔案里面的函式和符號相互連接起來,
預處理詳解
define
#define定義識別符號
說到define,我們最常見的就是用define定義一個識別符號,如下面代碼
#define MAX 999
#define MIN -999
#include<stdio.h>
int main()
{
printf("%d %d\n",MAX,MIN);
return 0;
}
只要我們定義了識別符號,在后面的代碼中我們就可以使用這些識別符號來代表我們的預定的值,
除此之外,使用deifne定義的識別符號的一個優點就是便于維護代碼,在我之前的關于一些小專案的博客中就穿插了使用define定義識別符號的方式來創建一些陣列之類,這樣當你后期想要修改或者維護代碼的時候你就只用修改定義的識別符號即可,
#define定義識別符號的注意點
如下圖代碼
#define MAX 999
#define MIN -999;
注意點就是在define定義識別符號的時候我們需不需要在后面加一個分號
答案是不需要加分號,這樣容易和你自己在陳述句中加入的分號造成語法錯誤
#define定義宏
除了定義識別符號之外,define還能定義宏,
宏
簡單來說,宏就是允許帶引數的識別符號,舉個栗子,看如下代碼
#define ADD(N1,N2) N1+N2
#include<stdio.h>
int main()
{
printf("%d\n",ADD(1,2));
//相當于代碼printf("%d\n",1+2);
return 0;
}
這里程式運行的結果是3,如上圖注釋,所謂宏,就是允許引數的替換
宏的易錯點
看下圖代碼
#define ADD(N1+N2) N1+N2
#include<stdio.h>
int main()
{
printf("%d\n",ADD(1,2)*5);
return 0
}
這段代碼輸出的答案是什么呢?是不是15呢?然而并不是,答案是11.這是為什么呢?
因為define替換的一個重要規則就是**“只替換,不計算”**
上圖中的printf代碼等價于下面代碼
printf("%d\n",ADD(1,2)*5);
//等價于printf("%d\n",1+2*5);
因此對于宏,首先的第一步就是替換,不要做計算!替換之后才按正常的做法去計算,
所以我們對于宏,我們可以在最外面加上括號,防止出現一些不必要的錯誤,如下圖代碼
#define ADD(N1+N2) (N1+N2)
#include<stdio.h>
int main()
{
printf("%d\n",ADD(1,2)*5);
return 0
}
這樣代碼就不容易出錯了
宏與函式對比
那么為什么要有宏呢?
同樣具有引數替換的東西我們自然而然會想到函式,那么它兩有啥區別呢?
1.宏比函式更快,因為宏是替換代碼,直接就可以運行,但函式是需要呼叫,同時開辟堆疊幀的,因此,宏的速度是快于函式的,
2.宏容易使代碼變得過長,因為宏的替換相當于是copy,對于一些較大的宏就容易讓代碼變得冗長,
3.宏是無法進行除錯的,因為它直接替換,
4.宏是無法進行遞回的,而函式是能進行遞回的
4.宏與型別無關,而函式是固定型別的,怎么理解呢?請看下圖代碼
#define MAX(a,b) a>b?a:b
這樣一個求兩個數之間最大值的代碼如果用宏來實作,無論a,b是何型別,都能求出二者的最大值
但是如果是函式呢?
我們就可能要寫很多主體代碼大致相同,但引數型別不相同的函式才能實作上面的功能了,
define替換規則
1.在呼叫宏時,首先對引數進行檢查,看看是否包含任何由#define定義的符號,如果有,那么進行替換
2.替換文本替換了之前宏之前原本的文本位置
3最后,在對結果進行掃描,看看它們是否有任何由#define定義的符號,如有,則進行上述步驟
4.注意,字串中并不會搜索#define定義的符號,
#和##的作用
#的作用
#的作用就是向字串中插入引數,也叫將引數字串化
因為之前我們說過,字串中的字符是不被檢查的,這樣的方式就讓我們能向一個字串中加入引數,如下圖代碼
#include<stdio.h>
#include<stdlib.h>
#define PRINT(FORMAT, VALUE) printf("the value of " #VALUE " is " FORMAT "\n", VALUE)
//向字串中插入引數
int main()
{
int i = 10;
PRINT("%d", i + 3);
//相當于PRINT("the value of i+3 is %d \n",i+3);
system("pause");
}
//輸出的結果是the value of i + 3 is 13
##的作用
##的作用就是將運算子的兩邊字符合成一個新的識別符號,如下圖代碼
#include<stdio.h>
#include<stdlib.h>
#define PRINT(FORMAT, VALUE) printf("the value of " #VALUE " is " FORMAT "\n", VALUE)
#define PRINTF(FORMAT,i) PRINT(FORMAT,NUM##i)
int main()
{
int i = 1;
int NUM1 = 10;
int NUM2 = 11;
int NUM3 = 13;
PRINTF("%d", 1);//NUM和1合成了新的識別符號NUM1
PRINTF("%d", 2);//NUM和2合成了新的識別符號NUM2
PRINTF("%d", 3);//NUM和3合成了新的識別符號NUM3
system("pause");
}
//輸出的結果是the value of NUM1 is 10
// the value of NUM2 is 11
// the value of NUM3 is 13
條件編譯
下面是常用條件編譯指令
1.
#if 常量運算式
//...
#endif
//常量運算式由前處理器求值,
如:
#define __DEBUG__ 1
#if __DEBUG__ //如果__DEBUG__為1,那么就執行之后的代碼,否則就不執行
//..
#endif
2.多個分支的條件編譯
#if 常量運算式
//...
#elif 常量運算式
//...
#else
//...
#endif //這是一組條件編譯的結束語
3.判斷是否被定義
#if defined(symbol)
#ifdef symbol //if defined(...)和ifdef ... 是等價的,都是如果定義了...就執行接下來的代碼
#if !defined(symbol)
#ifndef symbol
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
條件編譯的一個作用就是我們在測驗代碼的時候我們可以寫上一些測驗代碼,但是一加一刪又很麻煩,因此我們就可以采用條件編譯來選擇性的運行我們需要的代碼
頭檔案包含
兩中頭檔案包含的區別
在平常我們寫C語言的程序中,我們會使用以下兩種包含頭檔案的方式
#include<stdio.h>
#include"find.h"
那么<>和""這兩種方法有什么區別呢?
<>:此種包含方式,編譯器會直接從庫里面查找對應的頭檔案
“” :此種包含方式,編譯器會先在本地目錄下查找,即你自己定義的頭檔案,如果找不到,再從庫檔案中查找,
如何避免頭檔案被重復包含
為什么要避免頭檔案被重復包含?
因為對于在頭檔案中的全域變數,或者函式宣告,如果重復包含頭檔案,會造成重復定義或者重復宣告的錯誤,
方法一:使用條件編譯,在你的頭檔案中加入以下代碼
#ifdef __TEST__
#define __TEST__
//..頭檔案的內容
#endif
這段代碼的意思是,看是否定義了給定的識別符號,如果沒有定義,那么就是第一次參考頭檔案,就正常參考頭檔案,如果之前參考定義過,那么將跳過頭檔案內容,從而避免了頭檔案被重復包含的問題,
方法二:在頭檔案中加入以下代碼
#pragma once
//..頭檔案的內容
到這,學習了這些知識以后,我相信你已經對C語言如何變成一個程式,以及預處理的操作有一些了解了,我們從一個整體的角度來重新了認識了C語言,也為我們C語言的最侄訓畫個句點,
希望這篇文章對你有所幫助,有錯誤的地方歡迎指正,感謝觀看!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/275792.html
標籤:其他
上一篇:這可能最全的作業系統面試題
