首页 > 编程语言 >关于C++拷贝控制

关于C++拷贝控制

时间:2023-10-08 20:13:45浏览次数:33  
标签:folders 函数 C++ void Folder 关于 Message 拷贝 指针

通常来说,对于类内动态分配资源的类需要进行拷贝控制:要在拷贝构造函数、拷贝赋值运算符、析构函数中实现安全高效的操作来管理内存。但是资源管理并不是一个类需要定义自己的拷贝控制成员的唯一原因。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++中,指针与拷贝控制、内存管理一定要万分小心谨慎,一点小的差错也可能导致程序的灾难。

标签:folders,函数,C++,void,Folder,关于,Message,拷贝,指针
From: https://www.cnblogs.com/pkuqcy/p/17749681.html

相关文章

  • C++ OOP(1)
    目录类声明成员函数构造函数和析构函数文件组织this指针类声明classClassName{ private: //datamemberdeclarations;public: //memberfuntiondeclarations;};成员函数一般定义成员函数的方式是在类外,通过域解析运算符指定成员函数属于哪个类#includ......
  • 视频汇聚\视频融合平台分析算法开发平台 EasyCVR关于对工服检测功能的详细介绍
    在某些特定场景,例如工地、后厨、化工、电力等领域,佩戴适当的工装是必不可少的。这不仅是安全规定的要求,还可以降低工作风险并提高工作效率。智能分析网关通过实时监测和识别工人的工装穿着情况,确保他们符合安全要求并做出相应提示或警告。这种技术可以提供额外的保障,帮助管理者更......
  • C++接口自动注册
    #include<iostream>#include<memory>template<typenameT>classInterface{public:staticInterface&instance(){staticInterfaceinst;returninst;}staticvoidreg(T*impl){instance().m_im......
  • 关于折半查找的某个例题的理解
    1-习题展示2-习题解决我们都知道折半查找就是比较中间的数,然后决定查找左边还是右边。那么,对于这个题,我们只需要将序列按照二叉排序树的条件画出来,就会发现,B选项有分叉出现,不是左拐右拐的那种分叉。答案就出来啦~......
  • 关于数据库的复习
    数据库分位关系数据库和非关系数据库关系数据库中有Oracle、DB2、SQLServer、MySQL等。非关系数据库就是NOSQL。这老师MySQL的官网下载地址https://dev.mysql.com/downloads/windows/installer/8.0.html接下来就是使用图形客户端navicat来操作数据库了。再就是SQL语句:数据查......
  • 关于DATE_SUB的sql查询执行慢的优化
    背景:      因为订单表是分表的,需要每天定时从不同订单表里获取7天前到当前时间的数据。归档到一个表中进行统计分析之类的计算。因为每张表数据量比较大(千万级的数据),ORDER_CREATE_TIME是创建了索引的。ORDER_CREATE_TIME的type是datetime类型,通过ORDER_CREATE_TIM......
  • c++如何读取txt文件内容
    一、c++文件流:fstream //文件流ifstream //输入文件流ofstream //输出文件流 二、文件路径的表示1、绝对路径:inf.open("d://DEV_C++//LogFile//游泳数据//LUYINGYAN1039_SensorLog.txt");   注意:双斜线"\\" 2、相对路径:对相对路径而言,路......
  • DEV_C++新建项目及多线程实现
    一、单个C++程序(多线程)//实现txt文件的读入并重写入另外一个txt文件中~#include<fstream>//ifstream#include<iostream>#include<string>//包含getline()#include<string.h>#include<pthread.h>#include<windows.h>//#include<cmath>usin......
  • destoon注册会员关于邮箱可以重复以及不填写的操作方法
    首先要修改数据库中的destoon_member表中的email字段,因为之前的是:UNIQUEKEY`email`(`email`)因为这样所以插入数据库中的时候邮件就必须不能重复我们需要把改下数据表的属性,用以下语句进行修改:ALTERTABLEdestoon_member DROPINDEXemail;这样就取消了索引键然后我们......
  • 基于 Linux、C++实现的高性能内存池
    1.引入内存池的意义  内存池(MemoryPool)是一种内存分配方式,又被称为固定大小区块规划(fixed-size-blocksallocation)。通常我们习惯直接使用new、malloc等API申请分配内存,但是这种方式非常容易产生内存碎片,早晚都会申请内存失败。并且在比较复杂的代码或者继承的屎山......