如何解决C ++使用原始指针创建循环依赖的对象
我正在尝试创建一个连接图并对其执行某些计算。为此,我需要从该图中的每个节点访问其邻居,并从其邻居访问其邻居的邻居,依此类推。不可避免地会创建许多(有用的)循环依赖项。
下面是一个简化的示例,其中包含3个相互连接的节点(例如三角形的3个顶点),我不确定这种方法是否是一个好方法,特别是如果清理操作会导致任何内存泄漏的话:
#include <iostream>
#include <vector>
class A {
public:
int id;
std::vector<A*> partners;
A(const int &i) : id(i) {
std::cout << id << " created\n";
}
~A() {
std::cout << id << " destroyed\n";
}
};
bool partnerUp(A *a1,A *a2) {
if (!a1 || !a2)
return false;
a1->partners.push_back(a2);
a2->partners.push_back(a1);
std::cout << a1->id << " is now partnered with " << a2->id << "\n";
return true;
}
int main() {
std::vector<A*> vecA;
vecA.push_back(new A(10));
vecA.push_back(new A(20));
vecA.push_back(new A(30));
partnerUp(vecA[0],vecA[1]);
partnerUp(vecA[0],vecA[2]);
partnerUp(vecA[1],vecA[2]);
for (auto& a : vecA) {
delete a;
a = nullptr;
}
vecA.clear();
return 0;
}
我也知道我可以使用shared_ptr
+ weak_ptr
来完成任务,但是智能指针会带来开销,因此我希望尽可能避免这种情况(我也讨厌始终使用.lock()来访问数据,但这并不重要)。我使用智能指针重写了代码,如下所示,我想知道两段代码之间有什么区别(两个代码的输出是相同的)。
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class A {
public:
int id;
vector<weak_ptr<A>> partners;
A(const int &i) : id(i) {
cout << id << " created\n";
}
~A() {
cout << id << " destroyed\n";
}
};
bool partnerUp(shared_ptr<A> a1,shared_ptr<A> a2) {
if (!a1 || !a2)
return false;
a1->partners.push_back(a2);
a2->partners.push_back(a1);
cout << a1->id << " is now partnered with " << a2->id << "\n";
return true;
}
int main() {
vector<shared_ptr<A>> vecA;
vecA.push_back(make_shared<A>(10));
vecA.push_back(make_shared<A>(20));
vecA.push_back(make_shared<A>(30));
partnerUp(vecA[0],vecA[2]);
return 0;
}
解决方法
您可以使用所有权原则来防止内存泄漏:在每一点上,都需要有一个负责释放内存的所有者。
在第一个示例中,所有者是main
函数:它撤消所有分配。
在第二个示例中,每个图节点都具有共享所有权。 vecA
和链接的节点都共享所有权。他们都有责任,因为他们在必要时都会打免费电话。
因此,从这个意义上讲,两个版本的所有权都相对明确。第一个版本甚至使用了更简单的模型。但是:第一个版本存在一些异常安全问题。这些与这个小程序无关,但是一旦将此代码嵌入到较大的应用程序中,它们将变得相关。
问题来自所有权转移:您通过new A
执行分配。这并未明确说明所有者是谁。然后,我们将其存储到向量中。但是向量本身不会在其元素上调用delete;它仅调用析构函数(指针无操作)并删除其自身的分配(动态数组/缓冲区)。 main
函数是所有者,它仅在循环的最后一点释放分配。如果main函数提前退出(例如由于异常而退出),它将不会作为分配的所有者履行其职责-不会释放内存。
这是智能指针起作用的地方:它们清楚地说明所有者是谁,并使用RAII来防止出现异常情况:
class A {
public:
int id;
vector<A*> partners;
// ...
};
bool partnerUp(A* a1,A* a2) {
// ...
}
int main() {
vector<unique_ptr<A>> vecA;
vecA.push_back(make_unique<A>(10));
vecA.push_back(make_unique<A>(20));
vecA.push_back(make_unique<A>(30));
partnerUp(vecA[0].get(),vecA[1].get());
partnerUp(vecA[0].get(),vecA[2].get());
partnerUp(vecA[1].get(),vecA[2].get());
return 0;
}
该图仍可以使用原始指针,因为所有权现在仅由unique_ptr
负责,并且所有权由vecA
拥有,而所有权由main
拥有。 Main出口破坏了vecA
,这破坏了它的每个元素,而那些破坏了图节点。
但是,这仍然不是理想的,因为我们使用了一种不必要的间接方式。我们需要保持图节点的地址稳定,因为它们是从其他图节点指向的。因此,我们不应该在main中使用vector<A>
:如果通过push_back
调整其大小,这将更改其元素的地址-图形节点-但我们可能会将这些地址存储为图形关系。也就是说,我们可以使用vector
,但前提是我们没有创建任何链接。
即使在创建链接后,我们也可以使用deque
。 deque
在push_back
期间保持元素的地址稳定。
class A {
public:
int id;
vector<A*> partners;
// ...
A(A const&) = delete; // never change the address,since it's important!
// ...
};
bool partnerUp(A* a1,A* a2) {
// ...
}
int main() {
std::deque<A> vecA;
vecA.emplace_back(10);
vecA.emplace_back(20);
vecA.emplace_back(30);
partnerUp(&vecA[0],&vecA[1]);
partnerUp(&vecA[0],&vecA[2]);
partnerUp(&vecA[1],&vecA[2]);
return 0;
}
图中删除的实际问题是,当您在主目录中没有像vector
这样的数据结构时:可以保留指向一个或多个节点的指针,从中可以访问所有其他节点主节点。在这种情况下,您需要使用图遍历算法来删除所有节点。在这里它变得更加复杂,因此更容易出错。
就所有权而言,这里图本身将拥有其节点的所有权,而main仅拥有图的所有权。
int main() {
A* root = new A(10);
partnerUp(root,new A(20));
partnerUp(root,new A(30));
partnerUp(root.partners[0],root.partners[1]);
// now,how to delete all nodes?
return 0;
}
为什么会推荐第二种方法?
因为它遵循一种广泛的简单模式,可以减少内存泄漏的可能性。如果您始终使用智能指针,那么总会有一个所有者。毫无可能会导致所有权下降的错误。
但是,使用共享的指针,您可以形成多个元素保持活动状态的循环,因为它们在一个循环中相互拥有。例如。 A拥有B,B拥有A。
因此,典型的经验法则建议是:
- 使用堆栈对象,或者,如果不能使用,则使用
unique_ptr
,或者,如果不能,请使用shared_ptr
。 - 对于多个元素,请依次使用
container<T>
或container<unique_ptr<T>>
或container<shared_ptr<T>>
。
这些是经验法则。如果您有时间考虑它,或者有一些诸如性能或内存消耗之类的要求,那么定义自定义所有权模型是很有意义的。但是随后,您还需要花费时间来确保安全并进行测试。因此,值得付出所有确保安全的一切努力,这确实会给您带来巨大的好处。我建议不要假设shared_ptr
太慢。这需要在应用程序的上下文中看到并通常进行测量。正确定义自定义所有权概念太棘手。例如,在我上面的示例中,您需要非常小心调整向量的大小。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。