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[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,但前提是我们没有创建任何链接。

即使在创建链接后,我们也可以使用dequedequepush_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 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-