我们什么时候需要定义析构函数?
这个问题在这里已经有了答案:
三大规则与零规则
处理资源的好方法是用三条规则(现在的规则是因为移动语义),但最近另一条规则正在接管:零规则。
这个想法,但你应该真正阅读的文章是资源管理应该留给其他特定的类。
在这方面,标准库提供了一组好的工具,如: std::vector
, std::string
, std::unique_ptr
和std::shared_ptr
,有效地消除了对自定义析构函数的需求,移动/复制构造函数,移动/复制赋值和默认的构造函数。
如何将其应用于您的代码
在你的代码中你有很多不同的资源,这就是一个很好的例子。
字符串
如果您注意到brandname
实际上是一个“动态字符串”,则标准库不仅可以将您从C样式字符串中解救出来,还可以使用std::string
自动管理字符串的内存。
动态分配的B
第二个资源似乎是一个动态分配的B
如果您是为“我想要一个可选成员”以外的其他原因而动态分配的,那么您一定要使用std::unique_ptr
来自动处理资源(在适当情况下会释放资源)。 另一方面,如果你希望它是一个可选成员,你可以使用std::optional
来代替。
Bs的集合
最后一个资源只是一个B
s的数组。 这很容易与一个std::vector
。 标准库允许您根据您的不同需求从各种不同的容器中进行选择; 只要提到其中的一些: std::deque
, std::list
和std::array
。
结论
要添加所有的建议,你最终会得到:
class A {
private:
std::string brandname;
std::unique_ptr<B> b;
std::vector<B> vec;
public:
virtual void something(){} = 0;
};
这既安全又可读。
正如@nonsensickle所指出的那样,问题太广泛了......所以我会尽我所能解决这个问题......
重新定义析构函数的第一个理由将出现在三部规则中 , 该规则部分是Scott Meyers Effective C ++中的第 6项 ,但不完全。 三条规则说,如果你重新定义了析构函数,复制构造函数或者复制赋值操作,那么这意味着你应该重写它们的全部三个。 原因是如果你不得不重写你自己的版本,那么编译器的默认值将不再有效。
另一个例子是Scott Meyers在Effective C ++中指出的例子
当您尝试通过基类指针删除派生类对象并且基类具有非虚拟析构函数时,结果是未定义的。
然后他继续
如果一个类不包含任何虚函数,那通常表明它不能用作基类。 当一个类不打算用作基类时,使析构函数变为虚拟通常是一个坏主意。
他关于虚拟的析构函数的结论是
底线是无条件地声明所有析构函数是虚拟的,就像从不声明它们是虚拟的一样。 事实上,许多人通过这种方式总结了这种情况:当且仅当该类至少包含一个虚函数时,在类中声明一个虚析构函数。
如果它不是规则三种情况,那么也许你的对象中有一个指针成员,也许你在对象内部分配了内存,然后,你需要在析构函数中管理这个内存,这是第6项他的书
请务必查看@ Jefffrey对零规则的回答
有两件事情需要定义一个析构函数:
当你的对象被破坏时,你需要执行一些操作,而不是破坏所有的类成员。
这些行动绝大多数曾经是以RAII原理释放内存,这些行为已经进入了RAII容器的析构函数,编译器负责调用这些析构函数。 但是,这些操作可以是任何操作,例如关闭文件,或将一些数据写入日志,或...。 如果您严格遵循RAII原则,您将为所有这些其他操作编写RAII容器,以便只有RAII容器具有已定义的析构函数。
当你需要通过基类指针来破坏对象时。
当你需要这样做时,你必须定义析构函数在基类中是virtual
的。 否则,你的派生析构函数将不会被调用,不管它们是否被定义,以及它们是否是virtual
。 这里是一个例子:
#include <iostream>
class Foo {
public:
~Foo() {
std::cerr << "Foo::~Foo()n";
};
};
class Bar : public Foo {
public:
~Bar() {
std::cerr << "Bar::~Bar()n";
};
};
int main() {
Foo* bar = new Bar();
delete bar;
}
这个程序只打印Foo::~Foo()
, Bar
的析构函数不会被调用。 没有警告或错误消息。 只有部分被破坏的物体,带来所有后果。 因此,确保在出现时自己发现这种情况(或者将virtual ~Foo() = default;
〜Foo virtual ~Foo() = default;
到您定义的每个非派生类中。
如果这两个条件都不符合,则不需要定义析构函数,那么默认的构造函数就足够了。
现在来看你的示例代码:
当你的成员是一个指向某个东西的指针时(无论是作为指针还是引用),编译器都不知道......
...是否有其他指向这个对象的指针。
...指针是指向一个对象还是指向一个数组。
因此,编译器无法推断是否或者如何破坏指针指向的任何内容。 所以默认的析构函数决不会破坏指针后面的任何东西。
这适用于brandname
和b
。 因此,你需要一个析构函数,因为你需要自己去执行释放。 或者,您可以为它们使用RAII容器( std::string
和一个智能指针变体)。
此推理不适用于vec
因为此变量直接在对象中包含std::vector<>
。 因此,编译器知道vec
必须被破坏,反过来会破坏它的所有元素(毕竟这是一个RAII容器)。