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  323 );	// 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
      
posted @ 2021-06-16 12:49  skyuz  阅读(4610)  评论(1编辑  收藏  举报