运营商优先级和自动升级(避免溢出)

ide*_*n42 4 c casting integer-promotion

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

举例:

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;
}
Run Code Online (Sandbox Code Playgroud)

这里明显的错误是int会溢出(例如23171x23171 RGBA字节缓冲区).

乘以3个或更多 值时,促销的规则是什么?
(乘以一对值很简单)

我们可以安全地播放它:

size_t buf_size = (size_t)x * (size_t)y * (size_t)chan_count * (size_t)chan_size;
Run Code Online (Sandbox Code Playgroud)

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

size_t buf_size = ((((size_t)x * y) * chan_count) * chan_size;
Run Code Online (Sandbox Code Playgroud)

......哪个有效,但我的问题是.


是否存在确定性方法来乘以3个或更多值以确保它们会自动提升?
(以避免溢出)

或者这是未定义的行为?


笔记...

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

ric*_*ici 5

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

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

  2. 这是结果的类型.

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

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

结果类型不会自动加宽,以避免或减少溢出的可能性; 操作数都转换为通用类型"也是结果的类型".因此,如果乘以2 int,结果将是a int和溢出将导致未定义的行为.[注3]

语言的语法精确地定义了完整表达式的分组方式,并且需要进行评估以符合语法.表达式a + b + c必须与表达式具有相同的结果(a + b) + c,因为语法要求分组.编译器可以在其认为合适的情况下重新排列计算,前提是它可以证明所有有效输入的结果在语义上是相同的.但它无法决定更改任何运算符的结果类型.a + b + c必须具有从施加通常的算术转换的类型的结果的类型ab,然后再次把它们应用到该类型和种类c.[注4]

通常的算术转换在C标准的§6.3.1.8("常用算术转换")和C++的§5(表达式)的引言的第10段中有详细说明.粗略地说,它是这样的:

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

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

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

[注5]

现在,考虑一下a * b * c * d,在哪里a,都是b,c并希望产生一个.dintsize_t

从语法上讲,该表达式相当于(((a * b) * c) * d),并且通常的算术转换相应地应用于操作.如果转换asize_t使用cast((size_t)a * b * c * d),则转换将被应用,如同括号一样.因此,操作数和结果(size_t)a * b将是size_t,因此,这样会的结果(size_t)a * b * c,因此(size_t)a * b * c * d.换句话说,所有操作数都将转换为无符号size_t值,所有乘法将作为无符号size_t乘法执行.这是明确定义的,但如果任何值恰好是负面的话,可能毫无意义.

第二次或第三次乘法可能超过a的容量size_t,但由于size_t是无符号的,计算将以2 NN为模执行 ,其中是值的位数size_t.因此,演员在避免溢出的意义上是不安全的,但它确实至少避免了未定义的行为.


笔记

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

  2. 移位运算符是例外,这就是它说"很多二元运算符"的原因.移位运算符的结果类型恰好是其左操作数的(可能是提升的)类型,而不管右操作数的类型.所有按位运算符都限制为整数,因此涉及实数的"常用算术转换"部分不适用于这些运算符.

  3. 如果乘以2 unsigned int,则结果为a,unsigned int并为所有值定义计算:

    涉及无符号操作数的计算永远不会溢出,因为无法通过生成的无符号整数类型表示的结果将以比结果类型可以表示的最大值大1的数量为模.(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,则该操作数将被提升为int或者unsigned int.通常情况下,int即使它是未签名的,它也会被提升.仅当int不足以表示该类型的所有值时,才会将操作数提升为unsigned int.例如,在大多数架构的unsigned char操作数将被提升到一个int,而不是一个unsigned int(虽然结构,其中charint具有相同的宽度是可能的,它们是不常见的.)

    湾 最后,如果一个类型已签名而另一个类型未签名,则它们都将转换为:

    • 无符号的类型,如果它至少是一样宽的有符号的类型.(例如.unsigned int*int=> unsigned int)

    • 签约类型,如果它足够宽以容纳无符号的类型的所有值.(例如.unsigned int*long long=> long long如果long long宽于int)

    • 如果上述情况都不成立,则与签名类型对应的无符号类型.