none
inline函数中调用delete的问题。 RRS feed

  • 问题

  • vs2005中,如果一个文件a.h中定义了一个A类,此类中有类型为B的成员变量m_b,并有inline函数void Release() { delete m_b; } 如果此时a.h中只有class B;的声明(不是定义),那么Release()函数中的delete不会调用m_b的析构函数?但编译可以通过。只有a.h知道B的定义才能顺利调用m_b的析构函数?也就是在a.h中include "b.h"或在a.h中定义class B.

    如果有inline函数void Create(){m_b = new B;},如果此.h文件看不到B的定义,编译是不能通过的,但是delete竟然可以?

    这属于c++的标准吗?

    还是挺危险的。

     

    2011年6月11日 10:39

答案

  • 非常感谢您的解答,

    main.cpp中include的顺序以及test.h中的#include "container.h"是我的一个疏忽,起初我不确定delete如果看不到定义是否会造成只释放不调用析构,所以才有了这个测试程序,但问题出在我起初认为container.cpp这个单元在被编译的时候已经是看不到class Test;的详细定义的,理所当然Test的析构不会被调用,但事情不是这样,我忽略了main.cpp这个单元在编译时container是可以见到class Test;的定义的,所以他“有时”不会影响最后的实验结果,我这里确实只能用"有时"这两个字,您如果有兴趣可以下载我下面的链接,container.cpp这个单元或项目中如果有其他编译单元如果稍加改动就会影响最后的编译结果,因为他可能涉及到编译器在编译时的一些策略问题,可能和delete被展开的先后顺序有关.

    环境:vs2005

    http://www.yesaha.com/download/deletetest.rar

    言归正传,其实这帖子要解决的还是为什么new时必须有定义,delete时未必要定义,如果没有定义,只能释放而不去调用析构,并且编译器只有警告而不把他当成错误的问题,我把同样的解释也贴csdn了:

    ==

    这个帖子也有提到:
    http://stackoverflow.com/questions/1271307/why-is-vc-c4150-deletion-of-pointer-to-incomplete-type-only-a-warning

    c++ standard:
    http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1905.pdf

    5.3.5中的第5条确实说明了:
    If the object being deleted has incomplete class type at the point of deletion and the complete class has a non-trivial destructor or a deallocation function, the behavior is undefined.

    既然标准说了是未定义的,那就按标准办吧。

    ==

     下面是csdn关于这个问题的地址,我贴重复了:

    http://topic.csdn.net/u/20110611/18/1b848237-aec1-488e-87b5-72af6ea4e948.html?seed=1496947897&r=73811602#r_73811602
    • 已标记为答案 Rob Pan 2011年6月20日 2:42
    2011年6月12日 13:14
  • 补充一下,具体使用哪个展开的delete我想和编译顺序有关,vs中output最后编译的单元应该就是最终的结果。在cannot中只要能让main.cpp后于container.cpp编译(貌似编译单元的顺序与文件名和最后修改日期有关,最后修改的最先编译,比如将container.cpp重命名为zcontainer.cpp,但并不是每次都奏效。),就能取得相反的结果。但在can中这无论如何是不行的,因为Release是inline,即便让container.cpp最后编译,因为缺少调用,所以不会把Release函数在这个单元展开,所以delete自然也不会被展开,最后的结果还是取main.cpp展开的结果。另外cannot中container.cpp:

    #include "container.h"

    #include "test.h"

    之所以是这个顺序,也可以说明inline在每次调用时都会被"实时"的展开,所以不能保证有一个编译单元是这个顺序:

    #include "test.h"

    #include "container.h"

    其他的编译单元也一定就会这样编译。但我想最终delete只会被展成一个结果,不会在不同的编译单元存在不同的delete展开,也就是上面提到的那个最后被编译的单元,这或许也和链接器有关,我没仔细研究过,所以不好妄下定论。

    无论如何对一个有明确定义析构函数的对象的指针调用delete时一定要有这个对象的定义,或者加上#pragma warning(error:4150),或者仔细查看warning,否则确实很危险。


    本来是要解决delete调用析构函数的问题,但因为我的疏忽又引出了另外一个问题,可能误导了大家,真的很抱歉。



    • 已标记为答案 Rob Pan 2011年6月20日 2:42
    2011年6月12日 23:47

全部回复

  • 刚用一个简单的程序测试了下,是没有问题的,也就是说a.h中看不到class B的定义也是可以让delete顺利调用B的析构的。

    但换成我现在那个项目的那个文件确实存在这个问题,有人能解释下吗?

    2011年6月11日 10:59
  • 我现在遇到的问题是a.h中如果看不到B的定义,a.h中的inline函数void Release(){delete m_b;}不会调用B的析构,而只是释放,改成非inline后一切正常。
    2011年6月11日 11:12
  • 找到规律了,有兴趣的朋友按下面的方法试一下,注意test.h中的#include "container.h",这条语句如果注释掉,一切正常,否~Test()不能被正常调用。

    环境是vs2005.

    ====
    container.h:
    ====

    #ifndef _container_h_
    #define _container_h_

    class Test;

    class Container
    {
    public:
    Test *m_test;

    public:
    Container();

    ~Container()
    {
    if(m_test)
    delete m_test;
    }

    void Release()
    {
    if(m_test)
    {
    delete m_test;
    m_test = 0;
    }
    }
    };

    #endif

    ====
    container.cpp:
    ====
    #include "container.h"
    #include "test.h"

    Container::Container()
    {
    m_test = new Test;
    }

    ===
    test.h:
    ===
    #ifndef _test_h_
    #define _test_h_

    #include <stdio.h>
    #include "container.h" // If add this the program can't run properly.

    class Test
    {
    public:
    Test()
    {

    }

    ~Test()
    {
    int aaa = 0;
    printf("aaaaaaaaaaaaaa");
    }

    };

    #endif

    ===
    main.cpp
    ===
    #include "test.h"
    #include "container.h"

    int main()
    {
    Container cont;
    cont.Release();
    return 0;
    }
    2011年6月11日 11:26
  • http://msdn.microsoft.com/en-us/library/ba5dy3f2(vs.71).aspx http://social.msdn.microsoft.com/Forums/en-US/vcgeneral/thread/93e1de49-5104-4e2d-86c8-abb5bb324662/ 编译器看不到定义,new时会有这种情况: error C2512: 'Test' : no appropriate default constructor available 但delete时却当成一个warning: warning C4150: deletion of pointer to incomplete type 'Test'; no destructor called 这样会安全些: #pragma warning(error:4150) C4150为什么不设计成一个error?麻烦高手解释下,谢谢!
    • 已编辑 w3ishi 2011年6月12日 0:51 重新排版
    2011年6月12日 0:50
  • 补充一下:
    上面那个程序test.h中如果没有#include "container.h"也是会有warning C4150,但析构函数会被正确调用,一旦加上#include "container.h"析构就肯定不会执行了.
    2011年6月12日 1:10
  • 我们来看一下你这个问题的原因。首先每个工程的建立过程分为了,编译和连接。首先进行编译,编译过程是注册各种符号表和函数表生成obj文件,再通过连接,把这些obj或者用到的lib中的函数或对象的实现连接进来,当然还有可能包含一些资源。所以在编译过程中,符号都要有其已知的含义,否则就会编译不通过。由于编译器不知道该将未知的符号解释成什么内容。

    在你的例子中,看到在container.h中定义了一个Test的指针,这是由于你没有调用任何该指针的信息,所以他就代表了一个指向一个符号叫做Test的地址,但是编译器不知道Test是什么符号,所以你添加了class Test;告诉编译器,Test是一个类。但这时,编译器只知道Test是一个类,不知道这个类的任何实现细节,有哪些成员变量,有哪些函数,包括有没有析构函数。所以在这种情况,调用delete是不正确的,因为编译器不知道实现,他就开始抱怨,不知道该如何处理。于是只能尝试释放内存。

    来看看为什么在Test.h加#include "container.h"就不掉析构。

    我们只关注主函数所在的编译单元。

    首先是#include "test.h"

    在编译之前,会把这个test.h中的内容插入到#include的位置,

    就变成了,

    ===
    main.cpp
    ===
    #include <stdio.h>
    #include "container.h" // If add this the program can't run properly.
    // test.h中的实现内容
    #include "container.h"

    int main()
    {
    Container cont;
    cont.Release();
    return 0;
    }

    然后再把container.h的内容加进来。

    ===
    main.cpp
    ===
    #include <stdio.h>

    class Test;

    class Container
    {
    public:
    Test *m_test;

    public:
    Container();

    ~Container()
    {
    if(m_test)
    delete m_test;
    }

    void Release()
    {
    if(m_test)
    {
    delete m_test;
    m_test = 0;
    }
    }
    };
    //后面的部分我们就不在这写了。

    你会发现,在这时编译delete m_test的时候,编译器不知道Test的实现。所以会出现编译的问题。

    但是如果去掉#include "container.h"

    我们再来看看这个编译单元是什么样子。

    #include <stdio.h>
    class Test
    {
    public:
    Test()
    {

    }

    ~Test()
    {
    int aaa = 0;
    printf("aaaaaaaaaaaaaa");
    }

    };

    class Test;

    class Container
    {
    public:
    Test *m_test;

    public:
    Container();

    ~Container()
    {
    if(m_test)
    delete m_test;
    }

    void Release()
    {
    if(m_test)
    {
    delete m_test;
    m_test = 0;
    }
    }
    };
    int main()
    {
    Container cont;
    cont.Release();
    return 0;
    }

    很明显这时在delete m_test的时候,编译器已经看到了Test的实现,包括那个析构。所以编译器可以调用析构函数。

    但是这种方式是极不安全的。如果你的main文件写成

    #include "container.h"

    #include "test.h"

    int main()
    {
    Container cont;
    cont.Release();
    return 0;
    }

    一样会出现问题。

    所以不要尝试在没有看到实现时,调用delete


    麻烦把正确答案设为解答。
    2011年6月12日 10:02
    版主
  • 非常感谢您的解答,

    main.cpp中include的顺序以及test.h中的#include "container.h"是我的一个疏忽,起初我不确定delete如果看不到定义是否会造成只释放不调用析构,所以才有了这个测试程序,但问题出在我起初认为container.cpp这个单元在被编译的时候已经是看不到class Test;的详细定义的,理所当然Test的析构不会被调用,但事情不是这样,我忽略了main.cpp这个单元在编译时container是可以见到class Test;的定义的,所以他“有时”不会影响最后的实验结果,我这里确实只能用"有时"这两个字,您如果有兴趣可以下载我下面的链接,container.cpp这个单元或项目中如果有其他编译单元如果稍加改动就会影响最后的编译结果,因为他可能涉及到编译器在编译时的一些策略问题,可能和delete被展开的先后顺序有关.

    环境:vs2005

    http://www.yesaha.com/download/deletetest.rar

    言归正传,其实这帖子要解决的还是为什么new时必须有定义,delete时未必要定义,如果没有定义,只能释放而不去调用析构,并且编译器只有警告而不把他当成错误的问题,我把同样的解释也贴csdn了:

    ==

    这个帖子也有提到:
    http://stackoverflow.com/questions/1271307/why-is-vc-c4150-deletion-of-pointer-to-incomplete-type-only-a-warning

    c++ standard:
    http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1905.pdf

    5.3.5中的第5条确实说明了:
    If the object being deleted has incomplete class type at the point of deletion and the complete class has a non-trivial destructor or a deallocation function, the behavior is undefined.

    既然标准说了是未定义的,那就按标准办吧。

    ==

     下面是csdn关于这个问题的地址,我贴重复了:

    http://topic.csdn.net/u/20110611/18/1b848237-aec1-488e-87b5-72af6ea4e948.html?seed=1496947897&r=73811602#r_73811602
    • 已标记为答案 Rob Pan 2011年6月20日 2:42
    2011年6月12日 13:14
  • 补充一下,具体使用哪个展开的delete我想和编译顺序有关,vs中output最后编译的单元应该就是最终的结果。在cannot中只要能让main.cpp后于container.cpp编译(貌似编译单元的顺序与文件名和最后修改日期有关,最后修改的最先编译,比如将container.cpp重命名为zcontainer.cpp,但并不是每次都奏效。),就能取得相反的结果。但在can中这无论如何是不行的,因为Release是inline,即便让container.cpp最后编译,因为缺少调用,所以不会把Release函数在这个单元展开,所以delete自然也不会被展开,最后的结果还是取main.cpp展开的结果。另外cannot中container.cpp:

    #include "container.h"

    #include "test.h"

    之所以是这个顺序,也可以说明inline在每次调用时都会被"实时"的展开,所以不能保证有一个编译单元是这个顺序:

    #include "test.h"

    #include "container.h"

    其他的编译单元也一定就会这样编译。但我想最终delete只会被展成一个结果,不会在不同的编译单元存在不同的delete展开,也就是上面提到的那个最后被编译的单元,这或许也和链接器有关,我没仔细研究过,所以不好妄下定论。

    无论如何对一个有明确定义析构函数的对象的指针调用delete时一定要有这个对象的定义,或者加上#pragma warning(error:4150),或者仔细查看warning,否则确实很危险。


    本来是要解决delete调用析构函数的问题,但因为我的疏忽又引出了另外一个问题,可能误导了大家,真的很抱歉。



    • 已标记为答案 Rob Pan 2011年6月20日 2:42
    2011年6月12日 23:47
  • new 也是一样的原因找不到实现。所以编译不过。


    麻烦把正确答案设为解答。
    2011年6月13日 1:51
    版主
  • 我记得C++编译器在发现一个类的时候会自动找析构函数的,虽然你的析构函数不在这个头文件里,但是编译器编译的时候根本不管你的析构函数是什么,只是留一个析构函数符号在obj文件里。

    在链接时,若是inline,则把析构函数所在obj的二进制代码复制一份到符号的位置,要不是,直接指向那部分代码。记得不清了,仅供参考 :)


    0xBAADF00D
    2011年6月13日 13:58
    版主