两者有什么区别
int *p = malloc( h * w * sizeof(*p) );
和
int *p = malloc( sizeof (*p) * h * w );
当h
和w
属于int
类型时?
为什么在第一个设置sizeof(*p)
比在malloc
最后一个位置设置更安全?
我已经明白后一种形式用于确保数学size_t
并且int
操作数将在计算完成之前扩大到size_t
以防止有符号整数溢出,正如这里和这里所说的那样,但我不太明白它是如何工作的。
当你首先编写sizeof
运算时,你通常确保计算至少用size_t
数学来完成。让我们确定这意味着什么。
最后放置sizeof(X)
的问题:
想象一下这样的场景,h
具有200000
的价值,w
具有50000
的价值(可能是偶然获得的)。
假设一个int
可以容纳的最大整数值是2147483647
,这是常见的(您可以从宏INT_MAX
中读取的确切实现定义的值 - 标头<limits.h>
),两者都是int
可以持有的合法值。
如果现在使用malloc( h * w * sizeof(*p) );
,则首先计算算术表达式的求值顺序从左到右时,将首先计算部分h * w
。这样,您将获得有符号整数溢出,因为结果10000000000
(100 亿)无法用int
表示。
发生整数溢出的程序的行为未定义。C 标准声明整数溢出,即使作为规范中未定义行为的示例:
3.4.3
1 未定义的行为
使用不可移植或错误的程序构造或错误数据时的行为,本文档对此没有要求
2 注 1:可能的未定义行为包括完全忽略情况并产生不可预测的结果,在翻译或程序执行期间以环境特征的记录方式行事(无论是否发出诊断消息),到终止翻译或执行(发布诊断消息)。
3 注2:J.2概述了导致未定义行为的C程序的属性。
4示例 未定义行为的一个示例是整数溢出的行为。
来源: C18, §3.4.3
将sizeof(X)
放在第一位:
如果先使用sizeof
操作(如malloc( sizeof(*p) * h * w );
),则通常不会有整数溢出的风险。
这是因为两个原因。
-
sizeof
获得无符号整数类型的值size_t
。size_t
在最现代的实现中具有比int
更高的整数转换等级和大小。共同价值观:sizeof(size_t) == 8
和sizeof(int) == 4
。这对于第 2 点很重要,这称为算术表达式中的整数提升(算术转换)。
-
在表达式中经常发生操作数的自动类型转换。这称为整数或隐式类型升级。有关此内容的更多信息,您可以查看此有用的常见问题解答。
对于此升级,整数类型的转换秩很重要,因为整数转换排名较少的操作数的类型将提升为整数转换等级较高的操作数类型。
查看 C 标准中的确切短语:
"否则,如果两个操作数都有有符号整数类型或都有无符号整数类型,则整数转换秩较小的操作数将转换为秩较大的操作数的类型。">
来源: C18, §6.3.1.8/1
符号的转换也可以在这里发生,这在本例中很重要,稍后将介绍。
"否则,如果具有无符号整数类型的操作数的秩大于或等于其他操作数类型的秩,则具有有符号整数类型的操作数将转换为具有无符号整数类型的操作数的类型。">
....
"否则,如果具有有符号整数类型的操作数的类型可以表示具有无符号整数类型的操作数类型的所有值,则具有无符号整数类型的操作数将转换为具有有符号整数类型的操作数的类型。">
来源: C18, §6.3.1.8/1
如果
size_t
的整数转换排名高于或至少等于int
,并且int
无法表示size_t
的所有值(这是满足的,因为int
的大小通常小于前面所说的size_t
),则操作数h
和类型int
w
在计算之前提升为类型size_t
。
有
符号转换为无符号整数的重要性:
现在您可能会问:为什么有符号转换为无符号整数很重要?
这里还有两个原因,第二个更重要,但为了完整起见,我想涵盖这两个原因。
无符号整数始终比具有相同整数转换等级的有符号整数具有更宽的正范围。这是因为有符号整数也始终需要表示负值范围。无符号整数没有负范围,因此可以表示几乎是有符号整数的两倍的正值。
但更重要的是:
无符号整数永远不会溢出!
"涉及无符号操作数的计算永远不会溢出,因为无法由生成的无符号整数类型表示的结果被缩减为比结果类型可以表示的最大值大 1 的数字。">
来源:C18,§6.2.5/9(强调我的)
这就是为什么将sizeof
操作放在第一位的原因,就像在malloc( sizeof(*p) * h * w );
中
一样更安全。 但是,如果使用无符号整数,您将超过限制,因为围绕分配的内存的包装太小而无法将其用于所需的目的。访问未分配的内存也会调用未定义的行为。
但尽管如此,它还是可以保护您免受调用malloc()
本身时获得未定义的行为。
旁注:
请注意,将
sizeof
放在第二个位置malloc( h * sizeof(*p) * w )
在技术上可以实现相同的效果,尽管这可能会降低可读性。如果调用
malloc()
中的算术表达式只有一个或 2 个操作数(例如sizeof(x)
和int
)顺序无关紧要。但是为了坚持惯例,我建议使用相同的样式,将sizeof()
始终放在第一位:malloc(sizeof(int) * 4)
。通过这种方式,您就不会因为有 2 个int
操作数而冒着意外忘记它的风险。对
h
和w
使用无符号整数类型(如size_t
)也可以是更明智的选择。它确保首先不会发生未定义的溢出,此外,它更合适,因为h
和w
并不意味着具有负值。
相关:
- 如何检测无符号整数乘法溢出?
- 为什么定义了无符号整数溢出行为,但没有定义有符号整数溢出行为?
- 无符号 int 与 size_t
malloc中设置第一个位置比在最后一个位置设置sizeof(*p)更安全?
简短的回答是:它不是(或至少不应该)更安全。
更长的答案:
任何整数计算都可能溢出 - 有些结果具有未定义的行为 - 有些结果不正确,并且(可能)后续程序失败。
任何整数计算都必须考虑是否会发生溢出。如果您编写的程序在单个malloc
调用中分配超过 2G 的内存,我相信您已经意识到这一点,并确保h
和w
都有适当的类型。
此外,该标准没有确切说明整数类型的最大值是多少。因此,如果您想进行"安全"编程,请确保在运行时询问这些限制。
换句话说:"更安全">不是编程目标。如果你编写在边缘运行的程序,你就会使它们安全- 而不仅仅是"更安全">
虽然乘法是可交换的,但显然编译器不会提前扫描最大的类型,sizeof() 是size_t,它在 64 位计算机上是无符号长 (2^64 - 1),因此顺序对于防止溢出很重要,在许多语言中溢出是静默发生的,即使所有 CPU 都提供该信息作为状态位(如果不是作为中断)! 当然,一些程序员想要一个静默的溢出来获得驻留 mod 类型的大小,但这是一个让我们其他人受苦的可悲理由!