多态与虚函数
- 多态的引入
- 多态与虚函数
- 多态
- 编译时多态
- 运行时多态
- 多态的原理
- 静态联编和动态联编
多态的引入
学过C++继承的话应该都知道在继承中存在一种菱形继承,假设存在一个类(person),其派生出两个子类,分别是student类和Migrant类,我们但是这两个子类共同同派生出一个MigStu的类,我们可以通过画图来理解一下,
我们画出他的继承图会发现其是一个菱形,我们再画出他的内存分布图
会发现存在两个person类,那么其中的公共成员变量就会发生冲突,比如学生是男性,打工人是女性,那么我们的在校打工人是男是女,这很显然发生了冲突。为了解决这个问题C++引入了virtual关键字,在person中我们加入virtual关键字之后,就会只在类中生成一个person对象,在学生和打工人的结构中原本存放person的地方会存在一个指针,指向我们的person类,也就解决了冗余的问题。但是菱形继承的底层实现极为复杂,所以现在很少设置这种菱形继承。而我们使用virtual继承基类,被称为虚基类,但是其并非基类是虚的,而是继承方式是虚的,而使用多态时,便使用的是虚函数,同样时virtual关键字。
多态与虚函数
多态
多态性是面向对象程序设计的关键技术之一。若程序设计语言不支持多态性,则不能称为面向对象语言。多态性是考虑在不同层次的类中,同名的成员函数之间的关系。函数的重载,运算符的重载,属于编译时的多态性。以类的虚成员函数作为基础运行时的多态性是面向对象程序设计的标志性特征。
多态分为编译时多态和运行时多态
编译时多态
编译时多态也就是函数重载,函数名相同,参数不同,其本质上是代码在编译时期对函数进行了名字粉碎技术,将函数名,参数,返回值做了一个粉碎,导致其在运行时编译器认为其名字不相同从而实现函数重载,
int Max(int a,int b) {return a>b?a:b;}
char Max(char a,char b) {return a>b?a:b;}
double Max(double a,double b) {return a>b?a:b;}
运行时多态
在一些类中会存在一些共同的特性,为此会设置一个基类将所有的共性存放到这个类中,并将这些共性设置成虚函数,在派生类中再一次实现具体的操作,这样就避免了代码的复用,并且使得代码更容易维护,也就是说以后有其他类的话直接加入派生类中即可。在调用时必须使用指针或者引用才可以将虚函数绑定到派生类的对象上重写虚函数。
#include <iostream>
using namespace std;
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
virtual int area()
{
cout << "Parent class area :" <<endl;
return 0;
}
};
class Rectangle: public Shape{
public:
Rectangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Rectangle class area :" <<endl;
return (width * height);
}
};
class Triangle: public Shape{
public:
Triangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Triangle class area :" <<endl;
return (width * height / 2);
}
};
// 程序的主函数
int main( )
{
Shape *shape;
Rectangle rec(10,7);
Triangle tri(10,5);
// 存储矩形的地址
shape = &rec;
// 调用矩形的求面积函数 area
shape->area();
// 存储三角形的地址
shape = &tri;
// 调用三角形的求面积函数 area
shape->area();
return 0;
}
使用多态时我们必须存在三个条件:
- 必须是公有继承
- 必须加入virtual
- 必须使用指针或者引用来调用
定义虚函数需要注意: - 派生类中定义虚函数必须与基类中的虚函数同名,同参数表,同返回类型。否则会被认为是同名覆盖,不具有多态性,但存在一个例外:基类中返回基类指针,派生类中返回派生类指针(协变)。
- 只有类的成员函数才能说明是虚函数,因为虚函数仅使用于有继承关系的类对象。有缘函数和全局函数都不能作为虚函数
- 构造函数和拷贝构造函数不能作为虚函数,拷贝函数和拷贝构造函数是设置虚表指针。
- 析构函数可以定义为虚函数,构造函数不能定义为虚函数,因为在调用构造函数时对象还没有完成实例化(虚表指针没有设置)。在基类中及其派生类中都动态分配内存空间时,必须把析构函数定义为虚函数,实现撤销对象的时的多态性。
- virtual之正在函数声明前加,不能再函数定义时加入,正确的定义必须不包括virtual,函数默认参数值也必须在声明时加入,定义时不能加入。
多态的原理
多态的原理就是虚函数表简称为虚表,虚表就是虚函数指针的集合,虚函数指针表本质上是一个存储虚函数指针的指针数组,这个数组的首元素之上存储RTTI(运行时类型识别信息的指针),从数组下标开始依次存储虚表地址,最后放了一个nullptr,虚表存放于只读数据段,其在编译时确定,并且一个类只存在一个虚表。
class object{
private: int value;
pubic:
object(int X = 0) :value(x) {}
virtual void add() { cout << "object: :add()" << end1; }
virtual void fun() { cout << "object: :fun()" << endl; }
virtual void print() const
{ cout << "object::printO" << end1; }
};
class Base: pub1ic object
private:
int num;
public:
Base(int x = 0) :object(x) ,num(x + 10) {}
virtual void add() { cout << "Base: :add()" << end1; }
virtual void fun() { cout << "Base: :fun()" << end1; }
virtual void show() { cout << "Base::show()" << end1; }
};
class Test : public Basve{
private:
int count;
public:
Test(int x = 0) :Base(x), count(x + 10) {}
virtual void add() { cout << "Test: :add()" << end1; }
virtual void print() const
{ cout << "Test: :print()" << end]; }
virtual void show() { cout << "Test: :show()" << end1; }
};
int main(){
object *op = nu11ptr;
objece obj;
Base base;
Test test;
op = &test;
op->print() ;
return 0;
}
我们观察上面代码,化出虚表内存分布图以及虚表图如下:
如图便是三个类的虚表,实在编译时就确定了的,例如obj派生出了Base类,所以Base的虚表就是将obj的虚表拷贝了一份然后进行同名覆盖,而在创建对象的时候,我们以创建Test对象为例,创建该对象首先创建Base,而创建Base需要创建obiect,而当类中有虚函数时类的大小就会多4(32位)字节,用于存放指向虚表的指针,创建Test时,指针首先指向object的虚表,然后指向Base的虚表,最后指向Test的虚表,根据其构建顺序。这个过程是在运行时进行的,虚表创建在编译时。
静态联编和动态联编
其实两者的差异就在于关联(函数实现和函数调用关联)的时期不一样,静态联编在编译时就确定了关联,而动态联编是在运行时确定的关联关系。
静态关联就是用直接用对象调用函数,动态联编就是用指针调用函数,注意虚表指针是否指向虚表。
C++中函数重载,函数模板都是静态联编,使用指针引用都是动态联编