目录
一.多态特性
回顾一下多态特性的含义:
回顾多态特性的两大形成条件:
而普通调用和多态调用的本质区别在于:
二.理解调用
例一:
普通调用的理解1:
普通调用的理解2:
注:错误代码的使用
普通调用的理解3:
例二:注:例二与例一的父子类代码完全相同!
多态调用的理解1:
代码解析:
多态调用的理解2:
三.案例总结:
一.多态特性
在学习多态特性的过程中,我们总是不明白什么样的函数调用就是多态调用,所以我们今天来梳理一下多态调用。
回顾一下多态特性的含义:
多态!通俗来说,就是多种形态,具体上就是去完成某个行为,即当不同的对象去完成某个行为时会产生出不同的状态。
回顾多态特性的两大形成条件:
1.被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写;
2.必须是父类指针指向子类对象,然后父类指针调用重写后的函数;
之前我们在一般情况下调用的类成员函数,几乎都是普通调用,
而普通调用和多态调用的本质区别在于:
多态调用:父类指针-->与指向的类或对象有关,指向的对象是什么类,就调用什么类的函数;
普通调用:当对象调用函数时,与对象的类型有关,什么类型的对象或者指针调用函数,就调用什么类型的函数。
上面这几行内容是我们理解这两种调用的核心理念,所以我将会通过多个例子来帮助大家去理解多态调用和普通调用的本质。
二.理解调用
例一:
普通调用的理解1:
class Base {
public:
virtual void Func1() {
cout << "Base::Func1()" << endl;
}
void Func3() {
cout << "Base::Func3()" << endl;
}
protected:
string _name;
};
class Derive :public Base {
private:
virtual void Func1() {
cout << "Derive::Func1()" << endl;
}
void Func3() {
cout << "Derive::Func3()" << endl;
}
};
int main() {
cout << "普通调用1:" << endl;
Base bb;
Derive dd;
bb.Func1();
bb.Func3();
//以上两次都是父类对象调用自身的函数
dd.Func1();
dd.Func3();
//以上两次都是子类对象调用自身的函数
return 0;
}
代码解析:
这是两个不同类的类对象对各自成员函数调用,从而调用相应的重写函数。是类对象直接调用,并不符合多态特性形成的条件——这叫普通调用。
普通调用的理解2:
int main(){
cout << "普通调用2:" << endl;
Base bb;
Derive dd;
Derive* dtr = ⅆ
dtr->Func1(); //普通调用
Base* bpr=&bb;
bpr->Func1(); //普通调用
return 0;
}
代码解析:
这两个类指针对象调用虽然都调用了虚函数Func1,但本质上是各类的指针对象去指向各自类的对象,进而调用相应的成员函数,所以这也是各类的指针对象调用各个类的函数,不符合多态特性形成的条件2——所以也是普通调用。
注:错误代码的使用
Derive* dpr = &bb;
对于上面这句代码来讲是会报错的,因为编译器不支持将父类对象向下转换子类对象——也就是说子类指针不能指向父类对象。
Derive* dpr = new Base();
这句代码报错的原因也是与上面的一样,子类指针去指向父类的匿名对象,也是不支持的.
普通调用的理解3:
int main(){
cout << "普通调用3:" << endl;
Base bb;
Derive dd;
//父类指针指向子类对象
Base* bbtr =ⅆ
bbtr->Func3();
return 0;
}
父类指针指向子类的对象,符合多态形成的条件2,但也仅会指向子类对象中从父类继承过来的成员变量和成员函数,并不会指向子类对象自己新建的成员,因为bbtr调用Func3时,发现虚表中没有该函数,就确定了Func3并不是虚函数,采用的仍是普通调用(当对象调用函数时,与调用对象类型有关,类型是Base*,那么就调用Base类的Func3()函数!!!)。
而bbtr得类型是父类Base,所以还是调用子类对象中从父类继承过来的Func3函数,所以结果是Base:Func3()
以上就是在同一个案例中,不同的方式执行的普通调用,对于多态形成的两个条件而言,只要有一条不符合那就一定是普通调用!
例二:注:例二与例一的父子类代码完全相同!
多态调用的理解1:
class Base {
public:
virtual void Func1() {
cout << "Base::Func1()" << endl;
}
void Func3() {
cout << "Base::Func3()" << endl;
}
protected:
string _name;
};
class Derive :public Base {
public:
virtual void Func1() {
cout << "Derive::Func1()" << endl;
}
void Func3() {
cout << "Derive::Func3()" << endl;
}
};
int main() {
Base b;
Derive d;
//多态调用
cout << "多态调用1:" << endl;
Base* btr = &b;
btr->Func1();
btr = &d;
btr->Func1();
return 0;
}
代码解析:
Base* btr = &b;
btr->Func1();
由上可知,父类和子类各创建了一个对象而父类创建了一个指针,指向了父类对象,那么btr就会指向整个父类对象的地址,btr根据父类对象的地址找到vfptr(父类对象的虚表指针),调用函数Func1就是指针通过虚函数表中寻找该函数Func1的地址,找到后进行调用。
代码解析:
btr = &d;
btr->Func1();
多态调用完成(以上代码完全符合多态特性的形成条件)形成多态调用的基本要素:父类有指针指向父类自己的对象,且调用了虚函数,那么指向谁的对象,就调用谁的函数;
父类创建的指针btr,又指向了子类对象的地址,那么btr指向的只是子类对象中从父类继承过来的那部分成员变量和函数罢了——切片思想!
btr根据子类对象的地址找到了子类对象d的vfptr,因为子类Derive是继承的父类Base,那么父类的虚函数也会被继承下来,那么子类的虚函数表就是将父类的虚表拷贝一份到子类中,又因为子类重写了要调用的函数Func1,该函数的地址会进行更换,从而当btr调用函数时,从虚表中寻找该函数的地址,因为更换过Func1的地址,那么btr调用的就是子类的Func1了。
多态调用的理解2:
这是多态调用的另一种方式:父类指针指向类时,指向的是谁的类,调用的就是哪个类相应的函数。
int main(){
cout << "多态调用2:" << endl;
Base* btr2 =new Base;
btr2->Func1();
btr2 = new Derive;
btr2->Func1();
cout << endl;
return 0;
}
案例总结:
案例的难点在于多态调用1的例子:
当父类的指针指向了父类对象的地址时,父类指针指向的是整个父类对象,而当父类的指针指向了子类对象的地址时,父类指针指向的仅是子类对象中从父类继承过来的成员变量与成员函数,所以当父类指针指向子类对象地址后,我来解释一下:结果所调用的函数为什么是父类的函数!例:
Base* bbtr=ⅆ //dd是子类对象
bbtr->Func3();
bbtr调用的这个Func3函数有两个,从两个类的情况来看,一种是父类继承给子类的Func3(),一种是子类自己新建的Func3()。 而执行结果后,显示的是:指针调用了Base:Func3()
对于bbtr指针的路线,它并不是去到父类中调用Func3,而是因为它指向了子类对象的地址,这个地址说具体就是指向了子类对象从父类继承过来的Func3函数地址,然后bbtr指针发现Func3并不是虚函数后,就直接去调用了子类对象中从父类继承过来的Func3(),因为bbtr是父类指针,它并不认识子类自己新建的Func3(),由于切片思想,它指向的那块虚表地址空间(虚表也是从父类继承过来的!)中并没有子类自己新建的Func3() !!!
所以就是一句话,父类的指针在指向子类对象时,所调用的重写虚函数是如何能形成多态的,原因就在于父类指针能够轻松的的访问到子类的虚表地址空间中的相应虚函数。