文章目录
前言
define 定义标识符、#define定义宏、#和##、带副作用的宏参数、宏和函数对比、命名的约定、#undef、命令行定义、条件编译、文件包含、offsetof宏实现等的介绍
一、#define
1.、#define 定义标识符
#define name value
- name 是 标识符名字
- value 是 标识符的值
#include <stdio.h>
#define MAX 100
#define STR "hello csdn"
#define print printf("hehe\n")
int main()
{
printf("%d\n", MAX); // 100
printf("%s\n", STR); // hello csdn
print; // hehe
return 0;
}
- #define定义标识符,不加分号,否则在预编译阶段会将带分号的值替换为标识符
2.、#define定义宏
1. #define定义宏规则
- 宏的本质是在预处理阶段进行替换
#define 宏名(宏的参数) 宏的表达式/宏的值
- 宏名一般大写
- 宏名和宏参数必须紧挨,若有空格,则变成#define标识符(宏名)。
#include <stdio.h>
#define SQURE(X) X * X
int main()
{
int m = SQURE(3);
// 替换为
// int m = 3 * 3 // 9
printf("%d\n", m); // 9
return 0;
}
2. #define定义宏的表达式各项加括号
#define SQURE(X) (X) * (x)
例如(不加括号):
#include <stdio.h>
#define SQURE(X) X * X
int main()
{
int m = SQURE(3+1);
// 替换为
//int m = 3+1*3+1 // 7
printf("%d\n", m); // 7
return 0;
}
3. #define定义宏的表达式的值加括号
#define DOUBLE(X) ((X) + (x))
例如(不加括号):
#include <stdio.h>
#define DOUBLE(X) (X) + (X)
int main()
{
int m = 10 * DOUBLE(3 + 1);
// 替换为
//int m = 10 * (3+1)+ (3+1) // 44
printf("%d\n", m); // 44
return 0;
}
3. #dfine 替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,他们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置,对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意
- 宏参数和#define定义中可以出现其他#define定义的符号,但是对于宏,不能出现递归。
二、 #和##
首先要知道
printf("hello csdn");
printf("hello""csdn");
- 上述两个代码打印结果是一样的。
- 第二种情况,本质上会将两个字符串合并成第一种的一个字符串的形式进行打印。
1. # 可以传递变量名
例如:
#include <stdio.h>
int main()
{
int a = 10;
printf("the value of a is %d\n", a);// the value of a is 10
float b = 3.14f;
printf("the value of b is %f\n", b); // the value of b is 3.140000
return 0;
}
- 现在要实现随意替换 the value of a/b is %d中的变量名a / b ,函数是无法实现的。
- 但是宏的#可以实现。
#include <stdio.h>
#define PRINT(N) printf("the value of " #N " is %d\n", N)
int main()
{
int a = 10;
PRINT(a); // the value of a is 10
int b = 20;
PRINT(b); // the value of b is 20
return 0;
}
- 同时宏#可以实现打印两个不同类型的变量
#include <stdio.h>
#define PRINT(N, FORMATE) printf("the value of " #N " is "FORMATE"\n", N)
int main()
{
int a = 10;
PRINT(a, "%d"); // the value of a is 10
float b = 3.14f;
PRINT(b, "%f"); // the value of b is 3.140000
return 0;
}
2. ## 可以将两个字符合并
例如:
#include <stdio.h>
#define CST(a, b) a##b
int main()
{
int tele110 = 110;
printf("%d\n", CST(tele, 110)); // 110
return 0;
}
三、 带副作用的宏参数
当宏参数在宏定义中出现超过一次,如果参数带有副作用,则可能出现危险。
副作用就是表达式求值的时候出现的永久性效果。
例如:
#include <stdio.h>
#define MAX(a, b) (a) > (b) ? (a) : (b)
int main()
{
int a = 5;
int b = 4;
int m = MAX(a++, b++);
// 替换为
// int m = (a++) > (b++) ? (a++) : (b++)
// a++:5 a:6 b++:4 b:5 a++:6 a:7
printf("m=%d a=%d b=%d", m, a, b); // m=6 a=7 b=5
return 0;
}
四、 宏和函数的对比
求最大值也可使用函数实现
#include <stdio.h>
int Max(int x, int y)
{
return x > y ? x : y;
}
#define MAX(a, b) (a) > (b) ? (a) : (b)
int main()
{
int a = 5;
int b = 4;
int m = MAX(a, b);
int n = Max(a, b);
printf("%d\n", m); // 5
printf("%d\n", n); // 5
return 0;
}
但是这种简单的代码,我们一般选择宏
1. 宏的优点:
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏则可以适用于整型、长整型、浮点型等可以用 > 来比较的类型。宏是类型无关的。
2. 宏的缺点:
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
- 宏是没法调试的。宏是在预处理阶段替换的,但是调试是函数开始运行进行的。
- 宏由于类型无关,也就不够严谨。
- 宏可能会带来运算符优先级的问题,导致容易出现错误。
- 宏有时候可以做到函数做不到的事情,比如:宏的参数可以出现类型,但函数做不到。
#include <stdio.h>
#include <stdlib.h>
#define MALLOC(num, type) (type*)malloc(num * sizeof(type))
int main()
{
int* pc = MALLOC(10, int);
if (NULL == pc)
{
perror("MALLOC");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
pc[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", pc[i]);// 0 1 2 3 4 5 6 7 8 9
}
return 0;
}
3. 对比
五、命名约定
一般来讲函数和宏的使用语法很相似,所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:
- 宏名全部大写
- 函数名不要全部大写
六、#undef
这条指令用于移除一个宏定义
#include <stdio.h>
#define M 20
int main()
{
printf("%d\n", M);
#undef M
printf("%d\n", M);
return 0;
}
- 在打印第二个宏的时候会报错,因为#undef已经移除宏定义
七、命令行定义
一些C编译器可以在命令行中定义符号
八、条件编译
在编译一个程序的时候,我们如果要将一条语句编译或者放弃是很方便的,因为我们有条件编译指令
- 对于调和性的代码,可以选择性的编译
#include <stdio.h>
#define __DEBUG__
int main()
{
int arr[10] = { 0 };
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = i;
// 调试观察是否赋值成功
#ifdef __DEBUG__ // 如果定义了__DEBUG__就编译下面的语句
printf("%d ", arr[i]);// 0 1 2 3 4 5 6 7 8 9
#endif // __DEBUG__
}
return 0;
}
#include <stdio.h>
//#define __DEBUG__
int main()
{
int arr[10] = { 0 };
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = i;
// 调试观察是否赋值成功
#ifdef __DEBUG__ // 没有定义,下面的语句不参与编译
printf("%d ", arr[i]);
#endif // __DEBUG__
}
return 0;
}
常见的条件编译指令
1.
#if 常量表达式
//...
#endif
// 常量表达式由预处理器求值
2. 多个分支的条件编译
#if 常量表达式
// ...
#elif 常量表达式
// ...
#else
// ...
#endif
3.判断是否被定义
#if defined(symbol) // 如果定义了symbol
#ifdef symbol // // 如果定义了symbol
#if !defined(symbol) // 如果没定义 symbol
#ifndef symbod // // 如果没定义 symbol
4.嵌套指令
#if defined(OPTION)
#ifdef OPTION2
option2();
#endif
#ifdef OPTION3
option3();
#endif
#elif defined(OPTION1)
#ifdef OPTION4
option();
#endif
#endif
九、文件包含
1. 头文件包含
#include <stdio.h>
#include "test.h"
<> 和 ""的区别是查找的策略不同
#include <stdio.h>
- <>查找策略: 直接去库目录下查找
#include “test.h”
- “” 查找策略:
- 先去代码所在的路径下查找
- 如果上面找不到,再去库目录下查找
2. 嵌套文件包含
- 为了防止嵌套导致头文件被多次包含可以使用如下代码:
#ifndef __DEFAULT__ // 如果没有定义__DEFAULT__就编译下面的代码
#define __DEFAULT__ // 定义__DEFAULT__,如果头文件被重复包含,则只能包含一次
int Add(int x, int y)
{
return x + y;
}
#endif
#progma once //头文件只被包含一次
十、其他预处理指令
#error
#pragma
#line
...
#pragma pack()
offsetof宏的实现
- 自己写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
#include <stdio.h>
struct S
{
char c1;
int i;
char c2;
};
#define OFFSETOF(type, m_name) (int)&(((type*)0)->m_name)
int main()
{
struct S s = { 0 };
printf("%d\n", OFFSETOF(struct S, c1));// 0
printf("%d\n", OFFSETOF(struct S, i)); // 4
printf("%d\n", OFFSETOF(struct S, c2)); // 8
return 0;
}
总结
define 定义标识符、#define定义宏、#和##、带副作用的宏参数、宏和函数对比、命名的约定、#undef、命令行定义、条件编译、文件包含、offsetof宏实现等的介绍