前言:在VS2022中,使它在预处理完成后就停下来
点住解决方案资源管理器的项目名,右击鼠标,找到属性
但此时不能编译代码,可以用Ctrl+Fn+f7预处理到文件
接着Ctrl+o打开文件
.i文件就是预处理后的文件
一 #define
1 #define定义的标识符常量
标识符的内容是普通的常量
#define MAX 100
#define MIN 10.5
#define GG 'a'
#define STRING "str ing"
int main()
{
int a = MAX;
double b = MIN;
char c = GG;
char* str = STRING;
return 0;
}
左边是text.c, 右边是test.i(预处理后的文件)
标识符的内容有表达式
#define MAX1 100+200
#define ASS 1==1
#define MAX2 (100+200)
int main()
{
int a1 = MAX1 * 3;
int a2 = MAX2 * 3;
int b = ASS;
return 0;
}
左边是text.c, 右边是test.i(预处理后的文件)
所以#define定义的标识符仅仅是将它的内容替换进程序中,不会进行计算,
如果#define的标识符常量内容不加()可能会出现操作符优先级的问题
标识符的内容有分号
#define定义的标识符常量的内容仅仅是替换,不会考虑可能出现的后果,即使编译可能编不过去。
#define MAX 100;
#define MIN 10 50
int main()
{
int a = MAX;
int b = MIN;
return 0;
}
所以最好不要加分号,另外在选择语句中多一条空语句没加大括号容易导致编译错误。
标识符的内容可以是一段代码
#define PRINT printf("abcdefg")
int main()
{
PRINT;
return 0;
}
标识符的内容还可以是一个类型
#define us unsigned
int main()
{
us int a = 100;//就是unsigned int a = 100;
return 0;
}
2 #define定义宏
#define允许把参数替换到标识符的内容中,这种实现方式通常称为宏.
申明方式
#define name(parament-list) stuff
parament-list:参数列表,由逗号隔开的符号表
stuff:内容
例:
#define ADD(a, b) (a+b)
int main()
{
int a = ADD(5, 6);//相当于int a = (5+6);
return 0;
}
注意事项
1 左括号必须和宏名紧邻,否则()和()里的参数将都被视为标识符的内容.
例:
#define ADD (a, b) (a+b)
int main()
{
int a = ADD(3, 4);
return 0;
}
2 宏也不会对传过去的参数求值,仅仅是替换.
#define MUL1(a, b) a * b
#define MUL2(a, b) ( a * b )
#define MUL3(a, b) ( (a) * (b) )
int main()
{
int a =3 / MUL1(3, 4); //预处理结果希望是3/12
int b = 3 / MUL2(3+1, 4+1);//希望是3 / 20
int c = 3 / MUL3(3 + 1, 4 + 1);// 3/20
return 0;
}
预处理后的结果:
所以定义宏时,宏的内容尽可能多地加(),
避免使用宏时由于参数中的操作符和邻近操作符
出现运算符优先级的问题.
3 #define的替换规则
1 在调用宏时,首先对参数进行检查, 看是否有#define定义的标识符常量,它们首先被替换. 例:
#define MAX 100
#define ADD(a, b) ((a)+(b))
int main()
{
int c = ADD(MAX, MAX);//MAX首先被替换成100,即int c = ADD(100, 100);
return 0;
}
2 替换文本随后被插入到原来文本的位置。
int c = ((100)+(100));
//宏的参数名被值所替代,
//就是说宏的参数名是a和b
//传过去的100替代了它们
3 宏参数和#define定义的标识符可以出现其它#define定义的标识符
#define MAX 100
#define MIN (MAX-1)
int main()
{
int max = MAX;//相当于int max = 100;
int min = MIN;//相当于int min = 100-1;
return 0;
}
4 当预处理器搜索#define定义的标识符时,程序中双引号和单引号的内容不被搜索.
char a = 'MAX';
printf("MAX\n");
printf("%c", a);
4 #和##
它们出现在#define定义的标识符或宏的替换内容中
例如:
#define name stuff
#define name(a, b) stuff
(它们出现在stuff中)
预备知识
c语言中一个字符串可以拆分成多个段来写,例:
printf("abcefg\n");
printf("abc""efg\n");
printf("abc" "efg\n");//两个连续字符串之间有没有空格都一样
但两个连续的字符串之间不能有其它内容(空格除外)
#的使用
局限:仅在宏的替换内容中生效
功能:把传给宏的参数名变成一个字符串
例:
#define STRING(a) #a
int main()
{
int c = 0;
printf( STRING(c) );
return 0;
}
传过去的参数c代替了()里的a和内容里的a, 但内容的#c却直接转成字符串,可以被直接打印.
通过字符串可以分段的特性,可以写一个打印(变量名对应变量值)的宏。例:
#define PRINT(a) printf("the result of "#a" is %d\n", a)//#a相当于传过来的变量名的字符串
int main()
{
int c = 0;
int d = 1;
PRINT(c);
PRINT(d);
return 0;
}
##的使用
局限:可以出现在#define定义的宏的内容中,也能出现在#define定义的标识符常量中.
功能1:可以将两个符号合并成一个符号
例:
#define N(a, b) a##b
int main()
{
int class4 = 66;
printf("%d", N(class , 4));//N(class, 4)就是class4, 一个符号
//相当于 printf("%d", class4);
return 0;
}
功能2:甚至可以把分开的数字合并起来.
例:
#define N codf##a
#define K 100##55
#define M 100##.2
int main()
{
int codfa = 66;
int k = K;
double m = M;
printf("%d\n", N);
printf("%d\n", k);
printf("%.2lf\n", m);
return 0;
}
5 带副作用的参数
在达到某种目的的情况下,代码产生了额外的效果。
例:
int a = 2;
int b = a+1;//把3赋给b
b = ++a;//也把3赋给b,
//但是改变了a的值
当把这种带有副作用的参数传给宏时,结果可能都会难以控制,例:
#define MAX(a, b) ((a)>(b)?(a):(b))
int main()
{
int a = 5;
int b = 6;
int c = MAX(a++, b++);
//相当于 int c =( (a++)>(b++)?(a++):(b++) )
return 0;
}
6 宏与函数对比
接下来介绍:相对于函数而言,宏的优缺点。
宏的优点
一般来讲,宏被应用于简单的运算,例如求和,求两数最大值 (1) 使用函数解决问题的步骤有函数调用、计算、函数返回,
但宏仅仅是替换,它没有函数调用和函数返回的开销,它只有计算的过程,
所以宏的效率更高一些;
(2) 函数的参数必须声明为特定的类型,否则任务可能会出错,
而宏的参数与类型无关,只是简单地替换.
例如比较一个浮点数和一个整数.
#define MAX(a, b) ((a)>(b)?(a):(b))
int Max(int a, int b)
{
return a > b ? a : b;
}
int main()
{
int a = 3;
double b = 3.15;
double c = MAX(a, b);
double d = Max(a, b);
printf("%.2lf\n", c);
printf("%.2lf\n", d);
return 0;
}
(3) 宏甚至可以传格式或者数据类型,因为宏是完成替换的,不管传的是什么
例:
#define PRINT(a, format) printf("The result of "#a" is "format ,a )
#define MALLOC(elenum, type) (type*)malloc(elenum* sizeof(type))
int main()
{
double a = 3.15;
PRINT(a, "%.2lf");
int* pa = MALLOC(5, int);//向堆区申请开辟5个整型大小的空间
if (pa == NULL)
{
perror("MALLOC");
return 1;
}
free(pa);
pa = NULL;
return 0;
}
宏的缺点
(1)每次使用宏时,都会把替换内容的代码替换到程序中,如果替换内容较长,
可能会大幅增加程序的长度.
例:
#define PRINT printf("abcdefghijklnm----------")
int main()
{
PRINT;
PRINT;
PRINT;
return 0;
}
实际上编译时的代码:
但如果是函数,函数的代码只出现于一个地方.
每次使用函数都是调用同一份代码,不会增加程序的长度.
void Print(void)
{
printf("abcdefg---\n");
}
int main()
{
Print();
Print();
Print();
return 0;
}
(2)使用宏不便于调试代码,因为调试的代码和真正编译的代码是不一致的,
而函数调试代码更直观;
(3)宏与类型无关,不够严谨.
(4) 宏可能会带来运算符优先级的问题.
而函数会先计算出实参的值,然后把值拷贝给形参,
可以避免运算符优先级问题.
(5)宏体中可能出现多个参数,在传带有副作用的参数时,可能会产生不可预料的后果;
而函数会先计算出实参的值,然后把值拷贝给形参,结果更容易控制.
总结对比
| 属性 | 宏 | 函数 |
|---|---|---|
| 代码长度 | 每次使用宏时,宏体都会被替换到程序中,除非宏的内容比较短,否则程序长度会大幅度增长,加重编译器的负担 | 每次使用函数都是调用同一份代码,不会增加程序的长度 |
| 操作符优先级 | 宏不会对参数求值,只是替换,除非加上括号,否则邻近运算符、参数中运算符、宏体中运算符的优先级都可能使结果错误 | 会在函数调用时对实参求值,把结果拷贝给形参,有效避免运算符优先级问题 |
| 执行速度 | 更快 | 存在函数调用和返回的额外开销,相对慢一些 |
| 参数类型 | 宏与参数无关,传什么都可以(不保证编译不出现问题) | 函数的参数与类型有关,如果参数类型不同,就需要不同的函数,即使它们执行的任务是相同的 |
| 递归 | 不能递归 | 可以递归 |
| 带有副作用的参数 | 参数可能会被替换到宏体的多个位置,传这种参数可能导致计算结果难以预测 | 在传参时会计算一次实参,把计算结果拷贝给形参,参数值和结果都更容易控制 |
7 #undef
功能:用于移除一个宏定义或一个标识符常量.
移除一个标识符常量:
移除一个宏:
二 条件编译
可以使用【条件编译指令】来控制某段代码需不需要进行编译.【与if类似】
1 单分支
#if 常量表达式 //常量表达式由预处理器求值
//某一段代码
#endif //一定要加,与#if配对使用
常量表达式也可以包含由#define定义的标识符常量或宏
例:
#define CMP(a,b) a == b
#define NUM 1
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i < 10; i++)
{
arr[i] = i + 1;
#if NUM
printf("%d ", arr[i]);//会打印
#endif
}
#if CMP(1, 1)
printf("%d", 55);//会打印
#endif
return 0;
}
2 多分支
#if 常量表达式
#elif 常量表达式
#else
#endif
例:
#define NUM 2
int main()
{
#if NUM == 0
printf("%d", 1);
printf("a");
#elif NUM == 1
printf("%d", 2);
printf("a");
#else
printf("%d", 3);//打印出:3a
printf("a");
#endif
return 0;
}
3 判断符号是否被定义
只有符号已经被#define定义成宏名或标识符常量才会执行下面的代码
如果是局部变量名和全局变量名,都不行
形式1:
#ifdef symbol
//代码
#endif
形式2:
#if defined(symbol)
//代码
#endif
如果符号没有被#define定义,执行这段代码
形式1:
#ifndef symbol
//代码
#endif
形式2:
#if !defined(symbol)
//代码
#endif
三 头文件包含
包含头文件时<>和""的区别
#include "test.h" —— 先在源文件所在目录下查找,
找不到再去编译器提供的标准位置的目录下查找.
#include <stdio.h> —— 直接去编译器提供的标准位置的目录下查找.
避免头文件重复包含
形式1:
#ifndef __TEST_H__//如果没有定义该符号
#define __TEST_H__//首先定义这个符号
//头文件的内容
#endif
//下次如果再调用该头文件时,由于符号已经定义,不会把头文件的内容再进行拷贝
形式2:
#pragma once