c++ - 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[1]);
partnerUp(vecA[0], vecA[2]);
partnerUp(vecA[1], 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 退出,destroys vecA
,这会破坏它的每个元素,而这些元素会破坏图形节点。
但是,这仍然不理想,因为我们使用了一种不必要的间接方式。我们需要保持图节点的地址稳定,因为它们是从其他图节点指向的。因此我们不应该vector<A>
在 main 中使用:如果我们调整 via的大小push_back
,这会改变其元素的地址——图节点——但我们可能已经将这些地址存储为图关系。也就是说,我们可以使用vector
,但前提是我们没有创建任何链接。
我们deque
甚至可以在创建链接之后使用。Adeque
在 a 期间保持元素的地址稳定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
in main 这样的数据结构时:可以只保留指向一个或多个节点的指针,您可以从这些节点到达 main 中的所有其他节点。在这种情况下,您需要图遍历算法来删除所有节点。这是它变得更复杂,因此更容易出错的地方。
就所有权而言,这里的图本身将拥有其节点的所有权,而 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。
因此,典型的经验法则建议是:
- 使用堆栈对象,或者如果不可能,使用 a
unique_ptr
或者如果不可能,使用 ashared_ptr
。 - 对于多个元素,按该顺序使用 a
container<T>
或container<unique_ptr<T>>
orcontainer<shared_ptr<T>>
。
这些是经验法则。如果您有时间考虑它,或者有一些要求,例如性能或内存消耗,那么定义自定义所有权模型可能是有意义的。但是,您还需要花时间确保安全并对其进行测试。所以它应该真的给你一个很大的好处,值得为使它安全所需的所有努力。我建议不要假设这shared_ptr
太慢了。这需要在应用程序的上下文中查看,并且通常是衡量的。获得正确的自定义所有权概念太棘手了。例如,在我上面的一个示例中,您需要非常小心地调整矢量的大小。
推荐阅读
- javascript - 用于验证电话号码的 Javascript 正则表达式
- angular - 如何获得对 canActivate 进行验证的承诺的价值?
- linker-errors - 如何让 Linux 上的 Visual Studio Code 链接到共享库?
- python - 需要 python 库来使用数据透视表更新巨大的 Excel 文件
- python - TypeError:“类型”对象不可下标
- spring-boot - 尝试在weblogic上部署spring boot项目,无法从dispatcherServlet调用控制器
- javascript - ag-grid react 不自动适应列宽
- c++ - 来自 Leetcode 的大多数利润分配工作问题(问题编号 826)
- c++ - gcc:查找 libstdc++ 的目录
- java - Mesibo - 用户没有上网