在少数情况下,C++ 标准将其视为未定义的行为。例如,如果我分配 with new[]
,然后尝试使用delete
(not delete[]
) 释放未定义的行为 -任何事情都可能发生- 它可能会起作用,它可能会严重崩溃,它可能会默默地破坏某些东西并造成定时问题。
解释这个任何事情都可能发生在新手身上是很成问题的。他们开始“证明”“这有效”(因为它确实适用于他们使用的 C++ 实现)并询问“这可能有什么问题”?我能给出什么简洁的解释来激励他们不写这样的代码?
在少数情况下,C++ 标准将其视为未定义的行为。例如,如果我分配 with new[]
,然后尝试使用delete
(not delete[]
) 释放未定义的行为 -任何事情都可能发生- 它可能会起作用,它可能会严重崩溃,它可能会默默地破坏某些东西并造成定时问题。
解释这个任何事情都可能发生在新手身上是很成问题的。他们开始“证明”“这有效”(因为它确实适用于他们使用的 C++ 实现)并询问“这可能有什么问题”?我能给出什么简洁的解释来激励他们不写这样的代码?
未定义意味着明确不可靠。软件应该是可靠的。你不应该多说什么。
结冰的池塘是不确定的步行表面的一个很好的例子。仅仅因为您完成一次并不意味着您应该将捷径添加到您的纸质路线中,特别是如果您正在计划四个季节。
Two possibilities come to my mind:
You could ask them "just because you can drive on the motorway the opposite direction at midnight and survive, would you do it regularly?"
The more involved solution might be to set up a different compiler / run environment to show them how it fails spectacularly under different circumstances.
"Congratulations, you've defined the behavior that compiler has for that operation. I'll expect the report on the behavior that the other 200 compilers that exist in the world exhibit to be on my desk by 10 AM tomorrow. Don't disappoint me now, your future looks promising!"
简单地引用标准。如果他们不能接受这一点,他们就不是 C++ 程序员。基督徒会否认圣经吗?;-)
1.9 程序执行
本国际标准中的语义描述定义了一个参数化的非确定性抽象机。[...]
抽象机的某些方面和操作在本国际标准中描述为实现定义(例如,sizeof(int)
)。这些构成了抽象机的参数。每个实现都应包括描述其在这些方面的特征和行为的文档。[...]
抽象机的某些其他方面和操作在本国际标准中描述为未指定(例如,函数参数的评估顺序)。在可能的情况下,本国际标准定义了一组允许的行为。这些定义了抽象机器的不确定性方面。[...]
本国际标准中将某些其他操作描述为未定义(例如,取消引用空指针的效果)。[注:本国际标准对包含未定义行为的程序的行为没有任何要求。——尾注]
你再清楚不过了。
我会解释说,如果他们没有正确编写代码,那么他们的下一次绩效评估将不会是愉快的。这对大多数人来说已经足够“动力”了。
约翰伍兹:
简而言之,您不能在尚未定义元素的结构上使用 sizeof(),如果这样做,恶魔可能会飞出您的鼻子。
“恶魔可能从你的鼻子里飞出”简直是每个程序员的词汇的一部分。
更重要的是,谈论可移植性。解释程序如何经常被移植到不同的操作系统,更不用说不同的编译器了。在现实世界中,移植通常由原始程序员以外的人完成。其中一些端口甚至适用于嵌入式设备,在这些设备中,发现编译器的决定与您的假设不同可能会产生巨大的成本。
把人变成指针。告诉他们它们是指向人类类的指针,并且您正在调用函数“RemoveCoat”。当他们指着一个人说“RemoveCoat”时,一切都很好。如果这个人没有外套,不用担心——我们会检查这一点,所有 RemoveCoat 真正做的就是移除顶层的衣服(通过体面检查)。
现在,如果他们随机指着某个地方并说 RemoveCoat 会发生什么——如果他们指着一堵墙,那么油漆可能会剥落,如果他们指着一棵树,树皮可能会脱落,狗可能会刮胡子,USS Enterprise 可能在关键时刻降低它的护盾等等!
没有办法确定未针对该情况定义行为可能发生的情况 - 这称为未定义行为,必须避免。
让他们尝试自己的方式,直到他们的代码在测试期间崩溃。那么这些话就不需要了。
问题是新手(我们都去过那里)有一些自我和自信。没关系。事实上,如果你不这样做,你就不可能成为一名程序员。教育他们很重要,但支持他们也同样重要,不要因为破坏他们对自己的信任而打断他们的旅程。保持礼貌,但用事实而不是语言来证明你的立场。只有事实和证据才能奏效。
一个是...
“这个”用法不是语言的一部分。如果我们说在这种情况下编译器必须生成崩溃的代码,那么这将是一个特性,是编译器制造商的某种要求。该标准的作者不想对不受支持的“功能”进行不必要的工作。他们决定在这种情况下不提出任何行为要求。
C++ 并不是真正适合初学者的语言,简单地列出一些规则并毫无疑问地让它们遵守会让一些糟糕的程序员变得糟糕;我看到人们说的大多数最愚蠢的事情可能与这种盲目的规则跟随/律师有关。
另一方面,如果他们知道析构函数不会被调用,并且可能还有其他一些问题,那么他们会小心避免它。更重要的是,如果他们不小心这样做了,就有机会调试它,也有机会意识到 C++ 的许多特性有多危险。
由于有很多事情需要担心,没有任何一门课程或一本书可以让某人掌握 C++ 甚至可能变得那么好。
悄悄地覆盖 new、new[]、delete 和 delete[],看看他需要多长时间才能注意到;)
如果做不到这一点……就告诉他他错了,然后将他指向 C++ 规范。哦,是的..下次在雇用人员时要更加小心,以确保避免出现漏洞!
只需向他们展示 Valgrind。
关于未定义的行为尚未提及的一点是,如果执行某些操作会导致未定义的行为,则符合标准的实现可以合法地,也许是为了“有帮助”或提高效率,生成如果这样的操作会失败的代码被尝试过。例如,可以想象一种多处理器架构,其中任何内存位置都可能被锁定,并且尝试访问锁定位置(解锁它除外)将停止,直到相关位置被解锁为止。如果锁定和解锁非常便宜(如果它们在硬件中实现的话是合理的),那么这种架构在某些多线程场景中可能很方便,因为实现x++
as (atomically read and lock x; add one to read value; atomically unlock and write x) 将确保如果两个线程同时执行x++
,结果将为 x 加二。如果编写程序以避免未定义的行为,这样的体系结构可能会简化可靠的多线程代码的设计,而无需大笨重的内存屏障。不幸的是,*x++ = *y++;
如果x
和y
都是对同一存储位置的引用并且编译器试图将代码管道化为t1 = read-and-lock x; t2 = read-and-lock y; read t3=*t1; write *t2=t3; t1++; t2++; unlock-and-write x=t1; write-and-unlock y=t2;
. 虽然编译器可以通过避免交错各种操作来避免死锁,但这样做可能会降低效率。
编译并运行这个程序:
#include <iostream>
class A {
public:
A() { std::cout << "hi" << std::endl; }
~A() { std::cout << "bye" << std::endl; }
};
int main() {
A* a1 = new A[10];
delete a1;
A* a2 = new A[10];
delete[] a2;
}
至少在使用 GCC 时,它表明在进行单次删除时,只为其中一个元素调用析构函数。
仅仅因为他们的程序似乎有效,就不能保证什么都没有;编译器可以生成恰好可以工作的代码(当正确的行为未定义时,您甚至如何定义“工作” ?)但在周末格式化您的磁盘。他们是否将源代码读给他们的编译器?检查他们的反汇编输出?
或者仅仅因为它今天恰好“工作”而提醒他们,并不能保证在升级编译器版本时它可以工作。告诉他们找到从中爬出的任何微妙错误,玩得开心。
真的,为什么不呢?他们应该提供一个合理的论据来使用未定义的行为,而不是相反。除了懒惰之外,还有什么理由可以使用delete
?delete[]
(好吧,有std::auto_ptr
。但如果你使用std::auto_ptr
的是new[]
-allocated 数组,你可能应该使用 astd::vector
无论如何。)
告诉他们标准以及如何开发工具以符合标准。标准之外的任何东西都可能起作用,也可能不起作用,这就是UB。
打开 malloc_debug 和delete
带有析构函数的对象数组。free
块内的指针应该失败。把他们召集在一起并证明这一点。
你需要考虑其他例子来建立你的可信度,直到他们明白他们是新手并且有很多关于 C++ 的知识。
C 和 C++ 标准都使用术语“未定义的行为”来指代不同的实现以不同的、不兼容的方式处理构造可能有用的情况,其中一些行为可以预测,而另一些则不能。两者都使用相同的术语来描述 UB,虽然我不知道任何已发布的 C++ 标准的基本原理,但 C 标准的基本原理说:
未定义的行为允许实现者不捕获某些难以诊断的程序错误。它还确定了可能的符合语言扩展的领域:实现者可以通过提供官方未定义行为的定义来扩充语言。”
请注意,许多被 C 标准归类为未定义行为的操作被认为是在许多(如果不是全部)实现上完全定义的,但标准的作者希望让针对不寻常平台或应用程序领域的实现者能够偏离正常行为,如果这样做将使他们的客户受益。这种自由并非意在导致任意和反复无常地偏离先例,从而使程序员更难快速轻松地完成需要完成的工作。
不幸的是,许多使用 gcc 和 clang 的程序员并不了解他们的需求以及那些编译器的维护者,他们认识到,由于标准避免强制任何会损害永远不会接收恶意输入的应用程序效率的东西,或者只会在即使恶意程序也无法破坏任何东西的环境中运行,这意味着不需要任何实现来允许程序员轻松有效地编写适合在其他环境中使用的程序。