【问题标题】:What are the effects of this define preprocessor directive?这个定义预处理器指令的效果是什么?
【发布时间】:2020-01-30 18:08:02
【问题描述】:

我不明白这一点,但似乎这是一个函数:

#define GET_BE2(ptr) ((uint16_t)(ptr)[0] << 8 | (ptr)[1])

我需要理解这条线的含义,但我不能。 据我了解,您可以这样写:

int *my_ptr = 1234;
int var;
var = GET_BE2(my_ptr); 

这可能是非常错误的,但我只是想清楚地说明我不明白的地方。我也说不清是什么

   ((uint16_t)(ptr)[0] << 8 | (ptr)[1])

正在做。 ptr 没有提到我们正在使用数组,那么为什么我们可以使用 []?然后我可以告诉我们正在向左移动 8 位,or-ing 可能接下来的 8 位是什么。

【问题讨论】:

  • 称为类函数宏。
  • 您可以将[] 下标与任何指针一起使用。是否有效是一个单独的讨论,但等价 *(ptr + idx) === ptr[idx] === idx[ptr] 牢牢地内置在 C 中。使用 idx[ptr] 不是好的风格;你应该只在混淆 C 竞赛和类似的上下文中看到它。
  • @RobertoCaboni 虽然我认为您的编辑建议是有道理的,但在我看来,它对原始帖子的“重新解释”有点过多。请以这种方式评论并建议对 OP 进行有意义的标题更改。
  • @Yunnosch — OP 询问“ptr 没有提到我们正在使用数组,那么我们为什么可以使用 []?”我的评论解决了
  • @JonathanLeffler 你似乎在读一些我也看不到的东西。不过没关系。

标签: c pointers c-preprocessor


【解决方案1】:

分析宏

编写的宏旨在用作函数。

#define GET_BE2(ptr) ((uint16_t)(ptr)[0] << 8 | (ptr)[1])

可能的意图是将两个连续的字节值转换为一个 16 位整数,假设字节以大端顺序显示,因此ptr[0] 是更高有效字节,ptr[1] 是更低有效字节。

虽然没有显示文档,但您应该将指针传递给(至少)两个整数的数组,然后对其进行位操作以产生结果。因为它是一个宏,所以没有明确的类型约束。因此,它可以被以下任何一种调用:

signed   char      ptr0[2] = { 0x23, 0x37 };
signed   short     ptr1[2] = { 0x23, 0x37 };
signed   int       ptr2[2] = { 0x23, 0x37 };
signed   long      ptr3[2] = { 0x23, 0x37 };
signed   long long ptr4[2] = { 0x23, 0x37 };
unsigned char      ptr5[2] = { 0x23, 0x37 };
unsigned short     ptr6[2] = { 0x23, 0x37 };
unsigned int       ptr7[2] = { 0x23, 0x37 };
unsigned long      ptr8[2] = { 0x23, 0x37 };
unsigned long long ptr9[2] = { 0x23, 0x37 };

鉴于显示的数据值,它甚至会从所有这些中产生相同的结果。

宏的问题

但是,如果任何有符号值是负数,或者如果(转换后的)负值被分配给其他任何无符号类型的第二个元素(如上所示 0x37unsigned char(所以 @987654327 @..ptr9),那么您将无法得到预期的结果。

毫无疑问,ptr 应该是指向两个相邻unsigned char 值的指针。然后宏产生一个值,其中ptr[0] 中的值是uint16_t 值的高8 位,ptr[1] 中的值是uint16_t 值的低8 位。结果将是 0x2337。

如果类型大于char 或者如果类型是signed char(或者普通的char 类型是有符号的),并且如果ptr[1] 中的值是负数,你会得到与预期不同的结果。

演示宏的缺点

这是一个测试程序(其中有相当痛苦的重复——但摆脱重复也很痛苦,对于显示的两个测试用例来说不值得):

#include <stdio.h>
#include <stdint.h>

#define GET_BE2(ptr) ((uint16_t)(ptr)[0] << 8 | (ptr)[1])

static void test1(void)
{
    signed   char      ptr0[2] = { 0x23, 0x37 };
    signed   short     ptr1[2] = { 0x23, 0x37 };
    signed   int       ptr2[2] = { 0x23, 0x37 };
    signed   long      ptr3[2] = { 0x23, 0x37 };
    signed   long long ptr4[2] = { 0x23, 0x37 };
    unsigned char      ptr5[2] = { 0x23, 0x37 };
    unsigned short     ptr6[2] = { 0x23, 0x37 };
    unsigned int       ptr7[2] = { 0x23, 0x37 };
    unsigned long      ptr8[2] = { 0x23, 0x37 };
    unsigned long long ptr9[2] = { 0x23, 0x37 };

    unsigned long long result;

    printf("Two positive elements:\n");
    result = GET_BE2(ptr0);
    printf("ptr0[0] = 0x%.4hhX  ptr0[1] = 0x%.16hhX  ", ptr0[0], ptr0[1]);
    printf("signed   char      = 0x%.16llX\n", result);
    result = GET_BE2(ptr1);
    printf("ptr1[0] = 0x%.4hX  ptr1[1] = 0x%.16hX  ", ptr1[0], ptr1[1]);
    printf("signed   short     = 0x%.16llX\n", result);
    result = GET_BE2(ptr2);
    printf("ptr2[0] = 0x%.4X  ptr2[1] = 0x%.16X  ", ptr2[0], ptr2[1]);
    printf("signed   int       = 0x%.16llX\n", result);
    result = GET_BE2(ptr3);
    printf("ptr3[0] = 0x%.4lX  ptr3[1] = 0x%.16lX  ", ptr3[0], ptr3[1]);
    printf("signed   long      = 0x%.16llX\n", result);
    result = GET_BE2(ptr4);
    printf("ptr4[0] = 0x%.4llX  ptr4[1] = 0x%.16llX  ", ptr4[0], ptr4[1]);
    printf("signed   long long = 0x%.16llX\n", result);

    result = GET_BE2(ptr5);
    printf("ptr5[0] = 0x%.4hhX  ptr5[1] = 0x%.16hhX  ", ptr5[0], ptr5[1]);
    printf("unsigned char      = 0x%.16llX\n", result);
    result = GET_BE2(ptr6);
    printf("ptr6[0] = 0x%.4hX  ptr6[1] = 0x%.16hX  ", ptr6[0], ptr6[1]);
    printf("unsigned short     = 0x%.16llX\n", result);
    result = GET_BE2(ptr7);
    printf("ptr7[0] = 0x%.4X  ptr7[1] = 0x%.16X  ", ptr7[0], ptr7[1]);
    printf("unsigned int       = 0x%.16llX\n", result);
    result = GET_BE2(ptr8);
    printf("ptr8[0] = 0x%.4lX  ptr8[1] = 0x%.16lX  ", ptr8[0], ptr8[1]);
    printf("unsigned long      = 0x%.16llX\n", result);
    result = GET_BE2(ptr9);
    printf("ptr9[0] = 0x%.4llX  ptr9[1] = 0x%.16llX  ", ptr9[0], ptr9[1]);
    printf("unsigned long long = 0x%.16llX\n", result);
}

static void test2(void)
{
    signed   char      ptr0[2] = { 0x23, -0x00000037 };
    signed   short     ptr1[2] = { 0x23, -0x00003A37 };
    signed   int       ptr2[2] = { 0x23, -0x004B3A37 };
    signed   long      ptr3[2] = { 0x23, -0x5C4B3A37 };
    signed   long long ptr4[2] = { 0x23, -0x5C4B3A37 };
    unsigned char      ptr5[2] = { 0x23, -0x00000037 };
    unsigned short     ptr6[2] = { 0x23, -0x00003A37 };
    unsigned int       ptr7[2] = { 0x23, -0x4B4B3A37 };
    unsigned long      ptr8[2] = { 0x23, -0x5C4B3A37 };
    unsigned long long ptr9[2] = { 0x23, -0x5C4B3A37 };

    unsigned long long result;

    printf("One positive element, one negative element:\n");
    result = GET_BE2(ptr0);
    printf("ptr0[0] = 0x%.4hhX  ptr0[1] = 0x%.16hhX  ", ptr0[0], ptr0[1]);
    printf("signed   char      = 0x%.16llX\n", result);
    result = GET_BE2(ptr1);
    printf("ptr1[0] = 0x%.4hX  ptr1[1] = 0x%.16hX  ", ptr1[0], ptr1[1]);
    printf("signed   short     = 0x%.16llX\n", result);
    result = GET_BE2(ptr2);
    printf("ptr2[0] = 0x%.4X  ptr2[1] = 0x%.16X  ", ptr2[0], ptr2[1]);
    printf("signed   int       = 0x%.16llX\n", result);
    result = GET_BE2(ptr3);
    printf("ptr3[0] = 0x%.4lX  ptr3[1] = 0x%.16lX  ", ptr3[0], ptr3[1]);
    printf("signed   long      = 0x%.16llX\n", result);
    result = GET_BE2(ptr4);
    printf("ptr4[0] = 0x%.4llX  ptr4[1] = 0x%.16llX  ", ptr4[0], ptr4[1]);
    printf("signed   long long = 0x%.16llX\n", result);

    result = GET_BE2(ptr5);
    printf("ptr5[0] = 0x%.4hhX  ptr5[1] = 0x%.16hhX  ", ptr5[0], ptr5[1]);
    printf("unsigned char      = 0x%.16llX\n", result);
    result = GET_BE2(ptr6);
    printf("ptr6[0] = 0x%.4hX  ptr6[1] = 0x%.16hX  ", ptr6[0], ptr6[1]);
    printf("unsigned short     = 0x%.16llX\n", result);
    result = GET_BE2(ptr7);
    printf("ptr7[0] = 0x%.4X  ptr7[1] = 0x%.16X  ", ptr7[0], ptr7[1]);
    printf("unsigned int       = 0x%.16llX\n", result);
    result = GET_BE2(ptr8);
    printf("ptr8[0] = 0x%.4lX  ptr8[1] = 0x%.16lX  ", ptr8[0], ptr8[1]);
    printf("unsigned long      = 0x%.16llX\n", result);
    result = GET_BE2(ptr9);
    printf("ptr9[0] = 0x%.4llX  ptr9[1] = 0x%.16llX  ", ptr9[0], ptr9[1]);
    printf("unsigned long long = 0x%.16llX\n", result);
}

int main(void)
{
    test1();
    test2();
    return 0;
}

在运行 macOS Mojave 10.14.6 和 GCC 9.2.0 和 XCode 11.3.1 的 MacBook Pro 上,输出如下:

Two positive elements:
ptr0[0] = 0x0023  ptr0[1] = 0x0000000000000037  signed   char      = 0x0000000000002337
ptr1[0] = 0x0023  ptr1[1] = 0x0000000000000037  signed   short     = 0x0000000000002337
ptr2[0] = 0x0023  ptr2[1] = 0x0000000000000037  signed   int       = 0x0000000000002337
ptr3[0] = 0x0023  ptr3[1] = 0x0000000000000037  signed   long      = 0x0000000000002337
ptr4[0] = 0x0023  ptr4[1] = 0x0000000000000037  signed   long long = 0x0000000000002337
ptr5[0] = 0x0023  ptr5[1] = 0x0000000000000037  unsigned char      = 0x0000000000002337
ptr6[0] = 0x0023  ptr6[1] = 0x0000000000000037  unsigned short     = 0x0000000000002337
ptr7[0] = 0x0023  ptr7[1] = 0x0000000000000037  unsigned int       = 0x0000000000002337
ptr8[0] = 0x0023  ptr8[1] = 0x0000000000000037  unsigned long      = 0x0000000000002337
ptr9[0] = 0x0023  ptr9[1] = 0x0000000000000037  unsigned long long = 0x0000000000002337
One positive element, one negative element:
ptr0[0] = 0x0023  ptr0[1] = 0x00000000000000C9  signed   char      = 0xFFFFFFFFFFFFFFC9
ptr1[0] = 0x0023  ptr1[1] = 0x000000000000C5C9  signed   short     = 0xFFFFFFFFFFFFE7C9
ptr2[0] = 0x0023  ptr2[1] = 0x00000000FFB4C5C9  signed   int       = 0xFFFFFFFFFFB4E7C9
ptr3[0] = 0x0023  ptr3[1] = 0xFFFFFFFFA3B4C5C9  signed   long      = 0xFFFFFFFFA3B4E7C9
ptr4[0] = 0x0023  ptr4[1] = 0xFFFFFFFFA3B4C5C9  signed   long long = 0xFFFFFFFFA3B4E7C9
ptr5[0] = 0x0023  ptr5[1] = 0x00000000000000C9  unsigned char      = 0x00000000000023C9
ptr6[0] = 0x0023  ptr6[1] = 0x000000000000C5C9  unsigned short     = 0x000000000000E7C9
ptr7[0] = 0x0023  ptr7[1] = 0x00000000B4B4C5C9  unsigned int       = 0x00000000B4B4E7C9
ptr8[0] = 0x0023  ptr8[1] = 0xFFFFFFFFA3B4C5C9  unsigned long      = 0xFFFFFFFFA3B4E7C9
ptr9[0] = 0x0023  ptr9[1] = 0xFFFFFFFFA3B4C5C9  unsigned long long = 0xFFFFFFFFA3B4E7C9

对于数组元素中的值足够小以适合范围 0..SCHAR_MAX (127) 的简单情况,输出符合预期,因为当值提升时,没有符号位可用于使问题复杂化。

宏失败的原因

当值不是那么小时,表达式会有意想不到的结果。

#define GET_BE2(ptr) ((uint16_t)(ptr)[0] << 8 | (ptr)[1])

让我们再添加几个括号:

#define GET_BE2(ptr) ((((uint16_t)(ptr)[0]) << 8) | (ptr)[1])

移位运算符被赋予 LHS 操作数的提升值。这意味着ptr[0] 首先转换为uint16_t 值,然后转换为int(我am假设sizeof(int) != sizeof(uint16_t) 是一个“普通”机器。结果向左移动8 位。| 运算符的 RHS 也提升为 int;将两个 int 值组合并产生一个结果。注意将 signed char 转换为 int 符号扩展值.(我am假设 2 的补码表示;如果您担心 1 的补码或符号大小,请调整测试代码等以适应您的环境。)

这些因素导致在移位和/或运算符的操作数中设置各种无关位,从而导致“意外”结果。

修复宏

为了使宏代码安全,需要更加仔细地编写宏。它可以使用0xFF 进行掩码,也可以转换为uint8_t(或unsigned char)。

#define GET_BE2(ptr) (uint16_t)((((ptr)[0] & 0xFF) << 8) | ((ptr)[1] & 0xFF))

#define GET_BE2(ptr) ((((uint8_t)(ptr)[0]) << 8) | (uint8_t)(ptr)[1])

使用其中任何一个,输出都是相同且自洽的:

Two positive elements:
ptr0[0] = 0x0023  ptr0[1] = 0x0000000000000037  signed   char      = 0x0000000000002337
ptr1[0] = 0x0023  ptr1[1] = 0x0000000000000037  signed   short     = 0x0000000000002337
ptr2[0] = 0x0023  ptr2[1] = 0x0000000000000037  signed   int       = 0x0000000000002337
ptr3[0] = 0x0023  ptr3[1] = 0x0000000000000037  signed   long      = 0x0000000000002337
ptr4[0] = 0x0023  ptr4[1] = 0x0000000000000037  signed   long long = 0x0000000000002337
ptr5[0] = 0x0023  ptr5[1] = 0x0000000000000037  unsigned char      = 0x0000000000002337
ptr6[0] = 0x0023  ptr6[1] = 0x0000000000000037  unsigned short     = 0x0000000000002337
ptr7[0] = 0x0023  ptr7[1] = 0x0000000000000037  unsigned int       = 0x0000000000002337
ptr8[0] = 0x0023  ptr8[1] = 0x0000000000000037  unsigned long      = 0x0000000000002337
ptr9[0] = 0x0023  ptr9[1] = 0x0000000000000037  unsigned long long = 0x0000000000002337
One positive element, one negative element:
ptr0[0] = 0x0023  ptr0[1] = 0x00000000000000C9  signed   char      = 0x00000000000023C9
ptr1[0] = 0x0023  ptr1[1] = 0x000000000000C5C9  signed   short     = 0x00000000000023C9
ptr2[0] = 0x0023  ptr2[1] = 0x00000000FFB4C5C9  signed   int       = 0x00000000000023C9
ptr3[0] = 0x0023  ptr3[1] = 0xFFFFFFFFA3B4C5C9  signed   long      = 0x00000000000023C9
ptr4[0] = 0x0023  ptr4[1] = 0xFFFFFFFFA3B4C5C9  signed   long long = 0x00000000000023C9
ptr5[0] = 0x0023  ptr5[1] = 0x00000000000000C9  unsigned char      = 0x00000000000023C9
ptr6[0] = 0x0023  ptr6[1] = 0x000000000000C5C9  unsigned short     = 0x00000000000023C9
ptr7[0] = 0x0023  ptr7[1] = 0x00000000B4B4C5C9  unsigned int       = 0x00000000000023C9
ptr8[0] = 0x0023  ptr8[1] = 0xFFFFFFFFA3B4C5C9  unsigned long      = 0x00000000000023C9
ptr9[0] = 0x0023  ptr9[1] = 0xFFFFFFFFA3B4C5C9  unsigned long long = 0x00000000000023C9

使用内联函数

编写宏的人不太可能打算将它与char *unsigned char *signed char * 以外的任何东西一起使用(尽管很可能甚至没有考虑signed char *)。因此,最好使用一个函数——最好是inline 函数——来完成这项工作。这会迫使您使用正确的类型(或强制转换错误的类型):

static inline uint16_t get_be2(const unsigned char *ptr)
{
    return (ptr[0] << 8) | ptr[1];
}

如果由于某种原因,您的编译器过于陈旧以至于无法接受 inline(尽管在整个千年中这一直是标准 C 的一部分,但周围有这样的编译器),那么只需省略inline。编译器甚至可以自行将函数内联;它可以看到它的使用位置,因为它仅限于当前文件,并且可以决定避免实际函数调用的开销是有意义的。这是一个大大减少的测试用例——尽管它可以很容易地重新设计以消除大量重复。请注意使用 signed char 对调用的显式强制转换。

#include <stdio.h>
#include <stdint.h>

static inline uint16_t get_be2(const unsigned char *ptr)
{
    return (ptr[0] << 8) | ptr[1];
}
#define GET_BE2(ptr) get_be2(ptr)

static void test1(void)
{
    signed   char      ptr0[2] = { 0x23, 0x37 };
    unsigned char      ptr5[2] = { 0x23, 0x37 };

    unsigned long long result;

    printf("Two positive elements:\n");
    result = GET_BE2((unsigned char *)ptr0);
    printf("ptr0[0] = 0x%.4hhX  ptr0[1] = 0x%.16hhX  ", ptr0[0], ptr0[1]);
    printf("signed   char      = 0x%.16llX\n", result);

    result = GET_BE2(ptr5);
    printf("ptr5[0] = 0x%.4hhX  ptr5[1] = 0x%.16hhX  ", ptr5[0], ptr5[1]);
    printf("unsigned char      = 0x%.16llX\n", result);
}

static void test2(void)
{
    signed   char      ptr0[2] = { 0x23, -0x00000037 };
    unsigned char      ptr5[2] = { 0x23, -0x00000037 };

    unsigned long long result;

    printf("One positive element, one negative element:\n");
    result = GET_BE2((unsigned char *)ptr0);
    printf("ptr0[0] = 0x%.4hhX  ptr0[1] = 0x%.16hhX  ", ptr0[0], ptr0[1]);
    printf("signed   char      = 0x%.16llX\n", result);

    result = GET_BE2(ptr5);
    printf("ptr5[0] = 0x%.4hhX  ptr5[1] = 0x%.16hhX  ", ptr5[0], ptr5[1]);
    printf("unsigned char      = 0x%.16llX\n", result);
}

int main(void)
{
    test1();
    test2();
    return 0;
}

输出:

Two positive elements:
ptr0[0] = 0x0023  ptr0[1] = 0x0000000000000037  signed   char      = 0x0000000000002337
ptr5[0] = 0x0023  ptr5[1] = 0x0000000000000037  unsigned char      = 0x0000000000002337
One positive element, one negative element:
ptr0[0] = 0x0023  ptr0[1] = 0x00000000000000C9  signed   char      = 0x00000000000023C9
ptr5[0] = 0x0023  ptr5[1] = 0x00000000000000C9  unsigned char      = 0x00000000000023C9

普通 charunsigned charsigned char

共有三种不同的(单字节)字符类型:(普通)charsigned charunsigned char。普通的char 类型可以有符号或无符号;这是一个必须记录在案的实施决策。我没有费心在解释中显示char,因为它的行为与signed char(这是它在Mac 上的行为方式)或unsigned char 之一相同。然而,在实践中,代码通常使用普通的char 编写。如果您修改函数以获取普通的 char 指针,则无论普通的 char 类型是有符号还是无符号,都必须确保它正常工作。在这种情况下,您可以将传入的const char *ptr 转换为const unsigned char *uptr = (unsigned char *)ptr; 并引用uptr[0]uptr[1],或者像在固定宏变体中一样添加转换或掩码。

首选解决方案

使用inline 函数。它强制执行类型正确性。它完全避免了宏的问题。而且,由于这个函数足够小,编译器几乎可以肯定能够内联代码,与宏版本相比,它是免费的。

【讨论】:

  • inline 函数替换宏是一种减少所有类型恶作剧的方法,应该强烈考虑
  • @M.M — 我在原来的答案上费了很长时间,以至于我忘记了我打算讨论 inline 函数。感谢您的提醒——我会解决一些问题,将其作为一个明智的选择。当然,它也大大减少了测试用例的数量。
  • 这个很好的答案表明,有时,如果坚持不懈,一个可以结束的候选问题可以被挽救并产生有趣的答案。我不再提供答案,但在这种情况下,这是一件好事,因为我永远无法与这个答案竞争。 ;)(此评论可以在阅读后立即标记为删除)
猜你喜欢
  • 2012-06-29
  • 1970-01-01
  • 2020-04-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多