fre*_*low 384

未定义的行为是C和C++语言的一个方面,对于来自其他语言的程序员来说可能会令人惊讶(其他语言试图更好地隐藏它).基本上,有可能编写不能以可预测的方式运行的C++程序,即使许多C++编译器不会报告程序中的任何错误!

让我们看一个经典的例子:

#include <iostream>

int main()
{
    char* p = "hello!\n";   // yes I know, deprecated conversion
    p[0] = 'y';
    p[5] = 'w';
    std::cout << p;
}
Run Code Online (Sandbox Code Playgroud)

变量p指向字符串文字"hello!\n",下面的两个赋值尝试修改该字符串文字.这个程序做什么用的?根据C++标准的第2.14.5节第11段,它调用未定义的行为:

尝试修改字符串文字的效果是未定义的.

我可以听到人们尖叫"但是等等,我可以编译这个没问题并得到输出yellow"或"你的意思是什么未定义,字符串文字存储在只读内存中,所以第一次分配尝试会导致核心转储".这正是未定义行为的问题.基本上,一旦你调用未定义的行为(甚至是鼻子恶魔),标准允许任何事情发生.如果根据您的语言心理模型存在"正确"行为,那么该模型就是错误的; C++标准有唯一的投票期.

未定义行为的其他例子包括访问超出其边界的数组,解引用空指针,访问对象后,他们的寿命结束或写入据说聪明的表情一样i++ + ++i.

C++标准的第1.9节还提到了未定义行为的两个不太危险的兄弟,未指定的行为实现定义的行为:

本国际标准中的语义描述定义了参数化的非确定性抽象机器.

抽象机的某些方面和操作在本国际标准中描述为实现定义的(例如sizeof(int)).这些构成了抽象机器的参数.每个实施应包括描述其在这些方面的特征和行为的文件.

抽象机器的某些其他方面和操作在本国际标准中被描述为未指定的(例如,对函数的参数的评估顺序).在可能的情况下,本国际标准定义了一组允许的行为.这些定义了抽象机器的非确定性方面.

本国际标准中将某些其他操作描述为未定义(例如,取消引用空指针的效果).[ 注意:本国际标准对包含未定义行为的程序的行为没有要求.- 结束说明 ]

具体而言,第1.3.24节规定:

允许的未定义行为包括完全忽略不可预测的结果,在翻译或程序执行期间以环境特征(有或没有发出诊断消息)的特定行为,终止翻译或执行(发布时)一条诊断信息).

你能做些什么来避免遇到未定义的行为?基本上,你必须阅读那些了解他们所谈论内容的作者的优秀C++书籍.螺丝网络教程.螺旋公牛队.

  • @Pacerier有些东西在编译时无法检查.例如,并不总是可以保证永远不会取消引用空指针,但这是未定义的. (12认同)
  • @Benoit这是未定义的行为,因为标准表明它是未定义的行为,句点.在某些系统上,确实字符串文字存储在只读文本段中,如果您尝试修改字符串文字,程序将崩溃.在其他系统上,字符串文字确实会出现变化.该标准并未规定必须发生的事情.这就是未定义行为的含义. (7认同)
  • 这是一个奇怪的事实,由于合并导致这个答案仅涵盖C++,但这个问题的标签包括C.C有一个不同的"未定义行为"的概念:即使行为也被声明,它仍然需要实现给出诊断消息对于某些规则违规(约束违规)未定义. (5认同)
  • @FredOverflow,为什么一个好的编译器允许我们编译给出未定义行为的代码?究竟*good*可以编译这种代码给出什么?当我们尝试编译给出未定义行为的代码时,为什么所有好的编译器都没有给我们一个巨大的红色警告标志? (4认同)
  • @Celeritas,未定义的行为*可能是*不确定的.例如,不可能提前知道未初始化存储器的内容是什么,例如.`int f(){int a; 返回a;}`:`a`的值可能在函数调用之间发生变化. (3认同)
  • @Pacarier因为未定义的行为是为系统之间有用或预期的变化故意留下的空间。存在取消引用零地址获取零值的系统,并且取消引用空指针(未定义的行为!)以获取自动零值在这些系统上非常有用。存在硬件执行饱和算术的系统,并且上溢或下溢有符号整数(未定义的行为!)在该硬件上很有用。C 抽象了硬件变化,但 C 的主要用途是访问各种硬件特有的功能和权衡。 (3认同)
  • @fredoverflow 继续您的字符串文字示例 - 如果更改了字符串文字;它可能会影响其他变量 - 以上面的例子为例,并添加 `auto q= "hello!\n";` q 现在可能是也可能不是 "hello!\n" 或 "yellow\n" 甚至更糟;q 可能与 p 处于同一函数中,也可能不同。 (2认同)

AnT*_*AnT 93

嗯,这基本上是标准的直接复制粘贴

3.4.1 1 实现定义的行为未指定的行为,其中每个实现记录了如何进行选择

2示例实现定义的行为的示例是当有符号整数向右移位时高阶位的传播.

3.4.3 1 使用不可移植或错误的程序结构或错误数据时的未定义行为行为,本国际标准不对此要求

2注意可能的未定义行为包括完全忽略不可预测结果的情况,以及以环境特征(有或没有发出诊断消息)的文档化方式执行转换或程序执行,终止翻译或执行(带有发出诊断信息).

3示例未定义行为的示例是整数溢出的行为.

3.4.4 1 未指明的行为使用未指定的值,或本国际标准提供两种或更多种可能性的其他行为,并且在任何情况下都不会对其进行任何进一步的要求

2示例未指定行为的示例是评估函数参数的顺序.

  • @Zolomon:就像它说的那样:基本上是相同的,除了在实现定义的情况下,实现需要文档(以保证)到底会发生什么,而在未指定的情况下,不需要实现文档或保证任何事情. (24认同)
  • @Celeritas:超现代编译器可以做得更好.给定`int foo(int x){if(x> = 0)launch_missiles(); return x << 1; 编译器可以确定,因为所有调用不启动导弹的函数的方法都会调用Undefined Behavior,它可以调用`launch_missiles()`无条件. (6认同)
  • 实现定义和未指定行为之间的区别是什么? (2认同)
  • @northerner 正如引用所述,未指定的行为通常仅限于一组有限的可能行为。在某些情况下,您甚至可能得出结论,所有这些可能性在给定的上下文中都是可以接受的,在这种情况下,未指定的行为根本不是问题。未定义的行为完全不受限制(eb“程序可能会决定格式化您的硬盘”)。未定义的行为总是一个问题。 (2认同)
  • @northerner:不。在“未指定行为”的情况下,关键点是可能的行为集实际上是“指定的”和“受限制的”。通常它是一个易于监督的集合。未指定的是编译器将选择哪种特定可能性(从该集合中)。隐含的区别是,可能性集通常受到限制并且[相当]独立于平台。如果未定义的行为不受限制(因此在“有限状态数”推理下取决于平台)。 (2认同)
  • 语言规范没有在任何地方提到“格式化硬盘”。然而,这并不是任何“未指定”行为的可能性,而是任何“未定义”行为的可能性。这很好地强调了区别。 (2认同)

Ara*_*raK 56

也许简单的措辞可以比标准的严格定义更容易理解.

实现定义的行为
语言表示我们有数据类型.编译器供应商指定他们使用的大小,并提供他们所做的文档.

未定义的行为
你做错了什么.例如,您有一个非常大的值int,不适合char.你怎么把这个价值放进去char?实际上没有办法!任何事情都可能发生,但最明智的做法是将该int的第一个字节放入其中char.分配第一个字节是错误的,但这就是幕后发生的事情.

未指定的行为
首先执行这两个函数?

void fun(int n, int m);

int fun1()
{
  cout << "fun1";
  return 1;
}
int fun2()
{
  cout << "fun2";
  return 2;
}
...
fun(fun1(), fun2()); // which one is executed first?
Run Code Online (Sandbox Code Playgroud)

该语言未指定评估,从左到右或从右到左!因此,未指定的行为可能会或可能不会导致未定义的行为,但当然您的程序不应产生未指定的行为.


@eSKay我认为你的问题值得编辑答案澄清更多:)

for fun(fun1(), fun2());是不是"实现定义"的行为?毕竟编译器必须选择一个或另一个课程?

实现定义和未定义之间的区别在于编译器应该在第一种情况下选择行为,但在第二种情况下不需要.例如,实现必须只有一个定义sizeof(int).因此,它不能说sizeof(int)程序的某些部分为4,其他部分为8.与未指定的行为不同,编译器可以说OK,我将从左到右评估这些参数,并且从右到左评估下一个函数的参数.它可能发生在同一个程序中,这就是为什么它被称为未指定的原因.实际上,如果指定了一些未指定的行为,C++可能会变得更容易.看看Stroustrup博士对此的回答:

据称,为编制者提供这种自由所需的内容与要求"普通的从左到右的评估"之间的差异可能很大.我不相信,但是有无数的编译器"在那里"利用自由和一些人热情地捍卫自由,改变将是困难的,可能需要数十年才能渗透到C和C++世界的遥远角落.我很失望并非所有编译器都会对++ i + i ++等代码发出警告.同样,参数的评估顺序是未指定的.

IMO太多"事物"未定义,未指定,实现定义等.但是,这很容易说,甚至可以提供示例,但很难修复.还应该注意,避免大多数问题并产生可移植代码并不是那么困难.

  • 我不知道C++,但C标准说int转换为char是实现定义的,甚至是定义良好的(取决于实际值和类型的签名).见C99§6.3.1.3(C11中未更改). (4认同)
  • 对于“fun(fun1(), fun2());”,行为不是“实现定义的”吗?毕竟,编译器必须选择其中之一吗? (3认同)
  • @Lazer:肯定会发生.简单场景:foo(bar,boz())和foo(boz(),bar),其中bar是int,boz()是返回int的函数.假设一个CPU,其中参数预期在寄存器R0-R1中传递.函数结果以R0返回; 功能可能会废弃R1.在"boz()"之前评估"bar"需要在调用boz()然后加载该保存的副本之前在其他地方保存一个bar的副本.后评估"酒吧""博兹()"将避免内存存储和重新获取,并且是一种优化不论其顺序的许多编译器会做参数列表. (3认同)
  • @AraK:感谢您的解释。我现在明白了。顺便说一句,“我将从左到右评估这些参数,下一个函数的参数从右到左评估”我理解这种“可能”发生。我们现在使用的编译器真的是这样吗? (2认同)

Joh*_*itb 26

来自官方C理由文件

术语未指定的行为,未定义的行为和实现定义的行为用于对编写程序的结果进行分类,这些程序的属性标准不能或不能完全描述.采用这种分类的目的是允许实现中的某种变化,这允许实现的质量成为市场中的主动力量以及允许某些流行的扩展,而不去除与标准的一致性的标记.标准的附录F对属于这三个类别之一的行为进行了编目.

未指定的行为使实现者在翻译程序时具有一定的自由度.只要没有翻译程序,这个范围就不会延伸.

未定义的行为使实现者许可证不会捕获难以诊断的某些程序错误.它还标识了可能符合语言扩展的区域:实现者可以通过提供正式未定义行为的定义来扩充语言.

实现定义的行为使实现者可以自由选择适当的方法,但需要向用户解释此选择.指定为实现定义的行为通常是用户可以基于实现定义做出有意义的编码决策的行为.在决定实施定义应该有多广泛时,实施者应该牢记这个标准.与未指定的行为一样,只是无法转换包含实现定义的行为的源不是一个充分的响应.

  • 超现代的编译器作者也将“未定义的行为”视为授予编译器作者许可以假定程序将永远不会接收到会导致未定义的行为的输入,并可以任意更改程序在收到此类输入时的行为方式。 (2认同)
  • 我刚刚注意到的另一点是:C89 没有使用术语“扩展”来描述在某些实现上保证但在其他实现上没有保证的特性。C89 的作者认识到,大多数当时的实现将相同地对待有符号算术和无符号算术,除非结果以某些方式使用,并且即使在有符号溢出的情况下也适用这种处理;然而,他们没有将其列为附件 J2 中的常见扩展,这表明他们将其视为事态的自然状态,而不是扩展。 (2认同)

And*_*bel 10

未定义的行为与未指定的行为有一个简短的描述.

他们的最后总结:

总而言之,除非您的软件需要便携,否则通常您不应该担心这些行为.相反,未定义的行为总是不受欢迎的,永远不应该发生.


sup*_*cat 7

从历史上看,实现定义行为和未定义行为都代表了标准作者期望编写高质量实现的人会使用判断来决定哪些行为保证(如果有的话)对于在其上运行的预期应用领域中的程序有用的情况.预定目标.高端数字运算代码的需求与低级系统代码的需求大不相同,UB和IDB都为编译器编写者提供了满足这些不同需求的灵活性.这两个类别都没有规定实现的行为方式对任何特定目的有用,甚至出于任何目的.然而,声称适合特定目的的质量实施应该以符合此目的的方式运行,无论标准是否需要.

实现定义行为和未定义行为之间的唯一区别是,前者要求实现定义并记录一致的行为,即使在实现可能没有任何帮助的情况下也是如此.它们之间的分界线不是它是否通常对实现定义行为有用(编译器编写者应该在实际时定义有用的行为,无论标准是否要求它们),但是是否可能存在定义行为同时代价高昂的实现和无用的.判断这些实现可能不存在任何方式,形式或形式,暗示对支持其他平台上定义的行为的有用性的任何判断.

不幸的是,自20世纪90年代中期以来,编译器编写者开始将缺乏行为规定解释为行为保证即使在它们至关重要的应用领域也不值得花费的判断,甚至在他们几乎没有成本的系统上也是如此.编译器编写者不是将UB视为执行合理判断的邀请,而是将其视为这样做的借口.

例如,给出以下代码:

int scaled_velocity(int v, unsigned char pow)
{
  if (v > 250)
    v = 250;
  if (v < -250)
    v = -250;
  return v << pow;
}
Run Code Online (Sandbox Code Playgroud)

一个二进制补码的实现不必花费任何努力将该表达式v << pow视为二进制补码移位而不考虑v是正还是负.

然而,当今一些编译器编写者的首选哲学意味着,v如果程序要进行未定义的行为,那么只能是负面的,没有理由让程序剪辑为负范围v.尽管负值的左移过去常常被支持在每一个有意义的编译器上,并且大量现有代码依赖于这种行为,但现代哲学会解释标准说左移负值是UB的事实.暗示编译器编写者应该随意忽略它.


Sur*_*mas 6

实施定义 -

实施者希望,应该有详细记录,标准给出选择,但肯定编译

未指明 -

与实现定义相同但未记录

Undefined-

任何事情都可能发生,照顾它.

  • 我认为必须指出,“未定义”的实际含义在过去几年中发生了变化。曾经是给定的uint32_t s;,当s为33时评估1u &lt;&lt; s可能会产生0或可能产生2,但别无所求。但是,较新的编译器评估“ 1u &lt;&lt; s”可能会导致编译器确定,因为“ s”必须事先小于32,因此该表达式之前或之后的任何代码仅在“ s”为32时才有意义。或更大的值可以省略。 (2认同)

4pi*_*ie0 5

C ++标准n3337 § 1.3.10 实现定义的行为

行为,对于格式正确的程序构造和正确数据,取决于实施以及每个实施文档

有时,C ++ Standard并未在某些构造上强加特定的行为,而是说必须通过特定的实现(库版本)来选择和描述特定的,定义明确的行为。因此,即使Standard并未对此进行描述,用户仍然可以确切知道程序的行为方式。


C ++标准n3337 § 1.3.24 未定义行为

本国际标准不施加任何要求的行为[注:当本国际标准省略行为的任何明确定义或程序使用错误的构造或错误的数据时,可能会出现未定义的行为。允许的不确定行为包括:完全忽略具有无法预测结果的情况,以环境特征的书面形式在翻译或程序执行期间的行为(带有或不带有诊断消息),终止翻译或执行(带有发行)诊断消息)。许多错误的程序构造不会引起未定义的行为。他们需要被诊断。—尾注]

当程序遇到未按照C ++标准定义的构造时,它可以做任何想做的事情(可以给我发送电子邮件,或者给你发送电子邮件,或者完全忽略代码)。


C ++标准n3337 § 1.3.25 未指定的行为

行为,对于格式正确的程序构造和正确数据,取决于实现[注:不需要实现来记录发生哪种行为。可能的行为范围通常由本国际标准来描述。—尾注]

C ++ Standard并未在某些构造上强加特定的行为,而是说必须通过特定的实现(库的版本)选择特定的,定义明确的行为(无需描述bot)。因此,在没有提供任何描述的情况下,用户可能很难确切知道程序的行为方式。