gpt4 book ai didi

c - 将几个标志和小枚举作为函数参数传递 : hand-made int bitmasks or small struct?

转载 作者:太空宇宙 更新时间:2023-11-04 02:47:23 25 4
gpt4 key购买 nike

我使用嵌入式东西 (PIC),在我见过的所有 C 代码中,如果函数采用多个标志或少量枚举值,它是作为手工制作的 int 位掩码完成的,就像这样:

/* first flag */
#define MY_FIRST_FLAG (1 << 0)

/* second flag */
#define MY_SECOND_FLAG (1 << 1)

/*
* some param that could have three variants
*/
#define __MY_TRISTATE_OFFSET 2
#define __MY_TRISTATE_MASK (0x03 << __MY_TRISTATE_OFFSET)
#define MY_TRISTATE_ONE (0 << __MY_TRISTATE_OFFSET)
#define MY_TRISTATE_TWO (1 << __MY_TRISTATE_OFFSET)
#define MY_TRISTATE_THREE (2 << __MY_TRISTATE_OFFSET)

/* third flag */
#define MY_THIRD_FLAG (1 << 4)



void my_func(int opts)
{
if (opts & MY_FIRST_FLAG){
/* ... */
}

switch (opts & __MY_TRISTATE_MASK){
case MY_TRISTATE_ONE:
/* ... */
break;
case MY_TRISTATE_TWO:
/* ... */
break;
case MY_TRISTATE_THREE:
/* ... */
break;
default:
/* error! */
break;
}

/* ... */
}

但我不太喜欢这种“极客”方式:

  • 编译时根本没有类型安全,可能会错误地执行以下操作:my_func(MY_TRISTATE_ONE | MY_TRISTATE_TWO); 它会编译。
  • 同样,类型安全:如果我们有另一个带有不同标志的函数,我们可以自由地为另一个函数传递标志,编译器不会在这里提供帮助;
  • 定义所有这些标志时很容易出错;如果我们需要添加更多标志或参数,我们应该非常小心,不要混淆偏移量。
  • 如果有一天我们的三态变成某个五态变量怎么办?然后,我们应该更改它的掩码,并更改后面所有参数的偏移量。如果我们在这里犯了错误,没有人会帮助我们。因此,如果出现问题,我们应该反复检查那部分代码。

这种方法的唯一优点是二进制兼容性:数据在所有平台上以完全相同的方式组织,因此,不同的设备/程序可以使用此接口(interface)进行通信。但是,对我来说,这几乎总是出于担忧:大多数代码只在一个特定的设备上工作,不需要保持与其他世界的二进制兼容性。

所以,对我来说,定义一个小结构看起来要好得多:

enum my_tristate_e {
MY_TRISTATE_ONE,
MY_TRISTATE_TWO,
MY_TRISTATE_THREE,
};

struct my_func_opts_s {
unsigned my_first_flag : 1;
unsigned my_second_flag : 1;
enum my_tristate_e my_tristate : 2;
unsigned my_third_flag : 1;
};


void my_func(struct my_func_opts_s opts)
{
if (opts.my_first_flag){
/* ... */
}

switch (opts.my_tristate){
case MY_TRISTATE_ONE:
/* ... */
break;
case MY_TRISTATE_TWO:
/* ... */
break;
case MY_TRISTATE_THREE:
/* ... */
break;
default:
/* error! */
break;
}

/* ... */
}

这种方法的优点:

  • 我们不能错误地给 my_func 另一个结构(比如,我们有另一个带有另一个函数选项的结构),如果我们尝试编译器会产生错误;
  • 我们不必深入研究所有补偿问题;编译器为我们做了这些:所以,我们不能在这里犯错误;
  • 如果有一天我们的三态变成了五态变量,而我们忘记将字段宽度从 2 更改为 3,编译器会警告我们字段太窄;
  • 由于编译器可以按其认为合适的方式自由实现这些位域,因此它很可能会在任何平台上生成更优化的代码。

同样,如果我们需要程序是二进制兼容的,这种方法将不起作用,但它几乎总是令人担忧。

有人可能会争辩说调用该函数看起来太庞大了:在 int 标志的情况下,我们有:

my_func(MY_TRISTATE_TWO | MY_FIRST_FLAG);

但是在结构的情况下:

my_func2((struct my_func_opts_s){
.my_first_flag = true,
.my_tristate = MY_TRISTATE_TWO,
});

但无论如何,在我看来,强类型安全值得输入。而且,毕竟,有时候我什至更喜欢第二种方式,因为这段代码的 self 文档化程度更高。

我已经检查了两种情况下生成的 MIPS 反汇编(好吧,我不得不稍微更改函数以使它们执行某些操作,否则它们会被编译器优化掉),并且生成的代码几乎相同(优化是-Os 这在嵌入式世界中很常见):

但是,这种方法从未(或几乎从未)使用过。为什么?我错过了什么重要的事情吗?

最佳答案

struct 中使用位域的一个缺点是您不能直接在函数调用处写入标志,除非您可以访问 C99 中的“复合文字”。

假设您有一个如下所示的位域:

struct bit_field {
unsigned int a:1;
unsigned int b:1;
unsigned int c:1;
unsigned int d:1;
};

还有一个使用像这样声明的位域的函数:

void use_bit_field(struct bit_field a);

如果您使用 C99 编写,您可以像以前那样编写函数调用:

use_bit_field((struct bit_field){
.a = 1,
.b = 0,
.c = 1,
.d = 0
});

看起来很不错!但是为了让它工作,我们需要能够使用“复合文字”,这是 C99 的一个特性。不幸的是,您可能已经知道 C99 不像 C89 那样广泛可用。

没有接触过C99的人必须这样写函数调用:

struct bit_field bits = {
1, 1, 0, 1 // EDIT: Initializing the struct step by step is alot less error prone (mentioned in the comments).
};

/* ... */

use_bit_field(bits);

有些人会争辩说远不如:

use_bit_field(BITFIELD_A | BITFIELD_B | BITFIELD_D);

但这当然只是一个偏好问题。

另一个缺点是您失去了在位域上执行位操作的机会。

你不能写:

if (bit_field1 & bit_field2)

检查两个位域是否都打开了相同的位。你必须写一个丑陋的类型转换:

if (*(int8_t *)&bit_field1 & *(int8_t *)&bit_field2)

这些是我能想到的缺点。我个人仍然认为在 struct 中定义位域是更好的方法。

关于c - 将几个标志和小枚举作为函数参数传递 : hand-made int bitmasks or small struct?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25832224/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com