15

事实证明,许多看起来很无辜的东西在 C++ 中是未定义的行为。例如,一旦一个非空指针被打印delete出来,甚至打印出该指针值是未定义的行为

现在内存泄漏肯定很糟糕。但是他们是什么类的情况——定义的、未定义的或其他什么类的行为?

4

14 回答 14

29

内存泄漏。

没有未定义的行为。泄漏内存是完全合法的。

未定义的行为:是标准明确不想定义并留给实现的操作,以便在不违反标准的情况下灵活地执行某些类型的优化。

内存管理定义明确。
如果您动态分配内存并且不释放它。然后,内存仍然是应用程序的属性,可以按照它认为合适的方式进行管理。您丢失了对那部分内存的所有引用的事实既不存在也不存在。

当然,如果您继续泄漏,那么您最终将耗尽可用内存并且应用程序将开始抛出 bad_alloc 异常。但这是另一个问题。

于 2009-12-30T07:15:50.970 回答
7

内存泄漏肯定是在 C/C++ 中定义的。

如果我做:

int *a = new int[10];

其次是

a = new int[10]; 

我肯定会泄漏内存,因为无法访问第一个分配的数组,并且由于不支持 GC,因此不会自动释放该内存。

但是这种泄漏的后果是不可预测的,并且对于相同的给定应用程序会因应用程序和机器而异。假设由于在一台机器上泄漏而崩溃的应用程序可能在另一台具有更多 RAM 的机器上运行良好。同样对于给定机器上的给定应用程序,由于泄漏导致的崩溃可能会在运行期间的不同时间出现。

于 2009-12-30T06:43:53.633 回答
7

如果您泄漏内存,则执行将继续进行,就好像什么都没发生一样。这是定义的行为。

接下来,您可能会发现malloc由于没有足够的可用内存而导致调用失败。但这是 的定义行为malloc,其后果也很明确: malloc调用返回NULL

现在这可能会导致不检查结果的程序因malloc分段违规而失败。但是这种未定义的行为(来自语言规范的 POV)是由于程序取消引用无效指针,而不是早期的内存泄漏或失败的malloc调用。

于 2009-12-30T07:47:48.503 回答
6

我对这句话的解释:

对于具有非平凡析构函数的类类型的对象,在重用或释放对象占用的存储空间之前,程序不需要显式调用析构函数;但是,如果没有显式调用析构函数,或者如果没有使用删除表达式 (5.3.5) 来释放存储,则不应隐式调用析构函数以及依赖于析构函数产生的副作用的任何程序具有未定义的行为。

如下:

如果您以某种方式设法释放对象占用的存储空间而 没有在占用内存的对象上调用析构函数,那么如果析构函数不重要并且具有副作用,那么 UB 就是结果。

如果new用 分配malloc,原始存储可以用 释放free(),析构函数不会运行,并且会导致 UB。或者如果一个指针被强制转换为不相关的类型并被删除,内存被释放,但错误的析构函数运行,UB。

delete这与未释放底层内存的省略不一样。省略delete不是UB。

于 2014-06-10T09:53:48.313 回答
4

(下面的评论“注意事项:此答案已从Does a memory leak cause undefined behavior? ”移至此处-您可能必须阅读该问题才能获得此答案O_o的适当背景)。

在我看来,标准的这一部分明确允许:

  • 拥有一个自定义内存池,您可以将new对象放入其中,然后释放/重用整个事物,而无需花费时间调用它们的析构函数,只要您不依赖于对象析构函数的副作用

  • 分配一点内存并且从不释放它的库,可能是因为它们的函数/对象可以被静态对象的析构函数和注册的退出处理程序使用,并且不值得购买整个协调的破坏顺序或瞬态每次这些访问发生时,“凤凰”般的重生。

我不明白为什么标准选择在依赖于副作用时不定义行为 - 而不是简单地说这些副作用不会发生并让程序像你通常期望的那样定义未定义的行为那个前提。

我们仍然可以考虑标准所说的未定义行为。关键部分是:

“取决于析构函数产生的副作用具有未定义的行为。”

标准§1.9/12 明确定义副作用如下(下面的斜体为标准,表示引入了正式定义):

访问由volatileglvalue (3.10) 指定的对象、修改对象、调用库 I/O 函数或调用执行任何这些操作的函数都是副作用,它们是执行环境状态的变化。

在您的程序中,没有依赖关系,因此没有未定义的行为。

可以说,与§3.8 p4 中的场景相匹配的依赖关系的一个示例是:对未定义行为的需求或原因并不明显:

struct X
{
    ~X() { std::cout << "bye!\n"; }
};

int main()
{
     new X();
}

人们正在争论的一个问题是上述X对象是否会被考虑released用于 3.8 p4,因为它可能仅在程序终止后才发布到操作系统 - 从阅读标准中不清楚进程“生命周期”的那个阶段是否在标准行为要求的范围(我对标准的快速搜索并没有澄清这一点)。我个人认为 3.8p4 在这里适用,部分原因是只要它足够模棱两可,编译器编写者可能会觉得有权在这种情况下允许未定义的行为,但即使上面的代码不构成发布场景的容易修正了阿拉...

int main()
{
     X* p = new X();
     *(char*)p = 'x';   // token memory reuse...
}

无论如何,但是 main 实现了上面的析构函数有一个副作用- 每个“调用库 I/O 函数”;此外,程序的可观察行为可以说是“依赖于”它,因为如果它已经运行,会受到析构函数影响的缓冲区在终止期间被刷新。但是“取决于副作用”是否只是暗示如果析构函数没有运行,程序显然会有未定义的行为的情况?我会在前者方面犯错,特别是因为后一种情况不需要标准中的专用段落来记录行为未定义。这是一个明显未定义行为的示例:

int* p_;

struct X
{
    ~X() { if (b_) p_ = 0; else delete p_; }
    bool b_;
};

X x{true};

int main()
{
     p_ = new int();
     delete p_; // p_ now holds freed pointer
     new (&x){false};  // reuse x without calling destructor
}

x' 的析构函数在终止期间被调用时,b_将会false并且~X()因此delete p_对于已经释放的指针,创建未定义的行为。如果x.~X();在重用之前被调用,p_将被设置为 0 并且删除将是安全的。从这个意义上说,程序的正确行为可以说取决于析构函数,并且行为显然是未定义的,但是我们是否只是制作了一个与 3.8p4 描述的行为本身匹配的程序,而不是让行为成为结果3.8p4...?

有问题的更复杂的场景 - 太长而无法提供代码 - 可能包括例如一个奇怪的 C++ 库,其中文件流对象内部的引用计数器必须达到 0 才能触发某些处理,例如刷新 I/O 或加入后台线程等 -如果不做这些事情,不仅会导致无法执行析构函数显式请求的输出,而且还会无法从流中输出其他缓冲输出,或者在具有事务文件系统的某些操作系统上可能会导致早期 I/O 的回滚 -此类问题可能会改变可观察到的程序行为,甚至会使程序挂起。

注意:没有必要证明在任何现有编译器/系统上存在任何行为异常的实际代码;该标准清楚地保留了编译器具有未定义行为的权利……这才是最重要的。这不是你可以推理并选择忽略标准的事情 - 可能是 C++14 或其他一些修订版改变了这个规定,但只要它在那里,如果甚至可以说对副作用有一些“依赖”,那么存在未定义行为的可能性(当然,它本身允许由特定的编译器/实现定义,因此并不意味着每个编译器都必须做一些奇怪的事情)。

于 2014-06-10T09:26:26.400 回答
3

语言规范没有提到“内存泄漏”。从语言的角度来看,当您在动态内存中创建对象时,您正在这样做:您正在创建一个具有无限生命周期/存储持续时间的匿名对象。在这种情况下,“无限”意味着该对象只能在您明确解除分配它时结束其生命周期/存储持续时间,否则它会继续永远存在(只要程序运行)。

现在,我们通常认为动态分配的对象在程序执行时成为“内存泄漏”,此时对该对象的所有引用(通用“引用”,如指针)都丢失到无法恢复的地步。请注意,即使对人类来说,“所有引用都丢失”的概念也没有得到非常精确的定义。如果我们对对象的某些部分有引用,理论上可以“重新计算”为对整个对象的引用怎么办?是不是内存泄漏?如果我们没有对对象的任何引用,但是我们可以使用程序可用的其他一些信息(例如精确的分配顺序)以某种方式计算这样的引用怎么办?

语言规范本身并不关心这样的问题。无论您认为程序中是否出现“内存泄漏”,从语言的角度来看,这根本不是事件。从语言的角度来看,一个“泄露的”动态分配的对象只会继续快乐地生活,直到程序结束。这是唯一剩下的问题:当程序结束并且仍然分配一些动态内存时会发生什么?

如果我没记错的话,该语言并没有指定在程序终止时仍然分配的动态内存会发生什么。不会尝试自动销毁/释放您在动态内存中创建的对象。但是在这种情况下没有正式的未定义行为

于 2009-12-30T07:47:07.310 回答
3

举证责任在于那些认为内存泄漏可能是 C++ UB 的人。

自然没有提供任何证据。

简而言之,对于任何怀有任何疑问的人来说,这个问题永远无法得到明确的解决,除非非常可信地用响亮的贾斯汀比伯音乐威胁委员会,以便他们添加一个 C++14 声明来澄清它不是 UB。


有争议的是 C++11 §3.8/4:

对于具有非平凡析构函数的类类型的对象,在重用或释放对象占用的存储空间之前,程序不需要显式调用析构函数;但是,如果没有显式调用析构函数或者如果没有使用删除表达式(5.3.5) 来释放存储,则不应隐式调用析构函数以及依赖于析构函数产生的副作用的任何程序具有未定义的行为。

这段话在 C++98 和 C++03 中的措辞完全相同。这是什么意思?

  • 在重用或释放对象占用的存储空间之前,程序不需要显式调用析构函数
     
    ——这意味着可以获取变量的内存并重用该内存,而无需首先破坏现有对象。

  • 如果没有显式调用析构函数,或者如果没有使用删除表达式(5.3.5)来释放存储,则不应隐式调用析构函数
     
    ——意味着如果在内存重用之前不破坏现有对象,那么如果对象是这样的,它的析构函数被自动调用(例如,局部自动变量),那么程序具有未定义的行为,因为该析构函数随后将对不再存在的对象进行操作。

  • 并且任何依赖于析构函数产生的副作用的程序都有未定义的行为
     
    ——不能按字面意思表示,因为根据副作用的定义,程序总是依赖于任何副作用。或者换句话说,程序没有办法不依赖副作用,因为那样它们就不会是副作用。

很可能预期的不是最终进入 C++98 的东西,所以我们手头的东西是一个缺陷

从上下文可以猜测,如果程序依赖于静态已知类型对象的自动销毁T,其中内存已被重用于创建一个或多个不是对象的T对象,那么这就是未定义行为。


看过评论的人可能会注意到,上面对“shall”这个词的解释并不是我之前假设的意思。正如我现在所看到的,“shall”不是实现的要求,它被允许做什么。这是对程序的要求,允许代码做什么。

因此,这是正式的 UB:

auto main() -> int
{
    string s( 666, '#' );
    new( &s ) string( 42, '-' );    //  <- Storage reuse.
    cout << s << endl;
    //  <- Formal UB, because original destructor implicitly invoked.
}

但这可以通过字面解释:

auto main() -> int
{
    string s( 666, '#' );
    s.~string();
    new( &s ) string( 42, '-' );    //  <- Storage reuse.
    cout << s << endl;
    //  OK, because of the explicit destruction of the original object.
}

一个主要问题是,对于上面标准段落的字面解释,如果放置 new 在那里创建了一个不同类型的对象,那在形式上仍然是可以的,只是因为显式破坏了原始对象。但在这种情况下,这不是很实用。也许这被标准中的其他段落所涵盖,因此它也是正式的 UB。

这也可以,使用以下位置new<new>

auto main() -> int
{
    char* storage   = new char[sizeof( string )];
    new( storage ) string( 666, '#' );
    string const& s = *(
        new( storage ) string( 42, '-' )    //  <- Storage reuse.
        );
    cout << s << endl;
    //  OK, because no implicit call of original object's destructor.
}

正如我所见——现在。

于 2014-06-10T09:28:09.590 回答
2

明确定义的行为。

考虑服务器正在运行并继续分配堆内存并且即使没有使用它也没有释放内存的情况。因此最终结果将是最终服务器将耗尽内存并且肯定会发生崩溃。

于 2009-12-30T07:03:01.660 回答
2

除了所有其他答案之外,还有一些完全不同的方法。查看第 5.3.4-18 节中的内存分配,我们可以看到:

如果上面描述的对象初始化的任何部分76通过抛出异常而终止并且可以找到合适的释放函数,则调用释放函数以释放构造对象的内存,之后异常继续在新表达式的上下文。如果找不到明确匹配的解除分配函数,则传播异常不会导致对象的内存被释放。[注:这适用于被调用的分配函数不分配内存的情况;否则,很可能导致内存泄漏。——尾注]

会不会在这里引起UB,会提到,所以它“只是内存泄漏”。

在 §20.6.4-10 之类的地方,提到了可能的垃圾收集器和泄漏检测器。已经对安全派生指针等概念进行了很多思考。能够将 C++ 与垃圾收集器一起使用(C.2.10“对垃圾收集区域的最小支持”)。

因此,如果 UB 只是丢失了指向某个对象的最后一个指针,那么所有的努力都是没有意义的。

关于“当析构函数有副作用时,它永远不会运行 UB”我会说这是错误的,否则诸如此类的设施也std::quick_exit()将是固有的 UB。

于 2014-06-10T16:24:37.527 回答
1

If the space shuttle must take off in two minutes, and I have a choice between putting it up with code that leaks memory and code that has undefined behavior, I'm putting in the code that leaks memory.

But most of us aren't usually in such a situation, and if we are, it's probably by a failure further up the line. Perhaps I'm wrong, but I'm reading this question as, "Which sin will get me into hell faster?"

Probably the undefined behavior, but in reality both.

于 2009-12-30T15:53:29.753 回答
0

已定义,因为内存泄漏是您忘记自己清理。

当然,内存泄漏可能会在以后导致未定义的行为。

于 2009-12-30T06:35:11.590 回答
0

这显然不能是未定义的行为。仅仅因为UB必须在某个时间点发生,并且忘记释放内存或调用析构函数不会在任何时间点发生。所发生的只是程序在没有释放内存或调用析构函数的情况下终止;这不会以任何方式使程序或其终止的行为未定义。

话虽如此,在我看来,该标准在这段话中是自相矛盾的。一方面它确保在这种情况下不会调用析构函数,另一方面它表示如果程序依赖于析构函数产生的副作用,那么它具有未定义的行为。假设析构函数调用了exit,那么任何执行任何操作的程序都不能假装独立于它,因为调用析构函数的副作用会阻止它执行原本会执行的操作;但是文本还确保不会调用析构函数,以便程序可以继续不受干扰地执行其工作。我认为阅读这篇文章结尾的唯一合理方法是,如果程序的正确行为需要要调用的析构函数,则实际上没有定义行为;鉴于刚刚规定不会调用析构函数,因此这是多余的评论。

于 2014-06-10T14:36:56.347 回答
0

直截了当的回答:标准没有定义泄漏内存时会发生什么,因此它是“未定义的”。但是它是隐式未定义的,这比标准中显式未定义的东西更有趣。

于 2009-12-30T07:20:49.453 回答
-1

未定义的行为意味着,将发生的事情尚未定义或未知。内存泄漏的行为在 C/C++ 中肯定会吞噬可用内存。然而,由此产生的问题并不总是像游戏结束所描述的那样被定义和变化。

于 2009-12-30T07:00:09.153 回答