从OOP的角度重看C++(四)——从程序中看OOP
多重继承(在想要不要把这个放到上一节),就是说一个类的直接父亲不止一个。在现实中的例子就是一个类同时对应好几个类的特征,但是又有自己的特征。在这里,我们举“实习生”的例子:我们有4个类——person,employee,student,empstudent。继承关系如下所示:
从这里我们可以有两种解释:继承了一次person和继承了两次person。二义性的解决办法:由程序员自己决定;
如果想继承一次,那么employee 和 student 在继承person的时候就将person看出虚拟函数;反之,则正常继承两次。
顺序问题:虚拟函数优先、按继承顺序。上图中,如果employee和student都在printon()中的printwidth进行了修改,那么employee在实例化的时候的值是多少?根据原则,继承的时候按照顺序,那么就应该是最后继承的值。比如: class A : public x1, virtual public x2, public x3, virtual public x4{ },那么继承的顺序就是2,4,1,3;如果修改的话,也是和3的一样。这里还涉及到一个问题,父类中如果有同名函数,那么使用的时候怎么区分呢?是通过作用域符号来看的,比如empstudent的一个实例 es1, es1.employee::printon()和es1.student::printon()表示从不同地方继承来的。
总结上述,格,引发二义性;防止滥用,用单重继承和组装关系来代替(Java已经不用多重继承了);
说完继承,我们下面将通过3个经典的程序设计来看OOP的设计思维:分别是从“单链表”看类的分离与关联;同时,从“单链表”的设计看 语言机制的的必要性(friend, private继承);从“标准用户界面操作”看抽象类以及函数指针的使用。
经典程序设计之 单链表。
单链表在我们学习数据结构的时候已经知道了;当时是使用结构体来实现的: 首先定义一个node,这个node里面有值和指向后继Node的指针;使用链表的时候只要用一个指向Node的指针就可以了。但是,传统面向过程的方法最大的弊端就是没有体现出封装性,人们可以随意更改每个节点的值。这里我们用OOP的想法。OOP在设计的时候将一个系统(整体,这里就是链表)拆分成若干个可以协同工作的组件,这几个组件共同完成链表的功能。对于链表来说,可以分成3个类: slink 表示单个节点; slist 表示链表类,里面使用成员函数Insert, append和clear 来操作整个链表;slist_interator 作为循环控制机构的载体(原谅我C++基础太差,刚刚才知道啥是迭代器:是一种检查容器内元素并且遍历元素的数据类型。在C++中,定义一个容器,通常会跟着一个该种容器的迭代器,来实现对容器中元素的操作,就像是指针的封装)。
// 节点的定义
typedef void* ent;
class slink{
friend class slist;
friend class slist_iterator;
//slink 类根本没有public成员,用
//friend规定了外部访问范围
slink* next;
ent e;
slink(ent e, slink* p)
{
e=a;
next=p;
}
};
class slist{
friend class slist_iterator;
slink* last;
public:
int insert(ent a);
int append(ent a);
void clear();
slist(){last=0;}
slist(ent a)
{
last=new slink(a,0);
last->next=last;
}
~slist(){clear()};
};
class slist_iterator
{
slink *ce;
slist *cs;
public:
slist_iterator(slist& s)
{
cs=&s;
ce=0;
}
}
他们之间的关系如下所示:
从这个例子可以看出:
尽可能的把类设计单纯,这样将有利于重用;可以看出以上3个类的功能都非常非常简单,简单到,一个节点就是一个节点,连对他的操作也米有!
不同性质/不同层次的数据和操作,要分离到不同的类中,再设计相应的关联结构和协同操作,在这里,这三个类就体现了3种层次;
在单链表着简单的例子中,我们可以看出,语言提供的机制支持肯定是有用 的,比如这个例子就把friend用的很好:slink没有操作,没有public成员,就是因为他的friend可以用啊~~。满足了以上两点,但是我们也会觉得不好用,因为太过于分离,整体感不强。想想我们在初次接受数据结构的时候,那个时候的链表是什么?我们只是定义了一个节点,形成一个“链”也只是使用了他的指针而已!虽然没有封装性,但是大多数时候还是很方便滴~
如果想要很类似与传统的链表,那么我们可以定义一个“有类型操控和操作投影的外壳”:
struct nlist: private slist{ //<span style="color:#ff0000;">struct 是能见度全部为public的类,这里的private继承是让slist的操作全部不可见~终于也见到有用private继承的了~</span>
void insert(name * a)
{
slist:: insert(a);
}
void append(name * a)
{
slist::append(a);
}
nlist(){}
nlist(name * a): slist(a){}
};
这样,我们就可以用着比较顺手了。
接下来,我们将要看看程序设计的第二个例子:设计用户操作界面( 利用统一接口的方法指针):
// 界面类型(必须是Std_interface的子类)映射表
<span style="white-space:pre"> </span>map<string, Std_interface *> variable;
// 界面操作(必须符合统一的接口规范)映射表
<span style="white-space:pre"> </span>map<string, Pstd_mem> operation; //Pstd_men s; s=& Std_interface::suspend;
/* 将界面操作命令串中的类别和对应的对象指针、界面
操作命令串中的操作和对应的方法指针分别填入上面
两张表中。这一部分是会扩充的,但很单纯。
*/
// 解释执行特定界面类型的特定操作,这里是稳定的。
<span style="white-space:pre"> </span>void call_member(string var, string oper)
<span style="white-space:pre"> </span>{
<span style="white-space:pre"> </span> (variable[var]->*operation[oper])();
<span style="white-space:pre"> </span>}
这个例子说明了两个问题:不同的界面;不同的操作。定义的variable和operation分别说明了这两个的区别。看看我们的操作变的懂么简单!!!!本来界面和操作都再变,但是,只要统一他们的接口,在使用的时候就可以快速简单使用! 更重要的:
我个人认为他很棒的另一个原因是:如果每个页面都设计成一个类,里面的操作都不太一样,但是都不太一样,就好比是页面的个数和操作的个数做了个笛卡尔积的感觉~而现在的设计就好比是把一个大表拆成了两个小表!冗余减少,并且要添加新的页面和操作也非常容易!!如果要扩充系统,比如增加新的界面或者新的操作,那么,对系统的扩充从以往的扩充操作据变成了扩充类型!!(扩充操作的问题:扩充操作所带的数据结构的制约,操作代码的大量修改等),前提是要扩充的类型和已有的有比较密切的联系,这是很提倡用继承性机制的做法!
关于面向对象的讨论:对象,类(这可以看成是前几节的概括)
再次讨论一下什么是对象。对象有: state, behaviar and identiy。在一个完全面向对象的系统中,一个对象的标识(object Identity)和这个对象永久的结合在一起!他是唯一的,不管这个对象的状态和结构发生了怎样的变化!identify有两个相互正交的属性:表示能力和时态性(目前没有理解,但是仍然把图截下来:)
类可以有两种理解:一是也对象集合中对象特征的抽象(对象工厂,对象模板),还有是这个集合的整体表示(对象仓库)。类是运行时的概念,但不是用于检查程序是否有错的,而是用于产生和操纵对象。