编程语言和类型论中,多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口。 多态类型(英语:polymorphic type)可以将自身所支持的操作套用到其它类型的值上。
多态还可分为:
动态多态(dynamic polymorphism):通过类继承机制和虚函数机制生效于运行期。可以优雅地处理异质对象集合,只要其共同的基类定义了虚函数的接口。也被称为子类型多态(Subtype polymorphism)或包含多态(inclusion polymorphism)。在面向对象程序设计中,这被直接称为多态。
静态多态(static polymorphism):模板也允许将不同的特殊行为和单个泛化记号相关联,由于这种关联处理于编译期而非运行期,因此被称为“静态”。可以用来实现类型安全、运行高效的同质对象集合操作。C++STL不采用动态多态来实现就是个例子。
对于C++语言,带变量的宏和函数重载(function overload)机制也允许将不同的特殊行为和单个泛化记号相关联。然而,习惯上并不将这种函数多态(function polymorphism)、宏多态(macro polymorphism)展现出来的行为称为多态(或静态多态),否则就连C语言也具有宏多态了。谈及多态时,默认就是指动态多态,而静态多态则是指基于模板的多态。
最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。因为没有多态性,函数调用的地址将是一定的,而固定的地址将始终调用到同一个函数,这就无法实现一个接口,多种方法的目的了。
C++多态成立的三个条件:
- 存在继承
- 虚函数重写
- 父类指针指向子类对象
第二章 虚函数
继承关系作用下虚函数的手工调用 和 分析
- (1)一个类只有包含虚函数才会存在虚函数表,同属于一个类的对象共享虚函数表,但是有各自的vptr(虚函数表指针),当然所指向的地址(虚函数表首地址)相同。
- (2)父类中有虚函数就等于子类中有虚函数。话句话来说,父类中有虚函数表,则子类中肯定有虚函数表。因为你是继承父类的。
也有人认为,如果子类中把父类的虚函数的virtual去掉,是不是这些函数就不再是虚函数了?
只要在父类中是虚函数,那么子类中即便不写virtual,也依旧是虚函数。
但不管是父类还是子类,都只会有一个虚函数表,不能认为子类中有一个虚函数表+父类中有一个虚函数表,
得到一个结论:子类中有两个虚函数表。
子类中是否可能会有多个虚函数表呢?后续我们讲解这个事; - (3)如果子类中完全没有新的虚函数,则我们可以认为子类的虚函数表和父类的虚函数表内容相同。
但,仅仅是内容相同,这两个虚函数表在内存中处于不同位置,换句话来说,这是内容相同的两张表。
虚函数表中每一项,保存着一个虚函数的首地址,但如果子类的虚函数表某项和父类的虚函数表某项代表同一个函数(这表示子类没有覆盖父类的虚函数),
则该表项所执行的该函数的地址应该相同。 - (4)超出虚函数表部分内容不可知;
- 虚函数表跟着类走,虚函数指针跟着对象中
class Base
{
public:
virtual void f() { cout << "Base::f()" << endl; }
virtual void g() { cout << "Base::g()" << endl; }
virtual void h() { cout << "Base::h()" << endl; }
};
class Derive :public Base
{
virtual void g() { cout << "Derive::g()" << endl; }
/* void f() { cout << "Derive::f()" << endl; }
void g() { cout << "Derive::g()" << endl; }
void h() { cout << "Derive::h()" << endl; }*/
};
int main() {
// 继承关系作用下虚函数的手工调用
cout << sizeof(Base) << endl;
cout << sizeof(Derive) << endl;
Derive *d = new Derive(); //派生类指针。
Derive *d2 = new Derive(); //派生类指针。
long *pvptr = (long *)d; //指向对象的指针d转成了long *类型。
long *vptr = (long *)(*pvptr); //(*pvptr) 表示pvptr指向的对象,也就是Derive本身。Derive对象是4字节的,代表的是虚函数表指针地址。
long *pvptr2 = (long *)d2;
long *vptr2 = (long *)(*pvptr2);
for (int i = 0; i <= 4; i++) //循环5次;
{
printf("vptr[%d] = 0x:%p\n", i, vptr[i]);
}
typedef void(*Func)(void); //定义一个函数指针类型
Func f = (Func)vptr[0]; //f就是函数指针变量。 vptr[0]是指向第一个虚函数的。
Func g = (Func)vptr[1];
Func h = (Func)vptr[2];
//Func i = (Func)vptr[3];
//Func j = (Func)vptr[4];*/
f();
g();
h();
i();
return 0;
}
- 直接用子类对象给父类对象值,子类中的属于父类那部分内容会被编译器自动区分(切割)出来并拷贝给了父类对象。
所以Base base = derive;实际干了两个事情:
第一个事情:生成一个base对象
第二个事情:用derive来初始化base对象的值。
这里编译器给咱们做了一个选择,显然derive初始化base对象的时候,
derive的虚函数表指针值并没有覆盖base对象的虚函数表指针值,编译器没做这部分工作;
Base base = derive; // 直接用子类对象给父类对象值,子类中的属于父类那部分内容会被编译器自动区分(切割)出来并拷贝给了父类对象。
// 所以Base base = derive;实际干了两个事情:
// 第一个事情:生成一个base对象
// 第二个事情:用derive来初始化base对象的值。
// 这里编译器给咱们做了一个选择,显然derive初始化base对象的时候,
// derive的虚函数表指针值并没有覆盖base对象的虚函数表指针值,编译器帮我们做到了这点;
long *pvptrbase = (long *)(&base);
long *vptrbase = (long *)(*pvptrbase); //0x00b09b34 {project100.exe!void(* Base::`vftable'[4])()} {11538847}
Func fb1 = (Func)vptrbase[0]; //0x00b0119f {project100.exe!Base::f(void)}
Func fb2 = (Func)vptrbase[1]; //0x00b01177 {project100.exe!Base::g(void)}
Func fb3 = (Func)vptrbase[2]; //0x00b01325 {project100.exe!Base::h(void)}
Func fb4 = (Func)vptrbase[3]; //0x00000000
Func fb5 = (Func)vptrbase[4]; //0x65736142
- OO(面向对象) 和OB(基于对象)概念:
c++通过类的指针和引用来支持多态,这是一种程序设计风格,这就是我们常说的面向对象。object-oriented model;
OB(object-based),也叫ADT抽象数据模型【abstract datatype model】,不支持多态,执行速度更快,因为
因为 函数调用的解析不需要运行时决定(没有多态),而是在编译期间就解析完成,内存空间紧凑程度上更紧凑,因为没有虚函数指针和虚函数表这些概念了;
Base *pbase = new Derive();
Base &base2 = derive2;
但显然,OB的设计灵活性就差;
C++既支持面向对象程序设计(继承,多态)(OO),也支持基于对象(OB)程序设计。
面向对象的三大特性 —— 多态、继承、封装
面向对象主要有几个特性,封装、继承、多态。没有封装就不能继承,没有继承就没有运行时的多态。基于对象并不是单独的理论,而是面向对象的初级阶段,就是只有封装。只能是把属性、方法放进类中,实例化对象调用。学习面向对象要从基础知识入手,学会定义类、接口的定义、继承。然后要深入细致的研究现实事物,把现实事物或是需求文档中的名词抽象出来生成类或属性,如果是主语,多半还要根据整句的描述生成方法,定义类结构。
多继承虚函数表分析
-
说明
(1)一个对象,如果它的类有多个基类则有多个虚函数表指针(注意是两个虚函数表指针,而不是两个虚函数表);
//在多继承中,对应各个基类的vptr按继承顺序依次放置在类的内存空间中,且子类与第一个基类共用一个vptr(第二个基类有自己的vptr);(2)老师画图:适合vs2017。
(2.1)子类对象ins有里那个虚函数表指针,vptr1,vptr2
(2.2)类Derived有两个虚函数表,因为它继承自两个基类;
(2.3)子类和第一个基类公用一个vptr(因为vptr指向一个虚函数表,所以也可以说子类和第一个基类共用一个虚函数表vtbl)(这里共用指的是 子类把自己先建立的虚函数 放在了第一个vptr指向的内存),
因为我们注意到了类Derived的虚函数表1里边的5个函数,而g()正好是base1里边的函数。
(2.4)子类中的虚函数覆盖了父类中的同名虚函数。比如derived::f(),derived::i();
//子类
class Derived :public Base1, public Base2
{
public:
virtual void f() //覆盖父类1的虚函数
{
cout << "derived::f()" << endl;
}
virtual void i() //覆盖父类2的虚函数
{
cout << "derived::i()" << endl;
}
//如下三个我们自己的虚函数
virtual void mh()
{
cout << "derived::mh()" << endl;
}
virtual void mi()
{
cout << "derived::mi()" << endl;
}
virtual void mj()
{
cout << "derived::mj()" << endl;
}
};
辅助工具,vptr、 vtbl创建时机
第五节 辅助工具,vptr、vtbl创建时机
-
cl.exe:编译链接工具 —— 打印地址
cl /d1 reportSingleClassLayoutDerived project100.cpp(linux下)g++ -fdump-class-hierarchy -fsyntax-only 3_4.cpp
-
vptr(虚函数表指针)什么时候创建出来的?
vptr跟着对象走,所以对象什么时候创建出来,vptr就什么时候赋值的 —— 程序运行的时候(动态多态)
实际上,对于这种有虚函数的类,在编译的时候,编译器会往相关的构造函数中增加为vptr赋值的代码,这是在编译期间编译器为构造函数增加的。
这属于编译器默默为我们做的事,我们并不清楚。
当程序运行的时候,遇到创建对象的代码,执行对象的构造函数,那么这个构造函数里有 给对象的vptr(成员变量)赋值的语句,自然这个对象的vptr就被赋值了;
虚函数表是什么时候创建的?
实际上,虚函数表是编译器在编译期间(不是运行期间)就为每个类确定好了对应的虚函数表vtbl的内容。
然后也是在编译器期间在相应的类构造函数中添加给vptr赋值的代码,这样程序运行的时候,当运行到成成类对象的代码时,会调用类的构造函数,执行到类的构造函数中的 给vptr赋值的代码,这样这个类对象的vptr(虚函数表指针)就有值了;
单纯的类不纯时引发的虚函数调用问题
单纯的类:比较简单的类,尤其不包含 虚函数和虚基类。
-
如果类并不单纯,那么在构造函数中使用如上所示的memset或者拷贝构造函数中使用如上所示的memcpy方法,那么就会出现程序崩溃的情形;
那就是某些情况下,编译器会往类内部增加一些我们看不见 但真实存在的成员变量(隐藏成员变量),有了这种变量的类,就不单纯了;
同时,这种隐藏的成员变量的 增加(使用) 或者赋值的时机,往往都是在执行构造函数或者拷贝构造函数的函数体之前进行。
那么你如果使用memset,memcpy,很可能把编译器给隐藏变量的值你就给清空了,要么覆盖了;比如你类中增加了虚函数,系统默认往类对象中增加 虚函数表指针,这个虚函数表指针就是隐藏的成员变量。
virtual void virfunc()
{
cout << "虚函数virfunc()被执行" << endl;
}
void ptfunc()
{
cout << "普通函数ptfunc()被执行" << endl;
}
- 这个函数ptfunc()和virfunc()函数,是在编译的就确定好的;
静态联编 和 动态联编。
静态联编:我们编译的时候就能确定调用哪个函数。把调用语句和倍调用函数绑定到一起;
动态联编:是在程序运行时,根据时机情况,动态的把调用语句和被调用函数绑定到一起,动态联编一般旨有在多态和虚函数情况下才存在。
更明白:虚函数,多态,这种概念专门给指针或者引用用的; —— 父类指针 指向 子类(子类强转为父类)
多态:同样的语句再运行时有多种不同的表现形式(根据对象的实际类型来调用相应的函数,不会弱化成父类)
多态性:通过指向子类的父类指针或引用,可以访问子类中的同名覆盖的成员函数。
虚函数:根据指针指向的对象的类型,来执行不同类的同名覆盖函数,实现同一语句的不同行为。
虚函数关键字:virtual.
1. 被virtual声明的函数被重写后具有多态性。(通过指向子类的父类指针或引用,可以访问子类中的同名覆盖的成员函数)
2. 被virtual声明的函数叫虚函数。
3. 对可能要在继承时被重写的函数声明virtual 关键字。
多态的意义:
1. 多态是动态的意义,编译时无法预知实际调用,再运行时才展现具体的调用。
2. 重写函数必须用多态来实现。(避免无法访问子类的重写函数)
静态联编:在程序的编译器就决定具体的函数调用。函数重载
动态联编:在程序的执行期才决定具体的函数调用。虚拟函数的重写
函数覆盖:通过对象访问子类函数。通过作用域符,指针访问父类函数。
函数重写:通过对象访问子类函数。通过作用域符,指针访问父类函数。
函数多态:根据具体对象访问子类和父类函数。