|
@@ -8,6 +8,11 @@ Even a stupid problem can lead to some thoughts.
|
|
|
"""
|
|
|
+++
|
|
|
|
|
|
+{{% alert note %}}
|
|
|
+本文中对 C 语言标准章节的引用以 [N1570][N1570] 为准。
|
|
|
+[N1570]:/assets/std/c1x-n1570.pdf
|
|
|
+{{% /alert %}}
|
|
|
+
|
|
|
# 某笔试题中的未定义行为
|
|
|
|
|
|
昨天看到一道笔试题,要求找出以下程序中的所有错误(代码已经手动格式化)。
|
|
@@ -43,7 +48,7 @@ int *frequency(int size)
|
|
|
|
|
|
然而这么改就够了吗?当然不是!分析一下 `size * sizeof(int)` 这个东西,
|
|
|
它是一个整数乘法表达式,右边的子表达式 `sizeof(int)` 根据语言标准具有
|
|
|
-`size_t` 类型,而且它是无符号类型(6.5.3.4p2, 7.17p2)。而 `int`
|
|
|
+`size_t` 类型,而且它是无符号类型(6.5.3.4p5, 7.19p2)。而 `int`
|
|
|
是带符号类型(6.7.2p2, p5),把它们两个乘起来的时候... ... 根据标准,
|
|
|
会执行常规算术运算自动类型转换(usual arithmetic conversions)
|
|
|
(6.5.5p3),对于整数运算来说,它的规则如下(6.3.1.8p1):
|
|
@@ -82,7 +87,8 @@ int *frequency(int size)
|
|
|
|
|
|
> 以下规则适用于任何可以使用 `int` 或 `unsigned int` 的表达式:
|
|
|
>
|
|
|
-> * 如果对象或表达式具有整数类型,且其等级低于 `int` 和 `unsigned int`。
|
|
|
+> * 如果对象或表达式具有整数类型,且其等级小于等于 `int` 和 `unsigned int`,
|
|
|
+> 且本身又不是 `int` 或 `unsigned int`。
|
|
|
> * 如果是具有 `_Bool`、`int`、`singed int` 或 `unsigned int` 的位段。
|
|
|
>
|
|
|
> 如果 `int` 能表示原始值的所有类型,则将其值转换为 `int`;否则,
|
|
@@ -91,10 +97,10 @@ int *frequency(int size)
|
|
|
> 整数提升转换一定保留原始值不变,包括符号。正如之前章节已经讨论的,
|
|
|
> `char` 默认是否带符号由实现决定。
|
|
|
|
|
|
-分析我们的例子,如果 `size_t` 的等级小于 `int` 的等级,则它会被提升成
|
|
|
+分析我们的例子,如果 `size_t` 的等级小于等于 `int` 的等级,则它会被提升成
|
|
|
`unsigned int` ,而它的等级等于 `int` 的等级,于是 `int` 被转换成
|
|
|
`unsigned int` ,结果就是两个 `unsigned int` 相乘。如果 `size_t`
|
|
|
-的等级大于等于 `int` 的等级,则 `int` 会被转换成 `size_t`,就是两个
|
|
|
+的等级大于 `int` 的等级,则 `int` 会被转换成 `size_t`,就是两个
|
|
|
`size_t` 相乘,总之都是无符号乘无符号。很“好”,不会触发未定义行为!
|
|
|
|
|
|
然而我之前翻译的文章中早就说过,不触发未定义行为不代表没 bug 。
|
|
@@ -226,7 +232,7 @@ array = calloc(u_size, sizeof(int));
|
|
|
{{% /alert %}}
|
|
|
|
|
|
然而,无论是 `malloc` 还是 `calloc` 分配失败的时候都会返回 `NULL`
|
|
|
-(7.20.3.1p3, 7.20.3.3p3),如果解引用空指针,又会产生未定义行为,
|
|
|
+(7.22.3.2p3, 7.22.3.4p3),如果解引用空指针,又会产生未定义行为,
|
|
|
所以要把这种情况判断掉:
|
|
|
|
|
|
```c
|
|
@@ -249,7 +255,7 @@ while (scanf("%d", &i) == 1)
|
|
|
|
|
|
至于这个错误怎么处理我也不知道(没有接口文档)。然而这就够了吗?
|
|
|
如果用户干脆输入一个 `int` 无法表示的大数呢?语言标准规定
|
|
|
-(7.19.6.2p10):
|
|
|
+(7.21.6.2p10):
|
|
|
|
|
|
> 只要遇到了一个 `%` 限定符,输入项(或者对于 `%n` 限定符来说,
|
|
|
> 已经输入的字符个数)会被转换成与该限定符匹配的类型。
|
|
@@ -259,13 +265,13 @@ while (scanf("%d", &i) == 1)
|
|
|
> **如果该对象的类型与限定符不一致,或者转换结果不能被该对象表示,
|
|
|
> 则行为是未定义的。**
|
|
|
|
|
|
-再阅读一下 `%d` 限定符的说明(7.19.6.2p11):
|
|
|
+再阅读一下 `%d` 限定符的说明(7.21.6.2p11):
|
|
|
|
|
|
> d: 匹配一个或许带符号的十进制整数,其格式与 `strtol` 函数在 `base`
|
|
|
> 参数的值为 10 时对主体序列 (subject sequence) 的预期相同。
|
|
|
> 对应的参数应该是指向带符号整数的指针。
|
|
|
|
|
|
-什么是“主体序列”?再次翻阅标准(7.20.1.4p3):
|
|
|
+什么是“主体序列”?再次翻阅标准(7.22.1.4p3):
|
|
|
|
|
|
> 如果 `base` 在 2 到 36 之间(含),期望的主体序列是一个包含数字和字母,
|
|
|
> 表示一个基为 `base` 的整数的字符序列,它可以在开头带有一个正号或负号,
|
|
@@ -341,7 +347,7 @@ return array;
|
|
|
|
|
|
What the f\*\*k? 返回一个野指针让调用者怎么用?
|
|
|
调用者几乎无论如何都会触发未定义行为。那么就得把 `free` 删掉。
|
|
|
-当然这里肯定又有人说会泄露内存,那调用者去 `free` 内存就完了,
|
|
|
+当然这里肯定又有人说会泄露内存,那调用者去 `free` 内存就可以了,
|
|
|
[`asprintf`][1] 和 [`strdup`][2] 等许多标准函数都是这么干的。
|
|
|
如果非要说这样做不好,需要改函数接口,这是 [另一个问题][3] 了。
|
|
|
|