关于C++拷贝控制

关于,c++,拷贝,控制 · 浏览次数 : 25

小编点评

**问题所在:** 在 `Folder` 类中的 `remove_from_messages` 方法中,尝试从 `messages` 中删除 `Message` 指向的指针,然而该方法在调用之前先调用了 `addMsg` 方法,将 `Message` 指向的指针添加到 `messages` 中。这意味着在 `remove_from_messages` 方法被执行之前,`messages` 中可能已包含指向已删除的 `Folder` 的指针,导致解引用操作失败。 **解决方案:** 在 `remove_from_messages` 方法中,应该在删除 `Message` 指向的指针之前检查 `messages` 中是否存在指向该 `Message` 的指针。如果存在,则需先清除该指针,才能释放资源。 **修改后的 `Folder` 类 `remove_from_messages` 方法:** ```cpp void Folder::remove_from_messages(){ for (auto m : messages) { if (m != this) { m->remFolder(this); break; } } } ``` **其他建议:** 1. 在使用 `remove_from_folders` 方法之前,可以检查 `messages` 中是否存在指向当前 `Folder` 的指针,并进行清理操作。 2. 在定义 `Folder` 类时,可以考虑使用智能指针或其他资源管理技术来管理 `messages` 中的 `Message` 指向的指针。 3. 在使用 `Message` 和 `Folder` 类时,要注意对指针的管理,避免出现内存错误。

正文

通常来说,对于类内动态分配资源的类需要进行拷贝控制:要在拷贝构造函数、拷贝赋值运算符、析构函数中实现安全高效的操作来管理内存。但是资源管理并不是一个类需要定义自己的拷贝控制成员的唯一原因。C++ Primer 第5版 中给出了一个Message类与Folder类的例子,分别表示电子邮件消息和消息目录。每个Message可以出现在多个Folder中,但是,任意给定的Message的内容只有一个副本。如果一条Message的内容被改变,我们从任意的Folder中看到的该Message都是改变后的版本。为了记录Message位于哪些Folder中,每个Message都用一个set保存所在的Folder的指针,同样的,每个Folder都用一个set保存它包含的Message的指针。二者的设计如下图所示:

C++ Primer中并没有给出Folder类的实现。在对Message及Folder类的复现过程中,出现了一个问题,导致了严重错误。

Message及Folder类的初步设计如下:

Message类:

class Message
{
    friend class Folder;
private:
    string contents;
    set<Folder*> folders;

    //工具函数:在本消息的folders列表中加入/删除新文件夹指针f
    void addFolder(Folder* f);
    void remFolder(Folder* f);

    //工具函数:在本消息folders列表中的所有Folder中删除指向此消息的指针
    void remove_from_folders();

public:
    string getContents();
    set<Folder*> getFolders();

    //构造函数与拷贝控制
    Message(const string& s = " ") :contents(s) {};
    ~Message();

    //接口:将本消息存入给定文件夹f
    void save(Folder& f);
    //接口:将本消息在给定文件夹中删除
    void remove(Folder& f);
};

Folder类:

class Folder
{
    friend class Message;
private:
    set<Message*> messages;

    //工具函数:将给定消息的指针添加到本文件夹的messages中
    void addMsg(Message* m);
    //工具函数:将给定消息的指针在本文件夹中的messages中删除
    void remMsg(Message* m);

public:
    set<Message*> getMessages();
};

这两个类有对称的功能函数:Message.addFolder(Folder* f)与Folder.addMsg(Message* m),以及Message.remFolder(Folder* f)与Folder.remMsg(Message* m),用来实现Message的保存以及拷贝控制操作等。

所有成员函数的实现如下:

string Message::getContents()
{
    return contents;
}
set<Folder*> Message::getFolders()
{
    return folders;
}

void Message::addFolder(Folder* f)
{
    this->folders.insert(f);
}
void Message::remFolder(Folder* f)
{
    this->folders.erase(f);
}

//接口:将本消息存入给定文件夹f
void Message::save(Folder& f)
{
    this->addFolder(&f);
    f.addMsg(this);
}
//接口:将本消息在给定文件夹中删除
void Message::remove(Folder& f)
{
    this->remFolder(&f);
    f.remMsg(this);
}

void Message::remove_from_folders()
{
    for (auto f : folders)
    {
        f->remMsg(this);
    }
}

Message::~Message()
{
    remove_from_folders();
}

/*Folder的成员函数*/
//工具函数:将给定消息的指针添加到本文件夹的messages中
void Folder::addMsg(Message* m)
{
    messages.insert(m);
}
//工具函数:将给定消息的指针在本文件夹中的messages中删除
void Folder::remMsg(Message* m)
{
    messages.erase(m);
}

set<Message*> Folder::getMessages()
{
    return messages;
}

 在这个实现版本的代码测试中,出现了这样一个问题:程序会有运行时错误,主函数的返回值不为0。测试代码如下:

void test()
{
    Message m1("Hello,"), m2("World"), m3("!");
    Folder f1, f2;
    m1.save(f1); m1.save(f2);
    m2.save(f2);
    m3.save(f2);
    m2.remove(f2);
}

int main()
{
    test();
    system("pause");
    return 0;
}

运行结果:

 经调试排查原因之后,找到了问题所在:试图对已经被销毁了的对象的指针进行解引用。该bug和“函数返回指向局部变量的指针”所导致的问题类似。我们为Message类定义了析构函数:

Message::~Message()
{
    remove_from_folders();
}

这个析构函数的实现与C++ Primer上的实现完全一致。该析构函数意图在于当一个Message被销毁时,应该清除它的folders中的所有指向它的指针。这看上去合理,可是在这里却导致了内存错误。原因在于,remove_from_folders()操作会访问该Message所在的所有Folder的指针,而若这些Folder的销毁在该Message的销毁之前进行,则操作会试图通过指针解引用,来访问已被销毁的Folder对象。这会导致严重的运行时错误。在本例中,局部变量Folder f1的创建在m1之后,将m1加入f1,test()函数结束时,按照局部变量的销毁顺序,会先销毁后创建的对象f1,于是,m1的析构函数会试图解引用已被销毁对象f1的指针。出现这个问题,是因为在实现的时候没有按照C++ Primer上的设计正确地实现Folder的析构函数。我们按照如下实现Folder的析构函数:

class Folder
{
    /*其他Folder的声明不变*/

    /*加入Folder的析构函数,以及一个工具函数,对于将要销毁的Folder,这个工具函数负责删除该Folder中所有Message指向它的指针*/
private:  
    void remove_from_messages();
public:    
    ~Folder();
};

void Folder::remove_from_messages()
{
    for (auto m : messages)
        m->remFolder(this);
}

Folder::~Folder()
{
    remove_from_messages();
}

此时,Folder的析构函数在Folder被销毁时可以正确地删除所有Message中指向自身的指针,就避免了对已经销毁的对象进行解引用的操作。反过来,若先定义的是f1,后定义的是m1,在m1先销毁时,m1的析构函数也可以正确地删除所有Folder中指向m1的指针。所以,无论Folder先被销毁,还是Message先被销毁,都能够正确地执行析构操作。使用与上面同样的test()函数进行测试,程序可以正常地退出了:

这个例子也给了我们又一次提醒:在C++中,指针与拷贝控制、内存管理一定要万分小心谨慎,一点小的差错也可能导致程序的灾难。

与关于C++拷贝控制相似的内容:

关于C++拷贝控制

通常来说,对于类内动态分配资源的类需要进行拷贝控制:要在拷贝构造函数、拷贝赋值运算符、析构函数中实现安全高效的操作来管理内存。但是资源管理并不是一个类需要定义自己的拷贝控制成员的唯一原因。C++ Primer 第5版 中给出了一个Message类与Folder类的例子,分别表示电子邮件消息和消息目录

在 Net7.0环境下测试了 Assembly.Load、Assmebly.LoadFile和Assembly.LoadFrom的区别

一、简介 很长时间没有关注一些C#技术细节了,主要在研究微服务、容器、云原生、容器编排等高大上的主题了,最近在写一些框架的时候,遇到了一些和在 Net Framework 框架下不一样的情况,当然了,我今天主要测试的是,在通过【添加项目引用】和【手动拷贝DLL】的情况下,这三个方法加载程序集:Ass

CUDA C编程权威指南:2.1-CUDA编程模型

本文主要通过例子介绍了CUDA异构编程模型,需要说明的是Grid、Block和Thread都是逻辑结构,不是物理结构。实现例子代码参考文献[2],只需要把相应章节对应的CMakeLists.txt文件拷贝到CMake项目根目录下面即可运行。 1.Grid、Block和Thread间的关系 GPU中最

C#数据去重的这几种方式,你知道几种?

前言 今天我们一起来讨论一下关于C#数据去重的常见的几种方式,每种方法都有其特点和适用场景,我们根据具体需求选择最合适的方式。当然欢迎你在评论区留下你觉得更好的数据去重的方式。 使用HashSet去重 HashSet的唯一性: HashSet 中的元素是唯一的,不允许重复值。如果试图添加重复的元素,

C#实现生成Markdown文档目录树

前言 之前我写了一篇关于C#处理Markdown文档的文章:C#解析Markdown文档,实现替换图片链接操作 算是第一次尝试使用C#处理Markdown文档,然后最近又把博客网站的前台改了一下,目前文章渲染使用Editor.md组件在前端渲染,但这个插件生成的目录树很丑,我魔改了一下换成boots

C#的基于.net framework的Dll模块编程(二) - 编程手把手系列文章

今天继续这个系列博文的编写。接上次的篇幅,这次介绍关于C#的Dll类库的创建的内容。因为是手把手系列,所以对于需要入门的朋友来说还是挺好的,下面开始咯: 一、新建Dll类库; 这里直接创建例子的Dll类库项目,至于项目文件目录的存放布局后面的例子中会介绍。 在解决方案资源管理器上鼠标右键,选择“添加

关于19c RU补丁报错问题的分析处理

本文演示关于19c RU补丁常见报错问题的分析处理: 1.查看补丁应用失败的原因 2.问题解决后可继续应用补丁 3.发现DB的RU补丁未更新 4.opatchauto应用DB补丁报错解决 1.查看补丁应用失败的原因 补丁应用失败有详细日志记录原因; 故意使用oracle用户解压补丁,然后测试是否可以

C# 中关于 T 泛型【C# 基础】

泛型在 C# 中提供了更加灵活、安全和高效的编程方式,它可以提高代码的可重用性、可维护性和可扩展性,同时还能够减少错误并提高性能。这么好的东西必须得多安排安排!

关于 yield 关键字【C# 基础】

yield 关键字的用途是把指令推迟到程序实际需要的时候再执行,这个特性允许我们更细致地控制集合每个元素产生的时机。

C#中关于 object,dynamic 一点使用心得

首先说一下使用场景 WebAPI接口入参使用 object和 dynamic 后续解析和处理 1.object和dynamic 区别 在.NET中,object和dynamic也有一些区别: object:object是.NET中的顶级类,所有类都是object的子类。在C#中,您可以使用objec