一.预处理
1.预定义符号
预定义符号由编译器提供,在预处理阶段这些符号会被替换
1.__LINE__: 表示当前代码所在的行号。
__FILE__: 表示当前源文件的文件名。
__DATE__: 表示当前编译的日期,格式为"MMM DD YYYY",例如"Jul 29 2023"。
__TIME__: 表示当前编译的时间,格式为"HH:MM:SS",例如"10:30:36"。
__STDC__: 表示当前编译器是否符合C语言标准。如果定义了该符号,则表示编译器符合C语言标准;否则,表示不符合。
NULL: 表示空指针常量。
EOF: 表示文件结束符。
true 和 false: 表示布尔类型的真值和假值,C99引入了 <stdbool.h> 头文件来定义这两个符号。
下面是一些测试
#include <stdio.h>
int main()
{
printf("file:%s,line:%d\n", __FILE__);
printf("day:%s\n", __DATE__);
printf("time:%s\n", __TIME__);
return 0;
}
2.#define
a.#define定义常量,如下结果为200,在用#define定义常量时,也会在预处理阶段进行替换
#include <stdio.h>
#define MAX 1000
int main()
{
printf("%d", MAX / 5);
return 0;
}
b.#define定义语句
#include"stdio.h"
#define Print printf("hello world!")
int main()
{
Print; //预处理时会被替换为 printf("hello world!");
return 0;
}
c.#define定义宏函数
#include <stdio.h>
#define square(x) ((x)*(x))
int main()
{
printf("%d",square(4));
return 0;
}
当输入4时,运行结果如下
***这里需要注意一点,宏定义函数的时候括号内尽量不要出现运算,因为宏只会把它进行简单的替换,运行时可能无法得到想要的结果
2.带有副作用的宏参数
当宏参数在宏定义中出现超过一次的时候,就有可能出现无法预料的错误
#include <stdio.h>
#define Max(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
int a = 3;
int b = 5;
int ret = Max(a++, b++);
return 0;
}
在进行替换时,a++与b++出现两次,最后会出现a=4,b=7,Max函数返回6.
3.宏替换的规则
1.不带参数的宏定义
(1)宏名一般用大写
(2)使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。例如:数组大小常用宏定义
(3)预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查。
(4)宏定义末尾不加分号;
(5)宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头。
(6)可以用#undef命令终止宏定义的作用域
(7)宏定义可以嵌套,但不可递归
(8)字符串" "中的参数不会被宏替换
(9)宏定义不分配内存,变量定义分配内存。
2. 带参数的宏定义:
除了一般的字符串替换,还要做参数代换
格式: #define 宏名(参数表) 字符串
例如:#define S(a,b) a*b
area=S(3,2);第一步被换为area=a*b; ,第二步被换为area=3*2;
类似于函数调用,有一个哑实结合的过程:
(1)实参如果是表达式容易出问题
(2)宏名和参数的括号间不能有空格
(3)宏替换只作替换,不做计算,不做表达式求解
(4)函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存
(5)宏的哑实结合不存在类型,也没有类型转换。
(6)函数只有一个返回值,利用宏则可以设法得到多个值
(7)宏展开使源程序变长,函数调用不会
(8)宏展开不占运行时间,只占编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)
4.宏函数的对比
先来看一下,宏的优缺点:
优点:
-
用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
使用宏速度更快的原因
避免函数调用开销:在使用函数时,每次调用函数都需要进行函数调用的开销,包括函数栈帧的创建和销毁、参数传递等。而使用宏定义时,宏会在预处理阶段直接进行文本替换,不需要额外的函数调用开销。
减少函数调用的间接性:函数调用会引入间接性,即在调用函数时需要跳转到函数的代码段执行,然后再返回到调用处。而使用宏定义时,代码会直接嵌入到调用处,减少了跳转和返回的开销。
-
更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之 这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
举例1:这里计算面积的时候,参数类型不限
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
int main() {
int a = 5;
double b = 2.5;
int result1 = SQUARE(a);
double result2 = SQUARE(b);
printf("Square of %d is %d\n", a, result1);
printf("Square of %.2f is %.2f\n", b, result2);
return 0;
}
举例2:在用malloc开辟空间时
int* p = (int *)malloc(10 * sizeof(int));
我们可以使用宏定义,这样开辟的空间可以是任意类型
#define MALLOC(num, type)\
(type *)malloc(num * sizeof(type)) //对MALLOC进行宏定义
int* p = MALLOC(10, int);
缺点:
宏代码在进行文本替换时,会增加代码长度
宏无法调试,不检查类型,可能不够严谨
宏会带来运算符优先级的问题
二.#与##
1.#运算符
将宏的一个参数转换为字符串字面量,可以理解为“字符串化”它可以将宏参数替换为字符串,并在编译时进行字符串拼接操作。
示例:
#define STR(x) #x
printf("%s\n", STR(Hello)); // 输出 "Hello"
在上述示例中,#x 将宏参数 x 转换为字符串。当宏 STR(Hello) 被展开时,Hello 被转换成字符串字面量 “Hello”,然后作为参数传递给 printf 函数。
2.##运算符
在宏定义中,## 运算符用于将两个标识符连接在一起,形成一个新的标识符。它在宏展开时进行标识符的连接操作。
#define CONCAT(x, y) x##y
int xy = 10;
printf("%d\n", CONCAT(x, y)); // 输出 10
#define DECLARE_COUNTER(n) int counter##n = 0
DECLARE_COUNTER(1);
DECLARE_COUNTER(2);
counter1++; // 访问生成的变量 counter1
counter2++; // 访问生成的变量 counter2
三.命名约定
宏全大写,函数不要全大写
#undef用于移除一个宏定义
四.命令行定义
许多C编译器提供了一种能力,允许你在命令行中定义符号,用于启动编译过程。
如:定义任意长度数组
#include <stdio.h>
#include <string.h>
int main()
{
int buf[MAX_NUM] = {0};
memset(buf, 0, sizeof(buf));
int i;
for(i=0; i<MAX_NUM; i++)
{
buf[i] = i;
}
printf("\n ---------------------- \n");
for(i=0; i< MAX_NUM; i++)
{
printf(" %02X",buf[i]);
}
printf("\n ---------------------- \n\n");
return 0;
}
编译选项:
gcc -DMAX_NUM=5 test.c -o test
运行效果:
root@ubuntu:/mnt/hgfs/Ubuntu12-share/01_C/02_test# gcc -DMAX_NUM=5 test.c -o test
root@ubuntu:/mnt/hgfs/Ubuntu12-share/01_C/02_test# ./test
----------------------
00 01 02 03 04
----------------------
root@ubuntu:/mnt/hgfs/Ubuntu12-share/01_C/02_test#
五.条件编译
可以自行选择编译或不编译的代码,通过返回值的真假实现
int main()
{
#if 0
printf("haah");
#endif
return 0;
}
若#if后条件成立,则参与编译,若不满足则不参与编译(上为恒不满足条件的情况),注意条件编译都需要以#endif结尾
六.头文件包含
1.#include <filename>
此种方式意味着 编译器 去其定义的标准位置查找该头文件。标准位置有编译器规定,其说明文档应有说明。用户也可以修改标准位置和添加新的位置。
对于运行于UNIX上的C编译器而言,标准位置一般是指/user/include目录。
2.#include "filename"
标准允许编译器自行决定是否把本地形式的#include和标准形式的#include区别对待。用户可以对本地头文件先使用一种特殊的处理方式,如果失败,编译器再按照标准形式的处理方式进行处理。
常见的策略就是在源文件所在的当前目录进行查找(gcc 测试发现当前目录的子目录也是会去查找的) ,如果没有找到,再去标准位置查找。
3.#pragma once
为了防止同一头文件被包含多次,通常文件名为.h结尾的都会在文件开头加上#pragma once