多态性(polymorphism)是考虑不同层次的类中,以及在同一个类中,同名的成员函数之间的关系问题。函数的重载,运算符的重载,属于编译时的多态性(早期绑定:编译期就确定了调用关系)。以虚函数为基础的运行时的多态性(晚期绑定:程序在运行过程中才能确定调用关系 是面向对象程序设计的标志性特征,体现了类推和比喻的思想方法。
静态多态:函数重载,模板(函数模板和类模板)
编译器编译时就确定了调用关系,就叫做早期绑定,也叫作动态绑定。
编译阶段
,编译器需给这个类的类型产生一个唯一vftable
虚函数表,虚函数表中主要存储的内容就是RTTI指针(类型字符串)和虚函数的地址。.rodata
区(只读)vfptr
指向的都是同一张虚函数表。虚函数是一个类的成员函数,定义格式如下:
virtual 返回类型 函数名(参数列表);
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是 用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。 这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。
它是通过类继承关系public和虚函数来实现,目的是建立一种通用的程序。
继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数)。基类指针指向哪个派生类,就会调用哪个派生类对象的覆盖方法,称为多态。
多态的底层是通过动态绑定实现的,基类指针指向谁就会访问谁的vfptr
,进而访问其vftable
,由此访问的当然是对应的派生类对象的方法了。
注意
: 必须通过引用或者指针调动虚函数时,才能够达到运行时的多态的效果。
总结:运行时多态需要满足以下三个要求:
示例:
class Animal
{
private:
string name;
public:
Animal(const string & na):name(na){}
public:
virtual void eat(){}
virtual void walk(){}
virtual void tail(){}
virtual void PrintInfo(){}
string & Get_name(){return name;}
const string & Get_name() const {return name;}
};
class Dog:public Animal
{
private:
string owner;
public:
Dog(const string & ow,const string &na):Animal(na),owner(ow){}
virtual void eat(){cout<<"Dog eat:bone"<<endl;}
virtual void walk(){cout<<"Dog walk:run"<<endl;}
virtual void tail(){cout<<"Dog tail:Wang wang"<<endl;}
virtual void PrintInfo()
{
cout<<"Dog owner"<<owner<<endl;
cout<<"Dog name"<<Get_name()<<endl;
}
};
class Cat:public Animal
{
private:
string owner;
public:
Cat(const string & ow,const string &na):Animal(na),owner(ow){}
virtual void eat(){cout<<"Cat eat:fish"<<endl;}
virtual void walk(){cout<<"Dog walk:silent"<<endl;}
virtual void tail(){cout<<"Dog tail:Miao miao"<<endl;}
virtual void PrintInfo()
{
cout<<"Cat owner"<<owner<<endl;
cout<<"Cat name"<<Get_name()<<endl;
}
};
void fun(Animal & animal)
{
animal.eat();//animal->dog->dog::vftable::&dog::bark
animal.walk();
animal.tail();
animal.PrintInfo();
}
int main()
{
Dog dog("mk","erha");
Cat cat("bw","persian");
fun(dog);
fun(cat);
return 0;
}
注意:每一个存在虚函数的类实例化出的对象都存在虚表指针,但是虚表只有一份,
虚函数的实现是因为存在一张虚函数表,这一过程中发生了同名覆盖,有虚函数的类在编译过程中才会产生虚表。
覆盖具体指的就是虚函数表中虚函数地址的覆盖。
下面我们来辨析以下通过指针或引用调动和
对象调动虚函数的编联方案的异同:
比如下面的代码:注意,关注点在于:成员方法是普通函数还是虚函数
再来看下面的示例,更加清楚地了解同名覆盖以及虚表指针的转换过程(构造函数发挥作用):
其实,在类中存在虚函数时,构造函数除了构建对象,初始化对象的数据成员以外,还要将虚表地址传递给虚表指针.
在构造函数中也不能完成如下操作:
memset(this,0,sizeof(Base));
这样很危险,因为这样会将对象内的所有成员初始化成0,虚表指针也会被修改。
所以,我们向前面所说的一样,不能将构造函数定义成虚函数,
原因是: 虚函数的存在必须依赖以下两个前提:
调动虚函数时,我们需要查表,构造函数是虚表建立的前提,若构造函数为虚函数,那么此时我们调动虚化的构造函数,虚表还没有建立,所以构造函数,拷贝构造函数,移动构造函数都不能被建立成为虚函数。
注意
:问题:是不是虚函数的调用就一定是动态绑定? 肯定不是的,因为在构造函数中,调用的任何函数都是静态绑定的,调用虚函数,也不会发生静态绑定。
因此,虚函数通过指针或者引用调用,才会发生动态绑定。
继续探究:下面程序的执行结果为什么是:Base::print::10
前面我们提到了编联时需要确定的属性,也要注意重写虚方法时只需要保证三同(同函数名,同参数类型,同返回类型),并没有要求函数的默认值必须相同。
那么为什么可以通过op访问Base的私有虚函数呢?
因为,该程序的编译是可以通过的,在运行时编译器不会考虑访问属性的影响。
static静态成员方法不能加上virtual关键字,因为static修饰的方法并不依赖于对象。
先看下面的代码:
class Object
{
private:
int value;
public:
Object(int x = 0):value(x){cout<<"Create Object:"<<this<<endl;}
virtual void add(){cout<<"Object add"<<endl;}
virtual ~Object(){cout<<"Destroy Object"<<this<<endl;}
};
class Base:public Object
{
private:
int num;
public:
Base(int x = 0):num(x){cout<<"Create Base:"<<this<<endl;}
virtual void add(){cout<<"Base add"<<endl;}
~Base(){cout<<"Destroy Base"<<this<<endl;}
};
int main()
{
Object *op = new Base(10);
op->add();
delete op;
op = NULL;
return 0;
}
我们可以看到,其实它并没有调用Base对象的析构函数,仅仅调用了父类对象的虚构函数,究其原因是因为:二者的析构函数非虚函数(普通函数),所以编译器在解析时发现,op是一个Object的指针,此时就只会调动Object的析构函数。
为了做到运行时多态的释放,就必须让析构函数变成虚函数
因为,一旦父类的析构函数定义为虚函数,那么所有派生类的析构函数也都会变成虚函数,发生同名覆盖,因此无需在派生类中声明,在调动delete,传入父类指针时,就会将父类对象和子类对象都析构。
修改代码后执行结果如下:
那么为什么我们要将析构函数定义成虚函数呢?
就是因为在上述的应用场景下,当父对象的指针(引用)指向堆区new出来的派生类对象时,在释放父指针时,我们采用连级释放的机制,从而调动子类的析构,否则会导致派生类的析构函数无法调用。
注意
:在没有虚函数的类中,我们没必要将析构函数定义为虚函数,因为这个类并不会参与到继承关系中,也就不需要连级释放。
reset重置虚表,以上述代码为例,在析构过程中先析构子对象,然后此时整个Base对象就剩下隐藏基对象的资源和空间,然后重置虚表指针指向隐藏基对象的首地址,再对该对象进行析构。
纯虚函数是指被标明为不具体实现的虚拟成员函数,它用于这种情况:定义一个基类时,会遇到无法定义基类中虚函数的具体实现,其实现依赖于不同的派生类。
定义纯虚函数的一般格式是:
virtual 返回类型 函数名(参数列表) = 0;
"= 0
"表示程序员将不定义该函数,函数声明是为派生类保留一个位置,其本质是将指向函数体的指针定为NULL。