当前位置: 首页 > 知识库问答 >
问题:

可观察行为和未定义行为——如果我不调用析构函数会发生什么?

鲁杜吟
2023-03-14

注意:我见过类似的问题,但没有一个答案足够精确,所以我自己问这个问题。

C标准规定:

程序可以通过重用对象占用的存储,或通过显式调用具有非平凡析构函数的类类型的对象的析构函数来结束任何对象的生存期。对于具有非平凡析构函数的类类型的对象,在重用或释放该对象占用的存储之前,程序不需要显式调用析构函数;然而,如果没有显式调用析构函数,或者如果没有使用删除表达式来释放存储,则不应隐式调用析构函数,并且依赖析构函数产生的副作用的任何程序都具有未定义的行为。

我简直不明白“取决于副作用”是什么意思。

一般问题是:

一个具体的例子来说明我的观点:

考虑下面这样一个程序。还要考虑明显的变化(例如,如果我没有在另一个对象上构建一个对象,但我仍然忘记调用析构函数,如果我不打印输出以观察它,等等),会怎么样:

#include <math.h>
#include <stdio.h>

struct MakeRandom
{
    int *p;
    MakeRandom(int *p) : p(p) { *p = rand(); }
    ~MakeRandom() { *p ^= rand(); }
};

int main()
{
    srand((unsigned) time(NULL));        // Set a random seed... not so important
    // In C++11 we could use std::random_xyz instead, that's not the point

    int x = 0;
    MakeRandom *r = new MakeRandom(&x);  // Oops, forgot to call the destructor
    new (r) MakeRandom(&x);              // Heck, I'll make another object on top
    r->~MakeRandom();                    // I'll remember to destroy this one!
    printf("%d", x);                     // ... so is this undefined behavior!?!
    // If it's indeed UB: now what if I didn't print anything?
}

对我来说,说这表现出“未定义的行为”似乎很荒谬,因为x已经是随机的——因此将它与另一个随机数异或并不能真正使程序比以前更“未定义”,是吗?

此外,什么时候说程序“依赖”于析构函数是正确的?如果值是随机的,或者一般来说,如果我无法区分析构函数是运行还是未运行,它会这样做?如果我从来没有读过这个值呢?基本上:

究竟是哪个表达式或语句导致了这种情况,为什么?

共有3个答案

蒋默
2023-03-14

这在标准中确实不是一个很好的定义,但我将“依赖”解释为“受抽象机器规则影响的行为”。

该行为包括对易失性变量的读写序列和对库I/O函数的调用(其中至少包括标准库的I/O函数,如printf,但也可能包括任何给定实现中的任何数量的附加函数,如WinAPI函数)。具体措辞见1.9/9。

因此,如果析构函数的执行或缺少析构函数会影响该行为,则该行为是未定义的。在您的示例中,是否执行析构函数会影响x的值,但该存储无论如何都是死的,因为下一个构造函数调用会覆盖它,因此编译器实际上可以对其进行优化(很可能会)。但更重要的是,对rand()的调用会影响RNG的内部状态,这会影响另一个对象的构造函数和析构函数中rand()返回的值,因此它确实会影响x的最终值。它是“随机”(伪随机)的任何一种方式,但它将是一个不同的值。然后打印x,将修改转化为可观察的行为,从而使程序未定义。

如果您从未使用x或RNG状态执行任何可观察的操作,则可观察的行为将与是否调用析构函数无关,因此它不会未定义。

文国发
2023-03-14

如果您接受标准要求在析构函数影响程序行为的情况下通过破坏来平衡分配,这是有道理的。也就是说,唯一合理的解释是如果一个程序

  • 曾经无法调用对象上的析构函数(可能间接通过删除)和
  • 说析构函数有副作用,

那么这个节目注定是乌兰巴托之地。(OTOH,如果析构函数不影响程序行为,那么您就脱离了困境。您可以跳过调用。)

注:本文讨论了添加的副作用,我在这里不再重复。保守的推断是,“程序…依赖于析构函数”等同于“析构函数有副作用”

补充说明然而,该标准似乎允许更自由的解释。它没有正式定义程序的依赖性。(它确实将表达式的特定质量定义为依赖携带,但这不适用于这里。)然而,在“A依赖于B”和“A依赖于B”的衍生词的100多种使用中,它使用了这个词的传统意义:B的变化直接导致A的变化。因此,推断程序P依赖于副作用E似乎并不是一个飞跃,因为E的性能或非性能导致P执行期间可观察行为的变化。在这里,我们有坚实的基础。根据标准,程序的含义——它的语义学——等同于它在执行过程中的可观察行为,这是明确定义的。

一致性实施的最低要求是:

>

在程序终止时,写入文件的所有数据应与根据抽象语义学执行程序可能产生的结果之一相同。

交互设备的输入和输出动态应在程序等待输入之前实际交付提示输出。交互设备的构成由实现定义

这些统称为程序的可观察行为。

因此,根据标准的约定,如果析构函数的副作用最终会影响易失性存储访问、输入或输出,并且永远不会调用该析构函数,则程序具有UB。

换句话说:如果你的析构函数做了重要的事情,并且没有被一致调用,那么你的程序(称为标准)应该被考虑,并在此声明为无用。

对于一个语言标准来说,这是否过于严格,甚至过于迂腐?(毕竟,该标准防止了由于隐式析构函数调用而产生的副作用,然后如果析构函数在被调用的情况下会导致可观察行为的变化,则会对您造成不利影响!)也许是这样。但作为坚持形式良好的项目的一种方式,它确实有意义。

东方森
2023-03-14

我简直不明白“取决于副作用”是什么意思。

这意味着它取决于析构函数正在做的事情。在您的示例中,修改或不修改它。您的代码中存在这种依赖关系,因为如果不调用dctor,输出将不同。

在您当前的html" target="_blank">代码中,打印的数字可能与第二次rand()调用返回的数字不同。您的程序调用未定义的行为,但只是这里的UB没有不良影响。

如果您不打印该值(或以其他方式读取它),那么就不会对dcor的副作用有任何依赖,因此就没有UB。

因此:

忘记调用析构函数与忘记调用具有相同主体的普通函数有什么不同吗?

不,在这方面没有什么不同。如果您依赖于它被调用,那么您必须确保它被调用,否则您的依赖关系将无法得到满足。

此外,什么时候说程序“依赖”于析构函数是正确的?如果值是随机的,或者一般来说,如果我无法区分析构函数是运行还是未运行,它会这样做?

随机与否无关紧要,因为代码取决于要写入的变量。仅仅因为很难预测新值是什么并不意味着没有依赖性。

如果我从来没有读出价值呢?

然后没有UB,因为代码在写入变量后对变量没有依赖性。

在哪些条件下(如果有),该程序是否表现出未定义的行为?

没有条件。总是UB。

究竟是哪个表达式或语句导致了这种情况,为什么?

表达式:

printf("%d", x);

因为它引入了对受影响变量的依赖。

 类似资料:
  • 根据我的理解,是一个可以随时间变化的值(可以订阅,订阅者可以接收更新的结果)。这似乎与的目的完全相同。 何时使用与?使用比使用有好处吗?或者反之亦然?

  • 考虑以下代码: 旁白:请不要评论“这段代码很糟糕”,“你不能在全局对象中使用析构函数”(你可能不应该这样做),也不要在评论中探究XY问题。这是一个好奇的学生提出的学术问题,他知道如何更好地解决原来的问题,但却在探索C++的广阔天地时偶然发现了这个怪癖。

  • 我读到虚拟析构函数必须在具有虚拟方法的类中声明。我只是不明白为什么必须宣布它们是虚拟的。我从下面的例子中知道为什么我们需要虚拟析构函数。我只是想知道为什么编译器不为我们管理虚拟析构函数。关于虚拟析构函数的工作,有什么我需要知道的吗?以下示例显示,如果析构函数未声明为虚拟,则派生类的析构函数不会被调用,这是为什么?

  • 问题内容: var x int done := false go func() { x = f(…); done = true } while done == false { } 这是Go代码。我的恶魔告诉我,这是UB代码。为什么? 问题答案: Go Memory Model不保证该程序将始终遵守在goroutine中写入x的值。go常规销毁 部分中提供了一个类似的错误程序作为示例。 在本节中,G

  • 问题内容: 该功能运行什么?它只会运行吗? 问题答案: setState()将按以下顺序运行函数: 如果您的组件正在接收道具,它将使用上述功能运行该功能。

  • 我是RxJava的新手,正在尝试从link执行多个观测值的并行执行示例:RxJava并行获取观测值 虽然上面链接中提供的示例是并行执行可观察对象,但是当我在foreach方法中添加一个Thread.sleep(TIME_IN_MILLISECONDS)时,系统开始一次执行一个可观察对象。请帮助我理解为什么Thread.sleep停止可观察对象的并行执行。 下面是导致多个观测值同步执行的修改示例: