从OOP的角度重看C++(二)——OOP的更多语言机制

来源:互联网 发布:雅安数据恢复 编辑:程序博客网 时间:2024/05/01 15:47

从OOP的角度重看C++(二)——OOP的更多语言机制

        在上一篇《从OOP的角度重看C++(一)》提到了oop的发展起因、必须要有的机制(类、对象,消息传递,方法和继承性)。而这必须要有的机制就好比是一个大的工程中的基础设施,而要将整个工程做好做美,仅有基础设施是远远不够的,还有很多在实际应用中也会经常使用的一些机制来使得我们的工程更圆满,这些机制之所以重要,是因为他们必须要有语言层面的支持,换句话说,不像我们经常提到的设计模式、算法等这种需要我们聪明的大脑来做的,而是需要语言本身的支持,要不然是做不到的。不过,语言的支持越强大,就越灵活,想要使用的好就越需要技巧,越难,这也是为什么很多人觉得Java更好用的原因之一吧(也是为什么很多大神喜欢C++的原因)。

        这里我们提到的第一个机制就是——重置(overriding)。这个机制是由继承性来的。子类在继承了父类以后,发现了某些不适合的方法,于是就提出了问题: 能不能有选择的继承,同时又不改变父类的基本构造?(注意,这个不改变父类的基本构造是很重要的:因为一旦子类有和父类不一样的地方,如果就去改变父类的话,那么这个父类被继承的越多,他被改变的就越多,逐渐面目全非,那么继承本身的意义就不存在了。同时,这也给我们提供了一个原则:当我们对已有的或者别人写的程序不满意的时候,应该先继承下来,在此基础上写合适自己要求的程序,而不是考虑怎么修改人家的东西

        C++中的重置机制就是:虚拟函数(virtual functions)。使用方法是: 在父类中允许被子类重置的方法前加上virtual 关键字。其含义就是允许子类在使用的时候重置他。其优点是:

保持了OOP的自然性(子类和父类的接口一致,包括方法名和参数);

即使在父类定义的方法中对子类的实例进行操作,如果需要使用被子类重置过的方法,那么也会与子类中重新定义的方法体绑定。(这个其实还是运行时的动态绑定,大家还记得吗吐舌头

重置机制是很灵活的,想用哪里用哪里~

        在C++程序中,只要某个类含有虚拟函数,那么在编译的时候,编译程序就会为他分配一个“虚拟函数跳转表(virtual functions jump tables)。这个表由若干个虚拟函数体入口地址组成的一个线性表(可以理解为函数指针数组吗?),注意,是这些方法的入口地址,不是方法的代码(还有个vptr协同工作),子类的vtbl的前半部分由父类的得出,后半部分对应着自己新定义的虚拟函数。当定义对象的时候,每个对象在内存中都有自己的位置,并且他们都有个vptr,指向vtbl(因为这个表只有一个),当不同的对象调用方法是,根据他的类型,vptr指向不同的函数,实现了动态绑定。

        在上一篇中,我们从继承中得到了子类型(subtype)的概念: A Value of  a subtype can be used everywhere a value of the supertype can be expected.这就说明,子类型是父类的一个子集,即不具有重置机制。但是有了overriding这个概念, 就说明子类是可以不一样的。区别他俩简单的说就是 is a 和 is like 的关系。

      接下来,讲第二个重要的概念——多态。刚才的重置机制可以说是纵向的由父到子,而多态则可以理解成横向的(所有人都用)。 多态包含两种:一个是方法名的过载,一个是操作符的过载。

      第一种多态的起因是因为:需要对一个类的实例化采用一种以上的语义处理。根据constructor的命名规则,只能用类名,就决定了他必须要支持构造函数的过载多态;相似的,其他的函数也应该有。(哈哈,多么公平的事情啊)。

      编译器是怎么区分这些在我们看来名字一样的函数的呢?答案就是——将参数类型按既定规则依次转换成符号名。 这样,因为参数不同,函数其实是不一样的,但是名字一样又方便了语义上的理解。

        有了方法名的过载,操作符的过载变的比较好理解。因为,操作符也是一种函数。但是,并不是每一种操作符都可以过载,在C++中: 作用域操作符::,取方法操作符.,还有sizeof,typeid,唯一的一个三元操作符?:以及.*都是不允许被过载的(因为不好控制),同时也不允许组合定义操作符,如**。

        将操作符看做是:函数名为operator加上操作符符号的函数。使用的时候,既可以是按照函数的使用方法、也可直接在表达式中引用。定义函数的方式有3种:成员函数、friend函数以及普通函数。作为成员函数的时候,默认对this的各项进行操作;而在一个类中定义friend函数,则说明这个类对这个函数是不封装的(类似的,在一个类中声明friend 类,那么对这个类也是不封装的,意思就是:我把你当成朋友了,我的一切你都可以看了,但是至于你是怎么想的就不从得知了),这样会局部劈坏封装性,语言本身无法防止滥用这种机制。 而采用成员函数和friend函数的本质区别是: 成员函数可以被继承,而其他的函数,只是函数。

        如果我们过载一个二元操作符,也就是会所他的操作数有两个;那么如果我们把它写成成员函数,他的参数应该只能有一个,两外一个是默认的this对象;如果是非成员函数,就有两个参数。类似的,对于一元操作符的过载,如果是成员函数,那么就没有参数,其他函数就有一个参数;另外要注意,一元操作符有前缀和后缀之分,当为后缀一元操作符的时候,需要一个int 参数来作为标志,如: aa,operator@@(int)或者operator@@(aa,int),用以区分和前缀的区别。

        比如上图中我们标注差号的都是错的,因为:第一个&是二元操作符,作为成员函数的话只能有一个参数,他那样写就有3个参数了;第二个也是,/是二元操作符,这样只有一个参数;第三个是普通函数,-应该有2个参数;第4个多了一个参数;最后一个也是少了一个参数。 此外,注意operator--,int标志标志这个是后增操作。

        此外,对于C++,如果定义=,[],(),->,那么这个函数一定得是非静态的成员函数,并且保证第一操作数是左值,这两个条件其实说的是一个意思,即第一操作数都必须有存储地址!

        这次的心得:

 从横向和纵向来看,无论是重置还是过载,核心是类!因为没有类,重置就变成了过载,过载就会失控,这也是为什么不允许程序员自己设计过载的原因。

  此外,从语言设计重置机制的策略(vtbl 和vptr)来看,把程序中需要变化的部分用数据表示!是很棒滴~ 我们在做设计的时候,如果也能尽可能的用这种好的思想,应该是很好的!

语言的基本操作类型——操作符,最终都是用函数来实现的~这也是为什么过载为什么对函数和操作符都可以的原因!

0 0