第15章:位运算的妙用——如何用位运算实现权限管理?

权限管理,说白了就是「谁能不能干什么」。

我刚开始做嵌入式开发时,权限控制用的是结构体数组,每个用户存一堆布尔值。后来项目越做越大,用户角色越来越多,那套方案跑起来慢得让人抓狂。直到我学会了用位运算来做权限管理——嗯,真香。

15.1 为什么位运算适合做权限管理?

你想想看,权限本质上就是一堆「是/否」的开关。读权限?有或没有。写权限?有或没有。执行权限?有或没有。

这不就是二进制位吗?

一个32位的整数,可以表示32种不同的权限。每个位代表一种权限,1表示有,0表示没有。这样,一个int就能搞定32种权限的组合,而且判断、设置、清除都只需要一条指令。

核心思想:用二进制位的0和1来表示权限的有无,用位运算来操作这些位。

15.2 权限的位定义

我们先定义一些权限常量。每个常量对应一个位,用左移运算符来生成。

// 权限定义:每个权限占用一个位
#define PERM_READ    (1 << 0)  // 0x01,读权限
#define PERM_WRITE   (1 << 1)  // 0x02,写权限
#define PERM_EXEC    (1 << 2)  // 0x04,执行权限
#define PERM_DELETE  (1 << 3)  // 0x08,删除权限
#define PERM_ADMIN   (1 << 4)  // 0x10,管理权限
#define PERM_SHARE   (1 << 5)  // 0x20,分享权限

我个人习惯用宏定义,这样代码可读性高。你如果喜欢枚举也可以,但注意枚举值要手动指定为2的幂次。

15.3 权限的增删查

有了权限定义,接下来就是三个基本操作:添加权限、移除权限、检查权限。

// 添加权限:按位或
uint32_t add_permission(uint32_t current, uint32_t perm) {
    return current | perm;
}

// 移除权限:按位与 + 取反
uint32_t remove_permission(uint32_t current, uint32_t perm) {
    return current & ~perm;
}

// 检查权限:按位与
int has_permission(uint32_t current, uint32_t perm) {
    return (current & perm) == perm;
}

这三个函数,就是权限管理的全部核心了。简单吧?

小技巧:检查权限时,用 == perm 而不是 != 0。因为如果你检查的是多个权限的组合(比如同时检查读和写),!= 0 只能判断「是否有任何一个权限」,而 == perm 能判断「是否全部都有」。

15.4 实战:用户角色权限系统

我在项目中遇到过这样一个需求:一个文档管理系统,有普通用户、编辑、管理员三种角色。每种角色有不同的权限组合。

// 角色权限定义
#define ROLE_USER   (PERM_READ)                          // 普通用户:只能读
#define ROLE_EDITOR (PERM_READ | PERM_WRITE | PERM_EXEC) // 编辑:读+写+执行
#define ROLE_ADMIN  (PERM_READ | PERM_WRITE | PERM_EXEC | PERM_DELETE | PERM_ADMIN) // 管理员:全部

// 用户结构体
typedef struct {
    char name[32];
    uint32_t permissions;
} User;

// 初始化用户
void init_user(User *user, const char *name, uint32_t role) {
    strncpy(user->name, name, sizeof(user->name) - 1);
    user->permissions = role;
}

// 检查用户是否有某个权限
int user_has_perm(const User *user, uint32_t perm) {
    return has_permission(user->permissions, perm);
}

你看,一个uint32_t就搞定了所有权限。如果用布尔数组,你得写一堆if-else,而且扩展起来很麻烦。

15.5 权限的批量操作

有时候我们需要一次性给用户添加或移除多个权限。位运算同样可以轻松搞定。

// 批量添加权限
uint32_t add_permissions(uint32_t current, uint32_t perms) {
    return current | perms;
}

// 批量移除权限
uint32_t remove_permissions(uint32_t current, uint32_t perms) {
    return current & ~perms;
}

// 检查是否拥有全部指定权限
int has_all_permissions(uint32_t current, uint32_t perms) {
    return (current & perms) == perms;
}

// 检查是否拥有任意一个指定权限
int has_any_permission(uint32_t current, uint32_t perms) {
    return (current & perms) != 0;
}

注意:批量移除权限时,一定要用 current & ~perms。我曾经见过有人写成 current ^ perms,那是异或操作,会把原本没有的权限也加上去,结果权限越删越多——这bug查了我一下午。

15.6 权限的位图表示

当权限种类超过32种时,一个uint32_t就不够用了。这时候可以用位图(bitmap)——说白了就是多个uint32_t拼起来。

#define BITMAP_WORDS 4  // 4个32位整数,共128种权限

typedef struct {
    uint32_t bits[BITMAP_WORDS];
} PermissionBitmap;

// 设置某一位
void bitmap_set(PermissionBitmap *bmp, int bit) {
    int word = bit / 32;
    int offset = bit % 32;
    bmp->bits[word] |= (1 << offset);
}

// 清除某一位
void bitmap_clear(PermissionBitmap *bmp, int bit) {
    int word = bit / 32;
    int offset = bit % 32;
    bmp->bits[word] &= ~(1 << offset);
}

// 检查某一位
int bitmap_test(const PermissionBitmap *bmp, int bit) {
    int word = bit / 32;
    int offset = bit % 32;
    return (bmp->bits[word] >> offset) & 1;
}

嗯,这里要注意:位图虽然能支持更多权限,但操作起来比单个整数慢一些。如果你的权限种类不超过32种,用单个uint32_t就够了。

15.7 权限的掩码与过滤

有时候我们需要对权限做掩码过滤。比如,一个用户有所有权限,但某个操作只允许使用部分权限。

// 权限掩码:只允许读和写
#define MASK_READ_WRITE (PERM_READ | PERM_WRITE)

// 过滤权限:只保留掩码允许的部分
uint32_t filter_permissions(uint32_t current, uint32_t mask) {
    return current & mask;
}

// 使用示例
uint32_t user_perm = PERM_READ | PERM_WRITE | PERM_DELETE;
uint32_t allowed = filter_permissions(user_perm, MASK_READ_WRITE);
// allowed = PERM_READ | PERM_WRITE,DELETE被过滤掉了

这个技巧在权限继承和权限覆盖的场景中特别有用。我在做物联网设备管理平台时,就用这个实现了「角色基础权限 + 用户自定义权限」的叠加效果。

15.8 位运算权限管理的优势

特性 位运算方案 传统布尔数组方案
内存占用 4字节(32种权限) 32字节(32个布尔值)
判断速度 1条指令(与运算) 多次内存访问 + 比较
批量操作 1条指令(或/与/取反) 循环遍历
扩展性 轻松扩展到64/128位 需要修改结构体
代码可读性 需要理解位运算 直观易懂

说白了,位运算方案在性能和内存上完胜。代价就是你需要理解位运算的原理——不过看完这一章,你应该已经掌握了。

15.9 避坑指南

我曾经踩过几个坑,分享给你:

  • 不要用有符号整数:位运算操作无符号整数最安全。有符号整数的右移是算术右移还是逻辑右移,C标准没规定,不同编译器行为不同。
  • 注意移位位数不要越界:1 << 31 是合法的,但 1 << 32 在32位系统上是未定义行为。如果你需要第32位,用 (uint32_t)1 << 31 或者用 0x80000000
  • 权限常量用十六进制:我习惯写成 0x010x020x04 这样,比 1 << 0 更直观,而且编译器优化后是一样的。
  • 调试时打印权限:写一个函数把uint32_t按位打印出来,调试权限问题时会省很多时间。

15.10 知识体系总览

下面这张图总结了位运算权限管理的核心逻辑:

位运算权限管理核心逻辑 权限定义 每个权限占用一个二进制位 权限组合 按位或组合成角色权限 用户权限 存储为uint32_t 添加权限 current | perm 移除权限 current & ~perm 检查权限 current & perm 应用场景 用户角色权限 | 文件系统权限 | 物联网设备权限 | 数据库字段权限 优势:内存小 · 速度快 · 易扩展 · 一条指令搞定

15.11 总结

位运算做权限管理,说白了就是用二进制位来存储和操作权限。核心就三个操作:按位或添加、按位与取反移除、按位与检查。

我做了这么多年嵌入式开发,权限管理几乎都用位运算。它快、省内存、容易扩展。你只要记住:权限常量用2的幂次,操作时用无符号整数,检查权限时用 == perm 而不是 != 0

嗯,这一章就到这里。代码不多,但都是实战中提炼出来的精华。你可以在自己的项目里试试,把那些布尔数组换成位运算,感受一下什么叫「一行代码顶十行」。


公众号:蓝海资料掘金营,微信deep3321