目錄
前言
本章重點:
?1. 程序的翻譯環(huán)境和執(zhí)行環(huán)境
2. 詳解編譯+鏈接
2.1 翻譯環(huán)境?編輯
2.2 編譯本身也分為幾個階段
2.3 運行環(huán)境
3. 預處理詳解
3.1 預定義符號
3.2 #define
3.2.1 #define 定義標識符
3.2.2 #define 定義宏
2.2.3 #define 替換規(guī)則
3.2.4 #和##
?3.2.5 帶副作用的宏參數(shù)
3.2.6 宏和函數(shù)對比
3.2.7 命名約定
3.2.8簡化malloc
3.3 #undef
3.4 命令行定義
3.5 條件編譯
3.6 文件包含
3.6.1 頭文件被包含的方式
?3.6.2 嵌套文件包含
前言
????????本章就是c語言的最后一個板塊了,學完這章節(jié),我們將知道寫出的代碼如何變成可執(zhí)行程序的,這是非常重要的一個章節(jié),那讓我們一起進入本章的學習吧。
本章重點:
- 程序的翻譯環(huán)境
- 程序的執(zhí)行環(huán)境
- 詳解:C語言程序的編譯+鏈接
- 預定義符號介紹
- 預處理指令 #define
- 宏和函數(shù)的對比
- 預處理操作符#和##的介紹
- 命令定義
- 預處理指令 #include
- 預處理指令 #undef
- 條件編譯
?1. 程序的翻譯環(huán)境和執(zhí)行環(huán)境
在ANSI C的任何一種實現(xiàn)中,存在兩個不同的環(huán)境。
第1種是翻譯環(huán)境,在這個環(huán)境中源代碼被轉換為可執(zhí)行的機器指令。
第2種是執(zhí)行環(huán)境,它用于實際執(zhí)行代碼。
?
2. 詳解編譯+鏈接
2.1 翻譯環(huán)境
- 組成一個程序的每個源文件通過編譯過程分別轉換成目標代碼(object code)。
- 每個目標文件由鏈接器(linker)捆綁在一起,形成一個單一而完整的可執(zhí)行程序。
- 鏈接器同時也會引入標準C函數(shù)庫中任何被該程序所用到的函數(shù),而且它可以搜索程序員個人的程序庫,將其需要的函數(shù)也鏈接到程序中。
2.2 編譯本身也分為幾個階段
這些操作在Linux環(huán)境下進行,以輔助我們了解這些過程
1. 預處理 選項 gcc -E test.c -o test.i
預處理完成之后就停下來,預處理之后產生的結果都放在test.i文件中。
2. 編譯 選項 gcc -S test.c
編譯完成之后就停下來,結果保存在test.s中。
3. 匯編 gcc -c test.c
匯編完成之后就停下來,結果保存在test.o中。
4. 鏈接 gcc test.o -o test
鏈接目標文件和鏈接庫生成可執(zhí)行程序(二進制的程序)
5.? 總結
6.符號表
????????符號表的主要目的是為編譯器、解釋器或調試器提供有關程序中符號的信息,以便進行語義分析、類型檢查、內存分配和符號解析等操作。它還可以用于錯誤檢測、代碼優(yōu)化和代碼生成等編譯器相關任務。
????????例如:在連接階段會有符號表的合并,如果未找到該符號就會報鏈接錯誤。
2.3 運行環(huán)境
程序執(zhí)行的過程:
1. 程序必須載入內存中。在有操作系統(tǒng)的環(huán)境中:一般這個由操作系統(tǒng)完成。在獨立的環(huán)境中,程序的載入必須由手工安排,也可能是通過可執(zhí)行代碼置入只讀內存來完成。
2. 程序的執(zhí)行便開始。接著便調用main函數(shù)。
3. 開始執(zhí)行程序代碼。這個時候程序將使用一個運行時堆棧(stack),存儲函數(shù)的局部變量和返回地址。程序同時也可以使用靜態(tài)(static)內存,存儲于靜態(tài)內存中的變量在程序的整個執(zhí)行過程一直保留他們的值。
4. 終止程序。正常終止main函數(shù);也有可能是意外終止。
3. 預處理詳解
3.1 預定義符號
__FILE__ //進行編譯的源文件
__LINE__ //文件當前的行號
__DATE__ //文件被編譯的日期
__TIME__ //文件被編譯的時間
__STDC__ //如果編譯器遵循ANSI C,其值為1,否則未定義
例子:
#include<stdio.h>
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
return 0;
}
3.2 #define
3.2.1 #define 定義標識符
#define定義的變量,在預處理階段就已經完成了替換。
語法:
#define name stuff
舉個例子:
#define MAX 1000
#define reg register //為 register這個關鍵字,創(chuàng)建一個簡短的名字
#define do_forever for(;;) //用更形象的符號來替換一種實現(xiàn)
#define CASE break;case //在寫case語句的時候自動把 break寫上。
// 如果定義的 stuff過長,可以分成幾行寫,除了最后一行外,每行的后面都加一個反斜杠(續(xù)行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
提問:
在define定義標識符的時候,要不要在最后加上 ; ?
比如:#define MAX 1000; #define MAX 1000
建議不要加上 ; ,這樣容易導致問題。
比如下面的場景:? 像這樣就會造成語法錯誤了。
if(condition) max = MAX; else max = 0;
3.2.2 #define 定義宏
#define 機制包括了一個規(guī)定,允許把參數(shù)替換到文本中,這種實現(xiàn)通常稱為宏(macro)或
定義宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一個由逗號隔開的符號表,它們可能出現(xiàn)在stuff中。
注意:
參數(shù)列表的左括號必須與name緊鄰。
如果兩者之間有任何空白存在,參數(shù)列表就會被解釋為stuff的一部分。例如:
#define SQUARE( x ) x * x
這個宏接收一個參數(shù) x .如果在上述聲明之后,你把
SQUARE( 5 );
置于程序中,預處理器就會用下面這個表達式替換上面的表達式:
5*5
警告:這個宏存在一個問題。
觀察下面的代碼段:int a = 5; printf("%d\n" ,SQUARE( a + 1) );
乍一看,你可能覺得這段代碼將打印36這個值。
事實上,它將打印11.
為什么?
替換文本時,參數(shù)x被替換成a + 1,所以這條語句實際上變成了:printf ("%d\n",a + 1 * a + 1 );
這樣就比較清晰了,由替換產生的表達式并沒有按照預想的次序進行求值。
在宏定義上加上兩個括號,這個問題便輕松的解決了:#define SQUARE(x) (x) * (x)
這樣預處理之后就產生了預期的效果:
printf ("%d\n",(a + 1) * (a + 1) );
這里還有一個宏定義:
#define DOUBLE(x) (x) + (x)
定義中我們使用了括號,想避免之前的問題,但是這個宏可能會出現(xiàn)新的錯誤。
int a = 5; printf("%d\n" ,10 * DOUBLE(a));
這將打印什么值呢?
warning:
看上去,好像打印100,但事實上打印的是55.
我們發(fā)現(xiàn)替換之后:printf ("%d\n",10 * (5) + (5));
乘法運算先于宏定義的加法,所以出現(xiàn)了55 .
這個問題,的解決辦法是在宏定義表達式兩邊加上一對括號就可以了。#define DOUBLE( x) ( ( x ) + ( x ) )
提示:
????????所有用于對數(shù)值表達式進行求值的宏定義都應該用這種方式加上括號,避免在使用宏時由于參數(shù)中的操作符或鄰近操作符之間不可預料的相互作用。
2.2.3 #define 替換規(guī)則
在程序中擴展#define定義符號和宏時,需要涉及幾個步驟。
1. 在調用宏時,首先對參數(shù)進行檢查,看看是否包含任何由#define定義的符號。如果是,它們首先被替換。例子:
#define M 100 #define ADD(x,y) ((x)+(y)) int main() { int a = 10; int b = 20; int c = 4*ADD(M, b); //int c = 4 * a + b; printf("%d\n", c); printf("Master\n"); return 0; }
2. 替換文本隨后被插入到程序中原來文本的位置。對于宏,參數(shù)名被他們的值所替換。
3. 最后,再次對結果文件進行掃描,看看它是否包含任何由#define定義的符號。如果是,就重復上述處理過程。
注意:
1. 宏參數(shù)和#define 定義中可以出現(xiàn)其他#define定義的符號。但是對于宏,不能出現(xiàn)遞歸。
2. 當預處理器搜索#define定義的符號的時候,字符串常量的內容并不被搜索。
3.2.4 #和##
如何把參數(shù)插入到字符串中?
首先我們看看這樣的代碼:char* p = "hello ""world\n"; printf("hello"" world\n"); printf("%s", p);
這里輸出的是不是
hello world ?
答案是確定的:是。
我們發(fā)現(xiàn)字符串是有自動連接的特點的。
????????1. 那我們是不是可以寫這樣的代碼?:#define PRINT(FORMAT, VALUE) printf("the value is "FORMAT"\n", VALUE); PRINT("%d", 10);
這里只有當字符串作為宏參數(shù)的時候才可以把字符串放在字符串中。
1. 另外一個技巧是:
使用 # ,把一個宏參數(shù)變成對應的字符串。
比如:#define PRINT(FORMAT, VALUE)\ printf("the value of " #VALUE "is "FORMAT "\n", VALUE); PRINT("%d", i + 3);//產生了什么效果?
代碼中的 #VALUE 會預處理器處理為:
"VALUE" .
最終的輸出的結果應該是:the value of i+3 is 13
## 的作用
##可以把位于它兩邊的符號合成一個符號。
它允許宏定義從分離的文本片段創(chuàng)建標識符。PRINT("%d", i + 3);//產生了什么效果? #define ADD_TO_SUM(num, value) sum##num += value; ADD_TO_SUM(5, 10);//作用是:給sum5增加10.
注:
這樣的連接必須產生一個合法的標識符。否則其結果就是未定義的。
?3.2.5 帶副作用的宏參數(shù)
????????當宏參數(shù)在宏的定義中出現(xiàn)超過一次的時候,如果參數(shù)帶有副作用,那么你在使用這個宏的時候就可能出現(xiàn)危險,導致不可預測的后果。副作用就是表達式求值的時候出現(xiàn)的永久性效果。
例如:
x+1;//不帶副作用
x++;//帶有副作用
MAX宏可以證明具有副作用的參數(shù)所引起的問題。
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
輸出的結果是什么?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 這里我們得知道預處理器處理之后的結果是什么:
z = ( (x++) > (y++) ? (x++) : (y++));
????????此時,第一次x++,當前值是5,但++過后就變成了6;y++,當前值是8,但++過后就變成了9。表達式之后再次執(zhí)行y++,z得到了y的值9,但y還要++,值變成了10。
所以輸出的結果是:
x=6 y=10 z=9
3.2.6 宏和函數(shù)對比
宏通常被應用于執(zhí)行簡單的運算。
比如在兩個數(shù)中找出較大的一個。
#define MAX(a, b) ((a)>(b)?(a):(b))
那為什么不用函數(shù)來完成這個任務?
原因有二:
1. 用于調用函數(shù)和從函數(shù)返回的代碼可能比實際執(zhí)行這個小型計算工作所需要的時間更多。
所以宏比函數(shù)在程序的規(guī)模和速度方面更勝一籌。
2. 更為重要的是函數(shù)的參數(shù)必須聲明為特定的類型。
所以函數(shù)只能在類型合適的表達式上使用。反之這個宏怎可以適用于整形、長整型、浮點型等可以用于>來比較的類型。
宏是類型無關的。
宏的缺點:當然和函數(shù)相比宏也有劣勢的地方:
1. 每次使用宏的時候,一份宏定義的代碼將插入到程序中。除非宏比較短,否則可能大幅度增加程序的長度。
2. 宏是沒法調試的。
3. 宏由于類型無關,也就不夠嚴謹。
4. 宏可能會帶來運算符優(yōu)先級的問題,導致程容易出現(xiàn)錯。
宏有時候可以做函數(shù)做不到的事情。比如:宏的參數(shù)可以出現(xiàn)類型,但是函數(shù)做不到。
#define MALLOC(num, type)\
(type *)malloc(num * sizeof(type))
...
//使用
MALLOC(10, int);//類型作為參數(shù)
//預處理器替換之后:
(int *)malloc(10 * sizeof(int));
?宏和函數(shù)的一個對比
屬 性 | #define定義宏 | 函數(shù) |
代 碼 長 度 | 每次使用時,宏代碼都會被插入到程序中。除了非常 小的宏之外,程序的長度會大幅度增長 |
函數(shù)代碼只出現(xiàn)于一個地方;每 次使用這個函數(shù)時,都調用那個 地方的同一份代碼 |
執(zhí) 行 速 度 | 更快(只需計算即可) | 存在函數(shù)的調用和返回的額外開銷,所以相對慢一些 (1.函數(shù)調用,參數(shù)傳遞,棧幀創(chuàng)建。2.計算。3.函數(shù)返回) |
操 作 符 優(yōu) 先 級 | 宏參數(shù)的求值是在所有周圍表達式的上下文環(huán)境里, 除非加上括號,否則鄰近操作符的優(yōu)先級可能會產生 不可預料的后果,所以建議宏在書寫的時候多些括 號。 |
函數(shù)參數(shù)只在函數(shù)調用的時候求 值一次,它的結果值傳遞給函 數(shù)。表達式的求值結果更容易預 測。 |
帶 有 副 作 用 的 參 數(shù) | 參數(shù)可能被替換到宏體中的多個位置,所以帶有副作 用的參數(shù)求值可能會產生不可預料的結果。 |
函數(shù)參數(shù)只在傳參的時候求值一 次,結果更容易控制。 |
參 數(shù) 類 型 | 宏的參數(shù)與類型無關,只要對參數(shù)的操作是合法的, 它就可以使用于任何參數(shù)類型。 |
函數(shù)的參數(shù)是與類型有關的,如 果參數(shù)的類型不同,就需要不同 的函數(shù),即使他們執(zhí)行的任務是 不同的。 |
調 試 | 宏是不方便調試的 | 函數(shù)是可以逐語句調試的 |
遞 歸 | 宏是不能遞歸的 | 函數(shù)是可以遞歸的 |
3.2.7 命名約定
一般來講函數(shù)的宏的使用語法很相似。所以語言本身沒法幫我們區(qū)分二者。
那我們平時的一個習慣是:把宏名全部大寫,函數(shù)名不要全部大寫
3.2.8簡化malloc
像這樣,用宏定義malloc,就可以更簡便了。
#define MALLOC(num, type) (type*)malloc(num*sizeof(type))
int main()
{
//int*p = (int*)malloc(10 * sizeof(int));
//malloc(10, int);
//malloc(5, double);
int*p = MALLOC(10, int);
if (p == NULL)
{
//...
}
return 0;
}
3.3 #undef
這條指令用于移除一個宏定義。
#undef NAME
//如果現(xiàn)存的一個名字需要被重新定義,那么它的舊名字首先要被移除
3.4 命令行定義
許多C 的編譯器提供了一種能力,允許在命令行中定義符號。用于啟動編譯過程。
例如:當我們根據同一個源文件要編譯出不同的一個程序的不同版本的時候,這個特性有點用處。(假定某個程序中聲明了一個某個長度的數(shù)組,如果機器內存有限,我們需要一個很小的數(shù)組,但是另外一個機器內存大寫,我們需要一個數(shù)組能夠大寫。)
#include <stdio.h>
int main()
{
int array[SZ];
int i = 0;
for (i = 0; i < SZ; i++)
{
array[i] = i;
}
for (i = 0; i < SZ; i++)
{
printf("%d ", array[i]);
}
printf("\n");
return 0;
}
?編譯指令:
//linux 環(huán)境演示
gcc -D SZ=10 programe.c
3.5 條件編譯
????????在編譯一個程序的時候我們如果要將一條語句(一組語句)編譯或者放棄是很方便的。因為我們有條件編譯指令。
?比如說:調試性的代碼,刪除可惜,保留又礙事,所以我們可以選擇性的編譯。
#include <stdio.h>
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i < 10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]);//為了觀察數(shù)組是否賦值成功。
#endif //__DEBUG__
}
return 0;
}
??這段代碼只在DEBUG宏已定義時被編譯。
常見的條件編譯指令:
1.
#if 常量表達式
//...
#endif
//常量表達式由預處理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
2.多個分支的條件編譯
#if 常量表達式
//...
#elif 常量表達式
//...
#else
//...
#endif
3.判斷是否被定義
#if defined(symbol)
#ifdef symbol
#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
3.6 文件包含
????????我們已經知道, #include 指令可以使另外一個文件被編譯。就像它實際出現(xiàn)于 #include 指令的地方一樣。
這種替換的方式很簡單:
????????預處理器先刪除這條指令,并用包含文件的內容替換。
????????這樣一個源文件被包含10次,那就實際被編譯10次。
3.6.1 頭文件被包含的方式
-
本地文件包含
#include "filename
?????????查找策略:先在源文件所在目錄下查找,如果該頭文件未找到,編譯器就像查找?guī)旌瘮?shù)頭文件一樣在標準位置查找頭文件。
如果找不到就提示編譯錯誤。
Linux環(huán)境的標準頭文件的路徑:
/usr/include
VS環(huán)境的標準頭文件的路徑:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//這是VS2013的默認路徑
注意按照自己的安裝路徑去找。
- 庫文件包含
#include <filename.h>
查找頭文件直接去標準路徑下去查找,如果找不到就提示編譯錯誤。
這樣是不是可以說,對于庫文件也可以使用 “” 的形式包含?
答案是肯定的,可以。
但是這樣做查找的效率就低些,當然這樣也不容易區(qū)分是庫文件還是本地文件了。
?3.6.2 嵌套文件包含
如果出現(xiàn)這樣的場景:
comm.h和comm.c是公共模塊。
test1.h和test1.c使用了公共模塊。
test2.h和test2.c使用了公共模塊。
test.h和test.c使用了test1模塊和test2模塊。
這樣最終程序中就會出現(xiàn)兩份comm.h的內容。這樣就造成了文件內容的重復。
?如何解決這個問題?
答案:條件編譯。
每個頭文件的開頭寫:
ifndef __TEST_H__
#define __TEST_H__
//頭文件的內容
#endif //__TEST_H__
或者:
#pragma once
這樣就可以避免頭文件重復引用。
?文章來源:http://www.zghlxwxcb.cn/news/detail-722210.html
本章完!C語言知識講解完!文章來源地址http://www.zghlxwxcb.cn/news/detail-722210.html
到了這里,關于C語言--程序環(huán)境和預處理(宏)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!