通常来说,对于类内动态分配资源的类需要进行拷贝控制:要在拷贝构造函数、拷贝赋值运算符、析构函数中实现安全高效的操作来管理内存。但是资源管理并不是一个类需要定义自己的拷贝控制成员的唯一原因。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++拷贝控制的更多相关文章

  1. C++ Primer : 第十三章 : 拷贝控制之对象移动

    右值引用 所谓的右值引用就是必须将引用绑定到右值的引用,我们通过&&来绑定到右值而不是&, 右值引用只能绑定到即将销毁的对象.右值引用也是引用,因此右值引用也只不过是对象的别名 ...

  2. C++ Primer : 第十三章 : 拷贝控制之拷贝控制和资源管理

    定义行为像值的类 行为像值的类,例如标准库容器和std::string这样的类一样,类似这样的类我们可以简单的实现一个这样的类HasPtr. 在实现之前,我们需要: 定义一个拷贝构造函数,完成stri ...

  3. C++的那些事:类的拷贝控制

    1,什么是类的拷贝控制 当我们定义一个类的时候,为了让我们定义的类类型像内置类型(char,int,double等)一样好用,我们通常需要考下面几件事: Q1:用这个类的对象去初始化另一个同类型的对象 ...

  4. C++ Primer : 第十三章 : 拷贝控制之拷贝、赋值与销毁

    拷贝构造函数 一个构造函数的第一个参数是自身类类型的引用,额外的参数(如果有)都有默认值,那么这个构造函数是拷贝构造函数.拷贝构造函数的第一个参数必须是一个引用类型. 合成的拷贝构造函数   在我们没 ...

  5. Chapter13:拷贝控制

    拷贝控制操作:拷贝构造函数.拷贝赋值运算符.移动构造函数.移动赋值运算符.析构函数. 实现拷贝控制操作的最困难的地方是首先认识到什么时候需要定义这些操作. 拷贝构造函数: 如果一个构造函数的第一个参数 ...

  6. C++ 拷贝控制和资源管理,智能指针的简单实现

    C++ 关于拷贝控制和资源管理部分的笔记,并且介绍了部分C++ 智能指针的概念,然后实现了一个基于引用计数的智能指针.关于C++智能指针部分,后面会有专门的研究. 通常,管理类外资源的类必须定义拷贝控 ...

  7. c/c++ 拷贝控制 构造函数的问题

    拷贝控制 构造函数的问题 问题1:下面①处的代码注释掉后,就编译不过,为什么??? 问题2:但是把②处的也注释掉后,编译就过了,为什么??? 编译错误: 001.cpp: In copy constr ...

  8. c/c++ 拷贝控制 右值与const引用

    拷贝控制 右值与const引用 背景:当一个函数的返回值是自定义类型时,调用侧用什么类型接收?? 1,如果自定义类型的拷贝构造函数的参数用const修饰了:可以用下面的方式接收. Test t2 = ...

  9. C++ Primer 笔记——拷贝控制

    1.如果构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数.拷贝构造函数的第一个参数必须是引用类型(否则会无限循环的调用拷贝构造函数). 2.如果没有为一个类 ...

  10. 【C++ Primer 第13章】2. 拷贝控制和资源管理

    拷贝控制和资源管理 • 类的行为像一个值.意味着它应该有自己的状态,当我们拷贝一个像值得对象时,副本和原对象是完全独立的,改变副本不会对原对象有任何影响. • 行为像指针的类则共享状态.当我们拷贝一个 ...

随机推荐

  1. 前端vue实现页面加水印文字 单个页面所有页面加水印 水印颜色

    前端vue实现页面加水印文字, 可以实现系统所有页面加水印,也可以单个页面加水印, 可更改水印颜色, 下载完整代码请访问uni-app插件市场地址: https://ext.dcloud.net.cn ...

  2. 前端学习C语言 - 初级指针

    初级指针 本篇主要介绍:指针和变量的关系.指针类型.指针的运算符.空指针和野指针.指针和数组.指针和字符串.const 和指针.以及gdb 调试段错误. 基础概念 指针是一种特殊的变量.存放地址的变量 ...

  3. FFmpeg+SDL实时解码和渲染H264视频流

    前言 之前实现了Android手机摄像头数据的TCP实时传输,今天接着聊聊,如何在PC端把接收到的H264视频流实时解码并渲染出来.这次使用的语言是C++,框架有FFmpeg和SDL2. 解码 解码部 ...

  4. Python安装time库失败?不是吧阿sir你还不知道内置模块不用下载吧

    嗨嗨,今天给python安装time库,一直报错,换源等办法都试过了 直到我看到 Python中有以下常用模块不用单独安装 random模块 sys模块 time模块 os系统操作 re正则操作 js ...

  5. Python日志模块:实战应用与最佳实践

    本文详细解析了Python的logging模块,从基本介绍到实际应用和最佳实践.我们通过具体的代码示例解释了如何高效地使用这个模块进行日志记录,以及如何避免常见的陷阱,旨在帮助读者更好地掌握这个强大的 ...

  6. PerfView 洞察C#托管堆内存 "黑洞现象"

    一:背景 1. 讲故事 首先声明的是这个 黑洞 是我定义的术语,它是用来表示 内存吞噬 的一种现象,何为 内存吞噬,我们来看一张图. 从上面的 卦象图 来看,GCHeap 的 Allocated=85 ...

  7. AMH安装Nextcloud出现Access denied

    AMH部署了LAMP或者LNMP,然后常规安装Nextcloud,安装完成后跳转到首页就出现Access denied.Nextcloud的文件夹权限已经修改,可读写.http://127.0.0.1 ...

  8. 我开源了团队内部基于SpringBoot Web快速开发的API脚手架v1.6.0更新

    什么是 rest-api-spring-boot-starter rest-api-spring-boot-starter 适用于SpringBoot Web API 快速构建让开发人员快速构建统一规 ...

  9. linux 问题: ssh登录报错,ssh_exchange_identification,多次几次可以登录

    分析 怀疑是句柄数不够,和ssh的最大登录限制 确认 2.1 确认句柄数 过程: ~# systemctl status sshd | grep -i pid Main PID: 3767395 (s ...

  10. asp.net core之EfCore

    EF Core(Entity Framework Core)是一个轻量级.跨平台的对象关系映射(ORM)框架,用于在.NET应用程序中访问和操作数据库.它是Entity Framework的下一代版本 ...