良好的形式:指针 vs. 局部变量 vs. 数组索引

5
如果我的问题显得琐碎,请原谅——我通常是一个控制系统的人(PLC和自动化),但最近发现自己涉足了一些嵌入式微控制器和PC项目。
假设我有一个函数,它接受一个指向“命令字节”数组的指针,通常长度为5或10个字节,如下所示:
char cmd_i2c_read(unsigned char *cmd, unsigned short cmd_len) { ... }

我想解码命令字节(*cmd)。
更好的形式是什么?
  1. Create local variables indicating the purpose of each byte:

    unsigned char device_address = cmd[2];
    unsigned char register_address = cmd[3];
    unsigned char num_bytes = cmd[4];<br>
    // use the local variables:
    if(num_bytes ≤ 0xFF) {
        do_stuff(device_address, register_address, num_bytes);
    }
  2. Create local pointers:

    unsigned char *device_address = &cmd[2];
    unsigned char *register_address = &cmd[3];
    unsigned char *num_bytes = &cmd[4];<br>
    // use the pointers:
    if(*num_bytes ≤ 0xFF) {
        do_stuff(*device_address, *register_address, *num_bytes);
    }
  3. Index the *cmd array directly:
    if(cmd[4] <= 0xFF) { do_stuff(cmd[2], cmd[3], cmd[4]); }


看到我口味的意思了吧 :) - JonH
然后我们可以开始争论if语句是否应该在单行上,或者0xFF是否应该成为常量。 :-) - Kevin Gale
7个回答

4

选项1虽然清晰,但有点啰嗦。我一点也不喜欢选项2,而选项3则很难理解。个人认为,这种情况最好使用结构体。

typedef struct  {
   unsigned char whatever[2];
   unsigned char device_address;
   unsigned char register_address;
   unsigned char num_bytes;
   }  CMD;

CMD * pcmd = (CMD *)&cmd[0];

// use the local variables:
if(num_bytes ≤ 0xFF) {
    do_stuff(pcmd->device_address, pcmd->register_address, pcmd->num_bytes);

这是否假定 CMD 可以是任意对齐的? - Alok Singhal
"强制结构填充警告":当然,这应该小心地进行。结构体在内存中的元素并不一定是连续的,因此结构体可能无法完全匹配输入数据的布局。如果您的编译器具有像gcc的__attribute__((packed))这样的东西,那么使用它至少是一个好主意。这个警告(就像现在这样)甚至可能不能在同一编译器的不同版本之间移植。" - Tim Schaeffer
1
使用类似这样的结构体叠加可以避免#1中的数据重复、#1和#2中的额外变量以及#3中的歧义。作为额外的好处,它强调了这些值之间的关联性。 - bta
@John:我想知道这是否符合标准。 - Alok Singhal
@Alok:标准并不会详细说明这种情况,只是说即使插入填充也不会违反标准。但编译器的作者并不是傻瓜,标准也不是约束他们的唯一因素。 - John Knoeller
显示剩余2条评论

3

我更喜欢第三项,但这取决于个人偏好。


3

在我看来,第一种方式更好。相比于第三种方式,它更容易阅读,因为你不需要知道函数的签名就能理解参数。

对于更大的数据结构,我会选择第二种方式,即使用指针,这样就不需要复制值。但在这种情况下,差异并不显著,而且我认为 */& 稍微降低了可读性。


+1 我同意,device_address 比 cmd[2] 更容易阅读。 - Kevin Gale
4
如果使用预定义常量来代替数字索引,那么#3的阅读将会更加容易。 - Javier
为什么2比1更擅长不浪费空间?在1中,创建了三个“unsigned char”,而在2中创建了三个“unsigned char *”。 - David Thornley
@Kevin - 但这只是因为你知道cmd [2]是设备地址,那有什么区别呢?我可以很容易地将其分配给临时变量device_address来实现愿望...我的观点是程序员知道cmd [2]或cmd [index]代表特定类型的特定值。否则,他/她就不会知道如何调用do_stuff() //在这种情况下... - JonH
@JonH:如果你第一次阅读代码,你怎么知道第3步中的cmd[2]是什么?在1和2中,你不需要任何其他知识就可以理解代码的作用(假设do_stuff有一个有意义的名称),这在我看来更好和更易读。 - Wookai
显示剩余2条评论

2

当然是第一种方式。这样可以让代码更易读,而使用指针代替普通变量只会使事情变得更加复杂。无论你使用哪种方式,编译器都会优化掉微不足道的性能提升。


1
使用一个愚蠢的编译器,在大多数情况下,#3 将是最慢的。 - Javier
你也可以将本地变量声明为 const - 你会发现启用优化后,编译器会将这些变量优化掉。 - caf

1
如果是我,我会将其写成#3,但使用符号名称来表示数组索引以提高可读性:
#define DEVICE_ADDRESS 2
#define REGISTER_ADDRESS 3
#define NUM_BYTES 4

if (cmd[NUM_BYTES] <= 0xFF) {
    do_stuff(cmd[DEVICE_ADDRESS], cmd[REGISTER_ADDRESS], cmd[NUM_BYTES]);
}

当然,您可以使用const ints、enums等替换宏。我喜欢这个选项的原因是其他两个选项需要使用额外的本地变量。编译器可能会根据其实现和您选择的优化级别来优化它们,但它们对我来说似乎只是不必要的额外间接层。


0

我最喜欢1,比其他的易读得多。

如果性能是一个重要问题(我的意思是像“我们需要尽可能地提高0.01%的速度”这样重要),你将不得不进行基准测试,因为它真的取决于编译器哪个序列会产生最快的代码(1可能有不必要的复制,2可能包含由于指针别名限制而导致的多余负载,3可能会导致多余的负载,如果寄存器分配器真的混乱)


0
对我来说,这取决于您将如何使用缓冲区,
尽管所有事情都是平等的,但我可能更喜欢缓冲区是一个结构体(假设对齐警告),如果失败,则直接索引缓冲区,但不要使用魔术数字。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接