C++中的“不完全类型”



用delete删除一个只有声明但无定义的类型的指针,是危险的。这通常导致无法调用析构函数(包括对象本身的析构函数、成员/基类的析构函数),从而泄露资源。
示例代码:
引用
class C;                // 在另一个cpp文件中定义
C* createC();           // 在另一个cpp文件中定义
int main() {
    C* p = createC();
    delete p;           // 资源泄露
}


初步分析:类型C没有被定义,所以无法找到析构函数的原型(虽然析构函数是没有返回类型、没有参数,但这些信息还不足以确定整个析构函数的原型。例如:析构函数是否是private?析构函数是否是virtual?这些信息现在并不明确)。另外,也无法知道这个类是否重载了operator delete,但是最后一句delete p却实实在在的通过编译了。

这样的结果很让人困惑,为什么在有如此多信息不明确的情况下,delete p仍然可以通过编译?其实也有一个稍微好理解一点的情况:
引用
class C;
C& createC();
int main() {
    C& rc = createC();
    C* p = &rc;         // 使用operator &
}

这里使用了C::operator&。按照C++语法,如果没有定义C::operator&,则编译器会为它自动产生一个;如果定义了C::operator&,则编译器不再会自动产生。由于此处类型C没有被定义,显然不可能知道是否定义了C::operator&,但是这段代码仍然可以通过编译。它的实际行为就好象没有定义C::operator&一样。也就是说,即使用户(在别的地方)定义了C::operator&,编译器也不会看到,但是这里必须使用了,因此它自动产生一个operator&。

回过头来看前面的delete p,也可以理解了。编译器没有看到C::~C(),也没有看到C::operator delete,那么就当作程序员没有定义这些内容。对于析构函数,本来编译器应该在析构函数调用之前先调用基类的析构函数和成员的析构函数,但是现在基类和成员都无法确定,因此只有不调用。对于operator delete,编译器没有看到它,因此也当它不存在。
所以最后的结果就是:只释放指针所指的内存,不调用析构函数,也不调用基类和成员的析构函数。换句话说,前面例子中的delete p,实际上已经变成了delete (void*)p。后面一种写法的危险性是显而易见的。

或许你认为这个情况很傻,几乎不会遇到。在设计上,通常会成对的提供接口,比如有了createC就应该有destroyC。不过如果设计疏忽,上面的代码又不会有任何编译错误(实验发现VC6会出现警告,但对于operator&则不会有任何警告),则最终会导致问题。
但是有的设计者可能会这样做:提供一个“std::auto_ptr<C> createC()”,创建一个对象,并且在不需要的时候销毁之。由于std::auto_ptr内部仍然是调用的delete,所以问题仍然存在,并且埋藏得更深。
用boost::shared_ptr的话,情况就会好一些。假设main函数在A文件,createC函数在B文件,则对于std::auto_ptr来说,析构时实际上是在A文件中调用delete,由于A文件中class C没有定义,所以出现问题。但对于boost::shared_ptr来说,在构造时就把delete作为“删除器”传入boost::shared_ptr内部,因此在析构时实际上是调用B文件中的delete,由于在B文件中需要实际的创建class C,所以B文件通常是必须包含class C的定义的,这里使用delete没有问题。
把class C的析构函数定义为private的话,很容易的看到,std::auto_ptr的版本仍然可以通过编译,但boost::shared_ptr的版本则出现错误了。

要知道“目前正在编译的文件中,是否存在某个类型的定义”,可以用sizeof。某些编译器对未定义的类型执行sizeof会得到零(?),VC系列对于未定义类型执行sizeof会得到编译错误。为了统一接口,可以用BOOST_STATIC_ASSERT(sizeof(C) >= 0);,或者在没有安装boost的时候直接写static const char arr[sizeof(C)];
利用boost::checked_delete(当然还有一个boost::checked_array_delete)代替delete,可以检查类型是否已经有定义。如果未定义则给出一个编译期错误,如果有定义则直接执行delete操作。

最奇怪的就是:在类没有定义的情况下,不是所有的operator都按照“编译器默认产生的动作”来运行,例如使用operator=就会出现编译错误。
引用
C* p = create();
*p = *p;         // 编译错误。不会自动生成operator =


小结:
对于一个有声明但未给出定义的“不完整类型”,使用它时需要特别小心。它的析构函数、operator delete、operator &等,即使在别的文件中给出定义,在本文件中由于没有定义,仍然会按照编译器默认的方式执行(最危险的地方就是忽略了自定义的析构函数)。
特别小心一些智能指针(例如std::auto_ptr)内部实际上也是使用delete,所以在使用时应该确保类定义可见。一定要避免这样的设计:class C; std::auto_ptr<C> create();
对于operator&,最好的做法就是不要重载它。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值