柚子快報(bào)激活碼778899分享:開發(fā)語言 C語言詳解(預(yù)編譯)
柚子快報(bào)激活碼778899分享:開發(fā)語言 C語言詳解(預(yù)編譯)
Hi~!這里是奮斗的小羊,很榮幸您能閱讀我的文章,誠請?jiān)u論指點(diǎn),歡迎歡迎 ~~ ??個(gè)人主頁:奮斗的小羊 ??所屬專欄:C語言
?本系列文章為個(gè)人學(xué)習(xí)筆記,在這里撰寫成文一為鞏固知識,二為展示我的學(xué)習(xí)過程及理解。文筆、排版拙劣,望見諒。
目錄
前言1、預(yù)定義符號2、#define定義常量和標(biāo)識符3、#define定義宏4、帶有副作用的宏參數(shù)5、宏替換的規(guī)則6、宏和函數(shù)的對比7、#和##7.1 #運(yùn)算符7.2 ##運(yùn)算符
8、命名的約定9、#undef10、命令行定義11、條件編譯12、頭文件的包含12.1 頭文件被包含的方式12.1.1 本地文件包含12.1.2 庫文件包含
12.2 嵌套文件的包含
總結(jié)
前言
本篇文章將詳細(xì)介紹編譯過程中預(yù)編譯的具體細(xì)節(jié) 在C語言的學(xué)習(xí)中部分人可能會忽視這一部分的學(xué)習(xí),因?yàn)橄馰S這樣相對強(qiáng)大的集成開發(fā)環(huán)境,我們在寫好代碼后只需要開始執(zhí)行即可,所以部分人認(rèn)為這一部分不值得我們花費(fèi)時(shí)間去學(xué)習(xí) 其實(shí)不然,學(xué)習(xí)C語言預(yù)編譯過程可以幫助我們更深入地了解C語言的編譯過程和語法特性,提高代碼編寫的效率和質(zhì)量,以及拓展編程技能
1、預(yù)定義符號
C語言設(shè)置了一些預(yù)定義符號,可以直接使用,預(yù)定義符號也是在預(yù)編譯階段處理的
__FILE__:正在編譯的源文件的文件名__LINE__:文件當(dāng)前的行號__DATE__:文件被編譯的日期__TIME__:文件被編譯的時(shí)間__STDC__:如果編譯器遵循 ANSI C,其值為1,否則未定義
例如:
2、#define定義常量和標(biāo)識符
#define定義的常量和標(biāo)識符在預(yù)編譯階段完成替換
基本語法:
#define name stuff
特別的,為了區(qū)分普通常量這個(gè)name我們一般用大寫形式 比如:
#define MAX 10000
#define REG register
#define后面的代碼理論上講只能寫一行,但是如果后面的代碼過長,我們可以使用'\'來實(shí)現(xiàn)換行,相當(dāng)于轉(zhuǎn)義轉(zhuǎn)義字符'\'轉(zhuǎn)義了轉(zhuǎn)義字符'\n'
#define DEBUG_PRINT printf("file:%s\tline:%d\t\
date:%s\ttime:%s\n,\
__FILE__,__LINE__,\
__DATE__,__TIME__)
值得注意的是,行末最好不要加;,在某些場景下是沒什么問題,但是在大多數(shù)情況下是有語法錯(cuò)誤的,所以我們要養(yǎng)成良好的編程習(xí)慣,行末不加;
3、#define定義宏
#define機(jī)制包括了一個(gè)規(guī)定,允許把參數(shù)替換到文本中,這種實(shí)現(xiàn)通常稱為宏(macro)或定義宏(define macro) 基本語法:
#define name(parament_list) stuff
其中parament-list(參數(shù)列表)是一個(gè)由逗號隔開的符號表,它們可能出現(xiàn)在stuff中
注意: 參數(shù)列表的左括號必須與name緊鄰,如果兩者之間有任何空白存在,參數(shù)列表就會被解釋為stuff的一部分
舉例:輸入一個(gè)數(shù),輸出它的平方數(shù)
#include
#define SQUARE(x) x*x
int main()
{
int n = 0;
scanf("%d", &n);
int ret = SQUARE(n);
printf("%d\n", ret);
return 0;
}
上面的代碼看似沒有什么問題,但當(dāng)我們想計(jì)算n+1的平方數(shù)時(shí),就會出現(xiàn)問題:
#include
#define SQUARE(x) x*x
int main()
{
int n = 0;
scanf("%d", &n);
int ret = SQUARE(n + 1);
printf("%d\n", ret);
return 0;
}
這是為什么呢?
原因就是帶參數(shù)的宏在替換的時(shí)候括號內(nèi)的表達(dá)式是不做任何計(jì)算的
也就是說,上面替換后的形式是:5 + 1 * 5 + 1,為了解決這個(gè)問題,我們可以在定義宏的時(shí)候給x加上括號:
#define SQUARE(x) (x)*(x)
這樣替換后的結(jié)果就變成了:(5 + 1)*(5 + 1),但是這樣給單獨(dú)的參數(shù)加括號的形式在某些場景下還是存在問題,比如:
#include
#define SQUARE(x) (x)+(x)
int main()
{
int n = 0;
scanf("%d", &n);
int ret = 5 * SQUARE(n + 1);
printf("%d\n", ret);
return 0;
}
那為了解決這個(gè)問題,我們可以(x)+(x)整體加上括號:((x) + (x))
#include
#define SQUARE(x) ((x)+(x))
int main()
{
int n = 0;
scanf("%d", &n);
int ret = 5 * SQUARE(n + 1);
printf("%d\n", ret);
return 0;
}
所以,在寫宏的時(shí)候一定不要吝嗇括號
4、帶有副作用的宏參數(shù)
當(dāng)宏參數(shù)在宏的定義中出現(xiàn)超過一次的時(shí)候,如果參數(shù)帶有副作用,那么你在使用這個(gè)宏的時(shí)候就可能出現(xiàn)危險(xiǎn),導(dǎo)致不可預(yù)測的后果,副作用就是表達(dá)式求值的時(shí)候出現(xiàn)的永久性效果 例如:
x + 1; //不帶副作用x++; //帶有副作用
上面兩個(gè)表達(dá)式的值是相同的,但是第一個(gè)表達(dá)式x的本身沒有發(fā)生改變,而第二個(gè)表達(dá)式x本身發(fā)現(xiàn)了改變,這就是副作用
例如:使用宏實(shí)現(xiàn)求兩個(gè)數(shù)的較大值
#include
#define MAX(x, y) ((x)>(y)?(x):(y))
int main()
{
int a = 10;
int b = 20;
int ret = MAX(a, b);
printf("%d\n", ret);
return 0;
}
上面代碼中宏參數(shù)在宏定義中出現(xiàn)了兩次,我們使用MAX(a, b);時(shí)沒什么問題,但當(dāng)我們使用MAX(a++, b++);時(shí)問題就會出現(xiàn):
#include
#define MAX(x, y) ((x)>(y)?(x):(y))
int main()
{
int a = 10;
int b = 20;
int ret = MAX(a++, b++);
printf("%d\n", ret);
printf("a = %d, b = %d\n", a, b);
return 0;
}
可以發(fā)現(xiàn)a和b的值會發(fā)生改變,就是表達(dá)式求值的時(shí)候出現(xiàn)了永久性效果。
與函數(shù)對比:
#include
int MAX(int x, int y)
{
printf("a = %d, b = %d\n", x, y);
return (x > y ? x : y);
}
int main()
{
int a = 10;
int b = 20;
int ret = MAX(a++, b++);
printf("%d\n", ret);
return 0;
}
從上面的代碼中可以看出來,帶參數(shù)的宏替換和函數(shù)傳參是非常相似的,但是它們的傳參是有本質(zhì)區(qū)別的。 帶參數(shù)的宏替換是直接將參數(shù)做整體替換,替換過后的表達(dá)式是:((a++)>(b++)?(a++):(b++));而函數(shù)參過后的表達(dá)式是:(a > b ? a : b)。
5、宏替換的規(guī)則
在程序中擴(kuò)展#define定義符號和宏時(shí),需要涉及幾個(gè)步驟。
在調(diào)用宏時(shí),首先對參數(shù)進(jìn)行檢查,看看是否包含任何由#define定義的符號,如果有,它們首先被替換替換文本隨后被插入到程序中原來文本的位置,對于宏,參數(shù)名被他們的值所替換最后,再次對結(jié)果文件進(jìn)行掃描,看看是否包含任何由#define定義的符號,如果有,重復(fù)上述步驟
例如:
#include
#define M 10
#define N M + 2
#define MAX(x, y) ((x)>(y)?(x):(y))
int main()
{
int ret = MAX(M, N);
return 0;
}
MAX(M, N)首先被替換成:((10)>(M + 2)?(10):(M + 2)) 然后((10)>(M + 2)?(10):(M + 2))再被替換成:((10)>(10 + 2)?(10):(10 + 2))
注意:
宏參數(shù)和#define定義中可以出現(xiàn)其他#define定義的符號,但宏不能實(shí)現(xiàn)遞歸
比如:#define N M + 2這個(gè)是可以的,但#define N N + 2是不行的。
當(dāng)預(yù)處理器搜索#define定義的符號時(shí),字符串常量的內(nèi)容并不被搜索
比如:
#include
#define M 10
#define N M + 2
#define MAX(x, y) ((x)>(y)?(x):(y))
int main()
{
printf("MAX(M, N)");
return 0;
}
可以看到宏MAX(M, N)并沒有展開。
6、宏和函數(shù)的對比
宏通常被應(yīng)用于執(zhí)行簡單的運(yùn)算。
比如在兩個(gè)數(shù)中找較大數(shù),用宏實(shí)現(xiàn)更有優(yōu)勢:
#define MAX(x, y) ((x)>(y)?(x):(y))
那為什么不用函數(shù)呢?原因有二:
用于調(diào)用函數(shù)和從函數(shù)返回的代碼可能比實(shí)際執(zhí)行這個(gè)小型計(jì)算工作需要的時(shí)間更多,函數(shù)調(diào)用還需要一些入棧出棧的過程,所以宏比函數(shù)在程序的規(guī)模和速度方面更勝一籌。更為重要的是函數(shù)的參數(shù)必須聲明為特定的類型,所以函數(shù)只能在類型合適的表達(dá)式上使用。但宏可以使用于整型、長整型、浮點(diǎn)型等可以用于>來比較的類型,宏參數(shù)是無關(guān)類型的。
但是和函數(shù)相比宏還是有劣勢的:
每次使用宏的時(shí)候,一份宏定義的代碼將插入到程序中,除非宏比較短,否則可能大幅度增加程序的長度宏是不能調(diào)試的宏由于無關(guān)類型,也就不夠嚴(yán)謹(jǐn),所以宏定義是不夠安全的宏可能會帶來運(yùn)算符優(yōu)先級的問題,導(dǎo)致程序容易出錯(cuò)
宏有時(shí)候能做到函數(shù)做不到的事,比如:宏的參數(shù)可以出現(xiàn)類型,但是函數(shù)不行
#include
#define MALLOC(n, type) (type*)malloc(n * sizeof(type))
int main()
{
//int* p = (int*)malloc(10 * sizeof(int));
int* p = MALLOC(10, int);
//int *p = (int*)malloc(10 * sizeof(int));
return 0;
}
宏和函數(shù)的對比:
屬性#define定義宏函數(shù)代碼長度每次使用時(shí),宏代碼都會被插入到程序中,除了非常小的宏之外,程序的長度會大幅度增長函數(shù)代碼只出現(xiàn)于一個(gè)地方,每次使用這個(gè)函數(shù)時(shí),都調(diào)用那個(gè)地方的同一份代碼執(zhí)行速度更快存在函數(shù)的調(diào)用和返回的額外開銷,所以相對慢一些操作符優(yōu)先級宏參數(shù)的求值是在所有周圍表達(dá)式的上下文環(huán)境里,除非加上括號,否則鄰近操作符的優(yōu)先級可能會產(chǎn)生不可預(yù)料的結(jié)果,所以建議宏在書寫的時(shí)候多寫括號函數(shù)參數(shù)只在函數(shù)調(diào)用的時(shí)候求值一次,它的結(jié)果值傳遞給函數(shù),表達(dá)式的求值結(jié)果更容易預(yù)測帶有副作用的參數(shù)參數(shù)可能被替換到宏體中的多個(gè)位置,如果宏的參數(shù)被多次計(jì)算,帶有副作用的參數(shù)求值可能會產(chǎn)生不可預(yù)測的結(jié)果函數(shù)參數(shù)只在傳參的時(shí)候求值一次,結(jié)果更容易控制參數(shù)類型宏的參數(shù)與類型無關(guān),只要對參數(shù)的操作是合法的,它就可以使用任何參數(shù)類型函數(shù)的參數(shù)是與類型有關(guān)的,如果參數(shù)的類型不同,就需要不同的函數(shù),即使他們執(zhí)行的任務(wù)是相同的調(diào)試宏是不方便調(diào)試的函數(shù)是可以逐語句調(diào)試的遞歸宏是不能遞歸的函數(shù)是可以遞歸的
7、#和##
7.1 #運(yùn)算符
#運(yùn)算符將宏的一個(gè)參數(shù)轉(zhuǎn)換為字符串字面量,它僅允許出現(xiàn)在帶參數(shù)的宏的替換列表中 #運(yùn)算符所執(zhí)行的操作可以理解為“字符串化” 比如:當(dāng)我們有一個(gè)變量int a = 10;的時(shí)候,我們想打印出:the value of a is 10. 下面是常規(guī)寫法:
#include
int main()
{
int a = 10;
printf("the value of a is %d\n", a);
return 0;
}
如果我們想把打印的這條代碼通過宏替換來實(shí)現(xiàn),該怎么做呢?
#include
#define PRINT(format, n) printf("the value of n is "format"\n", n)
int main()
{
int a = 10;
PRINT("%d", a);
//printf("the value of n is ""%d""\n", a);
return 0;
}
如果寫成上面這種代碼很明顯并沒有解決問題,因?yàn)槿绻覀儗寫成%d時(shí)并不能打印出a,而只能打印出a的值,那為了能打印出a本身的字面量,我們就可以使用#操作符 如下:
#include
#define PRINT(format, n) printf("the value of "#n" is "format"\n", n)
int main()
{
int a = 10;
PRINT("%d", a);
//printf("the value of "a" is ""%d""\n", a);
double b = 3.14;
PRINT("%lf", b);
//printf("the value of "b" is ""%lf""\n", b);
return 0;
}
所以我們說:#運(yùn)算符所執(zhí)行的操作可以理解為“字符串化”,上面的代碼中是將a和b字符串化了。
當(dāng)n = a的時(shí)候,#n 就相當(dāng)于“a”
7.2 ##運(yùn)算符
##可以把位于它兩邊的符號合成一個(gè)符號,它允許宏定義從分離的文本片段創(chuàng)建標(biāo)識符。##被稱為記號粘合 這樣的連接必須產(chǎn)生一個(gè)合法的標(biāo)識符,否則其結(jié)果就是未定義的。
比如現(xiàn)在有這么一個(gè)問題:當(dāng)我們寫一個(gè)函數(shù)來求兩個(gè)數(shù)的較大值的時(shí)候,不同的類型我們就需要寫不同的函數(shù),這樣寫太繁瑣了,我們可以使用宏來簡化這件事:
#include
#define GENERIC(type) \
type type##_max(type x, type y)\
{\
return ((x) > (y) ? (x) : (y));\
}
GENERIC(int)
//int int_max(int x, int y)
//{
// return ((x) > (y) ? (x) : (y));
//}
GENERIC(double)
//double double_max(double x, double y)
//{
// return ((x) > (y) ? (x) : (y));
//}
int main()
{
printf("%d\n", int_max(10, 20));
printf("%lf\n", double_max(3.14, 6.28));
return 0;
}
上面的代碼中我們利用宏替換來實(shí)現(xiàn)創(chuàng)建不同類型的函數(shù),type##_max中的##操作符將type和_max連接成了一個(gè)新的標(biāo)識符
8、命名的約定
一般來講函數(shù)和宏的使用語法很相似,所以語言本身沒法幫我們區(qū)分二者,我們平時(shí)的習(xí)慣是:
把宏名全部大寫函數(shù)名不要全部大寫或不大寫
9、#undef
#undef這條指令用于移除一個(gè)宏定義
#include
#define M 10
int main()
{
printf("%d\n", M);
#undef M
printf("%d\n", M);
return 0;
}
如果現(xiàn)存的一個(gè)宏名需要被重新定義,那么它的舊名字首先需要被移除
10、命令行定義
許多C編譯器提供了一種能力,允許在命令行中定義符號,用于啟動(dòng)編譯過程。 例如:當(dāng)我們根據(jù)同一個(gè)源文件想要編譯出一個(gè)程序的不同版本的時(shí)候,這個(gè)特性有點(diǎn)用處。(假定某個(gè)程序中聲明了一個(gè)一定長度的數(shù)組,如果機(jī)器內(nèi)存有限,我們需要一個(gè)很小的數(shù)組,但是另外一個(gè)機(jī)器內(nèi)存大些,我們需要一個(gè)較大的數(shù)組)
編譯指令:
//linux 環(huán)境演示
gcc -D ARRAY_SIZE=10 programe.c
11、條件編譯
滿足條件,就參與編譯;不滿足條件,就不參與編譯 在編譯一個(gè)程序的時(shí)候我們?nèi)绻獙⒁粭l語句(一組語句)編譯或者放棄是很方便的,因?yàn)槲覀冇袟l件編譯指令:
1.
#if 常量表達(dá)式 //常量表達(dá)式由預(yù)處理器求值
//...
#endif
如:
#define _DEBUG_ 1
int main()
{
#if _DEBUG_
printf("a");
#endif
return 0;
}
2.多個(gè)分支的條件編譯
#if 常量表達(dá)式
//...
#elif 常量表達(dá)式
//...
#else
//...
#endif
如:
#define M 1
int main()
{
#if M == 1
printf("a");
#elif M == 2
printf("b"):
#else
printf("C");
#endif
return 0;
}
3.判斷是否被定義
//如果定義了
#if defined(symbol)或
#ifdef symbol
如:
#define M 2
int main()
{
#ifdef M
printf("a");
#endif
return 0;
}
//如果沒定義
#if !defined(symbol)或
#ifndef symbol
4.嵌套指令
#ifdef 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
條件編譯通常用于跨平臺性代碼的編譯
12、頭文件的包含
12.1 頭文件被包含的方式
12.1.1 本地文件包含
一般指自己創(chuàng)建的頭文件
#include "filename.h"
查找策略: 先在源文件所在目錄下查找,如果該頭文件未找到,編譯器就像查找?guī)旌瘮?shù)頭文件一樣在標(biāo)準(zhǔn)位置查找頭文件,如果找不到就提示編譯錯(cuò)誤。
12.1.2 庫文件包含
一般指標(biāo)準(zhǔn)庫中頭文件的包含
#include
查找策略: 直接去標(biāo)準(zhǔn)路徑下去查找,如果找不到就提示編譯錯(cuò)誤。 那這樣是不是就說明,對庫文件也可以使用" "的形式包含呢? 答案是可以的。但是這樣查找的效率比較低,也不容易區(qū)分是庫文件還是本地文件
12.2 嵌套文件的包含
我們已經(jīng)知道,#include指令可以使另外一個(gè)文件被編譯,就像它實(shí)際出現(xiàn)于#include指令的地方一樣。 這種替換的方式很簡單:預(yù)編譯器先刪除這條指令,并用被包含文件的內(nèi)容替換 一個(gè)頭文件被包含幾次,就會被實(shí)際編譯幾次,如果重復(fù)包含,編譯的壓力就比較大
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
return 0;
}
如果像上面這樣寫,test.h文件的內(nèi)容就會被拷貝5份,如果test.h文件比較大,這樣預(yù)處理后代碼量會劇增。 如果工程比較大,有公共使用的文件,被大家都能用,又不做任何的處理,那么后果會不堪設(shè)想。 為了解決頭文件被重復(fù)引入的問題,就要用到條件編譯 我們在每個(gè)頭文件的開頭這樣寫:
#ifndef __FILENAME_H__
#define __FILENAME_H__
//...
#endif
或者
#pragma once
就可以避免頭文件的重復(fù)引入。
總結(jié)
預(yù)編譯是C語言編譯過程的第一階段,在預(yù)編譯階段可以對源代碼進(jìn)行預(yù)處理,如宏定義、頭文件包含等。通過學(xué)習(xí)預(yù)編譯過程,可以更全面地理解C語言代碼的編譯過程。預(yù)編譯指令能夠簡化代碼結(jié)構(gòu)、提高代碼的重用性和可維護(hù)性。學(xué)習(xí)預(yù)編譯過程可以幫助程序員更好地利用預(yù)編譯指令優(yōu)化代碼結(jié)構(gòu),提高代碼的質(zhì)量。條件編譯是預(yù)編譯指令中的重要功能,可以根據(jù)不同條件編譯不同的代碼。通過學(xué)習(xí)預(yù)編譯過程,可以了解如何使用條件編譯來實(shí)現(xiàn)跨平臺編譯,提高代碼的可移植性。
柚子快報(bào)激活碼778899分享:開發(fā)語言 C語言詳解(預(yù)編譯)
推薦鏈接
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。