1. 預定義符號
C語言設置了一些預定義符號,可以直接使用,預定義符號也是在預處理期間處理的
1 __FILE__ //進行編譯的源文件
2 __LINE__//文件當前的行號
3 __DATE__ //文件被編譯的日期
4 __TIME__//文件被編譯的時間
5 __STDC__//如果編譯器遵循ANSI C,其值為1,否則未定義
舉個例子:
printf("file:%s line:%d\n",__FILE__,__LINE__);
2. #define 定義常量
語法:
#define name stuff
示例如下:
#define MAX 1000
#define reg register
#define do_forever for(;;)
#define CASE break;case
//如果定義的stuff太長,可以分為幾行寫,除了最后一行外,每行的后面都要加一個續行符(反斜杠)
#define DEBUG_PRINT printf("file:%s\tline:%s\t \date:%s\ttime:%s\n",\__FILE__,__LINE__,\__DATE__,__TIME__);
還有個問題,我們使用define定義標識符的時候,要不要最后加;
?
我建議不要,因為可能會導致一些問題。
比如:
#define MAX 1000;if(condition)max=MAX;
elsemax=0;
我們都知道,沒有大括號的if
語句只能跟一條語句,而if
后面的語句經過替換后會有兩個 ;
,也就是兩個語句,所以會出現語法錯誤
3. #define 定義宏
本質:帶參數的文本替換。類似函數但無函數調用的開銷
這種實現被稱為宏(macro)或定義宏(define macro)
宏的申明方式如下:
#define name(parament-list) stuff
parament-list
是一個由逗號隔開的符號表,可能出現在stuff
注:
參數列表的左括號必須和name
緊鄰,如果兩者之間有空格存在,參數列表就會被解釋為stuff
的一部分
4. 帶有副作用的宏參數
當宏參數在宏的定義中出現的次數超過一次時,如果參數帶有副作用,那使用這個宏就可能出現問題
例如:
#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=6 y=10 z=9
5. 宏替換的規則
在程序中擴展#define定義符號和宏時,涉及如下幾步:
- 在調用宏時,首先對參數進行檢查,看看是否包含由
#define
定義的符號。如果是,則首先被替換 - 替換文本隨后被插入到程序中原來文本的位置
- 最后,再次對結果文件進行掃描,看它是否包含任何由
#define
定義的符號。如果包含,就重復上述處理過程
注:
- 宏參數和
#define
定義中可以出現其他#define`定義的符號。但是對于宏,不能出現遞歸 - 當預處理器搜索
#define
定義的符號的時候,字符串常量的內容并不被搜索
6. 宏和函數的對比
宏通常被應用于執行簡單的運算
如在兩個數中找出較大的一個時:
#define MAX(a, b) ((a)>(b)?(a):(b))
那么問題來了,為什么不用函數呢?
原因有兩個:
- 用于調用函數和從函數返回的代碼可能比執行這個小型計算工作所需要的時間更多。所以宏比函數在程序的規模和速度方面更勝一籌
- 函數的參數必須申明為特定的類型,所以函數只能在類型適合的表達式上使用。而宏的參數是跟類型無關的,任何類型都可以參與
和函數相比宏的劣勢:
- 每次使用宏時,一份宏定義的代碼將插入到程序中。若宏比較長,會大幅度增加程序的長度
- 宏是沒法調試的
- 宏由于與類型無關,也就不夠嚴謹
- 宏可能會帶來運算符優先級的問題,從而導致程序可能出錯
宏有時也能做到函數做不到的事情。比如:宏的參數可以出現類型,但是函數做不到
如:
#define MALLOC(num,type) \(type*)malloc(num *sizeof(type))MALLOC(10,int);//類型作為參數
//預處理器替換之后:
(int*)malloc(10*sizeof(int));
7. # 和##
7.1 #運算符
#運算符會將宏的一個參數轉換為字符串字面量。它僅被允許出現在帶參數的宏的替換列表中
示例如下:
當有一個變量int a=10;
的時候,想打印出:the value of a is 10
#define PRINT(n) printf("the value of "#n" is %d",n);int a=10;
PRINT(a);//會打印出the value of a is 10
PRINT(a)
會被預處理為:
printf("the value of ""a" " is %d", a);
7.2 ##運算符
##
運算符可以把位于它兩邊的符號合成一個符號,它允許宏定義從分離的文本片段創建標識符。##
被稱為記號粘合
這樣的連接必須產生一個合法的標識符。不然會導致結果未定義
此運算符的應用場景舉例如下:
寫一個函數求兩個數最大值,比較不同的數據類型就得寫不同的函數:
int int_max(int x,int y)
{return x>y?x:y;
}float float_max(float x,float y)
{return x>y?x:y;
}
但如果用宏和##運算符來寫,就會少很多事:
#define GENERIC_MAX(type) \
type type##_max(type x,type y)\
{ \return (x>y?x:y); \
}
使用宏,定義不同函數:
GENERIC_MAX(int) //替換到宏體內后int##_max 生成了新的符號 int_max做函數名
GENERIC_MAX(float) //替換到宏體內后float##_max 生成了新的符號 float_max做函數名int main()
{//調用函數int m = int_max(2, 3);printf("%d\n", m);//輸出 3float fm = float_max(3.5f, 4.5f);printf("%f\n", fm);//輸出 4.500000return 0;
}
在實際開發過程中##
的使用很少,很難有非常貼切的例子
8. 命名約定
函數與宏的使用語法很相似,所以語言無法幫我們區分二者。
所以我們要有的一個習慣是:
把宏名全部大寫;函數名不要全部大寫
9. #undef
如果現存的一個宏名字需要被重新定義,那么它需要先被移除。這時我們就要用到了#undef
如下:
#undef NAME
10. 命令行定義
許多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;
}
編譯指令:
//linux 環境演?
gcc -D ARRAY_SIZE=10 programe.c
11. 條件編譯
在編譯一個程序的時候我們要將一條語句(或一組語句)編譯或者編譯是很方便的。我們可以用條件編譯語句
比如:
調試性的代碼,刪除可惜,保留?礙事,所以我們可以選擇性的編譯
#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]);//為了觀察數組是否賦值成功。 #endif //__DEBUG__}return 0;
}
常見的條件編譯指令:
1.
#if 常量表達式//...
#endif//常量表達式由預處理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__//..
#endif2.多個分?的條件編譯
#if 常量表達式//...
#elif 常量表達式//...
#else//...
#endif3.判斷是否被定義
#if defined(symbol)
#ifdef symbol#if !defined(symbol)
#ifndef symbol4.嵌套指令
#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif
#endif
12. 頭文件的包含
12.1 本地文件包含
#include"filename"
查找策略:先在源文件所在目錄下查找,如果該頭文件未找到,編譯器就像查找庫函數頭文件一樣在標準位置查找頭文件
如果找不到就提示編譯錯誤
linux環境下標準頭文件的路徑:
/usr/include
VS環境下標準頭文件的路徑:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//這是VS2013的默認路徑
12.2 庫文件包含
#include<filename.h>
查找頭文件直接去標準路徑下去查找,如果找不到就提示編譯錯誤。
這樣是不是可以說,對于庫?件也可以使用 “”
的形式包含?
答案是肯定的,可以,但是這樣做查找的效率就低些,當然這樣也不容易區分是庫?件還是本地?件了。
12.3 重復包含問題
test.h
void test();
struct Stu
{int id;char name[20];
};
test.c
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"int main()
{return 0;
}
若test.c文件中將test.h包含5次,那么test.h文件的內容將會被拷貝5份在test.c中
如果test.h 文件比較大,這樣預處理后代碼量會劇增。如果?程比較大,有公共使用的頭文件,被大家都能使用,用不做任何的處理,那么后果真的不堪設想
如何解決頭文件被重復引入的問題?
答:條件編譯
每個頭?件的開頭寫:
#ifndef __TEST_H__
#define __TEST_H__
//頭?件的內容
#endif //__TEST_H__
或
#pragma once
這樣就避免了頭文件的重復引入