4

以字节为单位查找某些数据的大小是一种常见的操作。

人为的例子:

char *buffer_size(int x, int y, int chan_count, int chan_size)
{
    size_t buf_size = x * y * chan_count * chan_size;  /* <-- this may overflow! */
    char *buf = malloc(buf_size);
    return buf;
}

这里明显的错误是整数会溢出(例如,23171x23171 RGBA 字节缓冲区)。

乘以3 个或更多 值时的提升规则是什么?
(将一对值相乘很简单)

我们可以稳妥行事,直接投:

size_t buf_size = (size_t)x * (size_t)y * (size_t)chan_count * (size_t)chan_size;

另一种选择是添加括号以确保乘法和提升的顺序是可预测的(并且对之间的自动提升按预期工作)......

size_t buf_size = ((((size_t)x * y) * chan_count) * chan_size;

...这行得通,但我的问题是。


是否有确定的方法可以将 3 个或更多值相乘以确保它们会自动提升?
(避免溢出)

或者这是未定义的行为?


笔记...

  • 在这里使用size_t不会防止溢出,它只是防止溢出该类型的最大值。
  • 在给出的示例中,参数也可以是有意义的size_t,但这不是这个问题的重点。
4

1 回答 1

5

在 C(和 C++)中,算术运算符的类型确定如下:

  1. 使用“通常的算术转换”将两个操作数转换为相同的类型。

  2. 这就是结果的类型。

许多期望算术或枚举类型的操作数的二元运算符会以类似的方式导致转换和产生结果类型。目的是产生一个通用类型,这也是结果的类型。这种模式称为通常的算术转换[注 1] [注 2]

没有其他规则,因此具有两个或多个运算符的表达式没有特殊情况。根据语法,每个操作都是独立键入的。

为避免或降低溢出概率,结果类型不会自动加宽;操作数都转换为通用类型“这也是结果的类型”。因此,如果您将两个ints 相乘,结果将是 anint并且溢出将导致未定义的行为。[注3]

语言的语法精确地定义了完整表达式的分组方式,并且需要评估以符合语法。表达式a + b + c必须与表达式具有相同的结果(a + b) + c,因为语法要求分组。编译器可以在它认为合适的时候重新安排计算,只要它可以证明所有有效输入的结果在语义上是相同的。但它不能决定改变任何运算符的结果类型。a + b + c必须具有将通常的算术转换应用于 and 的类型ab然后将它们再次应用于该类型和c. [注4]

通常的算术转换在 C 标准的第 6.3.1.8 节(“通常的算术转换”)和 C++ 的第 5 节(表达式)简介的第 10 段中有详细说明。粗略地说,它是这样的:

  1. 如果两个操作数都是浮点数,则两个操作数都转换为两种类型中较宽的一个;如果一个操作数是浮点数,则将另一个操作数转换为该浮点类型。

  2. 否则,如果两个操作数都是有符号整数类型,则它们都将转换为两种类型中最宽的一个int

  3. 否则,如果两个操作数都是至少与 一样大的无符号整数类型unsigned int,则它们都将转换为两种类型中较宽的类型。

[注5]

现在,以a * b * c * d, where a,和are all为例b,希望产生一个.cdintsize_t

在语法上,该表达式等价于(((a * b) * c) * d),并且通常的算术转换会相应地逐个操作地应用。如果您使用强制转换 ( ) 转换a 为,则转换将被应用,就好像它被括号括起来一样。所以 的操作数和结果将是,因此也将是 and 的结果。换句话说,所有操作数都将转换为无符号值,并且所有乘法都将作为无符号乘法执行。这是明确定义的,但如果任何值恰好为负值,则可能毫无意义。size_t(size_t)a * b * c * d(size_t)a * bsize_t(size_t)a * b * c(size_t)a * b * c * dsize_tsize_t

第二次或第三次乘法都可能超过 a 的容量size_t,但由于size_t它是无符号的,因此计算将以 2 N为模执行 ,其中N是 中的值位数size_t。因此,从避免溢出的意义上说,强制转换是不安全的,但它至少避免了未定义的行为。


笔记

  1. 引用来自 C++ 标准第 5 节第 10 段。C 标准在第 6.3.1.8 节中有一个稍微复杂的版本,因为 C11 包含复杂的算术类型。对于整数(和非复数浮点)操作数,C 和 C++ 具有相同的语义。

  2. 移位运算符是例外,这就是为什么它说“许多二元运算符”。移位运算符的结果类型恰好是其左操作数的(可能提升的)类型,而与右操作数的类型无关。所有按位运算符都仅限于整数,因此涉及实数的“通常算术转换”部分不适用于这些运算符。

  3. 如果将两个unsigned ints 相乘,则结果将为 an unsigned int,并且为所有值定义了计算:

    涉及无符号操作数的计算永远不会溢出,因为无法由结果无符号整数类型表示的结果会以比结果类型可以表示的最大值大一的数字为模减少。(C§6.2.5/9)

  4. C 和 C++ 标准在这一点上都非常清楚,并包含一些示例来说明这一点。通常,有符号整数和浮点运算符都不是关联运算符,因此如果计算仅涉及无符号整数算术,则可能只能重新组合和重新排列计算。

    C 标准第 5.1.2.3 节中的示例 6 和 C++ 标准第 1.9 节中的第 9 段中显示了禁止重新组合整数算术的示例。(同样的例子。)假设我们有一台 16 位ints 的机器,其中有符号溢出会导致陷阱。在这种情况下,a = a + 32760 + b + 5;不能重写为a = (a + b) + 32765;

    如果 a 和 b 的值分别为 -32754 和 -15,则 a + b 之和会产生陷阱,而原始表达式不会;

  5. 这些都是简单的,不麻烦的案例。通常你应该尽量避免其他的,但为了记录:

    一个。在上述发生之前,如果任一操作数的类型比 窄int,则该操作数将被提升为intor unsigned int。通常,它会被提升为int,即使它没有签名。只有当int它的宽度不足以表示该类型的所有值时,操作数才会被提升为unsigned int. 例如,在大多数架构上,unsigned char操作数将被提升为 an int,而不是 an unsigned int(尽管charint的宽度相同的架构是可能的,但它们并不常见。)

    湾。最后,如果一种类型是有符号的而另一种是无符号的,那么它们都将被转换为:

    • 如果它至少与有符号类型一样宽,则为无符号类型。(例如unsigned int* int=> unsigned int

    • 如果它足够宽以容纳无符号类型的所有值,则为有符号类型(例如unsigned int* long long=> long longiflong long比 宽int

    • 如果上述情况都不成立,则对应于有符号类型的无符号类型。

于 2015-05-19T00:19:16.783 回答