前言
本人的技术笔记会优先发布在个人站点longyxblg.cn
预定义符号
C语言设置了一些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的
c
__FILE__ //进行编译的源文件
__LINE__ //文件的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
C
printf("%d", __LINE__);

#define 定义常量
基本语法
c
#define name stuff
一般来说宏定义的名称统一为全大写
例如
c
// 宏定义是直接替换
printf("%d\n", NUM);
编译后实际执行的是
c
printf("%d\n", 10);

需要注意的是,用 #define 定义标识符的时候不要加 ; , 因为 #define 在编译时会直接整个替换,这个 ; 也会被替换进去,就会出现语法上的错误,例如
c
#define NUM_1 20;
printf("%d\n", NUM_1);

编译时实际执行的是
c
printf("%d\n", 20;);
如果定义的 stuff 过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠 ,这叫续行符
C
#define NUMS 10 + \
20 + \
30
printf("%d\n", NUMS);

#define 定义宏
#define 机制包含一个规定,允许把参数替换到文本中,这种实现通常称为宏
c
#define name( parament-list ) stuff
其中 parament-list 是一个由 , 隔开的符号表,可能出现在 stuff 中参数列表的左括号必须与 name 紧邻,如果两者之间有任何空白存在,参数列表就会被解释为 stuff 的一部分。
c
#define INT_MUL(x,y) x * y
int main()
{
int a = 10;
int b = 20;
printf("%d\n", INT_MUL(a, b));
return 0;
}
实际执行是
c
printf("%d\n", a * b);

但是这个宏有个问题
c
int a = 10;
int b = 20;
printf("%d\n", INT_MUL(a + 1, b));

实际执行的是
c
printf("%d\n", a + 1 * b);
* 优先级比 + 高,就变成了 a + b,得到了 30 ,解决方法就是在给参数加上 () 保证计算的顺序
c
int a = 10;
int b = 20;
printf("%d\n", INT_MUL(a + 1, b));

但是这样的话还是会有问题,这里我们重新定义一个宏
c
#define INT_ADD(x,y) (x) + (y)
int main()
{
int a = 10;
int b = 20;
printf("%d\n", 10 * INT_ADD(a, b));
return 0;
}

替换后执行的是
c
printf("%d\n", 10 * (10) + (20));
这里的优先级还是不对,解决方法是给整个替换后的表达式也加上括号
c
#define INT_ADD(x,y) ((x) + (y))
int main()
{
int a = 10;
int b = 20;
printf("%d\n", 10 * INT_ADD(a, b));
return 0;
}

总结:用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
带有副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果
例如
c
#define MAXNUM(x,y) ((x) > (y) ? (x) : (y))
int main()
{
int a = 10;
int b = 20;
// 带有副作用的宏参数
printf("%d\n", MAXNUM(a++, b++));
printf("a = %d\n", a);
printf("b = %d\n", b);
return 0;
}

替换为
c
printf("%d\n", ((a++) > (b++) ? (a++) : (b++)));
先比较 a++ 和 b++ , 比较后 a 的值自增为 11 , b 的值自增为 21 ,然后执行 b++ , 此时这个表达式的值为 21 ,执行后 b 的值自增为22
宏替换的规则
在程序中执行使用宏替换的时候要注意以下规则:
-
在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果是,它们首先被替换
-
替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
-
最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上述处理过程
注意:
宏参数和 #define 中可以出现其他 #define 定义的符号
c
#define NUM 10
#define NUM_1 20
#define MAX_NUM1(x) ((x) > (NUM_1) ? (x) : (NUM_1))
int main()
{
int a = 10;
printf("%d\n", INT_MUL(NUM, NUM_1));
printf("%d\n", MAX_NUM1(10));
return 0;
}

但是对于宏,不能出现递归
c
#define FACTORIAL(n) (n == 1 ? 1 : n * FACTORIAL(n - 1))
int main()
{
printf("%d\n", FACTORIAL(5));
return 0;
}

当预处理搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索
这个意思就是宏替换不会把字符串里的单词换掉
c
#define PI 3.14
int main()
{
printf("Π的值是 PI\n");
return 0;
}

宏和函数的对比
c
#define MAXNUM(x,y) ((x) > (y) ? (x) : (y))
int MaxNum(int x, int y)
{
return x > y ? x : y;
}
对比一下这个宏和函数
c
int a = 10;
int b = 20;
int num = 0;
num = MAXNUM(a, b);
num = MaxNum(a, b);
转到反汇编,这是宏定义的汇编

这是函数的汇编


可以看到函数的汇编要执行的步骤比宏定义多,并且,宏定义的调用并不强制限制类型
c
float a = 3.5f;
float b = 5.5f;
float c = MAXNUM(a, b);
printf("%f\n", c);

总结:
在进行小型工作时,用于调用函数和从函数返回的代码可能比实际执行小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹
函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。而宏的参数是类型无关的
宏的缺点:
使用宏时,一份宏定义的代码插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
c
#define PROCESS_SCORE(x, y, weight) \
do { \
float avg = (x) * (weight) + (y) * (1 - (weight)); \
printf("计算中...\n"); \
if (avg >= 60) { \
printf("及格,平均分: %.2f\n", avg); \
} else { \
printf("不及格,平均分: %.2f\n", avg); \
printf("建议补考!\n"); \
} \
} while(0)
int main()
{
int math = 80;
int English = 40;
int phys = 90;
int chem = 30;
int bio = 70;
int geo = 50;
PROCESS_SCORE(math, English, 0.6);
PROCESS_SCORE(phys, chem, 0.5);
PROCESS_SCORE(bio, geo, 0.7);
return 0;
}
展开后
c
do {
float avg = (math) * (0.6) + (english) * (1 - (0.6));
printf("计算中...\n");
if (avg >= 60) {
printf("及格,平均分: %.2f\n", avg);
}
else {
printf("不及格,平均分: %.2f\n", avg);
printf("建议补考!\n");
}
} while (0);
// ---------- 第二次调用展开 ----------
do {
float avg = (phys) * (0.5) + (chem) * (1 - (0.5));
printf("计算中...\n");
if (avg >= 60) {
printf("及格,平均分: %.2f\n", avg);
}
else {
printf("不及格,平均分: %.2f\n", avg);
printf("建议补考!\n");
}
} while (0);
// ---------- 第三次调用展开 ----------
do {
float avg = (bio) * (0.7) + (geo) * (1 - (0.7));
printf("计算中...\n");
if (avg >= 60) {
printf("及格,平均分: %.2f\n", avg);
}
else {
printf("不及格,平均分: %.2f\n", avg);
printf("建议补考!\n");
}
} while (0);
只写了 3 行调用,预处理后变成了将近 30 行代码
而如果采用函数
c
void process_score(int x, int y, float weight) {
float avg = x * weight + y * (1 - weight);
printf("计算中...\n");
if (avg >= 60) {
printf("及格,平均分: %.2f\n", avg);
}
else {
printf("不及格,平均分: %.2f\n", avg);
printf("建议补考!\n");
}
}
int main() {
// 无论调用多少次,函数体在内存中只有一份
process_score(80, 40, 0.6);
process_score(90, 30, 0.5);
process_score(70, 50, 0.7);
return 0;
}
无论调用多少次,函数体只存在于内存中的一个地方,程序长度几乎不会因为调用次数的增加而增加
宏是无法调试的
c
#define INT_ADD(x,y) ((x) + (y))
int int_add(int x, int y)
{
return x + y;
}
int main()
{
int c = 0;
c = INT_ADD(10, 20);
c = int_add(10, 20);
return 0;
}

宏由于类型无关,也就不够严谨
宏可能会带来运算符优先级的问题
宏有时候可以做函数做不到的事,比如宏的参数可以出现类型,函数就不行
c
#define MALLOC(num,type) \
(type*)malloc((num) * sizeof(type))
int main()
{
// 宏的参数可以出现类型
int* p = MALLOC(10,int);
free(p);
p = NULL;
return 0;
}

# 和
#运算符所执行的操作可以理解为字符串化
c
#define PRINT_1(n) printf(""#n"的值是%d\n",n);
#define PRINT_2(n) printf("n的值是%d\n",n);
int main()
{
int a = 10;
PRINT_1(a);
PRINT_2(a);
return 0;
}

可以把位于它两边的符号合成一个符号,允许宏定义从分离的文本片段创建标识符
c
#define GENERAL_MAX(type) \
type type##_max(type x , type y) \
{\
return (x > y ? x : y);\
}
GENERAL_MAX(int)
GENERAL_MAX(float)
int main()
{
printf("%d\n", int_max(1, 2));
printf("%f\n", float_max(1.2,2.2));
return 0;
}

#undef
这条指令用于移除一个宏定义

条件编译
单分支条件编译
c
#if 常量表达式
//...
#endif
c
#define NUM 0;
int main()
{
#if NUM
printf("666\n");
#endif
return 0;
}

多分支条件编译
c
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
c
#define NUM_1 0;
#define NUM_2 1;
int main()
{
// 多分支条件编译
#if NUM_1
printf("666\n");
#elif NUM_2
printf("哈哈哈!\n");
#else
printf("呵呵\n");
#endif
return 0;
}

判断是否被定义
c
#if defined
#ifdef
c
#define NUM_1 0;
#if defined(NUM_1)
printf("I LOVE C!\n");
#endif
#ifdef NUM_1
printf("C LOVE ME!\n");
#endif

判断是否不被定义
c
#if !defined
#ifndef
c
#define NUM_1 0;
#if !defined(NUM_1)
printf("I LOVE C!\n");
#endif
#ifndef NUM_1
printf("C LOVE ME!\n");
#endif

也可以像分支语句一样进行嵌套
c
#ifdef NUM_1
#if NUM_1
#elif NUM_2
printf("唱跳rap篮球!\n");
#endif
#endif

头文件包含
头文件包含有两种方式
c
#include <stdio.h>
#include "stdio.h"
"" 包含的头文件会优先在当前项目文件夹下寻找头文件,找不到编译器会去到标准路径下查找,如果再找不到提示编译错误
<> 包含的头文件会直接去标准路径下查找,找不到就提示编译错误
""也能查找库文件,但是效率不如<>高,所以包含库文件一般使用 <>
嵌套文件包含
#include 的指令可以使另外一个文件被编译,预处理器先删除这条指令,并用包含文件的内容替换。意味着一个头文件被包含几次就会被编译几次,如果出现重复包含,对编译压力较大。
解决方法如下:
在每个头文件的首行写
c
#ifndef __STDIO_H
#define __STDIO_H \
#include <stdio.h> // 写想要包含的头文件
#endif
或
c
#pragma once
这样就只会让同一个头文件被包含一次
其他预处理指令
只介绍了少数预处理指令,更多预处理指令可参考 《C语言深度解剖》