MFC六大关键技术(4)——永久保存(串行化)

2023-11-03

MFC六大关键技术(第四部分)——永久保存(串行化)

先用一句话来说明永久保存的重要:弄懂它以后,你就越来越像个程序员了!

如果我们的程序不需要永久保存,那几乎可以肯定是一个小玩儿。那怕我们的记事本、画图等小程序,也需要保存才有真正的意义。

对于MFC的很多地方我不甚满意,总觉得它喜欢拿一组低能而神秘的宏来故弄玄虚,但对于它的连续存储(serialize)机制,却是我十分钟爱的地方。在此,可让大家感受到面向对象的幸福。

MFC的连续存储(serialize)机制俗称串行化。“在你的程序中尽管有着各种各样的数据,serialize机制会象流水一样按顺序存储到单一的文件中,而又能按顺序地取出,变成各种不同的对象数据。”不知我在说上面这一句话的时候,大家有什么反应,可能很多朋友直觉是一件很简单的事情,只是说了一个“爽”字就没有下文了。

要实现象流水一样存储其实是一个很大的难题。试想,在我们的程序里有各式各样的对象数据。如画图程序中,里面设计了点类,矩形类,圆形类等等,它们的绘图方式及对数据的处理各不相同,用它们实现了成百上千的对象之后,如何存储起来?不想由可,一想头都大了:我们要在程序中设计函数store(),在我们单击“文件/保存”时能把各对象往里存储。那么这个store()函数要神通广大,它能清楚地知道我们设计的是什么样的类,产生什么样的对象。大家可能并不觉得这是一件很困难的事情,程序有能力知道我们的类的样子,对象也不过是一块初始化了存储区域罢了。就把一大堆对象“转换”成磁盘文件就行了。

即使上面的存储能成立,但当我们单击“文件/打开”时,程序当然不能预测用户想打开哪个文件,并且当打开文件的时候,要根据你那一大堆垃圾数据new出数百个对象,还原为你原来存储时的样子,你又该怎么做呢?

试想,要是我们有一个能容纳各种不同对象的容器,这样,用户用我们的应用程序打开一个磁盘文件时,就可以把文件的内容读进我们程序的容器中。把磁盘文件读进内存,然后识别它“是什么对象”是一件很难的事情。首先,保存过程不像电影的胶片,把景物直接映射进去,然后,看一下胶片就知道那是什么内容。可能有朋友说它象录像磁带,拿着录像带我们看不出里面变化的磁场信号,但经过录像机就能把它还原出来。

其实不是这样的,比如保存一个矩形,程序并不是把矩形本身按点阵存储到磁盘中,因为我们绘制矩形的整个过程只不过是调用一个GDI函数罢了。它保存只是坐标值、线宽和某些标记等。程序面对“00 FF”这样的东西,当然不知道它是一个圆或是一个字符!

拿刚才录像带的例子,我们之所以能最后放映出来,前提我们知道这对象是“录像带”,即确定了它是什么类对象。如果我们事先只知道它“里面保存有东西,但不知道它是什么类型的东西”,这就导致我们无法把它读出来。拿录像带到录音机去放,对录音机来说,那完全是垃圾数据。即是说,要了解永久保存,要对动态创建有深刻的认识。

现在大家可以知道困难的根源了吧。我们在写程序的时候,会不断创造新的类,构造新的对象。这些对象,当然是旧的类对象(如MyDocument)从未见过的。那么,我们如何才能使文档对象可以保存自己新对象呢,又能动态创建自己新的类对象呢?

许多朋友在这个时候想起了CObject这个类,也想到了虚函数的概念。于是以为自己“大致了解”串行化的概念。他们设想:“我们设计的MyClass(我们想用于串行化的对象)全部从CObject类派生,CObject类对象当然是MyDocument能认识的。”这样就实现了一个目的:本来MyDocument不能识别我们创建的MyClass对象,但它能识别CObject类对象。由于MyClass从CObject类派生,我产的新类对象“是一个CObject”,所以MyDocument能把我们的新对象当作CObiect对象读出。或者根据书本上所说的:打开或保存文件的时候,MyDocument会调用Serialize(),MyDocument的Serialize()函会呼叫我们创建类的Serialize函数[即是在MyDocument Serialize()中调用:m_pObject->Serialize(),注意:在此m_pObject是CObject类指针,它可以指向我们设计的类对象]。最终结果是MyDocument的读出和保存变成了我们创建的类对象的读出和保存,这种认识是不明朗的。

有意思还有,在网上我遇到几位自以为懂了Serialize的朋友,居然不约而同的犯了一个很低级得让人不可思议的错误。他们说:Serialize太简单了!Serialize()是一个虚函数,虚函数的作用就是“优先派生类的操作”。所以MyDocument不实现Serialize()函数,留给我们自己的MyClass对象去调用Serialize()……真是哭笑不得,我们创建的类MyClass并不是由MyDocument类派生,Serialize()函数为虚在MyDocument和MyClass之间没有任何意义。MyClass产生的MyObject对象仅仅是MyDocument的一个成员变量罢了。

话说回来,由于MyClass从CObject派生,所以CObject类型指针能指向MyClass对象,并且能够让MyClass对象执行某些函数(特指重载的CObject虚函数),但前提必须在MyClass对象实例化了,即在内存中占领了一块存储区域之后。不过,我们的问题恰恰就是在应用程序随便打开一个文件,面对的是它不认识的MyClass类,当然实例化不了对象。

幸好我们在上一节课中懂得了动态创建。即想要从CObject派生的MyClass成为可以动态创建的对象只要用到DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏就可以了(注意:最终可以Serialize的对象仅仅用到了DECLARE_SERIAL/IMPLEMENT_SERIAL宏,这是因为DECLARE_SERIAL/IMPLEMENT_SERIAL包含了DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏)。

从解决上面的问题中,我们可以分步理解了:

1、  Serialize的目的:让MyDocument对象在执行打开/保存操作时,能读出(构造)和保存它不认的MyClass类对象。

2、  MyDocument对象在执行打开/保存操作时会调用它本身的Serialize()函数。但不要指望它会自动保存和读出我们的MyClass类对象。这个问题很容易解决,就直接在MyDocument:: Serialize(){

// 在此函数调用MyClass类的Serialize()就行了!即

MyObject. Serialize();        

}

3、  我们希望MyClass对象为可以动态创建的对象,所以要求在MyClass类中加上DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏。

但目前的Serialize机制还很抽象。我们仅仅知道了表面上的东西,实际又是如何的呢?下面作一个简单深刻的详解。

先看一下我们文档类的Serialize()

void CMyDoc::Serialize(CArchive& ar)

{

    if (ar.IsStoring())

    {

        // TODO: add storing code here

    }

    else

    {

        // TODO: add loading code here

    }

}

目前这个子数什么也没做(没有数据的读出和写入),CMyDoc类正等待着我们去改写这个函数。现在假设CMyDoc有一个MFC可识别的成员变量m_MyVar,那么函数就可改写成如下形式:

void CMyDoc::Serialize(CArchive& ar)

{

    if (ar.IsStoring())     //读写判断

    {

        ar<<m_MyVar;        //写

    }

    else

    {

        ar>>m_MyVar;        //读

    }

}

许多网友问:自己写的类(即MFC未包含的类)为什么不行?我们在CMyDoc里包含自写类的头文件MyClass.h,这样CMyDoc就认识MyDoc类对象了。这是一般常识性的错误,MyDoc类认识MyClass类对象与否并没有用,关键是CArchive类,即对象ar不认识MyClass(当然你梦想重写CArchive类当别论)。“>>”、“<<”都是CArchive重载的操作符。上面ar>>m_MyVar说白即是在执行一个以ar和m_MyVar 为参数的函数,类似于function(ar,m_MyVar)罢了。我们当然不能传递一个它不认识的参数类型,也因此不会执行function(ar,m_MyObject)了。

[注:这里我们可以用指针。让MyClass从Cobject派生,一切又起了质的变化,假设我们定义了:MyClass *pMyClass = new MyClass;因为MyClass从CObject派生,根据虚函数原理,pMyClass也是一个CObject*,即pMyClass指针是CArchive类可认识的。所以执行上述function(ar, pMyClass),即ar << pMyClass是没有太多的问题(在保证了MyClass对象可以动态创建的前提下)。]

回过头来,如果想让MyClass类对象能Serialize,就得让MyClass从CObject派生,Serialize()函数在CObject里为虚,MyClass从CObject派生之后就可以根据自己的要求去改写它,象上面改写CMyDoc::Serialize()方法一样。这样MyClass就得到了属于MyClass自己特有的Serialize()函数。

现在,程序就可以这样写:

……

#include “MyClass.h”

……

void CMyDoc::Serialize(CArchive& ar)

{

    //在此调用MyClass重写过的Serialize()

    m_MyObject. Serialize(ar);      // m_MyObject为MyClass实例

}

至此,串行化工作就算完成了,一即简单直观:从CObject派生自己的类,重写Serialize()。在此过程中,我刻意安排:在没有用到DECLARE_SERIAL/IMPLEMENT_SERIAL宏,也没有用到CArray等模板类的前提下就完成了串行化的工作。我看过某些书,总是一开始就讲DECLARE_SERIAL/IMPLEMENT_SERIAL宏或马上用CArray模板,让读者觉得串行化就是这两个东西,导致许多朋友因此找不着北。

大家看到了,没有DECLARE_SERIAL/IMPLEMENT_SERIAL宏和CArray等数据结构模板也依然可以完成串行化工作。

现在可以腾出时间讲一下大家觉得十分抽象的CArchive。我们先看以下程序(注:以下程序包含动态创建等,请包含DECLARE_SERIAL/IMPLEMENT_SERIAL宏)

void MyClass::Serialize(CArchive& ar)

{

    if (ar.IsStoring())     //读写判断

    {

        ar<< m_pMyVar;      //问题:ar 如何把m_pMyVar所指的对象变量保存到磁盘?

    }

    else

    {

        pMyClass = new MyClass; //准备存储空间

        ar>> m_pMyVar;      

    }

}

要回答上面的问题,即“ar<<XXX”的问题。和我们得看一下模拟CArchive的代码。

“ar<<XXX”是执行CArchive对运算符“<<”的重载动作。ar和XXX都是该重载函数中的一参数而已。函数大致如下:

CArchive& operator<<( CArchive& ar, const CObject* pOb)

{

    …………

        //以下为CRuntimeClass链表中找到、识别pOb资料。

        CRuntimeClass* pClassRef = pOb->GetRuntimeClass();

        //保存pClassRef即类信息(略)

        

        ((CObject*)pOb)->Serialize();//保存MyClass数据

    …………

}

从上面可以看出,因为Serialize()为虚函数,即“ar<<XXX”的结果是执行了XXX所指向对象本身的Serialize()。对于“ar>>XXX”,虽然不是“ar<<XXX”逆过程,大家可能根据动态创建和虚函数的原理料想到它。

至此,永久保存算是写完了。在此过程中,我一直努力用最少的代码,详尽的解释来说明问题。以前我为本课题写过一个版本,并在几个论坛上发表过,但不知怎么在网上遗失(可能被删除)。所以这篇文章是我重写的版本。记得第一个版本中,我是对DECLARE_SERIAL/IMPLEMENT_SERIAL和可串行化的数组及链表对象说了许多。这个版本中我对DECLARE_SERIAL/IMPLEMENT_SERIAL其中奥秘几乎一句不提,目的是让大家能找到中心,有更简洁的永久保存的概念,我觉得这种感觉很好!

顺便说一下:我的QQ:14653353被盗了,迟些才打算要回它。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

MFC六大关键技术(4)——永久保存(串行化) 的相关文章

  • 将带有非字符串关键字的 dict 传递给 kwargs 中的函数

    我使用具有签名功能的库f args kwargs 我需要在 kwargs 参数中传递 python dict 但 dict 不包含关键字中的字符串 f 1 2 3 4 Traceback most recent call last File
  • Javascript:typeof表示“函数”,但它不能作为函数调用

    这次我对 Javascript 真的很困惑 var x Array prototype concat call typeof x function x Uncaught TypeError x is not a function 这里到底发
  • C++:初始化结构体并设置函数指针

    我正在尝试使用函数指针初始化结构 但是除非使用全局函数完成 否则我很难这样做 以下代码有效 float tester float v return 2 0f v struct MyClass Example typedef float My
  • “对象不是函数” - onclick 事件

    在开始之前 不 我没有发现分号的问题 并且我没有向函数传递任何值 当我尝试从控制台执行函数 login 时 它工作得很好 但是当我单击 HTML 输入按钮来调用它时 我收到 Uncaught TypeError object is not
  • PHP 函数可以接受无限数量的参数吗? [复制]

    这个问题在这里已经有答案了 在 PHP 中有类似的函数unset 支持我们向它们抛出的任意数量的参数 我想创建一个类似的函数 它能够接受任意数量的参数并处理所有参数 任何想法 如何做到这一点 在 PHP 中 使用该函数func get ar
  • 错误 C2248: 'CObject::CObject' : 无法访问类 'CObject' afxwin.h 中声明的私有成员

    我试图让班级负责在灰色背景上放置一些文本 Score h pragma once class Score public Score Score void UpdateScore int points void UpdateLives int
  • 在C中更改函数内的数组

    我正在学习 C 并且很困惑为什么在 main 中创建的数组不会在函数内部更改 我假设传递的数组是一个指针 并且更改指针应该更改数组 对吧 有人可以解释这种情况下发生了什么吗 谢谢你的帮助 int main int i length 10 i
  • 函数类型有什么用?

    鉴于以下两个typedefs typedef void pftype int typedef void ftype int 我明白第一个定义pftype作为指向一个函数的指针 该函数接受一个int参数并且不返回任何内容 第二个定义ftype
  • 屏幕截图忽略了一些窗口

    我正在 MFC 中工作 我正在尝试捕获桌面的 bmp 我正在使用 GetDC NULL 来执行此操作 但它似乎忽略了特殊的皮肤窗口 它似乎忽略了用 UpdateLayeredWindow 绘制的窗口 此行为似乎仅发生在 Vista x64
  • 将数组中的所有值作为参数传递给函数

    我有一个值数组 a b c d 我需要将它们作为参数传递给函数 window myFunction a b c d 如果我可以将数组 对象传递到函数中 那么这会更容易 但这些函数是由其他人编写的或已经存在 我无法更改它们 它们需要作为单独的
  • 如何获取调用函数的“this”值?

    如果我有一个这样的函数 function foo this console log this function bar bar prototype func function foo this var test new bar test f
  • 从 jQuery 事件访问函数中的参数*和*事件

    这是我不久前问的另一个问题的后续问题 通常 您可以从 jQuery 事件的函数调用中访问事件 如下所示 item live click functionToCall 并在函数中 function functionToCall ev do s
  • 如何使用 jQuery 在第二次单击时反转 CSS 动画

    我制作了以下菜单图标 CSS 动画 当我点击它时会触发它 当我使用 jQuery 第二次单击它时 我想使其反向动画 path1 stroke dasharray 33px stroke dashoffset 33px animation l
  • Python argparse 作为函数

    以这种方式获取命令行参数有什么本质上的错误吗 我的意思是把参数解析放入它自己的函数中 它会被认为是非 Pythonic 或更严重吗 usr bin python import argparse def getArgs argv None p
  • 将数组传递给函数名称冲突

    Specs GNU bash 版本 3 1 17 无法升级 Premise 我一直在摆弄数组 我想知道是否有任何方法可以让函数的本地变量与所述函数外部的数组同名 Example 在下面的示例中 我将尝试显示该问题 Working bin b
  • python:函数中的变量,点前面是函数名

    我需要理解这个概念 其中我们可以在函数定义中的变量名中使用点 这里没有类定义 也没有模块 Python 不应该接受包含点的变量名 def f x f author sunder f language Python print x f aut
  • 有没有办法从画布上清除一个元素而不消除其他元素?

    我正在使用画布构建页面加载器 并使用 es6 类 虽然目前我无法使其正常工作 原因之一是我找不到清除画布的方法进展 到目前为止 这是我的代码 class Loader constructor width height this width
  • 在查询中实现函数调用(分组运行总计)

    我有一个函数叫做fxGroupedRunningTotal fxGRT 和查询 总计 我想在 Totals 中调用 fxGRT 以便获得一个显示分组运行总计的列 我只能通过导入总计查询来测试 fxGRT 使用总计并调用 fxGRT 的查询
  • 在 where 子句中使用聚合函数和不同的列条件

    select PO Order Qty Avg PO Order Qty as totalAverage FROM FirstStrike Retail custom Whse Pricing QR where item code 111
  • 我可以将参数作为数组传递吗?

    例如 而不是 assert eq add 2 3 5 有什么方法可以调用类似的东西 let params u32 2 2 3 assert eq call add params 5 我发现这个功能对于测试非常有用 例如 如果我想为需要大量参

随机推荐

  • 一个非常有用的函数——COALESCE

    很多人知道ISNULL函数 但是很少人知道Coalesce函数 人们会无意中使用到Coalesce函数 并且发现它比ISNULL更加强大 其实到目前为止 这个函数的确非常有用 本文主要讲解其中的一些基本使用 首先看看联机丛书的简要定义 返回
  • java对象创建过程(jvm)

    虚拟机遇到一条new指令时 开始进行对象的创建 1 检查这个指令的参数是否能在常量池中定位到一个类的符号引用 true 则继续下一步 false 说明这个类还没有被定义 会抛出ClassNotFoundException 2 检查这个符号引
  • centos7自定义镜像运行Flask

    一 centos7自定义镜像 1 支持22端口的sftp连接 参考链接 Centos7创建支持ssh服务器的docker容器 2 支持flask 上一步保存的镜像名为 centos7 dn 0119 docker run d p 8023
  • TCP通信过程详解以及tcp长连接和短连接

    1 TCP连接 当网络通信时采用TCP协议时 在真正的读写操作之前 server与client之间必须建立一个连接 当读写操作完成后 双方不再需要这个连接 时它们可以释放这个连接 连接的建立是需要三次握手的 而释放则需要4次挥手 所以说每个
  • Swing001——入门简介

    一 Swing简介 Swing是什么 Swing 是新一代的图形界面工具 特点 轻量级组件 采用纯 Java 实现 跨平台支持 不再依赖于本地平台的图形界面 更多的图形界面组件 可以开发出美观的图形界面程序 二 Swing 容器 创建图形用
  • word2vec

    Author kangbingbing Email kangb93 126 com Datawhale word2vec简介 Word2vec是Google实现word embedding的一种具体的方式 因为速度快效果好 而广为人知 而W
  • 一些好用的vscode插件

    一直用的sublime text 喜欢它的轻巧 不过最近试了下vscode 发现还是蛮中意的 插件生态丰富而且配置挺人性化 另外一些shortcuts功能确实提高编辑效率 缺点是如果安装太多插件 内存会飙高些 这里记下目前用的一些觉得不错的
  • 何为智能指针以及QT中的智能指针

    目的 对于指针的使用中有两个导致软件崩溃的问题 一是忘记释放动态申请的内存 二是指针指向的内存被释放 为了解决这个问题 出现了智能指针 效果 智能指针其实是一个类 在这个类的构造函数和析构函数实现以下功能 当有一个指针指向对象A时 计数器加
  • printf、sprintf、fprintf的区别

    printf sprintf fprintf的区别 int printf const char format int fprtintf FILE stream const char format int sprintf char str c
  • chromecast投屏_利用谷歌Chromecast,3个简单的步骤教你将手机投屏到电视上

    无论是在线观看电影 视频通话 展示度假照片还是在电视上玩游戏 把你的安卓 Android 手机的屏幕内容投屏到电视机上都很简单 放过你的家人和朋友们吧 与其让他们挤在你的Android手机或平板电脑的小屏幕上前浏览照片或观看最新的疫情播报视
  • python文件读写函数总结

    1 python读取csv文件 usr bin python coding UTF 8 df pd read csv filepath usecols func name para value df t df fillna value 对空
  • 线性回归算法及案例

    线性回归 寻找 一种能预测的趋势 回归问题的条件 前提 1 收集的数据 2 假设的模型 即一个函数 这个函数里含有未知的参数 通过学习 可以估计出参数 然后利用这个模型去预测 分类新的数据 案例 from sklearn datasets
  • DRBD分布式存储解决方案实战

    一 DRBD分布式存储解决方案 1 DRBD简介 DRBD的全称为 Distributed Replicated Block Device DRBD 分布式块设备复制 DRBD是由内核模块和相关脚本而构成 用以构建高可用性 HA 的集群 其
  • Cas5.3.2 服务端 自定义登入界面

    第一 项目整体结构 自定义页面涉及资源全部存放再src main resources 文件夹目录下 目录 含义 services 配置自定义登入网站模板 static 静态文件目录 用于存放js css代码的 templates 模板文件目
  • Linux mount 命令

    mount 命令用来挂载文件系统 其基本命令格式为 mount t type o options device dirdevice 指定要挂载的设备 比如磁盘 光驱等 dir 指定把文件系统挂载到哪个目录 type 指定挂载的文件系统类型
  • 使用Python,OpenCV执行视觉显著性检测(Visual Saliency Detection)

    使用Python OpenCV执行视觉显著性检测 Visual Saliency Detection 这篇博客将介绍如何使用Python OpenCV执行显著性检测 这是一个应用图像处理和计算机视觉算法来自动定位图像中最 显著 区域的过程
  • 使用python读取xml文件批量生成ground-truth标注图片

    目标检测中 在对比自己算法的检测效果时 需要和标注的真实值进行对比 在网上找到的大部分功能是一样的 只不过不完全符合画出真实标注的训练图片 import cv2 import numpy as np import xml dom minid
  • ASP.NET中新建Web网站并部署到IIS上(详细图文教程)

    场景 ASP NET中新建MVC项目并连接SqlServer数据库实现增删改查 https blog csdn net BADAO LIUMANG QIZHI article details 107024544 在上面实现了新建简单的MVC
  • String index out of range: 100 报错详解与解决方案

    问题出错情况 字符串截取长度 没有那么长的长度所以截取失败 在这里进行debug之后可以看到 异常在substring中 也就是判断字符串的时候报错 具体原因就是string字符串indexof的值本身只有5 然后在这里去取其第100 个字
  • MFC六大关键技术(4)——永久保存(串行化)

    MFC六大关键技术 第四部分 永久保存 串行化 先用一句话来说明永久保存的重要 弄懂它以后 你就越来越像个程序员了 如果我们的程序不需要永久保存 那几乎可以肯定是一个小玩儿 那怕我们的记事本 画图等小程序 也需要保存才有真正的意义 对于MF