如果我有:
unsigned int x;
x -= x;
很显然,在此表达式之后x
应该为零,但是我所看到的所有地方都说该代码的行为是不确定的,而不仅仅是x
(直到减法之前)的值。
两个问题:
此代码的行为确实未定义吗?
(例如,代码是否可能在兼容的系统上崩溃?)如果是这样,为什么C明确表示该行为
x
为零,为什么还说行为未定义?即,通过在此不定义行为有什么好处?
显然,编译器可以在变量内部简单地使用它认为“方便”的任何垃圾值,并且可以按预期工作……该方法有什么问题?
如果我有:
unsigned int x;
x -= x;
很显然,在此表达式之后x
应该为零,但是我所看到的所有地方都说该代码的行为是不确定的,而不仅仅是x
(直到减法之前)的值。
两个问题:
此代码的行为确实未定义吗?
(例如,代码是否可能在兼容的系统上崩溃?)
如果是这样,为什么C明确表示该行为x
为零,为什么还说行为未定义?
即,通过在此不定义行为有什么好处?
显然,编译器可以在变量内部简单地使用它认为“方便”的任何垃圾值,并且可以按预期工作……该方法有什么问题?
x -= x
。问题是,为什么访问所有未初始化的值都是UB。
Answers:
是的,这种行为是不确定的,但其原因与大多数人所知不同。
首先,使用单位化的值本身并不是不确定的行为,但是该值只是不确定的。如果该值恰好是该类型的陷阱表示,则访问该对象将是UB。无符号类型很少有陷阱表示,因此从这一点来讲,您将相对安全。
使行为不确定的原因是变量的附加属性,即变量“可以用声明register
”,因为它的地址永远不会被使用。之所以会特别考虑此类变量,是因为有些架构具有真正的CPU寄存器,这些寄存器具有某种“未初始化”的额外状态,并且不对应于类型域中的值。
编辑:该标准的相关短语是6.3.2.1p2:
如果左值指定了可以使用寄存器存储类声明的自动存储持续时间的对象(从未使用其地址),并且该对象未初始化(未使用初始化器声明,并且在使用前未对其进行任何分配) ),则行为未定义。
更清楚地说,以下代码在任何情况下都是合法的:
unsigned char a, b;
memcpy(&a, &b, 1);
a -= a;
a
和的地址在这里使用b
,因此它们的值是不确定的。unsigned char
永远不会有不确定值的陷阱表示只是不确定的,unsigned char
因此可能发生任何值。a
必须保留该值0
。Edit2: a
并且b
具有未指定的值:
3.19.3 在本国际标准不要求在任何情况下选择哪种值的情况下,相关类型的未指定值
有效值
unsigned
s肯定可以具有陷阱表示形式。您能指出标准的那一部分吗?我在§6.2.6.2/ 1中看到以下内容:“对于无符号char以外的无符号整数类型,对象表示的位应分为两组:值位和填充位(后者无须再填充)。 ...这将被称为值表示。未指定任何填充位的值。“”带有注释的注释:“⁴⁴⁾填充位的某些组合可能会生成陷阱表示”。
unsigned char
,但是此答案正在使用unsigned char
。不过请注意:严格合规的程序可以sizeof(unsigned) * CHAR_BIT
基于来计算和确定UINT_MAX
特定实现不可能具有的陷阱表示unsigned
。在该程序确定之后,即可继续执行此答案的确切工作unsigned char
。
memcpy
很分散注意力,也就是说,如果您的示例被替换,它是否仍然适用*&a = *&b;
。
unsigned char
并因此有所memcpy
帮助,因此*&
不清楚。一旦解决,我会报告。
C标准为编译器提供了很大的自由度来执行优化。如果您假设一个朴素的程序模型,其中未初始化的内存设置为某种随机位模式,并且所有操作均按写入顺序执行,则这些优化的结果可能会令人惊讶。
注意:以下示例仅由于x
从未获取其地址而有效,因此它是“类似寄存器的”。如果类型x
具有陷阱表示,则它们也将有效。对于无符号类型,这种情况很少发生(它需要“浪费”至少一小部分存储空间,并且必须进行记录),而对于而言则是不可能的unsigned char
。如果x
具有带符号的类型,则实现可以将不是-(2 n-1 -1)和2 n-1 -1之间的数字的位模式定义为陷阱表示。请参阅Jens Gustedt的答案。
编译器尝试将寄存器分配给变量,因为寄存器比内存快。由于程序使用的变量可能比处理器拥有的寄存器更多,因此编译器执行寄存器分配,这导致在不同时间使用同一寄存器产生不同的变量。考虑程序片段
unsigned x, y, z; /* 0 */
y = 0; /* 1 */
z = 4; /* 2 */
x = - x; /* 3 */
y = y + z; /* 4 */
x = y + 1; /* 5 */
当评估第3行时,第3x
行必须进行初始化,因此(由于编译器的原因)第3行必须是某种偶然,因为其他条件编译器不够聪明,无法弄清楚。由于z
在第4行之后不使用,并且x
在第5行之前不使用,因此可以将相同的寄存器用于两个变量。因此,此小程序被编译为在寄存器上执行以下操作:
r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;
的最终值x
是的最终值r0
,的最终值y
是的最终值r1
。这些值是x = -3和y = -4,而不是5和4(如果x
已正确初始化的话)。
有关更详细的示例,请考虑以下代码片段:
unsigned i, x;
for (i = 0; i < 10; i++) {
x = (condition() ? some_value() : -x);
}
假设编译器检测到condition
没有副作用。由于condition
未修改x
,因此编译器知道x
由于尚未初始化,因此可能无法访问循环中的第一个循环。因此,循环主体的第一次执行等效于x = some_value()
,无需测试条件。编译器可能会像您编写的那样编译此代码
unsigned i, x;
i = 0; /* if some_value() uses i */
x = some_value();
for (i = 1; i < 10; i++) {
x = (condition() ? some_value() : -x);
}
可以在编译器内部进行建模的方式是,只要依赖于任何值,只要未初始化,便x
具有任何方便的值x
。由于未定义未初始化变量而不是仅具有未指定值的变量时的行为,因此编译器无需跟踪方便值之间的任何特殊数学关系。因此,编译器可以通过以下方式分析上面的代码:
x
未对时间-x
进行评估。-x
具有未定义的行为,因此其值是方便的。condition ? value : value
condition; value
当遇到问题中的代码时,该相同的编译器会分析,在进行x = - x
评估时,值-x
是很方便的。因此,可以优化分配。
我没有寻找行为如上所述的编译器的示例,但这是优秀的编译器尝试进行的优化。遇到一个我不会感到惊讶。这是一个不太合理的编译器示例,程序会因此崩溃。(如果您以某种高级调试模式编译程序,这可能并不令人难以置信。)
该假设的编译器将每个变量映射到不同的内存页中,并设置页属性,以便从未初始化的变量中进行读取会导致调用调试器的处理器陷阱。首先,对变量的任何分配都确保其内存页被正常映射。该编译器不会尝试执行任何高级优化-处于调试模式,旨在轻松定位未初始化的变量等错误。在x = - x
评估时,右侧会导致陷阱,调试器将启动。
x
具有一个未初始化的值,但是访问时的行为会如果x没有类似寄存器的行为,则定义它。
x
,则无论其值是否已定义,都可以忽略对其的所有操作。如果后面的代码对于在产生零的情况下可能包含的if (volatile1) x=volatile2; ... x = (x+volatile3) & 255;
任何值0-255同样满意,我认为允许程序员省略不必要的写入的实现应被认为比其质量更高。会表现...x
volatile1
x
是的,程序可能会崩溃。例如,可能存在陷阱表示(无法处理的特定位模式),这可能会导致CPU中断,而未处理可能会使程序崩溃。
(在C11最新草案中有6.2.6.1所述)某些对象表示形式不必表示对象类型的值。如果对象的存储值具有这种表示形式,并且由不具有字符类型的左值表达式读取,则该行为是不确定的。如果这种表示是由副作用产生的,该副作用通过不具有字符类型的左值表达式修改对象的全部或任何部分,则该行为是不确定的。50)这种表示称为陷阱表示。
(此说明仅适用于unsigned int
可以使用陷阱表示的平台,这在现实世界的系统中很少见;有关详细信息和参考,请参见注释,以获取导致标准当前措辞的其他可能更常见的原因。)
(此答案的答案为C1999。有关C 2011的信息,请参阅Jens Gustedt的答案。)
C标准没有说使用未初始化的自动存储期限的对象的值是未定义的行为。C 1999标准在6.7.8 10中说:“如果未自动初始化具有自动存储持续时间的对象,则其值不确定。” (本段继续定义静态对象的初始化方式,因此我们关注的唯一未初始化的对象是自动对象。)
3.17.2将“不确定值”定义为“未指定值或陷阱表示”。3.17.3将“未指定值”定义为“在任何情况下本国际标准均不要求选择哪个值的情况下,相关类型的有效值”。
因此,如果未初始化unsigned int x
的值未指定,则x -= x
必须产生零。剩下的问题是它是否可能是陷阱表示。根据6.2.6.1 5.访问陷阱值的确会导致未定义的行为。
某些类型的对象可能具有陷阱表示形式,例如浮点数的信号NaN。但是无符号整数是特殊的。根据6.2.6.2,无符号int的N个值位中的每个表示2的幂,并且值位的每个组合表示从0到2 N -1的值之一。因此,无符号整数只能由于其填充位(例如奇偶校验位)中的某些值而具有陷阱表示形式。
如果在您的目标平台上,无符号int没有填充位,则未初始化的无符号int不能具有陷阱表示,并且使用其值不能导致未定义的行为。
x
有陷阱表示,那么x -= x
可能会陷阱,对吗?尽管如此,用于指出没有多余位的无符号整数的+1必须具有已定义的行为-显然与其他答案相反,并且(根据引号)这似乎是标准所暗示的含义。
x
具有陷阱表示,则x -= x
可能会陷阱。即使简单地x
用作值也可能会陷阱。(x
用作左值是安全的;写入对象不会受到其中的陷阱表示的影响。)
是的,它是未定义的。代码可能崩溃。C表示行为是不确定的,因为没有特殊原因可以对通用规则进行例外处理。优点是与其他所有未定义行为的情况具有相同的优点-编译器无需输出特殊代码即可完成此工作。
显然,编译器可以简单地在变量中使用它认为“方便”的任何垃圾值,并且可以按预期工作……该方法有什么问题?
您为什么认为那不会发生?这就是所采用的方法。不需要编译器使其正常工作,但不需要使编译器失败。
x
可以声明为register
,即从不使用其地址。我不知道您是否意识到这一点(如果您将其有效地隐藏起来),但是必须给出正确的答案。
对于任何类型的未初始化的变量或由于其他原因持有不确定值的变量,以下适用于代码读取该值:
否则,如果没有陷阱表示,则变量采用未指定的值。不能保证每次读取变量时此未指定的值都是一致的。但是,可以保证它不是陷阱表示,因此可以保证不调用未定义的行为[3]。
然后可以安全地使用该值,而不会导致程序崩溃,尽管此类代码不适用于具有陷阱表示形式的系统。
[1]:C11 6.3.2.1:
如果左值指定了可以使用寄存器存储类声明的自动存储持续时间的对象(从未使用其地址),并且该对象未初始化(未使用初始化器声明,并且在使用前未对其进行任何赋值) ),则行为未定义。
[2]:C11 6.2.6.1:
某些对象表示形式不必表示对象类型的值。如果对象的存储值具有这种表示形式,并且由不具有字符类型的左值表达式读取,则该行为是不确定的。如果这种表示是由副作用产生的,该副作用通过不具有字符类型的左值表达式修改对象的全部或任何部分,则该行为是不确定的。50)这种表示称为陷阱表示。
[3] C11:
3.19.2
不确定值
(未指定值)或陷阱表示3.19.3
未指定值
在任何情况下本国际标准均不要求选择哪个值的相关类型的有效值
注:未指定值不能是陷阱表示。3.19.4
陷阱表示
不需要表示对象类型值的对象表示
stdint.h
应始终使用它而不是C的本机类型。因为stdint.h
强制使用2的补码且没有填充位。换句话说,stdint.h
不允许这些类型充满垃圾。
尽管许多答案都集中在捕获未初始化注册访问的处理器上,但是即使在没有此类陷阱的平台上也可能会出现古怪的行为,使用的编译器不会特别努力地利用UB。考虑以下代码:
volatile uint32_t a,b;
uin16_t moo(uint32_t x, uint16_t y, uint32_t z)
{
uint16_t temp;
if (a)
temp = y;
else if (b)
temp = z;
return temp;
}
用于ARM之类的平台的编译器,其中除加载和存储以外的所有指令均在32位寄存器上运行,可能会以等同于以下方式的方式合理地处理代码:
volatile uint32_t a,b;
// Note: y is known to be 0..65535
// x, y, and z are received in 32-bit registers r0, r1, r2
uin32_t moo(uint32_t x, uint32_t y, uint32_t z)
{
// Since x is never used past this point, and since the return value
// will need to be in r0, a compiler could map temp to r0
uint32_t temp;
if (a)
temp = y;
else if (b)
temp = z & 0xFFFF;
return temp;
}
如果任一易失性读取产生非零值,则将为r0加载0 ... 65535范围内的值。否则,它将产生调用该函数时所持有的任何内容(即,传递给x的值),该值可能不是0..65535范围内的值。该标准缺少任何术语来描述类型为uint16_t但其值在0..65535范围之外的值的行为,只是说任何可能产生这种行为的动作都会调用UB。
uint16_t
,该变量有时可能读作123,有时也读作6553623。如果结果最终被忽略了……
register
,则它可能具有多余的位,从而使行为可能未定义。那正是你在说的,对吗?