分析宏
编写的宏旨在用作函数。
#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 };
鉴于显示的数据值,它甚至会从所有这些中产生相同的结果。
宏的问题
但是,如果任何有符号值是负数,或者如果(转换后的)负值被分配给其他任何无符号类型的第二个元素(如上所示 0x37)unsigned 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
普通 char 与 unsigned char 和 signed char
共有三种不同的(单字节)字符类型:(普通)char、signed char 和 unsigned 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 函数。它强制执行类型正确性。它完全避免了宏的问题。而且,由于这个函数足够小,编译器几乎可以肯定能够内联代码,与宏版本相比,它是免费的。