我想继承std::map
,但据我所知std::map
没有任何虚拟析构函数。
因此可以在析构函数中显式调用std::map
的析构函数以确保正确的对象销毁吗?
答案 0 :(得分:31)
析构函数确实被调用,即使它不是虚拟的,但这不是问题。
如果您尝试通过指向std::map
的指针删除类型的对象,则会出现未定义的行为。
使用组合而不是继承,std
容器不应该被继承,你不应该。
我假设您要扩展std::map
的功能(假设您想要找到最小值),在这种情况下,您有两个更好的,合法选项:
1)如建议的那样,您可以使用构图:
template<class K, class V>
class MyMap
{
std::map<K,V> m;
//wrapper methods
V getMin();
};
2)自由功能:
namespace MapFunctionality
{
template<class K, class V>
V getMin(const std::map<K,V> m);
}
答案 1 :(得分:17)
存在一种误解:继承 - 除了纯粹的OOP的概念,C ++不是 - 只不过是“具有未命名成员的组合,具有衰变能力”。
缺少虚函数(并且析构函数在这个意义上并不特殊)会使您的对象不是多态的,但如果您正在做的只是“重用它行为并公开本机接口”,那么继承完全符合您的要求
不需要相互明确地调用析构函数,因为它们的调用始终由规范链接。
#include <iostream>
unsing namespace std;
class A
{
public:
A() { cout << "A::A()" << endl; }
~A() { cout << "A::~A()" << endl; }
void hello() { cout << "A::hello()" << endl; }
};
class B: public A
{
public:
B() { cout << "B::B()" << endl; }
~B() { cout << "B::~B()" << endl; }
void hello() { cout << "B::hello()" << endl; }
};
int main()
{
B b;
b.hello();
return 0;
}
将输出
A::A()
B::B()
B::hello()
B::~B()
A::~A()
使用
将A嵌入到B中class B
{
public:
A a;
B() { cout << "B::B()" << endl; }
~B() { cout << "B::~B()" << endl; }
void hello() { cout << "B::hello()" << endl; }
};
输出完全相同的。
“如果析构函数不是虚拟的,则不导出”不是C ++强制性结果,而只是一个普遍接受的未写入(规范中没有任何内容:除了基础上的UB调用删除)规则在C ++ 99之前出现,当时动态继承和虚函数的OOP是C ++支持的唯一编程范例。
当然,世界各地的许多程序员用这种学校制作他们的骨头<(相同的教导iostreams作为原始的,然后移动到数组和指针,并在最后一课上老师他说:“哦... tehre也是具有矢量,字符串和其他高级功能的STL”,而今天,即使C ++成为多范式,仍然坚持使用这种纯粹的OOP规则。
在我的示例中,A ::〜A()与A :: hello完全不是虚拟的。这是什么意思?
简单:出于同样的原因,调用A::hello
不会导致调用B::hello
,调用A::~A()
(通过删除)将不会导致B::~B()
。 如果你能接受 - 你的编程风格 - 第一个断言,你就没有理由不接受第二个。在我的示例中,没有A* p = new B
会收到delete p
,因为A ::〜A不是虚拟的,我知道这意味着什么。
使用B的第二个例子A* p = &((new B)->a);
与delete p;
完全没有相同的理由,尽管这第二个案例与第一个案例完全兼容,看起来并不有意义。明显的原因。
唯一的问题是“维护”,在某种意义上 - 如果由OOP程序员查看yopur代码 - 将拒绝它,不是因为它本身是错误的,而是因为他被告知这样做。
事实上,“如果析构函数不是虚拟的,则不导出”是因为大多数程序员都认为有太多的程序员不知道他们不能在指向a的指针上调用delete基。 (对不起,如果这不礼貌,但经过30多年的编程经验,我看不出任何其他原因!)
但你的问题不同:
调用B :: ~B()(通过删除或范围结束)将始终导致A ::〜A(),因为A(无论是嵌入还是继承)在任何情况下都是乙强>
遵循Luchian评论:在他的评论中上面提到的Undefined行为与指向对象的指针上的删除有关,没有虚拟析构函数。
根据OOP学校的说法,这导致规则“如果没有虚拟析构函数,则不会派生”。
我在这里指出的是,该学校的原因取决于每个面向OOP的对象必须是多态的,并且一切都是多态的,必须通过指向基址的指针来寻址,以允许对象替换。 通过做出那些断言,该学校故意试图使派生和不可替换之间的交叉无效,这样纯粹的OOP程序就不会经历那个UB。
我的立场简单地承认,C ++不仅仅是OOP,并且默认情况下并非所有C ++对象都必须面向OOP,并且承认OOP并不总是必需的,并且承认C ++继承并不总是必然的服务于OOP替代。
std :: map不是多态的,所以它不可替换。 MyMap是相同的:不是多态的,不可替换的。
它只需重用std :: map并公开相同的std :: map接口。继承只是避免重写函数的冗长模板的方法,只需调用重用的函数。
MyMap不会有虚拟dtor,因为std :: map没有。对我而言,这足以告诉C ++程序员这些不是多态对象,不能在另一个对象的位置使用。
我不得不承认,大多数C ++专家今天都没有这个立场。但我认为(我唯一的个人观点)这只是因为他们的历史,与OOP作为服务的教条有关,而不是因为C ++的需要。 对我来说,C ++不是纯粹的OOP语言,在未遵循或不需要OOP的环境中,不一定总是遵循OOP范例。
答案 2 :(得分:12)
我想继承
std::map
[...]
为什么?
继承有两个传统原因:
前者没有任何意义,因为map
没有任何virtual
方法,因此您无法通过继承来修改其行为;后者是对继承使用的歪曲,最终只会使维护变得复杂。
如果不清楚你的预期用法(你的问题缺乏上下文),我会假设你真正想要的是提供一个类似地图的容器,并带有一些额外的操作。有两种方法可以实现这一目标:
std::map
,并提供足够的界面std::map
后者更简单,但它也更开放:std::map
的原始界面仍然是敞开的;因此它不适合限制操作。
前者更重要,毫无疑问,但提供了更多的可能性。
由您来决定哪两种方法更合适。
答案 3 :(得分:2)
@Matthieu M. 你说
我想继承std :: map [...]
为什么?
继承有两个传统原因:
- 到重用其界面(以及针对它编码的方法)
- 至重复使用其行为
醇>前者在这里没有意义,因为map没有任何虚拟方法,所以你不能通过继承来修改它的行为;后者是对继承使用的歪曲,最终只会使维护变得复杂。
关于&#34;前者&#34;:
clear()
函数是虚函数,对我而言,使用迭代器删除std::map<key,valueClass*>::clear()
在派生类中重写是很有意义的,该迭代器删除了值类的所有指向的实例在调用基类clear()
以防止意外内存泄漏之前,这是我实际使用过的技巧。至于为什么有人想要使用映射指向类的指针,良好的多态性和不可重新赋值的引用意味着不能在STL容器中使用。您可能会建议使用reference_wrapper或智能指针,例如shared_ptr
(C ++ 11功能),但是当您编写一个您希望某人限制为C ++ 98编译器的库时能够使用,这些都不是一个选择,除非你要求提升,这也是不可取的。如果您确实希望地图拥有其内容的唯一所有权,那么您不希望使用reference_wrapper或大多数智能指针实现。
关于&#34;后者&#34;:
如果你想要一个地图指向自动删除指向内存的指针,那么重用&#34; all&#34;其他地图行为和覆盖清除对我来说很有意义,当然,当你复制地图时,你也想要覆盖赋值/复制构造函数来克隆指向的对象,这样你就不会加倍删除指向valueClass
的实例。
但这只需要极少量的编码来实现。
我还使用受保护的typedef std::map<key,valueClass*> baseClassMap;
作为派生类映射声明的前2行,这样我可以在重写后的baseClassMap::clear();
函数中调用clear()
迭代器循环删除派生映射中包含的valueClass*
的所有实例,这样可以在valueClass*
的类型发生变化时更轻松地进行维护。
重点是,虽然在良好的编码实践中可能适用性有限,但我并不认为从地图下载绝对不是一个好主意。但也许你有一个更好的想法,我没有想到如何在不增加大量额外源代码的情况下实现相同的自动内存管理效果(例如聚合std::map
)。