繁体   English   中英

使用非虚拟析构函数扩展基类是否危险?

[英]Is extending a base class with non-virtual destructor dangerous?

在以下代码中:

class A {
};
class B : public A {
};
class C : public A {
   int x;
};

int main (int argc, char** argv) {
   A* b = new B();
   A* c = new C();

   //in both cases, only ~A() is called, not ~B() or ~C()
   delete b; //is this ok?
   delete c; //does this line leak memory?

   return 0;
}

当使用具有成员函数的非虚析构函数(如C类)调用类上的delete时,内存分配器可以告诉对象的正确大小是什么吗? 如果没有,记忆是否泄露?

其次,如果类没有成员函数,并且没有明确的析构函数行为(比如B类),那么一切都好吗?

我问这个是因为我想创建一个扩展std::string的类,(我知道这不推荐,但为了讨论它只是承担它),并重载+=+运算符。 -Weffc ++给了我一个警告,因为std::string有一个非虚拟析构函数,但是如果子类没有成员并且不需要在它的析构函数中做任何事情那么重要吗?

FYI +=重载是为了进行正确的文件路径格式化,因此可以使用路径类,如:

class path : public std::string {
    //... overload, +=, +
    //... add last_path_component, remove_path_component, ext, etc...
};

path foo = "/some/file/path";
foo = foo + "filename.txt";
std::string s = foo; //easy assignment to std::string
some_function_taking_std_string (foo); //easy implicit conversion
//and so on...

我只是想确保有人这样做:

path* foo = new path();
std::string* bar = foo;
delete bar;

不会导致内存分配问题?

不,从没有虚拟析构函数的类公开继承是不安全的,因为如果通过基类删除派生,则输入未定义的行为。 派生类的定义是无关紧要的(数据成员与否等):

§5.3.5/ 3:在第一个备选(删除对象)中,如果操作数的静态类型与其动态类型不同,则静态类型应为操作数的动态类型的基类, 静态类型应具有虚析构函数或行为未定义。 (强调我的。)

代码中的这两个示例都会导致未定义的行为。 您可以非公开地继承,但这显然会破坏使用该类然后扩展它的目的。 (因为不再可能通过基指针删除它。)

这是(一个原因*)为什么你不应该继承标准库类。 最好的解决方案是使用自由函数扩展它。 事实上,即使你可以,你也应该更喜欢自由功能


*另一个存在:你真的想用新的字符串类替换所有的字符串用法,只是为了获得一些功能吗? 这是很多不必要的工作。

所以每个人都说你不能这样做 - 它会导致不确定的行为。 但是在某些情况下它是安全的。 如果您从不动态创建类的实例,那么您应该没问题。 (即没有新的电话)

也就是说,这通常被认为是一件坏事,因为有人可能会尝试在以后某个时候创建​​一个多态。 (你可能可以通过私有的未实现的运算符来防止这种情况,但我不确定。)

我有两个例子,我不讨厌从具有非虚拟析构函数的类派生。 第一个是使用临时创造语法糖...这是一个人为的例子。

class MyList : public std::vector<int>
{
   public:
     MyList operator<<(int i) const
     {
       MyList retval(*this);
       retval.push_back(i);
       return retval;
     }
   private: 
     // Prevent heap allocation
     void * operator new   (size_t);
     void * operator new[] (size_t);
     void   operator delete   (void *);
     void   operator delete[] (void*);
};

void do_somthing_with_a_vec( std::vector<int> v );
void do_somthing_with_a_const_vec_ref( const std::vector<int> &v );

int main()
{
   // I think this slices correctly .. 
   // if it doesn't compile you might need to add a 
   // conversion operator to MyList
   std::vector<int> v = MyList()<<1<<2<<3<<4;

  // This will slice to a vector correctly.
   do_something_with_a_vec( MyList()<<1<<2<<3<<4 );

  // This will pass a const ref - which will be OK too.
   do_something_with_a_const_vec_ref( MyList()<<1<<2<<3<<4 );

  //This will not compile as MyList::operator new is private
  MyList * ptr = new MyList();
}

我能想到的另一个有效用法来自于C ++中缺少模板typedef。 这是你如何使用它。

// Assume this is in code we cant control
template<typename T1, typename T2 >
class ComplicatedClass
{
  ...
};

// Now in our code we want TrivialClass = ComplicatedClass<int,int>
// Normal typedef is OK
typedef ComplicatedClass<int,int> TrivialClass;

// Next we want to be able to do SimpleClass<T> = ComplicatedClass<T,T> 
// But this doesn't compile
template<typename T>
typedef CompilicatedClass<T,T> SimpleClass;

// So instead we can do this - 
// so long as it is not used polymorphically if 
// ComplicatedClass doesn't have a virtual destructor we are OK.
template<typename T>
class SimpleClass : public ComplicatedClass<T,T>
{
  // Need to add the constructors we want here :(
  // ...
   private: 
     // Prevent heap allocation
     void * operator new   (size_t);
     void * operator new[] (size_t);
     void   operator delete   (void *);
     void   operator delete[] (void*);
}

下面是一个更具体的例子。 您希望将std :: map与自定义分配器一起用于许多不同类型,但您不希望不可维护

std::map<K,V, std::less<K>, MyAlloc<K,V> >

通过你的代码乱七八糟。

template<typename K, typename V>
class CustomAllocMap : public std::map< K,V, std::less<K>, MyAlloc<K,V> >
{
  ...
   private: 
     // Prevent heap allocation
     void * operator new   (size_t);
     void * operator new[] (size_t);
     void   operator delete   (void *);
     void   operator delete[] (void*);
}; 

MyCustomAllocMap<K,V> map;

如果将派生类型的内存地址存储在基类型中,然后在基类型上调用delete,则可能会出现问题:

B* b = new C();
delete b;

如果B有一个虚拟析构函数,则会调用C的析构函数,然后调用B。 但是如果没有虚拟析构函数,则会出现未定义的行为。

以下2个删除导致没有问题:

B* b = new B();
delete b;
C* c = new C()
delete c;

这不是您的问题的答案,而是您尝试解决的问题(路径格式化)。 看一下boost::filesystem ,它有更好的方法来连接路径:

boost::filesystem::path p = "/some/file/path";
p /= "filename.txt";

然后,您可以在平台中立格式和平台特定格式中将路径检索为字符串。

最好的部分是它已被TR2接受,这意味着它将成为未来C ++标准的一部分。

在没有虚拟析构函数的情况下私有地继承基类是安全的。 公共继承可以通过基类指针删除派生类,这是C ++中未定义的行为。

这是C ++中私有继承的唯一合理用法之一。

添加到已经说过的内容。

  • 您实际上正在考虑将方法添加到已经被认为是膨胀的类中。
  • 您希望将path类视为string即使许多操作没有意义

我建议你使用Composition over Inheritance 这样你只会重新实现(转发)那些对你的类真正有用的操作(例如,我不认为你真的需要下标操作符)。

此外,您可能会考虑使用std::wstring ,或者也许是ICU字符串,以便能够处理多于ASCII的字符(我是一个挑剔,但我是法语,而较低的ASCII不足以用于法语)。

这确实是一个封装问题。 如果您决定有一天正确处理UTF-8字符并更改您的基础类别......您的客户很可能会绊倒您。 另一方面,如果你已经使用过构图,只要你小心翼翼地使用界面,它们就永远不会有任何问题。

最后,正如已经提出的那样,自由功能应该引领方向。 再次因为它们提供了更好的封装(只要它们不是朋友......)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM