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

时间:2010-04-08 02:10:44

标签: c++ memory-management polymorphism

在以下代码中:

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有一个非虚拟析构函数,但是如果子类没有成员并且不需要在它的析构函数中做任何事情那么重要吗?

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

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;

不会导致内存分配问题吗?

6 个答案:

答案 0 :(得分:13)

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

  

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

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

这是(一个原因*)为什么你不应该继承标准库类。 best solution是使用自由函数扩展它。事实上,即使你能做到prefer free-functions anyway


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

答案 1 :(得分:5)

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

尽管如此,人们通常认为这是一件坏事,因为有人可能会在以后某个时候尝试创建一个多态。 (你可以通过新的私有操作符来防止这种情况,但我不确定。)

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

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;

答案 2 :(得分:2)

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

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

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

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

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

答案 3 :(得分:2)

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

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

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

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

答案 4 :(得分:1)

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

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

答案 5 :(得分:1)

添加到已经说过的内容。

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

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

另外,您可以考虑使用std::wstring代替ICU字符串,以便能够处理超过ASCII字符(我是一个挑剔,但我是法语和低级ASCII对于法语而言是不够的。)

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

最后,正如所建议的那样,自由功能将引领潮流。再次因为它们提供了更好的封装(只要它们不是朋友......)。