【问题标题】:How to refer to recursive structs through pointers using vectors如何使用向量通过指针引用递归结构
【发布时间】:2012-01-26 16:59:09
【问题描述】:

我有结构体,我们称它们为 sn,看起来像:

struct sn {
    string name;
    vector<sn*> connected_to;
};

现在,假设我已经声明了从 0 到 9 的 connected_to 向量;我正在将 sn A 连接到 sn B:

A.connected_to[0] = &B; 

我感觉我做错了。基本上我要做的是在连接结构时避免复制结构......即:

struct sn {
    string name;
    vector<sn> connected_to;
};

// ... 

A.connected_to[0] = B; 

这会复制什么吗?更根本的问题当然是我不明白向量、指针和引用是如何真正深入工作的。

【问题讨论】:

  • 尝试一下通常是了解它们如何真正工作的好方法。我可以使用原始类型处理简单的 & 和 *,并通过引用等将它们传递给函数——但以上需要在某种程度上理解 struct 和 vector 的实现方式等 =)
  • 要学习,我建议您实施您考虑的不同方法的最小测试用例;使用 gdb 或其他调试器来运行这些最小的示例并详细观察它们的行为 - 这样您就可以做出明智的决定。

标签: c++ pointers vector struct


【解决方案1】:

您的第二种方法是probably illegal。但是,在标准库的某些实现中,它可能会起作用。在这些情况下,您添加的对象将被复制(包括它们的所有子对象 - 当复制标准容器时,它包含的所有元素也会被复制)。因此,这样的数据结构只适合表示一棵树。

另一方面,您的第一种方法很好,因为指向不完整类型的指针本身就是有效类型(§3.9.2/3 - [basic.compound])。由于您只存储一个指针,因此不会复制该对象。但是,当您开始删除此图表时,您必须小心。根据您要建模的图表类型,实现它们时有三种场景:

There are some restrictions。请注意,在您的情况下,类型仅在定义(sn)内不完整-在您实际使用它时,sn 是完整的,因此您也可以将其删除。

Trees

对于一棵树,每个孩子都只有一个父母。因此,在删除结构时,您将从根开始,每个节点只需删除其所有子节点。这将对没有子节点的叶子递归地起作用。

为了有效地实现这一点,您可以将子项存储在 boost::ptr_vector&lt;sn&gt; 中。因此,您不必自己编写析构函数 - ptr_vector 将删除其所有元素。

Directed Acyclic Graphs (DAG)

在 DAG 中,一个节点可以有多个父节点,因此您必须注意不要两次删除同一个节点(如果每个节点都删除其所有子节点,则会发生这种情况 - 因此,ptr_vector 将不起作用这里)。处理这个问题的一种方法是使用引用计数 - 每个节点计算有多少其他节点指向它,并且只有当引用计数达到零时,才会实际删除该节点。您可以通过将节点存储在 std::vector&lt;std::shared_ptr&lt;sn&gt; &gt;(或 boost::shared_ptr,如果您使用 C++11 之前的编译器)中来自动执行此操作。 shared_ptr 在内部管理引用计数,只有在没有更多 shared_ptr-instances 指向该对象时(当引用计数为零时)才会删除它所指向的对象。

Cyclic Graphs

在循环图中,一个节点也可以是它自己的父节点(直接,如果它包含循环,或者间接,通过循环)。因此,如果每个节点都删除了它的所有子节点,这将导致析构函数调用的无限循环。 shared_ptr 也可能在此处失败,因为当您拥有 cycle of shared_ptr referencing each other 时,它们的引用计数将永远不会达到零。现在是时候考虑拥有一个对象和引用它之间的区别了。每个节点应该只有一个 拥有它的父节点,但可以有多个引用它的父节点。所有者,并且只有所有者,负责删除该节点。正如我在上面链接的出色答案中所解释的,这可以使用shared_ptrweak_ptr 的组合来实现。

【讨论】:

  • 您能否评论一下我在删除时如何小心/如何删除?谢谢:) 第二种方法怎么样?在那种情况下会被复制吗?
  • 进一步:您想确定谁拥有这些结构 - 即在您完成后删除它们的责任是谁。通常这可能是一个包含所有结构的更大结构。
  • @Deniz:我现在没有时间,但我稍后会回复这个答案(不是其他人先这样做)。
  • 感谢@BjörnPollex,期待。
  • @Deniz:我更新了我的答案 - 希望它有所帮助。我没有添加任何代码示例,因为那样会变得很长 - 我希望它可以帮助您决定使用哪种方法。如果您在这方面遇到问题,您可以提出一个具体的问题。
【解决方案2】:

A.connected_to[0] = &amp;B;

复制一些东西:表达式&amp;B的临时指针值。

向量模板类总是会自动进行复制构造和销毁,但是原始类型的复制构造等价于原始类型的赋值和销毁,包括指针,是无操作的。

指针是一种非常基本的类型 - 使用指针时几乎不会自动为您完成任何事情。在引擎盖下,它只是一个对应于内存地址的整数值。当您取消引用一个指针时,编译器只是相信您该指针持有(或“指向”)正确类型的对象的地址。

例如,给定的类 Foo 和 Bar 没有继承关系:

Foo *ptr1, *ptr2;
Bar *ptr3;
  // All pointers are uninitialized. 
  // Dereferencing them is undefined behavior. Most likely a crash.
  // The compiler will almost certainly issue a warning.
ptr1= new Foo(); // ptr1 now points to a valid Foo.
ptr2 = ptr1; // ptr2 points to the same Foo.
ptr3=(Bar*)ptr1; // This is an obvious programmer error which I am making here for demonstration.
 // ptr3 points to the same block of memory as ptr1 & 2.
 // Dereferencing it is likely to do strange things.
delete ptr1; // The compiler is allowed to set ptr1 to 0, but you can't rely on it.
  // In either case dereferencing ptr1 is once again undefined behavior
  // and the value of ptr2 is unchanged.

如果在删除后看到ptr1 被取消引用,编译器发出警告的可能性要比初始化前小得多。如果您在通过ptr1 删除对象后取消引用ptr2,它几乎不会发出警告。如果您没有像其他人警告您的那样小心,您的指针向量可能会导致您无意中以这种方式调用未定义的行为。

我将Foo*非常错误 转换为Bar*,以说明编译器对您的绝对信任。编译器允许您这样做,并且当您取消引用 ptr3 时,它会愉快地将这些位视为 Bar。

C++ 标准库提供了一些模板类,这些模板类提供了类似指针的行为和更多的自动安全性。例如std::shared_pointer:

std::shared_ptr 是一个管理对象生命周期的智能指针, 通常分配有new。几个shared_ptr 对象可以管理 同一个对象;对象在最后剩下的时候被销毁 shared_ptr 指向它被破坏或重置。对象是 使用删除表达式或提供的自定义删除器销毁 施工期间发给shared_ptr

如果您的环境还没有提供 c++11 标准库,它可能会提供 boost 库或 std::tr1:: 命名空间。两者都提供了一个非常相似的shared_ptrstd::auto_ptr,你肯定有,它是类似的,但只允许一个 auto_ptr 在给定时间引用一个对象。 (C++11 引入了std::unique_ptr 作为auto_ptr 的预期替代品。auto_ptr 与大多数标准模板容器不兼容。unique_ptr 可以放在带有std::move 的模板容器中。)

可以通过保留或获取指针并使用它来破坏这些类中的任何一个,例如

Foo *basic_ptr=new Foo();
std::auto_ptr<Foo> fancy_ptr(basic_ptr);
delete basic_ptr; // Oops! This statement broke our auto_ptr.

如果你在自动存储中传入一个变量的地址,你也会破坏它们:

Foo aFoo;
std::auto_ptr<Foo> fancy_ptr(&aFoo); // automatic storage automatically breaks auto_ptr

如果你只使用std::shared_ptr&lt;sn&gt; fresh_sn(new sn()),然后使用std::vector&lt; std::shared_ptr&lt;sn&gt; &gt;,你会没事的。

【讨论】:

  • +1:很好的答案。两个注意事项:1)从Foo*Bar*非常错误 转换仅被编译器接受,因为该示例使用了不安全的C 样式转换。出于这个原因,使用 C++ 风格的强制转换被认为是一种很好的做法 - static_cast 会在此处导致编译器错误。 2) 一些较旧的编译器也提供std::tr1::shared_ptr,即使没有 C++11 支持。
  • @Xeo 现在是。感谢您的提醒。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-13
  • 2010-12-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多