🚀【C语言宏进阶】两个你必须掌握的宏技巧:位交换 + 结构体偏移量计算(带原理详解)
最近在复习 C 语言的宏(Macro)时刷到两个非常有代表性的题目,它们都充分体现了"宏能做编译器本来做不到的事"这一特性,也能让我们对底层内存与位运算有更深刻的理解。
今天我就结合我自己做题的过程,把这两个题目整理成一篇博客,方便自己以后翻阅,也分享给正在学习宏的同学。
本文包含两个经典宏题目:
- 使用宏交换整数的奇偶位(二进制位交换)
- 实现 OFFSETOF 宏:计算结构体成员相对于结构体首地址的偏移
两段代码你都已经提供,我会在此基础上做进一步讲解,保证你看完就能彻底理解。
📌 题目一:使用宏交换整数的奇数位与偶数位
🔍 题目要求
编写一个宏,完成:
将一个整数的二进制位中的奇数位与偶数位互换。
示例:
10(二进制:1010)
交换奇偶位后 → 0101(十进制:5)
🧠 思路分析(重点)
先看 32 位整数的二进制位编号:
位: 31 30 29 ... 3 2 1 0
类型:奇 偶 奇 ... 奇 偶 奇 偶
要交换奇数位与偶数位,核心做法是:
-
保留奇数位 :
用二进制掩码:
0101 0101 ... 0101十六进制:
0x55555555x & 0x55555555然后把奇数位左移一位
<< 1 -
保留偶数位 :
掩码:
1010 1010 ... 1010十六进制:
0xAAAAAAAAx & 0xAAAAAAAA然后右移一位
>> 1 -
将两部分相加即可完成交换
✅ 宏实现
c
#define SWAP(x) (((x & 0x55555555) << 1) + ((x & 0xaaaaaaaa) >> 1))
示例代码(来自你的文件)
c
int main() {
int a = 10; // 1010b
a = SWAP(a);
printf("%d\n", a); // 输出:5
return 0;
}
🔚 结果验证
10 → 1010
交换奇偶位 → 0101 = 5
完全正确 ✔
📌 题目二:使用宏计算结构体成员的偏移量(OFFSETOF 原理解释)
这个题更经典!很多人第一次看到 (type*)0 都会懵。
🔍 题目要求
写一个宏 OFFSETOF(type, member)
用于计算结构体中某个成员相对于结构体起始地址(首地址)的偏移量。
这与标准库 offsetof() 功能完全相同。
你的结构体定义如下:
c
struct S {
char c1;
int i;
char c2;
};
🧠 核心原理详细讲解(这段很多人都不懂)
我们来看关键语句:
c
(size_t)&((type*)0)->member
逐步拆解👇
① (type*)0
这是把数字 0 强制转换成指向 struct S 的指针:
0x00000000 ------> struct S*
注意:这里没有真正访问地址 0,只是用它来做指针运算。不会产生段错误。
② ((type*)0)->member
假设结构体从 0 地址开始,那么成员 member 的地址就是它的偏移量。
示例:
(struct S*)0 -> c1 地址 = 0 + c1 的偏移量 = 0
(struct S*)0 -> i 地址 = 0 + i 的偏移量 = 4(可能有对齐)
(struct S*)0 -> c2 地址 = 0 + c2 的偏移量 = 8(可能有对齐)
即它返回的是一个"假想的地址",但是这个地址的数字大小就是偏移量!
③ &((type*)0)->member
对该成员取地址,就得到:
结构体成员的偏移量(字节数)
🎯 于是宏可以写成:
c
#define OFFSETOF(type, member) (size_t)&((type*)0)->member
🔬 示例代码
c
int main() {
printf("%d\n", OFFSETOF(struct S, c1));
printf("%d\n", OFFSETOF(struct S, i));
printf("%d\n", OFFSETOF(struct S, c2));
return 0;
}
若结构体按默认对齐:
| 成员 | 类型 | 偏移量 |
|---|---|---|
| c1 | char | 0 |
| i | int | 4 |
| c2 | char | 8 |
程序输出也会是:
0
4
8
📝 总结:宏的威力远超你想象
这两个题展示了宏的两个强大能力:
✔ 1. 进行复杂的位级操作(如奇偶位交换)
宏比函数快,不需要参数压栈,不会产生运行时代码。
✔ 2. 操纵类型、指针与编译期行为(如 OFFSETOF)
这是函数无法完成的工作,因为 offsetof 必须在编译期求值,而非运行期。
🎯 学习宏应该注意什么?
- 宏不检查类型,容易导致 bug
- 宏展开可能产生副作用
- 宏必须加括号
- 宏适合做 常量 、小型计算 、类型运算
- 不要用宏写复杂逻辑代码
掌握这两个例子,你已经能理解宏的核心用途了。