c语言宏定义详细讲解,以及实操迅速理解掌握

33 阅读13分钟

本博客分为以下三个部分,如果了解宏定义的可以直接到宏的函数定义张杰看看泛型和技巧

  1. 宏定义讲解和介绍
  2. 宏定义提升
  3. 宏定义实践实现一个自编写报错

宏定义讲解和介绍

首先什么是宏定义??

宏定义本质可以理解成一种文本替换,文本替换就是把你这个东西直接复制到使用的位置,注意文本替换是重点,但是这先不说我们先说宏定义的两种格式

  1. 对象式定义(其实就是定义一个对象)
  2. 函数式定义(其实就是定义一个函数)

1.对象式定义

我们先来看对象式定义,也就是定义一个对象语法上格式为 #define 宏定义名 值,比如下面的例子,注意这个值必须是常量

#define PI 3.14//PI代表圆周率那运算我们就可以写成
s=r*r*PI;

那使用宏定义写的好处是什么呢?

实用原因: 1.容易读懂 你看这个宏定义名字就知道它代表的内容

2.方便修改: 例如,计算多组圆的面积,看下面例子

//假设不使用这个宏定义而是直接用一个数代表圆周率
s1=r1*r2*3.14,s2=r2*r2*3.14,s3=r3*r3*3.14;
//使用宏定义
s1=r1*r2*PI,s2=r2*r2*PI,s3=r3*r3*PI;

当我感觉PI精度不够需要从3.14换成这个3.141592的时候,第一种就需要一个个修改如果是使用宏定义直接修改#define PI 3.141592;即可

那使用宏d对象定义有什么坏处吗? 有的有的兄弟,首先我们抓住它文本替换的特点那么就决定了它

  1. 本身是文本不是变量没有类型
  2. 没有这个作用域,使用#undef终止
  3. 没有内存分配
  4. 是文本不支持调试 因此对于如果你想使用常量我的建议还是使用const关键字定义,有类型,可调试,有内存分配,有作用域 那有的人就问了那合着白看来直接用const就一劳永逸了,但是注意上面只是宏对象定义,我个人认为宏定义的精髓很大一部分是宏函数定义接下来我们一起来看

2.宏的函数定义

格式: #define 函数名(参数列表) 实现

例如: #define MAX(a,b) ((a)>(b))?(a):(b)

注意函数名和参数列表之间无空格,空格在实现和参数列表之间 上面的代码就是定义了一个宏定义的比较最大的函数,可能有的朋友感觉嗯……这我自己定义一个函数不是也可以吗?为什么还写宏定义,但是我们记住宏定义的特点是文本替换,注意这个性质就保证了它有很多意想不到的效果

泛型编程的使用

泛型就是什么类型都支持的意思,对于一个函数swap交换函数如果我们使用普通函数实现你会发现我必须是下面这样,声明好参数类型然后只能使用者这个类型,那如果你有多种类型的交换呢定义swap_int,swap_double?

void swap(int a,int b);

我们来看看宏定义,宏定义是文本替换就是简单把你给的值传递过去,不需要管类型我什么类型都能传递,也就是我们的这个SWAP所有类型可用例如说下面的代码

#include<stdio.h>  
#define SWAP(type,a,b) do{\    
     type temp=a;\  
     a=b;\  
    b=temp;\  
}while(0)  
int main(){  
    int a=10,b=20;  
    SWAP(int,a,b);  
    printf("%d %d",a,b);  
}
  1. 因为宏定义需要是写在一行你加上\其实意思就类似代表这一行没结束衔接下面
  2. type是你传递过来的类型 可以尝试照着写试一下,do-while(0){}一会再看

思考小问

注意为什么明明我的这个宏定义函数我传递的是值也会更改呢不是说函数中值传递不更改值吗? 因为这个地方不是函数调用我们在重复一遍宏定义本质是文本替换也就是说其实我们代码变成了这个样子

int main(){  
    int a=10,b=20;  
    do{  
        int temp=a;  
        a=b;  
        b=temp;  
    }while(0);  
    printf("%d %d",a,b);  
}

如果有比较细心的兄弟就会发现了,这个do-while(0)的作用妙不可言了,我do-while(0);本身语法是需要;结尾的宏定义中没有写这个;这里刚刚好补上了,可能有人就会说那你本身不用这个do-while(0)用括号不就好了吗?但是如果用括号我们来看假设下面的情况

if(cond)
SWAP(a,b);//
else
...
//文本替换展开
if(cond)//此处我们将这个do-while(0)换成括号
{
};//注意这个地方有个分号会断开后面的else,使用do-while(0)就能吸收了这个;
else
...

可能有的同学感觉没问题我if{}不就可以了吗,但是这个宏定义可能不止你一个用有的人就是喜欢使用if SWAP(a,b);然后他逻辑完全没问题,但是这个错误他会一直找不到

因此注意宏定义是文本替换无法调试,虽然可以很好解决泛型问题但是要严谨严谨,有多条语句就使用do-while(0) 当然除了这个宏定义还有一些别的毛病,宏定义因为其本质其实是文本替换,因此经常会触发二义性 ####宏定义二义性问题(常考)

参数未加括号
#include<stdio.h>
#define MUL(x) x*x
int main(){
    printf("%d",MUL(9+1));
}

运行结果为19,因为我们直接文本替换xx会变成9+19+1=19 修改成下面形式即可,(注意我们上面的代码就没加,上面不加是较少繁琐内容先帮助大家理解)

#include<stdio.h>
#define MUL(x) (x)*(x)
int main(){
    printf("%d",MUL(9+1));
}
参数加上但是整体未加
#include<stdio.h>
#define MUL(x) (x)*(x)
int main(){
    printf("%d",200/MUL(9+1));//输出为200
}

本来结果应该是2,但是因为只是文本替换会变成200/(9+1) (9+1)结果就是先/10后10大小不变,因此我们还需要整体加上()变成200/((9+1)**(9+1));

#include<stdio.h>
#define MUL(x) ((x)*(x))
int main(){
    printf("%d",200/MUL(9+1));
}
虽然都加上了括号但是因为操作导致问题
#include<stdio.h>
#define MUL(x) ((x)*(x))
int main(){
    int i=1;
    while(i<=5){
        printf("%d\n",MUL(i++));//输出2 12 30,为什么不是5次i*i因为每次其实i++会执行两次
    }
}

因此一些建议对于宏定义

  1. 每个参数加上这个()避免踩坑
  2. 使用do-while()
  3. 不要在传参数的时候运算 宏定义的文本替换除了解决了泛型问题其实在这个设计上也有帮助(下期结合例子说),其次就是这个编程的技巧

技巧

根据文本替换的特点我们可以偷很多懒比如说常用的for循环每次都写三参数太麻烦了,Printf每次写着太麻烦了都可以宏定义实现(注意这样可能导致别人很难看你的代码),效果运行一下你就知道了,包邪修的

#include<stdio.h>  
#define FOR(n) for(int i=0;i<(n);i++) //for循环不取等  
#define FORE(n) for(int i=0;i<(n);i++) //取等  
#define Printf(n,ch) printf("%d%c",(n),(ch))//给这个数以及用什么隔开  
#define Scanf(n) scanf("%d",&(n))  
int main(){  
    int n;  
    Scanf(n);  
    FOR(n){  
        Printf(i,' ');  
    }  
    FORE(n){  
        Printf(i,'\n');  
    }  
}

宏定义提升

宏使用技巧

1. 字符串化(Stringification):# 操作符

将宏参数转为字符串字面量:在参数前面假设#会让它变成字面量也就是比如你传入age不在是age对应的值而是"age"一个字符串

#define PRINT_INT(x) printf(#x " = %d\n", x)

PRINT_INT(age); // 展开为:printf("age" " = %d\n", age);
                // 输出:age = 25

2. 连接符(Token Pasting):## 操作符

拼接 token:

#define REG_ADDR(base, offset) (base ## _BASE + offset)
#define GPIOA_BASE 0x40020000

uint32_t addr = REG_ADDR(GPIOA, 0x00); // → GPIOA_BASE + 0x00

3. 可变参数宏(Variadic Macros,C99)

可以使用...代表可变参数(不确定类型个数你想怎么传怎么传),##__VA_ARGS__展开这个参数

#define LOG(fmt, ...) printf("[LOG] " fmt "\n", ##__VA_ARGS__)

LOG("Error: %s", strerror(errno));

🔸 ##__VA_ARGS__ 是 GCC 扩展,用于处理空参数情况。

typeof 自动推导类型根据我们传入参数推导这个类型

可以更改我们上面的SWAP函数

#define SWAP(a,b) do{\  
typeof(a) temp = (a); \  
(a)= (b); \  
(b) = temp; \  
}while(0)

常用系统宏定义

含义典型用途
__FILE__当前源文件名错误定位
__LINE__当前行号崩溃追踪
__func__当前函数名日志打印
__DATE__编译日期固件信息
__TIME__编译时间版本追踪

container_of内核经典宏(重点)

为什么需要反推结构体呢?

特殊情况的数据查找

我自己想的可以不用管 举一个例子:我们现在有一个商品链表,需要找一个商品的归属者。为什么会通过商品反推归属者,而非从每个用户里查找该商品是否属于他? 因为从用户侧查找需要遍历每个用户的商品列表,即便做成普通链表,查找效率也一般;但如果是带分类的链表(类似数据库的索引设计),把商品链表先按类型分类、再按品牌细分,每个子链表的规模会大幅缩小。 简单来说,对内容做分类后,能快速定位到对应类型的子链表,在小范围内查找商品后,再反推其归属者,查找效率会大幅提升。

内核或者系统设计的封装、隔离、权限设计

1. 为了接口的通用性

比如实现一个定时器函数,面对多个不同设备(电机、LED、传感器),不可能为每个设备单独写一个定时器函数,只能统一接口并传入公共部分;而实际使用设备时,就需要从定时器结构体反推到对应的设备结构体。

void timer_expire(struct timer_node *node){//传入设备的时间上的结构体
  active(device);//时间到了激活设备
}
struct Motor {
    int speed;
    struct timer_node t;
};

struct LED {
    int brightness;
    struct timer_node t;
};

struct Sensor {
    int value;
    struct timer_node t;
};

2. 权限保护(核心是隔离)

还是上面的定时器例子,定时器模块本就不应该访问设备的所有信息,这样做会存在严重的安全隐患,通过只传入公共的定时器结构体,能严格隔离模块的访问权限。

✅ 面试经典题:手写container_of实现?

#define my_offsetof(type, member) ((size_t)&((type*)0)->member) 

分步骤理解核心逻辑

  1. (type*)0:假设将一个type类型的结构体,强行放置在内存地址0的位置;
  2. ((type*)0)->member:访问这个虚拟结构体中的member成员;
  3. &((type*)0)->member:取该成员的内存地址——因为结构体从0地址开始,成员的地址值就是它相对于结构体首地址的偏移量通俗举例: 知道我的位置x、你的位置y未知,想求彼此的距离(偏移量)。保持距离不变(结构体成员偏移量固定),让你站到0的位置,此时我的位置x就是我们之间的距离,这就是offsetof的核心思想。

Linux 内核经典宏:container_of

container_ofoffsetof的反向应用:已知成员的地址 + 成员的偏移量,反推结构体的首地址(这个地方就是上面的例子我知道我们之间的距离之后直接用我的位置减去距离即可)。

#define container_of(ptr, type, member) ({          \
   const typeof(((type *)0)->member) * __mptr = (ptr); \
   (type *)((char *)__mptr - offsetof(type, member)); \
})
// 使用
struct list_head *node = ...;
struct Task *task = container_of(node, struct Task, list_node);

###宏定义实战 编写一个自定义的报错头文件,可以在别的文件中引用,当我引用这个头文件的时候,就可以通过错误编号使用例如ERROR(ERR_EMPTY);,显示错误原因,错误所在文件,行,列和用户附带信息,并终止程序比如下面这个样子,并且可以点击帮助用户快速跳转到错误并知晓错误原因

main.c:13: [ERROR] main | reason: The current container is empty 
test: Error.h:49: my_error_handler: Assertion `0' failed.
Aborted (core dumped)
实现代码 知识点在下面
#ifndef ERROR_H
#define ERROR_H
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<assert.h>

// 错误码枚举
typedef enum ERRCode ERRCode;
enum ERRCode{
    ERR_PTR_NULL,   // 空指针
    ERR_OPT_FAIL,   // 操作失败
    ERR_MEM_FAIL,   // 内存申请失败
    ERR_EMPTY       // 容器为空
};

// 错误码对应描述
static const char* ErrStr[]={
    "current point is NULL",
    "current opt is failed",
    "memory allocation is failed", 
    "The current container is empty"
};

// 错误处理核心函数
static inline void my_error_handler(
   ERRCode code,//错误代码
   const char *file,//文件名
   const char *func,//函数名
   const int line,//所在行
   const char *msg//用户携带信息
){
   
    fprintf(stderr, "%s:%d: [ERROR] %s | reason: %s ", file, line, func, ErrStr[code]);
    
    // 自定义消息
    if(msg){
        fprintf(stderr, "| msg: %s ", msg);
    }
    // 系统错误(errno)
    if(errno){
     fprintf(stderr, "| system prompt: %s ", strerror(errno));
    }
   fprintf(stderr, "\n");
   #ifdef NDEBUG
   exit(EXIT_FAILURE);
   #else
   assert(0);
   #endif
}
//接下来两种错误模型
// 传入参数错误代码:兼容单/双参数,此时传入用户携带信息为空
#define _ERROR_MODE1(code) do{\
    my_error_handler((code),__FILE__,__func__,__LINE__,NULL);\
}while(0)
//传入参数错误代码和用户携带信息
#define _ERROR_MODE2(code,msg) do{\
    my_error_handler((code),__FILE__,__func__,__LINE__,msg);\
}while(0)
//_1,_2占位符就是你不使用前两个变量,第三个变量就是我的模型
#define _GET_MODE(_1,_2,NAME) NAME 
//...可变参数可能一个可能两个,然后如果是两个参数那么我的_ERROR_MODE2就是第三个参数我的模型
//如果是一个参数在加上一个_ERROR_MODE2,我的_ERROR_MODE1就可以变成我的模型
//__GET确定模型然后传入参数参数使用__VA_ARGS__展开参数
#define ERROR(...) do{\ 
    _GET_MODE(__VA_ARGS__,_ERROR_MODE2,_ERROR_MODE1) (__VA_ARGS__);\
}while(0)
#endif

使用的知识点

1.枚举类

将我们的一些字符替换成数字,具体来说比如ERR_PTR_NULL默认为0,往下继续枚举每次加1,ERR_OPT_FAIL是等于1的,直接运行下面代码看效果

#include<stdio.h>  
enum ERRCode{  
    ERR_PTR_NULL,   // 空指针  
    ERR_OPT_FAIL,   // 操作失败  
    ERR_MEM_FAIL,   // 内存申请失败  
    ERR_EMPTY       // 容器为空  
};  
int main(){  
    printf("ERR_PTR_NULL= %d\n",ERR_PTR_NULL);  
    printf("ERR_OPT_FAIL= %d\n",ERR_OPT_FAIL);  
    printf("ERR_MEM_FAIL= %d\n",ERR_MEM_FAIL);  
    printf("ERR_EMPTY= %d\n",ERR_EMPTY );  
    return 0;  
}

你如果想从3开始枚举直接写成

enum ERRCode{  
    ERR_PTR_NULL=3,   // 空指针  
    ERR_OPT_FAIL,   // 操作失败  
    ERR_MEM_FAIL,   // 内存申请失败  
    ERR_EMPTY       // 容器为空  
};  

ERR_PTR_NULL本质是数字那我们就可以把它作为下标,结合错误字符串数组char* ErrStr[]我们就可以直接使用ErrStr[ERR_PTR_NULL],访问这个对应的错误的文本提示

inline函数

也是函数只不过执行的时候是文本嵌套的格式执行大概,自行了解写不动了

自带的宏定义

| __FILE__ | 当前源文件名 | 错误定位 | | __LINE__ | 当前行号 | 崩溃追踪 | | __func__ | 当前函数名 | 日志打印 | ...可变参数,__VA_ARGS__展开这个参数,都在上面部分有介绍