1.预处理概述
- 意义
(1)预处理就是将一些繁杂的代码(头文件、宏定义、条件编译、注释等)提前展开或清除,将核心的代码编译交给编译器来编译。
(2)常见的预处理
/*头文件*/
#include <xxx.h>
#include "xxx.h"
/*宏定义*/
#define xxx yyy
/*注释*/
void func(void); //xxxxxx
/*条件编译*/
#ifndef xxx
#define xxx
#endif
- 宏定义的预处理
(1)宏定义被预处理后,原来的宏定义的代码消失,程序中所有被宏定义的字符被替换为原来的值;
(2)因此在编译器的编译阶段已经不包含宏定义了;但typedef定义的代码不变,不属于预处理范围内; - 头文件的预处理
(1)头文件包含被预处理后,原来的包含头文件的代码消失,预处理会将所包含的头文件里面的内容全部展开到程序中;
(2)#include <xxx.h>
与#include "xxx.h"
的区别:前者是用来包含系统提供的头文件,C语言编译器(预处理器、汇编器、链接器等统称为编译器)会自动到系统指定的目录下寻找相关头文件;
后者编译器则会先到当前目录下寻找头文件,若未找到再到系统目录下寻找该头文件,若都没有,则会报无此头文件;
(3)编译器还允许用-I来附加指定其他的包含路径)去寻找这个头文件(隐含意思就是不会找当前目录下) - 注释的预处理
由于注释的作用是给程序员看的而不是cpu,因此编译器再预处理时会直接删掉注释来减少内存的占用; - 条件编译的预处理
(1)常见的两种条件判定分别是#ifdef/#ifndef xxx
和#if (xxx)
,二者区别:前者在预处理时会判断在#ifdef/#ifnedf xxx
代码前,xxx
是否已经被定义,若被定义/未定义则条件成立,执行相关的代码;
(2)后者类似于C中的if语句,判断(xxx)
中的值为true
或false
,执行相关代码;
#include <stdio.h> //1、预处理会从系统目录中寻找stdio.h文件,2、将该行代码删除,3、将stdio.h文件展开到该.c文件中
#include "test.h" //1、预处理会先从当前目录中寻找test.h文件,若未找到则再到系统目录中寻找;
#define dp_int int * //预处理会将后面所有代码的dp_int字符替换回int *,并将此行代码删除
typedef char* tp_char; //预处理不会处理该行代码,也不会处理此后的tp_char字符
int main(void)
{
int a = 0;
d_int a, b; //预处理会将该行代码替换为int *a, b;即d_int a替换为int *类型,将b替换为int类型:即int *a, b;
t_char c, d; //编译器会将c和d都替换为char *类型
#ifndef dp_int //预处理器会判断是否未定义dp_int
printf("not defint d_int\n"); //若未定义则执行此行代码
#else //若未定义,则预处理器会直接删除这条判断相关的代码,若已经定义,则预处理器会删除上面未定义的代码,然后执行这条代码
printf("define d_number\n"); //则执行此行代码
#endif //结束条件判断
#if (a == 0)
printf("true\n");
#else
printf("false\n");
#endif
return 0;
}
2.宏定义详解
- 宏定义概述
(1)宏定义的作用就是原封不动的替换,而且仅限于字符替换,即便替换后会导致后面的代码逻辑或语法错误,宏定义也并不关心;
(2)宏定义中 标识符表示其后面的所有成分,即标识符后所有成分之间即便有空格也算作一个整体;
(3)宏定义的替换可以递归替换,直到最后的成分不是宏为止;
#define n 1 0
#define m n
int a[m] = {0};
//预处理后
int a[1 0] = {0}; //1、后面编译器编译时必然会报语法错误,但预处理对于宏定义的处理只时原封不动替换,对于其他并不关心
//2、尽管1和0之间有空格,但n仍表示为 ”1 0“
//3、n替换 ”1 0“,m替换n,最终m代表 “1 0”
- 宏定义示例
(1)MAX宏,求两者中最大者:注意宏定义时所有的独立元素都要加()
,否则直接替换后可能会导致运算出错
#define max(a, b) (((a) > (b))? (a) : (b))
int func(void)
{
int x = 2, y = 3;
max(x+2, 3);
reutrn 0
}
(2)宏定义求一年中有多少秒:注意类型的转换,因为C中默认时int类型,而秒数超出int范围
#define second (365*24*60*60UL) \\将60转为unsigned long类型
#define second (365*24*60*60)UL \\错误,编译器无法通过
(3)谨记宏定义的含义就是完全替换
#define FUNC (((a) > (b)) ? (a) : (b))
int func(int a, int b)
{
return a > b? a: b;
}
int main(int args, char *argv[])
{
int a = 1, b = 3
int c = FUNC(++a, b);
printf("a = %d\nb = %d\nc = %d\n", a, b, c); //输出为3 3 3
func(++a, b);
printf("a = %d\nb = %d\nc = %d\n", a, b, c); //输出为2 3 2
}
- 带参宏与带参函数
(1)带参宏预处理时会将宏原地展开,运行时直接在原地运行;而带参函数运行时会调用子函数,通过函数名(指针)跳转到子函数地址去执行,调用开销大,若子函数较小时,效率不如带参宏;
(2)带参函数运行时,结果的返回值是在定义函数时就必须设定的,因此在返回结果时,编译器会自动检查返回值的类型是否正确;
(3)带参宏计算的结果返回值不包含数据类型,预处理时仅进行字符替换,因此也不会报任何警告错误,但运行结果可能会因为数据的类型不匹配等导致结果错误;
/**********带参函数************/
#define max(a, b) (((a) > (b))? (a) : (b))
int func(int a, int b) //由于a 和 b的类型与int 不匹配,因此编译器警告
{
if(a > b)
return a;
else
return b;
}
int main(void)
{
float a = 3.14;
float b = 6.18;
float c = func(a, b); //由于形参和实参的类型 不匹配,因此编译器警告
int d = max(a, b); //由于带参宏返回值不附带数据类型,因此编译器不报任何异常
printf("d = %f\n", d); //此处结果是错误的
}
- 宏定义来实现条件编译:
debug
宏
(#define #undef #ifdef)
#define DEBUG //定义一个宏
#undef DEBUG //取消这个宏定义
#ifdef DEBUG //若定义了这个宏
#define DEBUG(format,...) printf("file:"__FILE__", line:%d, "format"\n", __LINE__, ##__VA_ARGS__) //用debug代替 print函数
#else //若没定义
#define DEBUG(format...) //debug就是空
#endif
int main(void)
{
char str[] = "hello";
DEBUG("%s", str);
}
//输出结果:file:main.c, line:16, test, hello
- offsetof宏与container_of宏
(1)offsetof宏:用来计算结构体某成员指针与结构体指针的偏移量:
#define offsetof(type, member) (size_t)&(((type*)0)->member)
// ((type*)0):将0地址强制类型转换为指针类型,指向一个type类型的结构体,即0地址为结构体的首地址( int *0 <==> int *p = 0 //p中存放着某个int变量的地址,将2赋给p即将2赋给int变量的地址即2中存放着int变量);
//1、(((type*)0)->member):得到通过结构体指针得到member成员的空间(参数名);
//2、&(((type*)0)->member):得到member成员的地址;
//3、(size_t)&(((type*)0)->member):将member成员的地址转为int类型即偏移量
//4、因为type结构体的首地址为0,因此member成员的地址就是member成员地址的偏移量
(2)container_of宏:通过结构体某成员的指针计算结构体指针:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
//ptr:某成员的指针;type:结构体类型;member:某成员名
//1、( ((type *)0)->member ):得到某成员在内存的空间;
//2、const typeof( ((type *)0)->member ):获取member的类型 //typeof是根据变量的值获取变量类型的类型
//3、const typeof( ((type *)0)->member ) *__mptr:定义一个名为__mptr,类型为指向member的类型的指针;
//4、const typeof( ((type *)0)->member ) *__mptr = (ptr):将member的指针赋给之前定义的__mptr变量
//5、offsetof(type,member):获取当前成员的指针相对于结构体指针的偏移;
//6、( (char *)__mptr - offsetof(type,member) ):用当前的指针地址-指针偏移 == 结构体的起始地址(此时为char *类型的地址)
//7、(type *)( (char *)__mptr - offsetof(type,member) ):将当前的char*类型地址转化为结构体type类型。
- 内联函数和inline关键字
(1)内联函数就是在定义函数的前面加inline
关键字,来实现该函数既可以原地展开,又可以通过编译器来检查参数和返回值的类型。
(2)一般内联函数都是用在代码比较少的小型函数上。
int func(int a, int b) //由于a 和 b的类型与int 不匹配,因此编译器警告
{
if(a > b)
return a;
else
return b;
}
int main(void)
{
float a = 3.14;
float b = 6.18;
float c = func(a, b); //此函数运行时会在原地展开运行而非跳转,同时编译器也会报警告
printf("c = %f\n",c);
}