我目前正在编写经典数据结构的实现,例如搜索树。我从B +树开始。
所涉及的课程如下:
template <typename Key, typename Record>
class BPlusNode {
/* ... */
}
template <typename Key, typename Record>
class BPlusINode : public BPlusNode<Key, Record> {
/* ... */
}
template <typename Key, typename Record>
class BPlusLeaf : public BPlusNode<Key, Record> {
/* ... */
}
template <typename Key, typename Record>
class BPlusTree {
/* ... */
private:
BPlusNode<Key, Record> *root;
/* ... */
}
我正在为我的树编写复制构造函数。它有点复杂,因为它涉及在原始树上进行BFS搜索以逐个复制每个节点(并相应地编辑子节点和父节点)。在复制过程中的某个时刻可能发生内存分配失败(或其他任何错误)。因此,我必须抛出异常来表示对象创建失败。但是我创建的所有节点会发生什么?它们会被自动销毁还是我必须清理它们?
编辑:有关复制构造函数的一些准确性
template <typename Key, typename Record>
BPlusTree<Key, Record>::BPlusTree(BPlusTree<Key, Record> &tree) {
std::list<BPlusNode<Key, Record>*> to_cpy;
BPlusNode<Key, Record> *n = nullptr, *p = nullptr, *cpy = nullptr;
to_cpy.push_back(tree.root);
while (!to_cpy.empty()) {
n = to_cpy.front();
n.listChildren(to_cpy) // Push all @n's children at the back of @to_cpy
// (In order)
to_cpy.pop_front();
cpy = n.clone(); // May fail.
/*
* Some mechanisms to track who is the parent node for @cpy
* and to edit the children pointers when all have been copied
*/
}
}
奖励问题:我将root保留为指针,因为当树进化时,由于B +树不会从上到下而是从下到上生长,因此根可以改变。它是正确的解决方案吗? (右边,我的意思是最多C ++ - esque)
答案 0 :(得分:5)
如果构造函数失败,那么所有的析构函数都是完全的 调用构造的子对象,但不是析构函数 构造函数失败的对象。
处理此问题的经典方法(由所有人使用)
我见过的std::vector
的实现,例如)
将内存管理放在私有基类中
像:
class TreeBase
{
Note* root;
friend class Tree;
TreeBase() : root( nullptr ) {}
~TreeBase() { delete root; } // Or whatever is needed for cleanup.
};
因为在您输入之前将完全构建此基类 进入树的实际构造函数代码,它的析构函数 将被召唤。
只要改变root
,就没有问题
通过root
可访问的结构仍然足够连贯
允许适当的清理。甚至这种约束也可以解除
在短时间内,您确定没有例外
被提高。 (例如,重新平衡树的时候;那个
操作只涉及指针操作,永远不会
提出异常。)
答案 1 :(得分:4)
如果构造函数抛出,则不会调用析构函数。
因此,您在此时创建的依赖于析构函数进行清理的任何内容都必须由您清理。
在异常之前在初始化列表中构造的对象成员将使用其析构函数进行清理。因此,例如,如果您的类包含一些智能指针,则会进行清理。
您的问题“子节点是否会被破坏”?如果它们存储在智能指针或类似对象中。节点可以有一个弱的链接回到它们的父节点。
如果你不能在你的类中存储智能指针,并依赖你的析构函数来删除,那么在构造函数本身中使用一个智能指针,直到你知道它将不再抛出的点。您可以使用std::unique_ptr
(如果可用)或std::auto_ptr
(如果有的话)。无论哪种方式,您都可以使用release
将其分配给类成员指针。如果你需要在向量中使用它们,那么unique_ptr
是有用的,因为你可以将它们存储在(临时)向量中,然后在最后运行并在所有向量上调用release
。
否则,如果您不想要隐式的两阶段构造,请在幕后进行。
class ChildNodeManager
{
friend class Node; // this implements detail of Node
ChildNodeManager() {} // constructor that never throws, might initialise
// some pointers to nullptr.
void addNode( Node * node ); // might throw but will leave in a stable state
// if it does, i.e. how it was before you tried addNode. Destructor
// will work safely
~ChildNodeManager() { // do cleanup of added nodes }
// probably disable copy construction and assignment
};
class Node
{
ChildNodeManager myChildren;
public:
Node( ... ) // might throw
};
由于ChildNodeManager是Node构造函数体中的完全构造成员,即使Node的构造函数在中间某处失败,它也会被正确销毁。已经添加到其中的任何节点都将被清除。