【问题标题】:delete a NULL pointer does not call overloaded delete when destructor is written删除 NULL 指针在写入析构函数时不会调用重载删除
【发布时间】:2010-11-09 15:37:24
【问题描述】:
class Widget
{
    public:
        Widget() {
            cout<<"~Widget()"<<endl;
        }
        ~Widget() {
            cout<<"~Widget()"<<endl;
        }

    void* operator new(size_t sz) throw(bad_alloc) {
        cout<<"operator new"<<endl;
        throw bad_alloc();
    }

    void operator delete(void *v) {
        cout<<"operator delete"<<endl;
    }

};

int main() 
{
    Widget* w = 0;
    try {
        w = new Widget();
    }
    catch(bad_alloc) {
        cout<<"Out of Memory"<<endl;
    }

    delete w;
    getch();
    return 1;
}

在这段代码中,delete w 在析构函数存在时不会调用重载的delete 运算符。如果省略析构函数,则调用重载的delete。为什么会这样?

写入~Widget()时的输出

运营商新
内存不足

~Widget() 未写入时的输出

运营商新
内存不足
运算符删除

【问题讨论】:

  • 这是一个非常有趣的问题!我刚刚查看了 Scott Meyer 的 Effective C++(不幸的是我只有第二版),对此一无所知。

标签: c++ memory-management destructor


【解决方案1】:

您试图删除一个 NULL 指针。所以,析构函数没有被调用。

class Widget
{   
public:        
    Widget()
    {            
        cout<<"Widget()"<<endl;        
    }       

    ~Widget() 
    {          
        cout<<"~Widget()"<<endl;    
    }    

    void* operator new(size_t sz) throw(bad_alloc) 
    {      
        cout<<"operator new"<<endl;  
        return malloc(sizeof(Widget));
        //throw bad_alloc();    
    }  

    void operator delete(void *v)
    {               
        cout<<"operator delete"<<endl;   
    }
};

int main()
{

    Widget* w = NULL; 
    try 
    {   
        w = new Widget();
        //throw bad_alloc();
    }   
    catch(bad_alloc) 
    {        
        cout<<"Out of Memory"<<endl;  
    }   
    delete w; 
}

输出:

新运营商
小部件()
~Widget()
运算符删除

【讨论】:

  • 是的,但问题是,为什么在使用 NULL 调用 delete 时会调用他的自定义 delete? (但前提是类没有析构函数?)
  • 问题不在于为什么不调用析构函数。问题是,当我不提供析构函数时(在这种情况下,编译器会为我创建一个),为什么会调用重载的删除??
【解决方案2】:

想要发表评论,而不是回答,作为新成员没有足够的权限。

在创建对象期间引发了异常。 没有调用析构函数,因为没有创建对象本身。

您也可以观察到,因为来自构造函数和析构函数的消息没有显示出来。

但是,当未定义析构函数时,将调用删除。 如果直接考虑当没有定义析构函数时,C++编译器将其视为任何其他运算符,编译器默认在未定义时提供析构函数。

【讨论】:

    【解决方案3】:

    原因是如果你有一个析构函数,对删除操作符的调用是在标量删除析构函数中完成的,在 VC 中它包含对你的析构函数和删除操作符的调用。编译器提供了检查您是否尝试删除 NULL 指针的代码。删除这样的指针当然是合法的,但是不能调用这样的对象的析构函数,因为它可能包含成员变量的使用。因此避免了对标量删除析构函数的调用,因此也避免了对删除运算符的调用。

    当没有析构函数时,编译器直接调用删除操作符,不生成标量删除析构函数。因此,在这种情况下,删除操作符毕竟会被调用。

    【讨论】:

    • 这不太正确 - delete 显然不能在析构函数中完成,因为对于具有静态和自动存储的类的对象也会调用析构函数,并调用 @987654322 @ 那些显然是不可取的。相反,VC++ 生成了一个名为“标量删除析构函数”的 extra 函数,它首先调用适当的析构函数(仍然作为另一个单独的函数生成),然后执行delete this。然后在该类型的对象上执行delete 时调用该标量删除析构函数。
    • 感谢您的更正,帕维尔。相应地修正了我的答案。
    • 我认为这是正确的答案,因为如果从 Widget 中删除了 dtor,但添加了具有 dtor 的对象,您将获得相同的行为。 >
    • 但是如果用户不提供dtor,编译器按照标准提供默认dtor。这是否意味着编译器生成的默认 dtor 总是调用 operator-delete 并且仅在用户提供的 dtor 的情况下才检查空指针?令人困惑的恕我直言...
    • 编译器“提供”了一个生成的析构函数,你可以在代码中引用它。但由于这样的析构函数对样本类型完全没有任何作用(没有 vtable、没有基类,也没有需要销毁的成员),因此它被有效地优化为无操作,甚至不会生成“标量删除析构函数”。
    【解决方案4】:

    我记得不久前在 comp.lang.c++.moderated 中关于 operator delete 的类似内容。我现在找不到它,但答案是这样的..

    不幸的是,语言 规格不够 明确控制是否应该去 进入重载的“操作员删除” 当调用删除表达式时 在对应的空指针上 类型,即使标准确实 说那个删除表达式 空指针是无操作的。

    James Kanze特别说:

    这仍然是责任 运算符删除(或删除[])到 查看;标准不保证 不会给它一个空指针; 该标准要求它是一个 如果给定一个空指针,则无操作。或者那个 允许实现调用 它。根据最新草案, "第一个参数的值 提供给释放函数 可能是空指针值;如果是这样的话, 如果释放函数是 标准库中提供的一个, 通话没有效果。”我不太清楚 确定“是什么意思” 标准库中提供的一个" 应该是——从字面上理解, 因为他的功能不是提供的 由标准库,句子 似乎不适用。但不知何故, 这没有意义

    我记得这是因为我曾经有过类似的问题,并将答案保存在 .txt 文件中。

    UPDATE-1:

    哦,我找到了here。 另请阅读此链接defect report。 所以,答案是未指定5.3.5/7 章。

    【讨论】:

    • +1 听起来就像我们将得到的答案一样接近......尽管我屏住呼吸等待进一步的发展。
    • 如果它必须是空操作,编译器可能不会执行任何用户代码,因为该代码可能会导致副作用(即执行打印操作)
    【解决方案5】:

    首先,这实际上可以简化为delete (Widget*)0 - 您的main() 中的其他所有内容都不需要重现。

    这是一个代码生成人工制品,因为 1) 用户定义的 operator delete 必须能够处理 NULL 值,以及 2) 编译器尝试生成可能的最佳代码。

    首先让我们考虑不涉及用户定义的析构函数的情况。如果是这种情况,除了operator delete 之外,没有可在实例上运行的代码。在将控制权转移到operator delete 之前检查 null 是没有意义的,因为后者无论如何都应该进行检查;所以编译器只会生成operator delete 的无条件调用(你会看到后者打印一条消息)。

    现在第二种情况——析构函数被定义了。这意味着您的delete 语句实际上扩展为两个调用——析构函数和operator delete。但是不能在空指针上安全地调用析构函数,因为它可能会尝试访问类字段(编译器可能会发现您的特定析构函数并没有真正做到这一点,因此使用 null this 调用是安全的,但看起来像他们在实践中不会打扰)。所以它在析构函数调用之前在那里插入一个空检查。一旦检查已经存在,它也可以使用它跳过对operator delete的调用——毕竟它无论如何都需要是一个空操作,它会在operator delete中节省额外的对空值的无意义检查以防指针实际为空。

    就我所见,ISO C++ 规范对此没有任何保证。只是两个编译器在这里做了同样的优化。

    【讨论】:

    • 很好的解释!恕我直言,还有第三种情况:定义了虚拟析构函数。如果指针为 NULL,则在运行时找不到这个。 NULL 指针的删除不能调用析构代码的另一个原因......
    【解决方案6】:

    在删除操作符之前调用对象析构函数。所以我的猜测是它试图调用析构函数,因此意识到指针为NULL

    1. 不调用需要实例的析构函数
    2. 在那里停止删除操作(恕我直言,这是一种速度优化)。

    正如尼尔所说,如果 w 包含一个小部件,它应该可以工作。

    【讨论】:

    • 如果它不调用析构函数,那么当我不编写析构函数时应该显示相同的行为(在这种情况下编译器应该为我创建一个)。这里的重点是为什么这两种情况下的行为不同
    • 因为当没有指定析构函数时,直接调用删除操作符(默认行为),而当它被指定时,我在这里说明的实例有这个“预检查”。在这里,我正在解释 with 析构函数的情况,比较是隐含的 - 不过可能不够清楚。
    【解决方案7】:

    我没有一个好的答案,但我稍微简化了这个问题。下面的代码去掉了操作符 new 和异常处理:

    #include <iostream>
    using namespace std;
    
    class Widget {
    
      public:
        Widget() {
            cout<<"Widget()"<<endl;
        }
        ~Widget() {
            cout<<"~Widget()"<<endl;
        }
    
      void operator delete(void *v) {
           cout << "operator delete" << endl;
      }
    };
    
    int main() {
        Widget* w = 0;
        cout << "calling delete" << endl;
        delete w;
    }
    

    这在 VC++ 和 g++ 上仍然表现出相同的行为和设计。

    当然,删除 NULL 指针是空操作,因此编译器不必调用 operator delete。如果实际分配了一个对象:

        Widget* w = new Widget;
    

    然后事情按预期进行。

    【讨论】:

      猜你喜欢
      • 2013-10-10
      • 2014-12-30
      • 1970-01-01
      • 2012-03-13
      • 2013-10-01
      • 2010-10-15
      • 2021-04-04
      • 2013-09-30
      相关资源
      最近更新 更多