●🧑個人主頁:你帥你先說.
●📃歡迎點贊👍關注💡收藏💖
●📖既選擇了遠方,便只顧風雨兼程,
●🤟歡迎大家有問題隨時私信我!
●🧐著作權:本文由[你帥你先說.]原創,CSDN首發,侵權必究,
目錄
- 1. 程式的翻譯環境和執行環境
- 2. 詳解編譯+鏈接
- 2.1 翻譯環境
- 2.2編譯階段
- 2.3運行環境
- 3. 預處理詳解
- 3.1 預定義符號
- 3.2 #define
- 3.2.1定義識別符號
- 3.2.2定義宏
- 3.2.3#define 替換規則
- 3.2.4#和##
- 3.2.5 帶副作用的宏引數
- 3.2.6宏和函式對比
- 3.3#undef
- 3.4 命令列定義
- 3.5條件編譯
- 3.6 檔案包含
1. 程式的翻譯環境和執行環境
在ANSIC的任何一種實作中,存在兩個不同的環境,
第1種是翻譯環境,在這個環境中源代碼被轉換為可執行的機器指令,
第2種是執行環境,它用于實際執行代碼,
這樣說太抽象了,我們用圖來解釋,

2. 詳解編譯+鏈接
2.1 翻譯環境

組成一個程式的每個源檔案通過編譯程序分別轉換成目標代碼(object code),
每個目標檔案由聯結器(linker)捆綁在一起,形成一個單一而完整的可執行程式,
聯結器同時也會引入標準C函式庫中任何被該程式所用到的函式,而且它可以搜索程式員個人的程式庫,將其需要的函式也鏈接到程式中,
2.2編譯階段
直接看代碼
test.c
#include <stdio.h>
extern int Add(int x, int y);
int main()
{
int a = 10;
int b = 20;
int ret = 0;
ret = Add(a, b);
printf("ret = %d\n", ret);
return 0;
}
add.c
int Add(int x, int y)
{
return x + y;
}
接下來我們圖解這段程式發生了什么




這邊只是對這些程序進行個粗略的講解,想了解詳細程序的可以看《程式員的自我修養》這本書,這里邊對這些程序講解的非常細致,
2.3運行環境
程式執行的程序:
- 程式必須載入記憶體中,在有作業系統的環境中:一般這個由作業系統完成,在獨立的環境中,程式的載入必須由手工安排,也可能是通過可執行代碼置入只讀記憶體來完成,
- 程式的執行便開始,接著便呼叫main函式,
- 開始執行程式代碼,這個時候程式將使用一個運行時堆疊(stack),存盤函式的區域變數和回傳地址,程式同時也可以使用靜態(static)記憶體,存盤于靜態記憶體中的變數在程式的整個執行程序一直保留他們的值,
- 終止程式,正常終止main函式;也有可能是意外終止,
3. 預處理詳解
3.1 預定義符號
FILE //進行編譯的源檔案
LINE //檔案當前的行號
DATE //檔案被編譯的日期
TIME //檔案被編譯的時間
STDC //如果編譯器遵循ANSI C,其值為1,否則未定義
舉個🌰:

3.2 #define
3.2.1定義識別符號
這個操作我們已經很熟悉了,前面的文章都有講到,這里不再次講解,
這里來說幾個比較特別的定義
#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 MAX 100; 此時在替換的程序中會把所有的MAX都替換成 100;,替換后的陳述句就會出現兩個分號的情況,
3.2.2定義宏
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一個由逗號隔開的符號表,它們可能出現在stuff中,
例如
#define SQUARE( x ) (x) * (x)
SQUARE( 5 );
這個時候編譯器里就會計算 5*5
最終就會得到結果25
我相信肯定有人會疑惑為什么兩個x要加(),舉個🌰,
#define SQUARE( x ) x*x
int main()
{
int a=5
printf("%d",SQUARE(a+1));
}
此時編譯器里的計算是這樣的
5+1*5+1
所以加上()保證傳進去的引數是一個整體,
3.2.3#define 替換規則
在程式中擴展#define定義符號和宏時,需要涉及幾個步驟,
- 在呼叫宏時,首先對引數進行檢查,看看是否包含任何由#define定義的符號,如果是,它們首先被替換,
- 替換文本隨后被插入到程式中原來文本的位置,對于宏,引數名被他們的值替換,
- 最后,再次對結果檔案進行掃描,看看它是否包含任何由#define定義的符號,如果是,就重復上述處理程序,
💡:
1. 宏引數和#define 定義中可以出現其他#define定義的變數,但是對于宏,不能出現遞回,
2. 當前處理器搜索#define定義的符號的時候,字串常量的內容并不被搜索,
3.2.4#和##
先說說#,#是把一個宏引數變成對應的字串,
#define PRINT(n) printf("the value of "#n" is %d\n", n)
int main()
{
int a = 10;
PRINT(a);
此時#n會被替換成字串"a",最終結果是the value of a is 10
int b = 20;
PRINT(b);
此時#n會被替換成字串"b",最終結果是the value of a is 20
return 0;
}
再來說##,##可以把位于它兩邊的符號合成一個符號,
舉個🌰:

3.2.5 帶副作用的宏引數
當宏引數在宏的定義中出現超過一次的時候,如果引數帶有副作用,那么你在使用這個宏的時候就可能出現危險,導致不可預測的后果,副作用就是運算式求值的時候出現的永久性效果,
例如:
x+1;//不帶副作用
x++;//帶有副作用
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
int a = 5;
int b = 8;
//宏的引數是不計算直接替換進去的
//替換進去進去后參與運算
int m = MAX(a++, b++);
//int m = MAX(a++, b++);
//int m = ((a++) > (b++) ? (a++) : (b++));
// 5 8 6 9
//所以最終b相當于++了兩次,最終b是10
return 0;
}
3.2.6宏和函式對比
#define MAX(a, b) ((a)>(b)?(a):(b))
以剛剛這個例子為例
雖然存在副作用,但這里用宏來實作比函式更好,
1. 用于呼叫函式和從函式回傳的代碼可能比實際執行這個小型計算作業所需要的時間更多,所以宏比 函式在程式的規模和速度方面更勝一籌,
2. 更為重要的是函式的引數必須宣告為特定的型別,所以函式只能在型別合適的運算式上使用,反之 這個宏怎可以適用于整形、長整型、浮點型等可以用于>來比較的型別,宏是型別無關的,
當然和宏相比函式也有劣勢的地方:
- 每次使用宏的時候,一份宏定義的代碼將插入到程式中,除非宏比較短,否則可能大幅度增加程式的長度,
- 宏是沒法除錯的,
- 宏由于型別無關,也就不夠嚴謹,
- 宏可能會帶來運算子優先級的問題,導致程容易出現錯,
但宏有自己可以做到函式做不到的事情,比如:宏的引數可以出現型別,但是函式做不到,
舉個🌰:
#define MALLOC(num, type) \
(type*)malloc(num*sizeof(type))
int main()
{
int* p = MALLOC(100, int);
//int* p = (int*)malloc(100 * sizeof(int));
return 0;
}
宏和函式的一個對比

命名約定
一般來講函式的宏的使用語法很相似,所以語言本身沒法幫我們區分二者,
那我們平時的一個習慣是:
把宏名全部大寫
函式名不要全部大寫
3.3#undef
這條指令用于移除一個宏定義,
#undef NAME
//如果現存的一個名字需要被重新定義,那么它的舊名字首先要被移除,
3.4 命令列定義
許多C 的編譯器提供了一種能力,允許在命令列中定義符號,用于啟動編譯程序,
例如:當我們根據同一個源檔案要編譯出不同的一個程式的不同版本的時候,這個特性有點用處,(假定某個程式中宣告了一個某個長度的陣列,如果機器記憶體有限,我們需要一個很小的陣列,但是另外一個機器記憶體大寫,我們需要一個陣列能夠大寫,)
#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; }
編譯指令
gcc -D SZ=10 programe.c
3.5條件編譯
滿足條件才進行編譯
int main()
{
#if 1 //條件為真才進行編譯
printf("hello world\n");
#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 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 "filename"
查找策略:先在源檔案所在目錄下查找,如果該頭檔案未找到,編譯器就像查找庫函式頭檔案一樣在標準位置查找頭檔案,
如果找不到就提示編譯錯誤,
庫檔案包含
#include <filename.h>
查找策略:查找頭檔案直接去標準路徑下去查找,如果找不到就提示編譯錯誤,
按照這個邏輯,那么庫檔案的包含是不是也可以用" ",答案是可以的,但不推薦,這樣做查找的效率就低些,當然這樣也不容易區分是庫檔案還是本地檔案了,
嵌套檔案包含

comm.h和comm.c是公共模塊,
test1.h和test1.c使用了公共模塊,
test2.h和test2.c使用了公共模塊,
test.h和test.c使用了test1模塊和test2模塊,
這樣最終程式中就會出現兩份comm.h的內容,這樣就造成了檔案內容的重復,
那要怎么解決呢?
第一種方法是條件編譯
#ifndef __TEST_H__
#define __TEST_H__
//頭檔案的內容
#endif //__TEST_H__
第二種方法
#pragma once
就可以避免頭檔案的重復引入,
看到這,全部內容就講完了,
離開前,別忘了點贊👍關注💡收藏💖(長按👍可一鍵三連,別白嫖了,球球了)

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/308899.html
標籤:其他
上一篇:【演算法入門09】矩形覆寫
