03.C語言進階——宏的使用


C語言之宏的使用技巧(宏嵌套/宏展開/可變參數宏)

1.前言

最近在看庫代碼及源代碼與開源項目的時候經常會遇到一些特殊的宏用法。預處理器在源代碼編譯之前對其進行一些文本性質的操作。它的主要任務包括刪除注釋、插入被#include指令包含的文件的內容、定義和替換由#define指令定義的符號以及確定代碼的部分內容是否根據一些條件編譯指令進行編譯。”文本性質”的操作,就是指只是簡單粗暴的由一段文本替換成另外一段文本,而不考慮其中任何的語義內容(僅僅就是文本一字不漏願意替換)。

注:為了偏於閱讀和視覺解放,此篇部分宏命名采用小寫范式,但工程項目開發強烈建議:大寫!😏

2. 宏定義

2.1 常見形式:

#define 宏名	替代文本 // 替代文本可以是列表,也可以為空(即什么都沒有,僅此聲明宏名而已)
#define GOOD 		 // 空宏

空宏為啥會存在?

  1. 空的宏的作用是預留下以后平台移植時的其它選項的定義,是為了移植的方便。
  2. 跟條件編譯一起用:#define GOOD ; #ifdef GOOD

eg1:當替代文本為空,常用於條件編譯:

#define DEBUG		// 替代文本:空
...
#ifdef DEBUG
...
#ifndef DEBUG
...

2.1.1 宏命名規則:

  1. 宏的名字中不允許有空格,必須遵循C變量的命名規則(只能使用字母、數字、下划線),一般習慣大寫;
  2. 空宏在預編譯時被替換成空字符串;
  3. 宏定義中可以包含其他宏,即嵌套宏;
  4. 預處理部分不是C語言的定義語句,所以宏定義完成之后是不帶分號(😉

2..1.2 宏的作用域:

#define的作用域從文件中的的定義點開始,直到用#undef指令取消宏為止或者直到文件尾為止(由二者中最先滿足的那個結束宏的作用域)。

2.1.3 宏的作用:

  1. 很大程度上是為了提高代碼的可移植性
  2. 增強代碼的可讀性,例如利用宏定義常量:#define PI 3.14159
  3. 做函數功能無法完成的功能(也稱之為母函數)

2.1.4 宏的幾點注意:

  1. 不要在宏中使用增量和減量運算符,容易產生副作用(后述案例分析);
  2. 為防止歧義,替代列表中的參數要用括號括起來;
  3. 替代列表最外層用括號括起來,整體使用,防止替代后出現歧義(出現因運算優先級和結合性等歧義問題);
  4. 帶參宏的標識符與左括號之間間不能有空格,否則會被認定為無參數宏;
  5. 宏定義中使用 =
  6. 宏定義中的末尾使用分號結尾
#define MAX(x,y) ((x) > (y) ? (x): (y)) 	     // 替換列表,參數用括號括起來
#define IS_EVEN(n) ((n) % 2 == 0)		     // 判斷偶數

2.2 宏的運算符

2.2.1 \

名稱:宏延續符:也稱之為宏的換行符;

作用:當定義的宏不能用一行表達完整時,可以用\表示下一行繼續此宏的定義。

注意:換行不能切斷單詞,只能在空格的地方進行。

范例分析:編譯器:gnu C++ DevC++5.1.5

// 為了偏於閱讀,采用小寫范式宏命名,工程項目開發強烈建議:大寫
#include <iostream>
#include <stdio.h>
#include <string.h>
// 換行\
#define NAME "Zhang"  \
			   "fei"  \
			  " 你好!"

int main(int argc, char **argv) {
     // 范例1
    std::cout << NAME << std::endl;
    
	return 0;
}

運行結果:

image-20210407132210456

2.2.2 #

名稱:字符串化運算符;

作用:將宏定義中的傳入參數名轉換成用一對雙引號括起來參數名字符串;

范圍:能用於有傳入參數的宏定義中,且必須置於宏定義體中的參數名前;

范例分析:編譯器:gnu C++ DevC++5.1.5

// 為了偏於閱讀,采用小寫范式宏命名
#include <iostream>
#include <stdio.h>
#include <string.h>

// 宏定義
#define example( instr ) #instr  		// instr 前后都有空格,最終都預處理時忽略掉

int main(int argc, char **argv) {
    // 范例1
    string str1 = example( abc );
    std::cout << str1 << std::endl;
    
    // 范例2
    string str2 = example( abc  123 );	// abc和123之間有2個空格
    std::cout << str2 << std::endl;
    
    return 0;
}

運行結果:

image-20210407120215899

運行結果分析:對空格的處理

  1. 忽略傳入參數名前面和后面的空格;
  2. 當傳入參數名間存在空格時,編譯器將會自動連接各個子字符串,用每個子字符串中只以一個空格連接,忽略其中多余一個的空格。

2.2.3 ##:

名稱:記號粘貼運算符,也稱之為連接符;

作用:將宏定義的多個形參名連接成一個實際參數名;

范圍:只能用於有傳入參數的宏定義中,且必須置於宏定義體中的參數名前;

范例分析:編譯器:gnu C++ DevC++5.1.5

#define fffA "OK"
#define f(a) fff ## a 		// ## 連接符
#define g(a) ggg ## a 		// ## 作為參數名

#include <stdio.h>
int main()
{
    // 范例1
    printf("%s\n", f(A))
    
    // 范例2 
    int g(a) = 12;		    // 定義int參數名
    printf("%d\n",g(a));
    return 0;
}

運行結果:

image-20210407123114610

運行結果分析:對空格的處理

  1. 當用##連接形參時,##前后的空格可有可無;
  2. 連接后的實際參數名,必須為實際存在的參數名或是編譯器已知的宏定義名

2.2.4 #@

名稱:字符化運算符

作用:將傳入的單字符參數名轉換成字符,以一對單引用括起來。

范圍:只能用於有傳入參數的宏定義中,且必須置於宏定義體中的參數名前。

注意:只能在Microsoft 的VC編譯專用,而gnu官方的g++編譯器並不認可;

范例分析:編譯器:VS2019

#include <iostream>
#include <stdio.h>

#define ToChar(x) #@ x

int main()
{
    // 范例1
    char foo = 'a';
    std::cout << foo << std::endl;
    
    // 范例2
    foo = ToChar(F);
    std::cout << foo << std::endl;
	return 0;
}

運行結果:

image-20210407124933571

運行結果分析:對空格的處理

  1. 當用#@連接形參時,#@之后的空格可有可無;
  2. 形參只能#@符的后面;

2.3 無參數的宏

仿對象宏(object-like)以”替代文本“替換每次出現的被定義的宏名,也稱之為無參數宏。代表值的宏稱之為:類對象宏;

無參數宏形如:

#define MACRO  1234  // 常見用法
#define LIMIT 1000   // 常見用法
#define GOOD         // 空宏

直接宏替換,將宏的簡單替換,展現淋漓盡致,也是宏的性質之一;

2.4 帶參數的宏

仿函數宏(function-like)以”替代文本“替換每次出現的被定義 標識符,可選地接受一定量的實參,它們隨即替換掉 替換列表 中出現的任何對應的形參,也稱之為類函數宏。

2.4.1 非變參宏

#define ADD(x,y) ((x) + (y)) 	// 加法運算
#define SQRT(x)  ((x) * (x)) 	// 平方運算

范例分析1:編譯器:VS2019

#include <iostream>
#include <stdio.h>

#define PSQR(x) printf("The square of " #x " is %d\n",(x) * (x))  // #x 被定義為const char*,3段字符串進行拼接,打印出來
#define PSQT(x) printf("The square of x is %d\n",(x) * (x))		  // x 不會被翻譯對應的字符,原樣打印出來

int main()
{
	// 范例1
	PSQR(100);
	
    // 范例2
    PSQT(100);
	return 0;
}

運行結果:

image-20210407145519455

范例分析2:編譯器:VS2019

#include <iostream>
#include <stdio.h>

#define WARN_IF(EXP) if(EXP) std::cerr << #EXP << std::endl 		// 末尾不加分號
#define warn_if(x) do{ if(x) printf("warn: " #x "\n");} while(0)	// 末尾不加分號

int main()
{
    // 范例1
	int div = 10;		// 調用仿宏函數時,聲明實參變量;
	WARN_IF(div);
	
    // 范例2
	int eric = 3;		// 調用仿宏函數時,聲明實參變量;
	warn_if(eric == 3);
	
	return 0;
}

運行結果:

image-20210407183738137

2.4.2 可變參宏

2.5 帶遞歸的宏

遞歸宏也稱之為宏自身迭代,這是個很頭疼的問題;

2.5.1 遞歸宏展開順序:

范例分析1:編譯器:VS2019

#define CAT(s1, s2)   	s1 ## s2
#define f2(a)       	fff a
#define ab          	AB
#define AB          	100

int main(int argc, char** argv){
    // 范例1
	std::cout << cat(cat(1, 2), 3) << std::endl; 		// 遞歸宏調用
	std::cout << cat(a,b) << std::endl;					// 直接宏展開
	std::cout << f(cat(cat(1, 2), 3)) << std::endl;		// 遞歸宏作為宏參數調用
    
    return 0;    
}

VS2019編譯器設置,查看宏展開代碼:(屬性->預處理器->預處理到文件,在DEBUG目錄下查看xxx.i文件,xxx:文件名)

image-20210407195531497

范例分析2:編譯器:gnu C++

#define CAT(s1, s2)   s1 ## s2
#define F2(a)       	fff a
#define ab          	AB
#define AB          	100

int main(int argc, char** argv){
    // 范例1
    std::cout << CAT(a, b) << std::endl;						// 直接宏展開				
    std::cout << CAT(CAT(1, 2), 3) << std::endl;				// 遞歸宏調用
    std::cout << F2(CAT(CAT(1, 2), 3)) << std::endl;			// 遞歸宏作為宏參數調用

    return 0;
}

g++編譯器設置,查看宏展開代碼:(g++ -E )

image-20210407194626527

遞歸宏分析:

  1. VS編譯器和g++編譯器:都是從最外層開始替換,遇到需要展開的宏則調用宏展開;
  2. VS編譯器中遞歸宏作為宏參數時,並不總是從最外層開始宏展開,而是從內開始,這和g++編譯器正好相反!

2.5.2 遞歸宏展開終止:

一般宏展開是一個替換的過程,考慮對宏的調用則是一個無限遞歸的展開過程:

#include <iostream>
#include <stdio.h>
#include <string.h>

#define x (4 + y)		// 宏x中包含y 
#define y (2 * x)		// 宏y中包含x

int main()
{
	// 范例1
    std::cout << x << std::endl;
	std::cout << y << std::endl;
	return 0;
}

x和y的展開過程如下:

x    → (4 + y)
     → (4 + (2 * x))	// 宏展開時會做一個標記,再次對於展開時就不再對其進行替換

y    → (2 * x)
     → (2 * (4 + y))	// 宏展開時會做一個標記,再次對於展開時就不再對其進行替換

VS2019編譯器設置,查看宏展開代碼:

image-20210407201154730

總結:為了防止無限遞歸這種情況的產生,宏展開時會做一個標記,再次對於展開時就不再對其進行替換,這種情況同樣適用於非直接遞歸調用的情況,

2.6 宏展開問題

2.6.1 宏展開過程

在宏預掃描(macro prescan)階段,宏參數首先會被替換,替換之后,再經過下次的掃描完成最后宏的展開(macro expand)。在宏預處理階段,還有一條排外規則,那就是若宏參數被用於字符串化(#)或者與粘貼標簽連接(##),則不會被替代!

g++編譯器官方GNU解釋:

    Macro arguments are completely macro-expanded before they are substituted into a macro body, unless they are stringified or pasted with other tokens. After substitution, the en-tire macrobody, including the substituted arguments, is scanned again for macros to be expanded.

那么,宏展開順序大致可以歸結為:

第1步:首先用實參代替形參,將實參代入宏文本中;

第2步:如果實參也是宏,則展開實參;

第3步:最后繼續處理宏替換后的宏文本,如果仍包含宏,則繼續展開;

注意:如果在第2步,實參代入宏文本后,實參之前或之后遇到###,實參不再展開,即宏只展開一次。

宏展開細節處理:

1)字符集轉換

2)換行連接\

3)注釋被替換成空格

4)執行預處理命令,如#inlcude、#define、#pragma、#error

5)轉義字符替換

6)相鄰字符串拼接

7)將預處理記號替換為詞法記號

第4)步即如何展開宏函數的規則:在展開當前宏函數時,如果形參有#或##則不進行宏參數的展開,否則先展開宏參數,再展開當前宏。

范例分析1:編譯器:VS2019(經典帶參宏展開)

#include<stdio.h>

#define f(a,b) a##b
#define g(a)   #a
#define h(a)   g(a)

int main()
{
    printf("%s\n", h(f(1,2)));	// 12
    printf("%s\n", g(f(1,2)));	// f(1,2)
    return 0;
}

2.6.2 宏展開的副作用

實現一個沒有BUG的MAX宏,要求:MAX(a++, 6) ,a的初值為7,函數返回值為7,a的值變為8;

范例分析:編譯器:gnu C++

#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))		// MAX宏求最大值
int main(int argc, char** argv) {
    int a = 7;									// a = 7
    printf("%d, %d\n", MAX(a++, 6), a);			// a++ 這種情形會會產生副作用
    return 0;
}

運行結果:

image-20210408182520402

g++編譯器設置,查看宏展開代碼:(g++ -E )

image-20210408185711665

分析:

  1. a++ // 連續展開兩次
  2. MAX(a++, 6) = 8 // a++ 被當作++a使用
  3. a = 9 // a的值就是連續調用a++的值

解決辦法:

#include <stdio.h>
#define MAX(a, b) ({ \
    __typeof(a) _a = (a); \
    __typeof(b) _b = (b); \
    _a > _b ? _a : _b; \
})

int main(int argc, char** argv) {
    int a = 7;
    printf("%d, %d\n", MAX(a++, 6), a);
    return 0;
}

運行結果:

image-20210408184429672

g++編譯器設置,查看宏展開代碼:(g++ -E )

image-20210408184719486

總結:定義臨時變量接一下整個a++只引用一次,完成取最大值功能;這也是宏本身一個缺陷,無法事先獲知變量類型,通過c語言擴展關鍵字__typeof()獲知變量類型,完美解決缺陷問題;

2.6.3 引用表達式類型typeof()

作用:可以取得變量的數據類型,或者表達式的數據類型或者用已知數據類型來定義變量,C/C++都是強類型語言,獲知數據類型至關重要!如果將typeof用於表達式,則該表達式並不會執行。只會得到該表達式的數據類型。

范圍:是GNU C提供的一種特性,可參考C-Extensions

局限:typeof構造中的已知類型名不能包含存儲類說明符,如 extern 或 static,但可以允許包含類型限定符,如 constvolatile

int a = 3;
__typeof__(volatile int) b1 = a; 	// 合法
__typeof__(const int) b2 = a; 		// 合法
__typeof__(static int) b3 = a;		// 非法
__typeof__(extern int) b4 = a;		// 非法

支持:GNU系列的g++編譯器都支持,目前MSVC並不支持這個功能,被識別”未被定義標識符“;

image-20210408192323993

關於 typeof()__typeof__()__typeof() ,Stack Overflow 有一段解釋,大概意思是:

__typeof()__typeof__() 是C語言的編譯器特定擴展,因為標准C語言是不含這樣的運算符的。在標准C要求編譯器用雙下划線__前綴擴展語言。(這也是為什么你不應該為自己的函數,變量加雙下划線的原因);

typeof() 運算符也是完全相同。這三個運算符都一樣,就是在編譯時檢查類型的。使用此關鍵字的語法看起來像sizeof,但是該構造在語義上類似於使用定義的類型名稱typedeftypeof()關鍵字常見用法:

范例分析:編譯器:gnu C++ DevC++5.1.5

#include <iostream>
#include <stdio.h>

int main(int argc, char **argv) {
	int i = 10;	
	// 1. __typeof__()
	__typeof__(i) tempa = i;
	printf("%d\n",tempa);

	// 2. __typeof()
	__typeof(i) tempb = i;
	printf("%d\n",tempb);
	
	// 3. typeof()
	typeof(i) tempc = i;
	printf("%d\n",tempc);
    
    return 0;
}

范例分析:編譯器:gnu C++ DevC++5.1.5

#include <iostream>
#include <stdio.h>

#define max(x, y) ({                	\
		typeof(x) _max1 = (x);          \
		typeof(y) _max2 = (y);          \
		(void) (&_max1 == &_max2);      \
		_max1 > _max2 ? _max1 : _max2; })

int main(int argc, char **argv) {

	int a = 3;
	float b = 4.0;
    // 范例1
	// 在宏中作類型檢查
	// 兩者類型不一致,編譯時通不過而報錯
	// a和b的類型不一致時,編譯直接報錯!
	int r = max(a, b);			
	printf("r:%d\n", r);
    
    // 范例2
	char *p1;
	typeof (*p1) ch = 'a';				// ch為char類型,不是char* 取的是*p1。
	printf("%d, %c\n", sizeof(ch), ch);	// 1, a

	// 范例3
	// 64位機上指針占8個字節
	// 32位機上指針占4個字節
	char *p2;
	typeof(p2) p3 = "hello world";		// 此時的p3才是char *類型,
	printf("%d, %s\n", sizeof(p3), p3);	// 4, hello world
    
    // 范例4
    int ans = 2;
    typeof(int*) sp;					// 用已知數據類型來定義變量,類似typedef
    sp = &ans;
    printf("%d\n", *sp);
    return 0;
}

附加點:

C語言規定:
1,以兩個下划線開頭的標識符被保留,程序員不應當使用
2,以一個下划線緊接一個大寫字母開頭的標識符被保留,程序員不應當使用
3,以一個下划線緊接一個小寫字母開頭的標識符被作為文件鏈接作用域保留,程序員不應當使用
4,還有已知庫函數類型保留

2.7 宏與函數比較

宏相比函數而言的優勢主要在於:

  1. 宏因為是文本替換,沒有函數棧的維護代價;
  2. 宏參數不帶類型,可以做函數不能做的工作。
  3. 摘自《C和指針》,詳細對比:

image-20210407132748695

2.8 預定義宏

2.8.1 ANSI C預定義宏

ANSI C定義了許多宏。在編程中您可以使用這些宏,但是不能直接修改這些預定義的宏。

描述
__DATE__ 當前日期,一個以 "MMM DD YYYY" 格式表示的字符串常量。
__TIME__ 當前時間,一個以 "HH:MM:SS" 格式表示的字符串常量。
__FILE__ 這會包含當前文件名,一個字符串常量。
__LINE__ 這會包含當前行號,一個十進制常量。
__STDC__ 當編譯器以 ANSI 標准編譯時,則定義為 1。
__func__ 函數名/非標准
__FUNC__ 函數名/非標准
__PRETTY_FUNCTION__ 更詳細的函數信息/非標准

范例分析:編譯器:gnu C++ DevC++5.1.5

#include <stdio.h>

int main()
{
   printf("File :%s\n", __FILE__ );			// 文件名 
   printf("Date :%s\n", __DATE__ );			// 當前日期
   printf("Time :%s\n", __TIME__ );			// 當前時間
   printf("Line :%d\n", __LINE__ );			// 當前行號
   printf("ANSI :%d\n", __STDC__ );			// 當以ANSI編譯時,定義為1
	return 0;
}

運行結果:

image-20210407133758650

2.8.2 宏用法案例展示

// 1,防止一個頭文件被重復包含
#ifndef COMDEF_H // 如果沒有定義COMDEF_H,則定義COMDEF_H
#define COMDEF_H 
//頭文件內容 ...
#endif 

// 2,重新定義一些類型,防止由於各種平台和編譯器的不同,而產生的類型字節數差異,方便移植。
typedef  unsigned long int  uint32;      /* Unsigned 32 bit value */ 

// 3,得到指定地址上的一個字節或字
#define  MEM_B( x )  ( *( (byte *) (x) ) )
#define  MEM_W( x )  ( *( (word *) (x) ) ) 

// 4,求最大值和最小值
#define  MAX( x, y )  ( ((x) > (y)) ? (x) : (y) ) // 求最大值
#define  MIN( x, y )  ( ((x) < (y)) ? (x) : (y) ) // 求最小值

// 5,得到一個field在結構體(struct)中的偏移量
#define FPOS( type, field )   ( (dword) &(( type *) 0)-> field )

// 6,得到一個結構體中field所占用的字節數
#define FSIZ( type, field ) sizeof( ((type *) 0)->field ) 

// 7,按照LSB格式把兩個字節轉化為一個word
#define  FLIPW( ray ) ( (((word) (ray)[0]) * 256) + (ray)[1] ) 

// 8,按照LSB格式把一個word轉化為兩個字節
#define  FLOPW( ray, val ) \
  (ray)[0] = ((val) / 256); \
  (ray)[1] = ((val) & 0xFF) 

// 9,得到一個變量的地址(word寬度)
#define  B_PTR( var )  ( (byte *) (void *) &(var) )
#define  W_PTR( var )  ( (word *) (void *) &(var) ) 

// 10,得到一個字的高位和低位字節
#define  WORD_LO(xxx)  ((byte) ((word)(var) & 255))
#define  WORD_HI(xxx)  ((byte) ((word)(var) >> 8)) 

// 11,返回一個比X大的最接近的8的倍數
#define RND8( x )       ((((x) + 7) / 8 ) * 8 ) 

// 12,將一個字母轉換為大寫
#define  UPCASE( c ) ( ((c) >= 'a' && (c) <= 'z') ? ((c) - 0x20) : (c) ) 

// 13,判斷字符是不是10進值的數字
#define  DECCHK( c ) ((c) >= '0' && (c) <= '9') 

// 14,判斷字符是不是16進值的數字
#define  HEXCHK( c ) ( ((c) >= '0' && (c) <= '9') || \
                       ((c) >= 'A' && (c) <= 'F') || \
                       ((c) >= 'a' && (c) <= 'f') ) 

// 15,防止溢出的一個方法
#define  INC_SAT( val )  (val = ((val)+1 > (val)) ? (val)+1 : (val)) 

// 16,返回數組元素的個數
#define  ARR_SIZE( a )  ( sizeof( (a) ) / sizeof( (a[0]) ) ) 

// 17,對於IO空間映射在存儲空間的結構,輸入輸出處理
#define inp(port)         (*((volatile byte *) (port)))
#define inpw(port)        (*((volatile word *) (port)))
#define inpdw(port)       (*((volatile dword *)(port))) 

#define outp(port, val)   (*((volatile byte *) (port)) = ((byte) (val)))
#define outpw(port, val)  (*((volatile word *) (port)) = ((word) (val)))
#define outpdw(port, val) (*((volatile dword *) (port)) = ((dword) (val))) 

// 18,使用一些宏跟蹤調試
// ANSI標准說明了五個預定義的宏名.打印調試日志信息。
// 當定義了_DEBUG,輸出數據信息和所在文件所在行 
#ifdef _DEBUG
#define DEBUGMSG(msg,date) printf(msg);printf(“%d%d%d”,date,_LINE_,_FILE_)
#else
#define DEBUGMSG(msg,date) 
#endif 

// 19,宏定義防止使用是錯誤 
// 用小括號包含。
// 例如:#define ADD(a,b) ((a) + (b))
// 用do{}while(0)語句包含多語句防止錯誤
// 例如:#difne DO(a,b) a+b;\ 
					   a++;
//應用時:
if(….)
	DO(a,b); //產生錯誤
else        
//解決方法: 
#difne DO(a,b) do{a+b;\
a++;}while(0) 



2.9 取消定義宏

#undef 宏名

3. 宏的感受

  1. 宏有很多奇技淫巧;😏
  2. 宏也有很多騷操作;😈
  3. 很多人抵觸的情緒;😢
  4. 宏參類型無法事先預知,導致編譯階段出現很多致命錯誤!
  5. 邁入標准C++的時代了,還是用constinline吧!
  6. 借助於泛型編程思維,減少宏調用;
  7. 借助於編譯器的宏展開功能,查看宏展開的代碼分析,關鍵詞”宏展開“;

經典帶參宏展開分析:

#include<stdio.h>

#define f(a,b) a##b
#define g(a)   #a
#define h(a)   g(a)

int main()
{
    printf("%s\n", h(f(1,2)));// 12
    printf("%s\n", g(f(1,2)));// f(1,2)
    return 0;
}

分析思維:

  1. 把握重點:傳入形參是#或者## ,否則宏只展開一次
  2. 對於h(a) 宏名中形參a,沒有並作要求,所以有:h( f(1, 2) ) ->h(1 ## 2) -> h(12) -> g(12) ->#12 -> "12"(字符串化)
  3. 對於f(a, b) 作為形參傳入方式,幾點思考:
  • h(a) g (a) 那么h(f(1, 2)),會不會直接替換成:g(f(1, 2)) ?
  • g(f(1, 2)) ,對於g(a) ,形參是有要求的:#a,那么宏參數不再繼續進行展開;
TEXT MacroSubstitute(TEXT Macro , TEXT macro[])
{
    掃描該Macro分離出該Macro的參數TEXT parameter[...](如果有的話);
    if(該Macro不被#和##修飾)
    	Macro=為其定義的宏;     //參數還沒有展開,只針對宏體
    else
        return Macro;		//如果被修飾則不對它展開直接返回
    for(對該Macro的參數進行遍歷 : i=0 -> N)
        if(parameter[i]存在於macro[]中)
				parameter[i]=MacroSubstitute(parameter[i],macro); //對參數進行展開,遞歸調用宏替換程序
         if(Macro在macro[]中)	//被展開的宏體仍然是宏
        Macro(...)=Macro(parameter[0],parameter[1]...); //用已經展開的參數替換原來的參數形成新的宏
     return MacroSubstitute(Macro,macro); //最后把這個新宏再按照宏替換的方式展開返回
}

4.文件包含

定義:#include指令告訴預處理器打開一個特定的文件,將它的內容作為正在編譯的文件的一部分包含進來。編譯器支持2種不同類型的#include文件包含:庫函數文件和本地文件。

作用:通常是提供編譯器用於產生可執行代碼的信息,例如:函數聲明,宏定義,類型定義,結構聲明、類聲明等等;

用法:

  1. 搜索系統目錄: #include <stdio.h>
  2. 搜索當前目錄: #include "stdafx.h"
  3. 搜索指定目錄: #include "GL/bin/Cust.h" // 指定多級目錄

5. 條件編譯

條件編譯的主要指令有:#if、#ifdef、#ifndef、#elif、#else、#endif。其中#ifdef等價於#if defined(標識符),#ifndef等價於#if !defined(標識符)

6. 其他指令

6.1 #error指令

功能:通常與條件編譯一起,用於檢測正常編譯過程中不應出現的情況。遇到#error指令預示程序中出現了嚴重錯誤,通常編譯器會立即停止編譯。

#if INT_MAX < 100000
#error int type is to small
#endif

6.2 #line指令

  • 用來改變程序行編號。
  • #line n這條指令導致后續的編號為n、n+1、n+2…
  • #line n “文件”,指令后面的行會被認為來自文件,行號由n開始。

6.3 #pragma指令

6.3.1 為啥用#pragma指令?

C和C++程序的每次執行都支持其所在的主機或操作系統所具有的一些獨特的特點,例如有些程序需要精確控制數據存放的內存區域或控制某個函數接收的參數。#pragma為編譯器提供了一種在不同機器和操作系統上編譯以保持C和C++完全兼容的方法。而#pragma指令是由機器和相關的操作系統定義的,通常來說每個編譯器是不同的。注意#pragma指令施加目標是編譯器,能讓編譯器接受特殊的指令。

6.3.2 #pragma指令常見語法:

  1. #pragma once 保證所在頭文件只會被包含一次,它是基於磁盤文件的,而#ifndef則是基於宏的。

  2. #pragma warning 允許有選擇性的修改編譯器的警告消息的行為。有如下用法:

    #pragma warning(disable:4507 34; once:4385; error:164) //等價於:   
    
    #pragma warning(disable:4507 34) 			// 不顯示4507和34號警告信息   
    #pragma warning(once:4385)       			// 4385號警告信息僅報告一次   
    #pragma warning(error:164)       			// 把164號警告信息作為一個錯誤
    #pragma warning(default:176)     			// 重置編譯器的176號警告行為到默認狀態 
    
    //同時這個pragma warning也支持如下格式,其中n代表一個警告等級(1---4):             
    #pragma warning(push)   					// 保存所有警告信息的現有的警告狀態  
    #pragma warning(push,n) 					// 保存所有警告信息的現有的警告狀態,並設置全局報警級別為n   
    #pragma warning(pop)    //
    #pragma warning(push)   
    #pragma warning(disable:4705)
    #pragma warning(disable:4706)
    #pragma warning(disable:4707)
    #pragma warning(pop)          				// 在這段代碼后,恢復所有的警告信息(包括4705,4706和4707)。
    
  3. #pragma hdrstop 表示預編譯頭文件到此為止,后面的頭文件不進行預編譯。BCB可以預編譯頭文件以 加快鏈接的速度,但如果所有頭文件都進行預編譯又可能占太多磁盤空間,所以使用這個選項排除一些頭文件.

  4. #pragma message在標准輸出設備中輸出指定文本信息而不結束程序運行。用法如下:

    #pragma message("消息文本")					// 當編譯器遇到這條指令時就在編譯輸出窗口中將“消息文本”打印出來。
    
  5. #pragma data_seg 一般用於DLL中,它能夠設置程序中的初始化變量在obj文件中所在的數據段。如果未指定參數,初始化變量將放置在默認數據段.data中,有如下用法:

    #pragma data_seg("Shared")   				// 定義了數據段"Shared",其中有兩個變量a和b
    int a = 0;                   				// 存儲在數據段"Shared"中
    int b;                       				// 存儲在數據段".bss"中,因為沒有初始化
    #pragma data_seg()           				// 表示數據段"Shared"結束,該行代碼為可選的
    

    對變量進行專門的初始化是很重要的,否則編譯器將把它們放在普通的未初始化數據段中而不是放在shared中。如上述的變量b其實是放在了未初始化數據段.bss中。

    #pragma data_seg("Shared")					// 定義了數據段"Shared",其中有個變量j
    int j = 0;                      			// 存儲在數據段"Shared"中
    #pragma data_seg(push, stack1, "Shared2")   // 定義數據段Shared2,並將該記錄賦予別名stack1,然后放入內部編譯器棧中
    int l = 0;                      			// 存儲在數據段"Shared2"中
    #pragma data_seg(pop, stack1)   			// 從內部編譯器棧中彈出記錄,直到彈出stack1,如果沒有stack1,則不做任何操作
    int m = 0;                      			// 存儲在數據段"Shared"中,如果沒有上述pop段,則該變量將儲在數據段"Shared2"中
    
  6. #pragma code_seg 它能夠設置程序中的函數在obj文件中所在的代碼段。如果未指定參數,函數將放置在默認代碼段.text中,有如下用法:

    void func1() {  }               			// 默認存儲在代碼段.text中
    #pragma code_seg(".my_data1")  				// 存儲在代碼段.my_data1中
    

void func2() { }
#pragma code_seg(push, r1, ".my_data2") // 存儲在代碼段.my_data2中
void func3() { }
#pragma code_seg(pop, r1) // 存儲在代碼段.my_data1中
void func4() { }
```

  1. #pragma pack 用來改變編譯器的字節對齊方式。常規用法為:

    #pragma pack(n)   			// 將編譯器的字節對齊方式設為n,n的取值一般為1、2、4、8、16,一般默認為8
    #pragma pack(show)		 	// 以警告信息的方式將當前的字節對齊方式輸出
    #pragma pack(push) 			// 將當前的字節對齊方式放入到內部編譯器棧中
    #pragma pack(push,4) 		// 將字節對齊方式4放入到內部編譯器棧中,並將當前的內存對齊方式設置為4
    #pragma pack(pop) 			// 將內部編譯器棧頂的記錄彈出,並將其作為當前的內存對齊方式
    #pragma pack(pop,4) 		// 將內部編譯器棧頂的記錄彈出,並將4作為當前的內存對齊方式
    
    // r1為自定義的標識符
    // 將內部編譯器中的記錄彈出,直到彈出r1,並將r1的值作為當前的內存對齊方式;
    // 當r1不存在,不做任何操作;
    #pragma pack(pop,r1) 
    
    // 以如下結構為例: 
    struct buffer {
        char a;
        WORD b;
        DWORD c;
        char d;
    };
    
    // 在Windows32位默認結構大小: sizeof(struct) = 4+4+4+4=16;
    // 與#pragma pack(4)一樣
    // 若設為 #pragma pack(1), 則結構大小: sizeof(struct) = 1+2+4+1=8;
    // 若設為 #pragma pack(2), 則結構大小: sizeof(struct) = 2+2+4+2=10;
    // 在#pragma pack(1)時:空間是節省了,但訪問速度降低了;
    // 有什么用處???
    // 在系統通訊中,如和硬件設備通信,和其他的操作系統進行通信時等,必須保證雙方的數據一致性。
    
  2. #pragma comment 將一個注釋記錄放置到對象文件或可執行文件中。其格式為:

    // comment-type是一個預定義的標識符,指定注釋的類型,如:compiler,exestr,lib,linker,user之一。
    #pragma comment( comment-type [,"commentstring"] )
    #pragma comment(lib,“ .../Debug/Test.lib ”)		// 表示鏈接Test.lib文件
    #pragma comment(linker,"/ENTRY:main_function")	 // 表示指定鏈接器選項/ENTRY:main_function
    
    // compiler:放置編譯器的版本或者名字到一個對象文件,該選項是被linker忽略的。
    
    // exestr:在以后的版本將被取消。
    
    /* lib:
    放置一個庫搜索記錄到對象文件中,這個類型應該與commentstring(指定Linker要搜索的lib的名稱和路徑)所指定的庫類型一致。在對象文件中,庫的名字跟在默認搜索記錄后面;linker搜索這個這個庫就像你在命令行輸入這個命令一樣。你可以在一個源文件中設置多個庫搜索記錄,它們在obj
    */
    
    // 文件中出現的順序與在源文件中出現的順序一樣。
    
    // 如果默認庫和附加庫的次序是需要區別的,使用/Zl編譯開關可防止默認庫放到object模塊中。
    
    // linker:指定一個連接選項,這樣就不用在命令行輸入或者在開發環境中設置了。只有下面的linker選項能被傳給Linker:
    
    // 1. /DEFAULTLIB
    // 2. /EXPORT
    // 3. /INCLUDE
    // 4. /MANIFESTDEPENDENCY
    // 5. /MERGE
    // 6. /SECTION
    

    Linker參數詳解介紹:

    (1)/DEFAULTLIB:library
    
    /DEFAULTLIB選項將一個library添加到LINK在解析引用時搜索的庫列表。用/DEFAULTLIB指定的庫在命令行上指定的庫之后和obj文件中指定的默認
    
    庫之前被搜索。
    
    忽略所有默認庫(/NODEFAULTLIB)選項重寫/DEFAULTLIB:library。如果在兩者中指定了相同的library名稱,忽略庫(/NODEFAULTLIB:library)選項
    
    將重寫/DEFAULTLIB:library。
    ​  
    (2)/EXPORT:entryname
    
    使用該選項,可以從程序導出函數以便其他程序可以調用該函數,也可以導出數據。通常在DLL中定義導出。
    
    entryname是調用程序要使用的函數或數據項的名稱。ordinal為導出表的索引,取值范圍在1至65535;如果沒有指定ordinal,則LINK將分配一個。
    
    NONAME關鍵字只將函數導出為序號,沒有entryname。DATA 關鍵字指定導出項為數據項。客戶程序中的數據項必須用extern __declspec
    
    (dllimport)來聲明。
    
    有三種導出定義的方法,按照建議的使用順序依次為:
    
    1. 源代碼中的__declspec(dllexport)
    2. .def文件中的EXPORTS語句
    3. LINK命令中的/EXPORT規范
    
    所有這三種方法可以用在同一個程序中。LINK在生成包含導出的程序時還要創建導入庫,除非在生成過程中使用了.exp 文件。
    
    LINK使用標識符的修飾形式。編譯器在創建obj文件時修飾標識符。如果entryname以其未修飾的形式指定給鏈接器(與其在源代碼中一樣),則LINK
    
    將試圖匹配該名稱。如果無法找到唯一的匹配名稱,則LINK發出錯誤信息。當需要將標識符指定給鏈接器時,請使用Dumpbin工具獲取該標識符的修飾
    
    名形式。
    ​  
    (3)/INCLUDE:symbol
    
    /INCLUDE選項通知鏈接器將指定的符號添加到符號表。若要指定多個符號,請在符號名稱之間鍵入逗號(,)、分號(;)或空格。在命令行上,對每個符號需指定一次/INCLUDE:symbol。
    
    鏈接器通過將包含符號定義的對象添加到程序來解析symbol。該功能對於添加不會鏈接到程序的庫對象非常有用。
    
    用該選項所指定的符號將覆蓋通過/OPT:REF對該符號進行的移除操作。
    ​    
    (4)/MANIFESTDEPENDENCY:manifest_dependency
    
    /MANIFESTDEPENDENCY允許你指定位於manifest文件的<dependency>段的屬性。/MANIFESTDEPENDENCY信息可以通過下面兩種方式傳遞給LINK:
    
    直接在命令行運行/MANIFESTDEPENDENCY
    
    通過#pragma comment
    ​    
    (5)/MERGE:from=to
    
    /MERGE選項將第一個段(from)與第二個段(to)進行聯合,並將聯合后的段命名為to的名稱。
    
    如果第二個段不存在,LINK將段(from)重命名為to的名稱。
    
    /MERGE選項對於創建VxDs和重寫編譯器生成的段名非常有用。
    ​    
    (6)/SECTION:name
    
    /SECTION選項用來改變段的屬性,當指定段所在的obj文件編譯的時候重寫段的屬性集。
    
    可移植的可執行文件(PE)中的段(section)與新可執行文件(NE)中的節區(segment)或資源大致相同。
    
    段(section)中包含代碼或數據。與節區(segment)不同的是,段(section)是沒有大小限制的連續內存塊。有些段中的代碼或數據是你的程序直接定義和
    
    使用的,而有些數據段是鏈接器和庫管理器(lib.exe)創建的,並且包含了對操作系統來說很重要的信息。
    
    /SECTION選項中的name是大小寫敏感的。
    
    不要使用以下名稱,因為它們與標准名稱會沖突,例如,.sdata是RISC平台使用的。
    .arch
    .bss
    .data
    .edata
    .idata
    .pdata
    .rdata
    .reloc
    .rsrc
    .sbss
    .sdata
    .srdata
    .text
    .xdata
    
    為段指定一個或多個屬性。屬性不是大小寫敏感的。對於一個段,你必須將希望它具有的屬性都進行指定;如果某個屬性未指定,則認為是不具備這個屬
    性。如果你未指定R,W或E,則已存在的讀,寫或可執行狀態將不發生改變。
    要對某個屬性取否定意義,只需要在屬性前加感嘆號(!)。
    E:可執行的
    R:可讀取的
    W:可寫的
    S:對於載入該段的鏡像的所有進程是共享的
    D:可廢棄的
    K:不可緩存的
    P:不可分頁的
    注意K和P是表示否定含義的。
    PE文件中的段如果沒有E,R或W屬性集,則該段是無效的。
    ALIGN=#選項讓你為一個具體的段指定對齊值。
    user:放置一個常規注釋到一個對象文件中,該選項是被linker忽略的。
    
    1. #pragma section 創建一個段,其格式為:

      #pragma section( "section-name" [, attributes] )
      

      section-name是必選項,用於指定段的名字。該名字不能與標准段的名字想沖突。可用/SECTION查看標准段的名稱列表。

      attributes是可選項,用於指定段的屬性。可用屬性如下,多個屬性間用逗號(,)隔開:

      read:可讀取的

      write:可寫的

      execute:可執行的

      shared:對於載入該段的鏡像的所有進程是共享的

      nopage:不可分頁的,主要用於win32的設備驅動程序中

      nocache:不可緩存的,主要用於win32的設備驅動程序中

      discard:可廢棄的,主要用於win32的設備驅動程序中

      remove:非內存常駐的,僅用於虛擬設備驅動(VxD)中

      如果未指定屬性,默認屬性為read和write。

      在創建了段之后,還要使用__declspec(allocate)將代碼或數據放入段中。

      例如:

      //pragma_section.cpp
      #pragma section("mysec",read,write) // 創建段mysec,並設置屬性:read,write
      int i = 0;							// i入了默認的數據段中
      __declspec(allocate("mysec"))		// 聲明數據段mysec打開
      int j = 0;							// j放入了mysec數據段中
      int main(){
      	return 0;
      }  
      
    2. #pragma push_macro與#pragma pop_macro 前者將指定的宏壓入棧中,相當於暫時存儲,以備以后使用;后者將棧頂的宏出棧,彈出的宏將覆蓋當前名稱相同的宏。例如:

      #include <stdio.h>
      #define X 1
      #define Y 2
      
      int main() {
          printf("%d",X);
          printf("\n%d",Y);
          #define Y 3   // C4005
          #pragma push_macro("Y")
          #pragma push_macro("X")
          printf("\n%d",X);
          #define X 2   // C4005
          printf("\n%d",X);
          #pragma pop_macro("X")
          printf("\n%d",X);
          #pragma pop_macro("Y")
          printf("\n%d",Y);
      }
      

      運行結果:

      1
      2
      1
      2
      1
      3
      


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM