C | 宏

1.C程序编译步骤

step1. 预处理
宏定义展开、头文件展开、条件编译等,同时将代码中的注释删除,这里并不会检查语法。

编译程序之前,先由预处理器检查程序(因此称为预处理器)。
根据程序中使用的预处理器指令,预处理器用符号缩略语所代表的内容替换程序中的缩略语(宏定义展开)。
预处理器可以根据你的请求包含其他文件(头文件展开),还可以选择让编译器处理哪些代码(条件编译)。预处理器不能理解C,它一般是接受一些文本并将其转换成其他文本。

step2. 编译
检查语法,将预处理后文件编译生成汇编文件。
step3. 汇编
将汇编文件生成目标文件(二进制文件)。
step4. 链接
C 语言写的程序是需要依赖各种库的,所以编译之后还需要把库链接到最终的可执行程序中去。

2.宏

在 C 语言中,可以采用命令 #define 来定义宏。 该命令允许把一个名称指定成任何所需的文本,例如一个常量值或者一条语句。在定义了宏之后,无论宏名称出现在源代码的何处,预处理器都会把它用定义时指定的文本替换掉。

2.1 没有参数的宏(宏常量)

1) 宏常量

image

#include<stdio.h>
#define TWO 2
#define OW "Consistency is the last refuge of the unimagina\
tive. -Oscar Wilde" /* 反斜线把这个定义延续到下一行 */
#define FOUR TWO*TWO
#define PX printf("X is %d.\n", x)
#define FMT "X is %d.\n"

int main(){
	int x = TWO;

	PX;
	x = FOUR;
	printf(FMT, x);
	printf("%s\n", OW);
	printf("TWO: OW\n");
	return 0;
}

image

注意:

  1. PX; 变成了 printf("X is %d.\n", x);

这句表明,不仅仅可以使用宏代替常量。宏可以表示任何字符串,甚至可以表示整个C表达式。
但要注意,PX是个常量字符串,它只打印名为x的变量,若程序中为定义变量x,编译时将报错。

  1. x = FOUR; 先变成了 x = TWO*TWO; 再变成 x = 2*2;

根据终端输出结果可能会误以为用4代替了FOUR,但实际过程如上。宏展开过程在x = 2*2;便结束,因为C编译器在编译时对所有常量表达式(只包含常量的表达式)求值,所以,实际相乘过程发生在编译阶段,而不是预处理阶段。预处理器不进行计算,它只按照指令进行文字替换操作。

  1. printf(FMT, x); 变成了 printf("X is %d.\n", x);

宏定义中可包含其他宏(有些编译器不支持这种嵌套功能)。如果需要多次使用某个冗长的控制字符串,这种方法就比较方便。

  1. printf("TWO: OW\n"); 将打印出 TWO: OW ,而不是打印出 2: Consistency is the last refuge of the unimaginative. -Oscar Wilde

一般情况下,预处理器发现程序中的宏后,会用它的等价替换文本代替宏,若替换文本中还包含宏,则继续进一步替换这些宏。例外情况就是双引号中的宏。
要想打印出2: Consistency is the last refuge of the unimaginative. -Oscar Wilde ,可用:printf("%d: %s\n", TWO, OW);

2) #define宏常量和const常量的区别

define宏定义和const常变量区别:

  1. const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
  2. 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。规则5-2-1:在C++ 程序中只使用const 常量而不使用宏常量,即const 常量完全取代宏常量。
  3. define是宏定义,程序在预处理阶段将用define定义的内容进行了替换。因此程序运行时,常量表中并没有用define定义的常量,系统不为它分配内存。const定义的常量,在程序运行时在常量表中,系统为它分配内存。
  4. define定义的常量,预处理时只是直接进行了替换。所以编译时不能进行数据类型检验。const定义的常量,在编译时进行严格的类型检验,可以避免出错。
  5. define定义表达式时要注意“边缘效应”,例如如下定义:

#define N 2+3 //我们预想的N值是5,我们这样使用N,int a = N/2; 我们预想的a的值是2,可实际上a的值是3。
原因在于在预处理阶段,编译器将 a = N/2处理成了 a = 2+3/2;这就是宏定义的字符串替换的“边缘效应”因此要如下定义:#define N (2+3)
const定义的表达式则没有上述问题。const定义的常量叫做常变量原因有二:const定义常量像变量一样检查类型;const可以在任何地方定义常量,编译器对它的处理过程与变量相似,只是分配内存的地方不同。

2.2 带有参数的宏(宏函数)

1) 宏函数

带参数的宏外形与函数非常相似,宏的参数也用圆括号括起来。宏函数定义中,用圆括号括起一个或多个参数,随后这些参数出现在替换部分。
image

#include <stdio.h>
#define SQUARE(X) X*X
#define PR(X) printf("The result is %d.\n", X)

int main(){
	int x = 4;
	int z;

	printf("x = %d\n", x);

	z = SQUARE(x); // 替换为x*x
	printf("Evaluating SQUARE(x): ");
	// 先替换为printf("The result is %d.\n", z),再替换为printf("The result is %d.\n", x*x)
	PR(z); 

	z = SQUARE(2); // 替换为2*2
	printf("Evaluating SQUARE(2): ");
	// 先替换为printf("The result is %d.\n", z),再替换为printf("The result is %d.\n", 2*2)
	PR(z); 

	printf("Evaluating SQUARE(x+2): ");
	// 先替换为printf("The result is %d.\n", SQUARE(x+2)),再替换为printf("The result is %d.\n", x+2*x+2)
	PR(SQUARE(x+2)); 

	printf("Evaluating 100/SQUARE(2): ");
	// 先替换为printf("The result is %d.\n", 100/SQUARE(2)),再替换为printf("The result is %d.\n", 100/2*2)
	PR(100/SQUARE(2)); 

	printf("x is %d.\n", x);
	printf("Evaluating SQUARE(++x): ");
	// 先替换为printf("The result is %d.\n", SQUARE(++x)),再替换为printf("The result is %d.\n", ++x*++x)
	PR(SQUARE(++x)); 

	printf("After incrementing, x is %x.\n", x);

	return 0;
}

image

注意:

  1. SQUARE(x+2) 的结果为什么是14而不是 6*6 (即36)?

预处理器不进行计算,只进行字符串替换。所以PR(z)先替换为printf("The result is %d.\n", SQUARE(x+2)),再替换为printf("The result is %d.\n", x+2*x+2)
4+2*4+2 = 4+8+2 = 14。
只需将 #define SQUARE(X) X*X 改为 #define SQUARE(X) (X)*(X) ,即可输出36。但是这还不能解决所有问题,见2:

  1. 100/SQUARE(2) 的结果为什么是100而不是 100/(2*2) 即25?

100/SQUARE(2) 将变成 100/2*2 ,根据优先级规则,从左到右对表达式求值:(100/2)*250*2 即100 。
#define SQUARE(X) X*X 定义为#define SQUARE(X) ((X)*(X)) 可以解决这种混乱。这样就会产生100/(2*2) ,最后求出值为 100/4 即25。
从中得到的经验是使用必需的足够多的圆括号来保证以正确的顺序进行运算和结合。但这些措施还是无法避免3中的问题:

  1. SQUARE(++x) 的结果为什么是36?

SQUARE(++x) 变成++x*++x ,x进行了两次增量运算,其中一次在乘法操作前,另一次在乘法操作后。
++x*++x=5*6=30 因为对这些运算的顺序没有做出规定,所以有些编译器产生5*6的结果,而有些编译器可能在乘法运算前同时对x进行自加操作,从而产生6*6的结果。但在两种情况下,x的开始值均为4,终止值均为6.然而,从代码来看,x只进行一次增量操作(即只在SQUARE(++x) 时进行一次自增)。
解决这个问题的最简单的方法是避免在宏的参数中使用++x。一般来说,在宏中不要使用增量或减量运算符。注意,++x可作为函数参数,因为会先对++x进行计算得到值5,再把5传递给函数。

2) #运算符

下面定义一个宏函数: #define PSQR(X) printf("The square of X is %d.\n", ((X)*(X))) ,使用该宏 PSQR(8) 的输出结果为 The square of X is 64.
注意,引号中的字符串中的X被看作普通文本,而不是被看作一个可被替换的语言符号。
假设你确实希望再字符串中包含宏参数,再宏函数的替换部分中,#符号用作一个预处理运算符,它可以把语言符号转为字符串。例如,若x是一个宏参量,那么 #x 可以把参数名转化为对应的字符串。

#include <stdio.h>
#define PSQR(X) printf("The square of "#X" is %d.\n", ((X)*(X)))

int main(){
	int y = 5;
	PSQR(y);
	PSQR(2+4);
	return 0;
}

image

3) ##运算符

在C语言的宏中,"##"被称为 连接符(concatenator),它是一种预处理运算符, 用来把两个语言符号(Token)组合成单个语言符号。 这里的语言符号不一定是宏的变量。并且双井号不能作为第一个或最后一个元素存在.
##运算符可以将两个记号(例如标识符)“粘”在一起,成为一个记号。
例如,定义宏 #define XNAME(n) x##n ,调用该宏 XNAME(4) 会展开成下列形式: x4

#include <stdio.h>
#define XNAME(n) x##n
#define PRINT_XN(n) printf("x"#n" = %d\n", x##n)

int main(){
	int XNAME(1) = 14; //变为int x1 =14;
	int XNAME(2) = 20; //变为int x2 =20;
	PRINT_XN(1); //变为printf("x1 = %d\n", x1);
	PRINT_XN(2); //变为printf("x2 = %d\n", x2);

	return 0;
}

image

4) 使用宏还是使用函数

  • 宏的优点:宏相比普通函数的优点为以空间换时间。

宏产生内联代码:也就是说,再程序中产生语句。如果使用宏20次,则会把20行代码插入程序中。如果使用函数20次,那么程序中只有一份函数语句的拷贝,因此节省了空间。
另一方面,函数的调用需要为函数代码块中的变量入栈出栈,且需要记录调用函数的地址方便函数调用结束返回调用程序处,这比内联代码花费了更多的时间。

  • 使用场景:将频繁被使用的短小函数。
  • 注意事项:运算保证完整性,即带上必需的足够多的圆括号,用圆括号括住每个参数,并括住宏的整体定义。
posted @ 2023-01-04 22:45  就良同学  阅读(144)  评论(0编辑  收藏  举报