OOD的调用规范性(多态)

多态的作用,更多是体现为调用规范性。
重载,overload,严格意义上讲,并不是多态,因为在编译时,就已经找到了链接时需要采用的代码段。
覆盖,override,才是多态的实现方法。

为了实现多态,就需要使用到虚表和函数指针。
将本该在链接阶段就确定的代码段入口,
后移到运行时,创建对象,为对象分配内存时,才填充到虚表中的函数指针中。
在SOD设计中,这个填充的操作,是手工完成的,在init函数中手工填充函数指针。
在OOD设计,这个填充的操作,是编译器完成的,编译器为类额外生成了static的表,就是虚表,并填充了虚表中的各个函数指针。编译器为对象额外分配了内存,即虚表的指针,用来指向虚表的首地址。编译器为构建函数追加了一段代码,就是填充对象的虚表指针。
OOD的编译器,将人从init的填充工作中解放出来。

编译器将代码中出现的,对象调用虚函数的语句,编译为两个阶段的操作,
第一阶段,根据对象的虚表指针,找到对象所属的类的虚表,从虚表中找到函数指针。
第二阶段,根据函数指针,找到代码段入口地址,然后完成函数调用。

通过继承,完成对函数的具体扩展,在子类中完成对函数的覆盖。
通过向上抽象,将子类对象的句柄赋值给一个父类句柄,使得子类对象被视为一个父类的对象,这是一个相对更抽象的对象。
此时,如果由父类句柄来发起对虚函数的调用,按照上述的调用机制,将会由虚表中填充的,实际上是子类的代码段入口地址的函数指针,完成函数调用。
这就实现了向下具象。这就是多态。

这就是调用规范性的体现。
对于标准接口虚函数,
如果是用子类句柄来调用虚函数,从代码上就可以直接理解到,调用的执行代码段,必然是本类的函数。
如果是用父类句柄来调用虚函数,从代码上,并不能确定,调用的执行代码段,是本类中定义的函数,还是被其他的子类所定义的覆盖函数。

所以,使用父类句柄调用虚函数,其设计目的,只有一个,就是调用规范接口,执行流程操作,这个流程操作,是一个抽象概念,具体的操作结果,取决于具体的操作函数中的具体操作步骤。

虚函数,就是为了后续被覆盖的。
当我们在代码中调用了虚函数时,就已经意识到,这是抽象调用,从代码上,并不能确切知道,具体会有什么样的操作步骤。这个虚函数,会被子类所继承,然后被子类覆盖。
具体的操作步骤,是在子类中的覆盖函数中写的。

++++++++++++++++++++++++++++++++++++++++
补充:
C++的virtual关键字

基类声明的虚函数,在派生类中也是虚函数,即使不再使用virtual关键字。为了提高程序的可读性,建议后代中虚函数都加上virtual关键字。
如何理解?
当父类中声明了虚函数时,也就在虚表中分配了虚函数指针的占位。当子类继承父类时,自然也继承了虚表结构体,自然也就在子类的虚表中,也有了这个虚函数指针的占位。

在你设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的。从设计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现。通过这样的方法,就可以将对象的行为抽象化。
如果你发现基类提供了虚函数,那么你最好override它。

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” 。包含纯虚函数的类称为抽象类。

		virtual void shout()=0;          //纯虚函数
		virtual void toShout(){};        //虚函数

虚表中分配了纯虚函数指针的占位,但是没有具体代码段,所以可以理解为NULL。这时,如果要例化一个抽象类的对象时,链接器会将NULL填充到虚函数指针中,而NULL指针是违例的。
所以,抽象类不能构建抽象对象。

引入纯虚函数的原因:
在很多情况下,抽象类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。

不要在构造函数和析构函数里调用虚函数,即使没有语法错误,运行也不会如你所愿,就算有时会成功(不同编译器),到最后也会让人困惑不已。这种调用根本不是起到虚函数的作用,所以为了代码规范,可以说成是不可以调用。

构造函数不可以是虚函数,该问题编译器能够查看出来,是语法错误。
当一个类拥有虚函数的时候,析构函数应当也被声明为virtual。因为当使用多态时,父类的析构函数没有声明为virtual而调用delete时,会直接调用父类析构函数,子类没有被调用。为了程序的正确运行,防止只析构基类而不析构派生类的状况发生,应当记住这个规范。

virtual关键字,除了可以声明虚函数,在虚表中分配一个虚函数指针的占位,也可以用来声明虚继承。
虚继承由virtual关键字作为前导,提供了另一种继承方式。

实继承时,父类对象的结构体对象,以内嵌的形式,将父类的结构体对象内嵌在子类对象结构体中,作为一个成员存在。编译器在编译时,会自动解释层次关系,找到成员。
如果存在多继承,而多个父类,又都继承了同一个祖类时,此时使用实继承,就会同时内嵌了多个祖类结构体对象,当编译器试图自动解释层次关系时,就会失败,因为多条层次路径,都能定位到同名的成员。
这种情况下,就需要使用虚继承。

类似于虚函数在虚表中预留虚函数指针来占位,虚继承也是通过预留虚父指针来占位的。
不同于实继承的内嵌方式,虚继承时,使用的是关联方式。
内嵌方式下,在构建子类结构体对象时,会计算父类结构体对象的尺寸,并分配对应大小的内存,然后再填充初始化父类的成员。
关联方式下,在构建子类结构体对象时,会预留一个父类结构体对象的指针,分配指针所需的内存,并暂时填充初始化为NULL。这是第一阶段的构建,在后续阶段的构建中,会构建父类结构体对象,并修改子类中的虚父指针,指向构建的父类结构体对象,如果有多个虚父指针类型相同,则修改它们,指向同一个对象,从而完成关联。
这样,即使存在多继承,当编译器试图自动解释层次关系时,多条层次路径,也都是定位到同一个成员。

猜你喜欢

转载自blog.csdn.net/weixin_42418557/article/details/120677012
OOD