按位运算导致意外的可变大小

Cha*_*lts 30 c microcontroller sizeof bitwise-operators

语境

我们正在移植最初使用 8 位 C 编译器为 PIC 微控制器编译的 C 代码。用于防止无符号全局变量(例如,错误计数器)回滚回零的常用习惯用法如下:

if(~counter) counter++;
Run Code Online (Sandbox Code Playgroud)

这里的按位运算符将所有位取反,并且该语句仅在counter小于最大值时才为真。重要的是,无论大小如何,这都有效。

问题

我们现在的目标是使用 GCC 的 32 位 ARM 处理器。我们注意到相同的代码会产生不同的结果。据我们所知,按位补码操作返回的值与我们预期的大小不同。为了重现这一点,我们在 GCC 中编译:

uint8_t i = 0;
int sz;

sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1

sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4
Run Code Online (Sandbox Code Playgroud)

在第一行输出中,我们得到了我们所期望的:i是 1 个字节。然而,按位补码i实际上是四个字节,这会导致问题,因为现在与此比较不会给出预期的结果。例如,如果在做 (wherei是一个正确初始化的uint8_t):

if(~i) i++;
Run Code Online (Sandbox Code Playgroud)

我们会看到 i“环绕”从 0xFF 回到 0x00。与以前的编译器和 8 位 PIC 微控制器的预期工作方式相比,GCC 中的这种行为有所不同。

我们知道我们可以通过像这样转换来解决这个问题:

if((uint8_t)~i) i++;
Run Code Online (Sandbox Code Playgroud)

或者,通过

if(i < 0xFF) i++;
Run Code Online (Sandbox Code Playgroud)

然而,在这两种变通方法中,变量的大小必须是已知的,并且对于软件开发人员来说很容易出错。这些类型的上限检查发生在整个代码库中。有变量多种尺寸(例如,uint16_tunsigned char等),并在工作,不然代码库改变这些并不是我们期待的东西。

我们对问题的理解是否正确,是否有可用的选项来解决这个问题,不需要重新访问我们使用过这个习语的每个案例?我们的假设是否正确,像按位补码这样的操作应该返回与操作数大小相同的结果?这似乎会中断,具体取决于处理器架构。我觉得我正在服用疯狂的药丸,而 C 应该比这更便携。同样,我们对此的理解可能是错误的。

从表面上看,这似乎不是一个大问题,但这个以前工作的习惯用法在数百个地方使用,我们渴望在进行昂贵的更改之前了解这一点。


注意:这里有一个看似相似但不完全重复的问题:char 上的按位运算给出 32 位结果

我没有看到那里讨论的问题的实际症结所在,即按位补码的结果大小与传递给运算符的结果大小不同。

dbu*_*ush 30

您所看到的是整数提升的结果。在表达式中使用整数值的大多数情况下,如果值的类型小于int该值,则将其提升为int. 这记录在C 标准的第 6.3.1.1p2 节中:

以下可用于表达式中的任何地方intunsigned int可使用的地方

  • 具有整数类型(除了intor unsigned int)的对象或表达式,其整数转换等级小于或等于intand的等级unsigned int
  • 型的位域_Boolint ,符号整数, or无符号int`。

如果 anint可以表示原始类型的所有值(受宽度限制,对于位域),则将该值转换为int; 否则,它被转换为 unsigned int。这些称为整数促销。整数提升不会改变所有其他类型。

因此,如果变量具有类型uint8_t和值 255,则int在执行操作之前,对其使用除强制转换或赋值之外的任何运算符将首先将其转换为具有值 255 的类型。这就是为什么sizeof(~i)给你 4 而不是 1。

第 6.5.3.3 节描述了整数提升适用于~运算符:

~运算符的结果是其(提升的)操作数的按位补码(即,当且仅当未设置转换操作数中的相应位时,才设置结果中的每一位)。对操作数执行整数提升,结果具有提升的类型。如果提升的类型是无符号类型,则表达式~E等效于该类型中可表示的最大值减去E

所以假设一个 32 位int,如果counter有 8 位值0xff它被转换为 32 位值0x000000ff,并应用~到它给你0xffffff00

处理这个问题的最简单的方法可能是在不知道类型的情况下检查递增后值是否为 0,如果是则递减它。

if (!++counter) counter--;
Run Code Online (Sandbox Code Playgroud)

无符号整数的环绕适用于两个方向,因此将值递减为 0 会得到最大的正值。

  • 无论你怎么写,这都是丑陋且不可读的。如果你必须使用这样的习惯用法,**请每个维护程序员帮忙,并将其包装为内联函数**:类似于“increment_unsigned_without_wraparound”或“increment_with_saturation”。就我个人而言,我会使用通用的三操作数“clamp”函数。 (15认同)
  • 另外,您不能将其设为函数,因为它对于不同的参数类型必须有不同的行为。您必须使用[类型通用宏](http://port70.net/~nsz/c/c11/n1570.html#6.5.1.1)。 (5认同)

bru*_*uno 7

sizeof(i) 中;你请求变量i的大小,所以 1

sizeof(~i); 您请求表达式类型的大小,即int,在您的情况下为 4


使用

如果(〜我)

要知道是否不重视 255(在您使用 uint8_t 的情况下)不是很易读,只需执行

if (i != 255)
Run Code Online (Sandbox Code Playgroud)

你将拥有一个可移植和可读的代码


有多种大小的变量(例如,uint16_t 和 unsigned char 等)

要管理任何大小的 unsigned :

if (i != (((uintmax_t) 2 << (sizeof(i)*CHAR_BIT-1)) - 1))
Run Code Online (Sandbox Code Playgroud)

表达式是常量,因此在编译时计算。

#include <limits.h>用于CHAR_BIT#include <stdint.h>用于uintmax_t

  • 该问题明确指出他们有多种尺寸需要处理,因此 `!= 255` 是不够的。 (3认同)
  • 为了安全地使用更宽的类型,可以使用“((uintmax_t) 2 &lt;&lt; sizeof(i)*CHAR_BIT-1) - 1”。 (2认同)

Eri*_*hil 5

x考虑到x某种无符号整数类型,以下是实现“将 1 添加到但限制在最大可表示值”的几个选项:

  1. 添加一个当且仅当x小于其类型中可表示的最大值:

    x += x < Maximum(x);
    
    Run Code Online (Sandbox Code Playgroud)

    的定义见以下条目Maximum。这种方法很有可能被编译器优化为高效指令,例如比较、某种形式的条件设置或移动以及添加。

  2. 与类型的最大值进行比较:

    if (x < ((uintmax_t) 2u << sizeof x * CHAR_BIT - 1) - 1) ++x
    
    Run Code Online (Sandbox Code Playgroud)

    (这计算 2 N,其中N是 中的位数x,通过将 2 移位N ?1 位。我们这样做而不是移位 1 N位,因为按类型中的位数的移位不是由 C 定义的标准。这个CHAR_BIT宏可能有些人不熟悉;它是一个字节中的位数,因此sizeof x * CHAR_BIT是类型中的位数x。)

    这可以根据美学和清晰度的需要包装在宏中:

    #define Maximum(x) (((uintmax_t) 2u << sizeof (x) * CHAR_BIT - 1) - 1)
    if (x < Maximum(x)) ++x;
    
    Run Code Online (Sandbox Code Playgroud)
  3. x如果它回零,则增加并更正,使用if

    if (!++x) --x; // !++x is true if ++x wraps to zero.
    
    Run Code Online (Sandbox Code Playgroud)
  4. x如果它回零,则使用表达式递增并更正:

    ++x; x -= !x;
    
    Run Code Online (Sandbox Code Playgroud)

    这名义上是无分支的(有时有利于性能),但编译器可以实现与上述相同的实现,如果需要,可以使用分支,但如果目标体系结构具有合适的指令,则可能使用无条件指令。

  5. 使用上述宏的无分支选项是:

    x += 1 - x/Maximum(x);
    
    Run Code Online (Sandbox Code Playgroud)

    如果x是其类型的最大值,则计算结果为x += 1-1。否则,它是x += 1-0。然而,在许多架构上,划分有点慢。根据编译器和目标体系结构,编译器可以将其优化为无需除法的指令。

  • @CodyGray:“sizeof x”无法在 C 函数内部实现,因为“x”必须是具有某种固定类型的参数(或其他表达式)。它无法生成调用者使用的任何参数类型的大小。一个宏就可以。 (2认同)