感悟数据封装
通常,人们将“把数据和函数捆绑在一起”以及“隐藏实现”的操作称为数据封装。今天在实验室调试了一个同学矩阵加减乘除的程序,切身体会到数据封装之重要性。
情况是这样的,一个矩阵加减乘除的题目,要求矩阵是稀疏矩阵,他就建了一个三元组,分别存储矩阵中元素的行/列/数据。然后再将这个三元组存储在数组中构成这个矩阵。而老师要求他改为用类似于邻接链表的方法实现。在调试的过程中,我充分体会到了逻辑不清晰以及数据耦合度高所带来的痛苦。
因为他的矩阵是在数组中存储的,在整个程序中所有相关的操作都是通过访问数组下标实现的,也就是说,数据的存储结构在程序中的所有地方都是透明的,毫无抽象与封装的概念可言。那么在将矩阵的存储改为邻接矩阵时,就面临严峻的问题:要将程序中所有的下标运算都替换掉。那么我在建立邻接矩阵后添加一个得到某行某列数据的函数,将数组的下表及相关信息作为这个函数的参数来实现。如果当初的数据是封装好的,提供了一致的接口,那么就能够直接修改接口的代码而不需要对整个程序大动干戈。
封装之重要,在于其隐藏细节,提供接口。
隐藏细节,能够避免用户直接面对各种数据的内部存储方式,以及一些设计者使用的运算的中间值。提供接口类似于通过接口对对象发送消息,不关注其内部实现机制,用户只需要知道接口调用约定,而不需要知道内部的实现。这样,就能够将内部的实现细节隐藏,从而大大提高了程序的可扩展性。在程序的内部算法或存储结构发生变化的时候,只要其接口功能保持不变,就能够将要修改的内容限定在可控的范围之内。
一个典型的C语言库的结构是【结构+运行与该结构上的相关函数】。例如C语言的文件操作结构体FILE,如果要打开文件,一定要将该结构体的指针传递给fopen()函数,这样fopen才能够知道应该对哪个结构进行操作。而结构内部的实现细节是完全透明的,用户甚至可能将FILE结构体中的成员做出修改。而典型的C++库的特征是【对象+该对象所能发生的动作】。例如可以通过fstream类进行操作,那么函数调用就可以通过对象.动作来实现。相当于通过这样的函数向对象发送消息,对象相应地做出了相关的动作。它能够很好得实现数据隐藏与封装。
然而C++不是完全的面向对象语言,它只是一个混合产品,因为语言是为了实用,而不是为了追求某种纯粹的理想。Friend关键字就是为了解决部分的突发问题。
上面谈到的数据封装都只是开发者所使用的数据封装技术,是设计者对代码的控制。还有一类数据封装是为了真正的隐藏细节,比如说我开发了某一个库函数给大家使用,却不希望用户知道该库的一些核心存储结构与算法思想,就可以创建句柄类(Handleclass),将细节隐藏于二进制文件中。