目录
一.多态
1.多态的概念:多态性是面向对象程序设计的关键技术之一。若程序设计语言不支持多态性,不能称为面向对象的语言。
作用:利用多态性技术,可以调用相同函数名的函数,实现完全不同的功能。
C++中有三种:编译时多态(静多态),运行时多态(动多态)以及宏多态
- 编译时多态:函数重载与运算符重载,在函数或运算符调用前就知道具体调用哪个同名函数或运算符,即编译阶段确定函数的调用。
- 运行时多态:必须在程序执行过程中,根据执行的具体情况来动态地确定调用哪个同名函数。运行时多态是通过类继承关系和虚函数来实现的,目的也是建立一种通用的程序。
- 宏多态:预编译阶段确定函数的调用,其重要性几乎可以忽略,我在这里不在进行阐述。
二.虚函数
虚函数是一个类的成员函数,定义格式如下:
virtual 返回类型 函数名(参数表);
关键字virtual指明该成员函数为虚函数。virtual仅用于类定义中,如虚函数在类外定义,不可加virtual。
例如:
#include<iostream>
using namespace std;
class Base
{
public:
Base() :x(0)
{
}
~Base()
{
}
public:
void show() //比较是否有virtual关键字,子类对象调用show()方法的结果有啥不同?
{
cout << "这是基类Base的show方法" << endl;
}
private:
int x;
};
class D :public Base
{
public:
D() :y(0)
{
}
~D()
{
}
public:
void show()
{
cout << "这是子类D的show方法" << endl;
}
private:
int y;
};
int main()
{
D d;
Base* p = &d;
p->show();
return 0;
}
#include<iostream>
using namespace std;
class Base
{
public:
Base() :x(0)
{
}
~Base()
{
}
public:
virtual void show() //比较是否有virtual关键字,子类对象调用show()方法的结果有啥不同?
{
cout << "这是基类Base的show方法" << endl;
}
private:
int x;
};
class D :public Base
{
public:
D() :y(0)
{
}
~D()
{
}
public:
void show()
{
cout << "这是子类D的show方法" << endl;
}
private:
int y;
};
int main()
{
D d;
Base* p = &d;
p->show();
return 0;
}
我们发现仅仅是在基类的show()函数前加了一个virtual 关键字,就导致了执行结果大不相同,这是由于继承中虚函数引起的动多态导致的。具体原因我们后文会讲到,在这之前我们先来看一个例子,来了解虚函数的另一个特性,那就是当某一个类的一个类成员函数被定义为虚函数,则由该类派生出来的所有派生类中,与该函数同名,同参,同返回值类型的函数也保持虚函数的特征。
例如:
#include<iostream>
using namespace std;
class Base
{
public:
Base() :x(0)
{
}
~Base()
{
}
public:
virtual void show()
{
cout << "这是基类Base的show方法" << endl;
}
private:
int x;
};
class D :public Base
{
public:
D() :y(0)
{
}
~D()
{
}
public:
void show()
{
cout << "这是子类D的show方法" << endl;
}
private:
int y;
};
class E :public D
{
public:
void show()
{
cout << "这是子类E的show方法" << endl;
}
};
int main()
{
D d;
Base* pb = &d;
pb->show();
E e;
D* pd = &e;
pd->show();
return 0;
}
我们发现,虽然只有基类的show()函数被定义为虚函数,但是它的儿子类和孙子类中的show()函数都保持虚函数的特性,即当某一个类的一个类成员函数被定义为虚函数,则由该类派生出来的所有派生类中,与该函数同名,同参,同返回值类型的函数也保持虚函数的特征。
除此之外,我们定义虚函数时还需要特别注意以下几点:
- 派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是重载,而不是虚函数。如基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外,也可以构成多态。
- 只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。
- 静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
- 一个类对象的静态和动态类型是相同的,实现动态多态性时,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现动态的多态性。这既是我们前文所讲的继承的特性——赋值兼容规则。详见链接:https://blog.csdn.net/ThinPikachu/article/details/104298108
- 内联函数每个对象一个拷贝,无映射关系,不能作为虚函数(内联函数在函数的调用点直接展开,因此内联函数不可以取地址 ,不能成为虚函数。)
- 析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例化。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。
三.动多态的发生过程
为了使大家对虚函数以及动多态是如何“工作的”,我们用下面例子来讲解:
#include<iostream>
using namespace std;
class Base
{
public:
Base() :x(0)
{
}
~Base()
{
}
virtual void show()
{
cout << "这是基类的show方法" << endl;
}
private:
int x;
};
class Derive:public Base
{
public:
Derive() :y(0)
{
}
~Derive()
{
}
void show()
{
cout << "这是子类的show方法" << endl;
}
private:
int y;
};
int main()
{
Derive d;
Base b;
cout << "基类:" << sizeof(b) << endl;
cout << "派生类:" << sizeof(d) << endl;
return 0;
}
运行结果:
我们发现基类对象和派生类对象的大小分别是8和12,但是我们发现基类的成员变量只有一个x,派生类的成员变量除了本身的y还有继承与基类的x,那么他们的大小不应该是4和8吗?那么接着我们就来看看基类对象和派生类对象的内存布局吧。
在VS界面,我们依次点击工具->命令行->开发者命令提示 ,接着会弹出这样一个窗口
我们在这里分别输入命令:
cl main.cpp /d1reportSingleClassLayoutBase
cl main.cpp /d1reportSingleClassLayoutDerive
这里的main.cpp代表你当前源文件的名称,末尾的Base和Derive分别代表你要查看内存布局的类的类名,即可归纳成下面命令:
cl xxxxx /d1reportSingleClassLayoutxxxxx
接着机会出现我们要查看的基类对象和派生类对象的内存布局
我们发现基类对象和派生类对象的内存布局当中,除了有我们上文提到过的成员变量x和y,还有一个vfptr,这个vfptr就是我们下文所要介绍的虚函数指针。
四.虚函指针
虚函数表指针_ _vfptr(virtual fun point table ),该指针指向虚表,且一个类中无论虚函数有多少个,虚函数表指针vfptr只有一个,在32位系统中它的大小是4个字节。当大家看到这里时,应该就了解了为什么基类对象和派生类对象的大小分别为8和12了,因为它们除了包含类中本身的数据成员外,还包含一个vfptr指针。而vfptr又指向虚函数表(Virtual Function Table)。
生成虚函数指针的条件:只要类中有虚函数,那么由该类派生出来的对象的内存布局中就会含有虚函数指针,且该类中无论虚函数有多少个,虚函数指针vfptr只有一个
五.虚函数表
虚函数表存放在内存中的只读数据段,由虚函数指针指向。虚函数表分为三个部分,第一个部分我们称之为RTTI(运行时信息类型),第二部分存放的是指向该虚函数表的虚函数指针相当于当前作用域的偏移量,第三部分存放的就是虚函数的入口地址。
也许大家会对这个RTTI(运行时信息类型)有所疑问,其实RTTI(运行时信息类型)存放的是当前虚函数表是属于哪一个类的信息,即虚函数指针可以通过RTTI(运行时信息类型)得知当前访问的虚函数表是属于那一个类的。
有了这些了解后,我们现在来看一下我们上面例子中Base类和Derive类的虚函数表在内存中的布局
我们看到Base类的虚函数表的第一部分中(即RTTI运行时信息类型)存放的是&Base_meta,即表示当前虚函数表为Base类的虚函数表,而第二部分虚函数指针的偏移量为0,第三部分存放着Base类中的虚函数show的入口地址。
我们看到Derive类的虚函数表的第一部分中(即RTTI运行时信息类型)存放的是&Derive_meta,即表示当前虚函数表为Derive类的虚函数表,而第二部分虚函数指针的偏移量为0,第三部分存放着Derive类中的虚函数show的入口地址。
类和虚函数表之间的关系:一个类只有一张虚函数表
对象和虚函数表之间的关系:同一个类的多个对象共享一张虚函数表
有了这些知识后,我们在回过头来看一下动多态的发生过程,我们给出以下例子:
#include<iostream>
using namespace std;
class Base
{
public:
Base() :x(0){}
~Base(){}
virtual void show()
{
cout << "这是基类Base的show方法" << endl;
}
virtual void print()
{
cout << "这是基类Base的show方法" << endl;
}
private:
int x;
};
class Derive :public Base
{
public:
Derive() :y(0){}
~Derive(){}
void show()
{
cout << "这是子类Derive的show方法" << endl;
}
private:
int y;
};
int main()
{
Derive d;
Base* p = &d;
p->show();
return 0;
}
我们给出运行结果:
我们发现这里p调用的show()方式是Derive类的show()方法,即发生了动多态。
为了让大家对动多态的发生过程了解的更加深刻,我们对上述代码一步一步的分析:
首先是
Derive d;
构造了一个派生类对象d,因为Derive是继承于Base类的,而Base类中的show函数是一个虚函数,所以Derive类中的show函数也变为了虚函数,即当某一个类的一个类成员函数被定义为虚函数,则由该类派生出来的所有派生类中,与该函数同名,同参,同返回值的函数始终保持虚函数的特征。所以在生成派生类对象d时,d继承了基类中除构造函数和析构函数以外的所有成员,那么也就包括基类的虚函数指针和虚函数表。所以一开始派生类对象d的内存布局为下图:
(vfptr的优先级高于其他数据成员,其中红色代表派生类的数据成员)
接着我们来分析此时vfptr所指向的虚函数表,d继承了基类中除构造函数和析构函数以外的所有成员,那么也就包括基类虚函数表,
但我们知道一个类只有一张虚函数表,所以接下来就发生了虚函数表的合并,简称虚表合并
最后将两个虚函数指针也进行合并,虚函数指针的合并是由外向内进行的
最终我们得到的d的内存布局如下图
接着
Base* p = &d;
构造了一个Base类型的指针p,而p指向派生类对象d内存中基类的部分,即
最后
p->show();
通过p来调用show()函数,因为show是虚函数,所以其实是由p指向的派生类对象d内存中基类那部分中的vfptr通过查询虚函数表中show函数的入口地址,来调动show()函数,又因为原基类中的show函数的入口地址被派生类中的show函数的入口地址替换了,所以此时调动的是派生类中的show()函数。
至此动多态结束。