C语言 未定义、未指定和实现定义的行为

f0ofjuux  于 2023-08-03  发布在  其他
关注(0)|答案(9)|浏览(111)

C和C++中的 * 未定义行为 *(UB)是什么?那么 * 未指定的行为 * 和 * 实现定义的 * 行为呢?它们之间有什么区别?

uqzxnwby

uqzxnwby1#

  • 未定义行为 * 是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;
}

字符串
变量p指向字符串文字"hello!\n",下面的两个赋值语句试图修改该字符串文字。这个程序是做什么的?根据C标准的2.14.5节第11段,它调用了 undefined behavior
尝试修改字符串文字的效果未定义。
我可以听到人们尖叫“但是等等,我可以编译这个没有问题,并得到输出yellow”或“你是什么意思,未定义的字符串字面量存储在只读内存中,所以第一次赋值尝试导致核心转储”。这正是未定义行为的问题所在。基本上,一旦调用未定义的行为(甚至是鼻音恶魔),该标准就允许任何事情发生。如果有一个“正确”的行为根据你的心理模型的语言,该模型是简单的错误; C
标准拥有唯一的投票权,句号。
未定义行为的其他示例包括访问超出其边界的数组,dereferencing the null pointeraccessing objects after their lifetime ended或写入allegedly clever expressions(如i++ + ++i)。
C++标准的1.9节也提到了未定义行为的两个不太危险的兄弟,unspecified behaviorimplementation-defined behavior
本标准中的语义描述定义了一个参数化的非确定性抽象机。
抽象机的某些方面和操作在本国际标准中被描述为实现定义(例如sizeof(int))。这些构成了抽象机器的参数。每个实现都应包括描述其在这些方面的特性和行为的文档。
抽象机的某些其他方面和操作在本国际标准中被描述为未指定(例如,函数参数的求值顺序)。在可能的情况下,本国际标准定义了一组允许的行为。这些定义了抽象机器的非确定性方面。
某些其他操作在本国际标准中被描述为undefined(例如,解引用空指针的效果)。[ * 注 本标准对包含未定义行为的程序的行为没有任何要求。- 尾注 * ]
具体而言,第1.3.24节规定:
允许的未定义行为包括完全忽略情况并产生不可预测的结果,在翻译或程序执行期间以环境特征的记录方式行为(有或没有发出诊断消息),终止翻译或执行(发出诊断消息)。
你能做些什么来避免遇到未定义的行为?基本上,你必须阅读那些知道自己在说什么的作者写的good C++ books。避免网络教程。避免bullschildt。

csbfibhn

csbfibhn2#

好吧,这基本上是C标准的直接复制粘贴:

3.4.11实现定义的行为未指定的行为,其中每个实现记录了如何做出选择

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

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

可能的未定义行为范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式(有或没有发出诊断消息)进行行为,到终止翻译或执行(发出诊断消息)。
3示例未定义行为的一个示例是整数溢出的行为。

3.4.41未指定的行为使用未指定的值,或本标准提供两种或两种以上可能性且在任何情况下均未对其施加进一步要求的其他行为

2示例未指定行为的一个示例是函数参数的求值顺序。

toe95027

toe950273#

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

实现自定义行为:

语言说我们有数据类型。编译器供应商指定他们应该使用什么大小,并提供他们所做的文档。

未定义行为:

你做错事了。例如,您在int中有一个非常大的值,它不适合char。如何将该值放入char中?其实没办法!任何事情都可能发生,但最明智的做法是将该int的第一个字节放入char中。分配第一个字节是错误的,但这就是幕后发生的事情。

未指定行为:

这两个函数中哪一个先执行?

void fun(int n, int m);

int fun1() {
    std::cout << "fun1";
    return 1;
}
int fun2() {
    std::cout << "fun2";
    return 2;
}

//...

fun(fun1(), fun2()); // which one is executed first?

字符串
该语言没有指定求值,从左到右还是从右到左!因此,一个未指定的行为可能会也可能不会导致一个未定义的行为,但你的程序肯定不应该产生一个未指定的行为。
@eSKay我认为你的问题值得编辑答案来澄清更多:)
对于fun(fun1(), fun2());,行为不是“定义的实现”吗?编译器必须选择一个或另一个过程,毕竟?
实现定义和未定义的区别在于,编译器应该在第一种情况下选择一个行为,但在第二种情况下不必这样做。例如,一个实现必须有且只有一个sizeof(int)的定义。因此,它不能说sizeof(int)在程序的某些部分是4,而在其他部分是8。与未指定的行为不同,编译器可以说:“好的,我将从左到右计算这些参数,而下一个函数的参数将从右到左计算。”这可能发生在同一个程序中,这就是为什么它被称为 * unspecified *。事实上,如果指定了一些未指定的行为,C本可以变得更容易。看看这里在博士。Stroustrup对此的回答是:
有人声称,给予编译器这种自由和要求“普通的从左到右求值”之间的差异可能是显着的。我不相信,但是有无数的编译器“在那里”利用这种自由,有些人热情地捍卫这种自由,改变将是困难的,可能需要几十年才能渗透到C和C
世界的遥远角落。令我失望的是,并非所有的编译器都对诸如++i+i++之类的代码发出警告。同样,参数的求值顺序也是未指定的。
在我看来,太多的“事情”是未定义的,未指定的,这很容易说,甚至给予例子,但很难解决。还应该注意的是,避免大多数问题并产生可移植的代码并不是那么困难。

2hh7jdfx

2hh7jdfx4#

来自官方C原理文档
术语 unspecified behavior、undefined behavior和 implementation-defined behavior用于对编写程序的结果进行分类,这些程序的属性标准没有或不能完全描述。采用这种分类的目标是允许实现中的某种多样性,这允许实现的质量成为市场上的活跃力量,以及允许某些流行的扩展,而不去除与标准一致性的声望。本标准的附录F列出了属于这三类之一的行为。

  • 未指定的行为 * 在翻译程序时给了实现者一些自由。这个自由度并没有延伸到无法翻译程序的程度。
  • 未定义的行为 * 允许实现者不捕获某些难以诊断的程序错误。它还确定了可能的符合语言扩展的领域:实现者可以通过提供官方未定义行为的定义来扩充语言。
  • 实现定义的 * 行为给予实现者选择适当方法的自由,但要求向用户解释这种选择。被指定为实现定义的行为通常是用户可以基于实现定义做出有意义的编码决策的那些行为。实现者在决定实现定义应该有多广泛时应该记住这个标准。与未指定的行为一样,简单地不转换包含实现定义的行为的源代码并不是一个适当的响应。
js81xvg6

js81xvg65#

Undefined Behavior vs. Unspecified Behavior有一个简短的说明。
最后总结:
总而言之,未指定的行为通常是您不应该担心的事情,除非您的软件需要可移植。相反,未定义的行为总是不受欢迎的,永远不应该发生。

goqiplq2

goqiplq26#

实现定义-
实现者希望,应该有很好的文档,标准给出选择,但一定要编译
未指明─
与实现定义相同,但未记录
未定义-
任何事情都有可能发生,小心点。

r6vfmomb

r6vfmomb7#

从历史上看,实现定义的行为和未定义的行为都代表了这样的情况,即标准的作者希望编写高质量实现的人会使用判断来决定什么行为保证(如果有的话)对在预期目标上运行的预期应用领域中的程序有用。高端数字处理代码的需求与低级系统代码的需求有很大的不同,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;
}

字符串
二进制补码实现将不必花费任何努力来将表达式v << pow处理为二进制补码移位而不管v是正还是负。
然而,当今一些编译器作者的首选哲学认为,由于v只能在程序要进行未定义行为时为负,因此没有理由让程序裁剪v的负范围。尽管每一个重要的编译器都支持左移负值,并且大量现有代码依赖于这种行为,但现代哲学将标准说左移负值是UB的事实解释为暗示编译器编写者可以随意忽略这一点。

rslzwgfq

rslzwgfq8#

C标准n3337**§1.3.10实现定义的行为**
对于格式良好的程序构造和正确的数据,依赖于实现的行为,并且每个实现文档
有时候C
标准并没有对某些结构强加特定的行为,而是说一个特定的、定义良好的行为必须由特定的实现(库的版本)来选择和描述。因此,即使标准没有描述,用户仍然可以确切地知道程序将如何运行。
C标准n3337**§1.3.24未定义的行为**
本国际标准未提出要求的行为[注:当本标准省略了任何明确的行为定义或程序使用了错误的结构或错误的数据时,可能会出现未定义的行为。允许的未定义行为的范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境的文档化方式(有或没有发出诊断消息)进行行为,到终止翻译或执行(发出诊断消息)。许多错误的程序构造不会产生未定义的行为;他们需要被诊断。- 尾注]
当程序遇到没有根据C
标准定义的构造时,它可以做任何它想做的事情(可能给我发电子邮件,也可能给你发电子邮件,或者完全忽略代码)。
C标准n3337**§1.3.25未指定的行为**
行为,用于格式良好的程序构造和正确的数据,这取决于实现[注:实现不需要记录发生的行为。可能的行为的范围通常由本国际标准描述。- 尾注]
C
标准并没有对某些结构强加特定的行为,而是说一个特定的,定义良好的行为必须由特定的实现(库的版本)选择(但不一定要描述)。因此,在没有提供描述的情况下,用户可能很难确切地知道程序将如何运行。

jum4pzuy

jum4pzuy9#

未定义的行为是丑陋的,就像“好的,坏的,丑陋的”。
好:一个程序,编译和工作,为正确的理由。
坏的:一个程序有一个错误,一种编译器可以检测和抱怨。
丑陋的:一个程序有一个错误,编译器不能检测和警告,这意味着程序编译,可能看起来正确的一些时间,但也奇怪的失败的一些时间。这就是未定义的行为。
一些程序语言和其他形式化系统试图限制“未定义的鸿沟”--也就是说,它们试图安排事情,以便大多数或所有程序都是“好”或“坏”的,只有很少的程序是“丑”的。然而,C语言的一个特征是它的“未定义的鸿沟”相当宽。

相关问题