前言:
什么是多态?它是干嘛的?
顾名思义,多态就是多种形态,不过这个多种形态是对于某一动作而言的。好抽象啊,别卖关子,多态到底是什么。
好,我讲个小故事,高考都参加过吧,数学压轴题往往是有多个答案(虽然做出来的人不多),对于它的解题步骤,可以说是千人千样,每个人都有自己的做法,这就是一种多态,每个对象对于某一行为的执行都有自己特有的状态。
上一篇结尾提了一嘴虚函数,事实上,多态的实现要归功于虚函数。
文章目录
1️⃣多态的定义
⚫产生
面向对象有三个特点,封装继承多态,多态诞生于继承,多态是通过借用父类型的引用或指针去访问子类中的函数,并允许要执行的函数进行动态绑定。这有点打破封装的意味哈,也是继承本身就是一种打破封装的手段。
🔵动态绑定和静态绑定
直接说动态绑可能会有点懵,好先举个例子,有一个正常人,这个人这一生要经历生老病死、娶妻生子,现在这个人已经到了青年阶段,他要结婚娶媳妇了,刚刚好女朋友也要结婚了,没错他俩这就叫静态绑定,即早期绑定,假如说这个小伙没有女朋友,也没有心仪的对象,但是家里人通知他要结婚了,早怎n多年前给他定下了娃娃亲,这个小伙就很懵(哎命好),他知道自己要结婚但是在没见到这个新娘之前,不知道要跟谁结,这种就是动态绑定,即晚期绑定,我把他称为薛定谔的新娘绑定。
好例子举完了再来看看代码吧。
class A{
};
class B :public A{
};
void show(int a){}
void fun(A* a){}
void main(){
show(1);
B b;
fun(&b);
}
是的,这里的fun(&b)就是晚期绑定,为什么?
那就浅浅解释一下吧,我们再把b传进去的时候,传给了个A的指针,这一步并不是简单的赋值,他们之间的转化也是有独立的编译过程(详情参考继承里的切蛋糕),也就是并不能直接编译的,要经历过选择才能进行绑定的。
早期绑定是指在编译时确定调用的函数或方法,编译器可以直接将函数调用与特定的函数实现绑定在一起。这种绑定是在编译阶段完成的,因此称为静态绑定。
而晚期绑定是指在运行时根据对象的实际类型来确定调用的函数或方法。它允许在运行时动态地选择调用的函数。晚期绑定通常与多态性、虚函数和继承相关。在使用晚期绑定的情况下,编译器不能直接将函数调用与特定的函数实现绑定,而是需要在运行时根据对象的实际类型来动态选择调用的函数。
因此,晚期绑定通常需要虚函数的支持,通过基类指针或引用调用虚函数时,可以根据对象的实际类型来动态选择调用的函数。
🟤实现
回归主题,多态的实现是有具体要求的。
在继承中要构成多态有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
class A {
public:
virtual void show() {
cout << "A::show" << endl;
}
};
class B :public A {
public:
virtual void show() {
cout << "B::show" << endl;
}
};
上述代码即是多态的实现
肯定有小伙伴有疑问,你这是个*的多态啊,这就行了?
好我解释一下,想象一下这个A类中有十个show的同名函数,如果我不把这个show加上virtual,是不是子类中的show就会直接否定掉所有的show,但是我的意图是要重写当前这个show,因此加上virtual后我做的只是针对目标show进行的多态覆盖。
2️⃣原理
🟣虚函数表
之前对于继承的观察,很明显的可以看到子类会有一个父类(宏观上不能说子类对象中有一个父类对象),正由于多态也是在继承中衍生出来的,我们再观察其存储方式时也与之类似。
对于虚函数,会有一个虚函数表,这个表会指向一个空间,该空间会存放虚函数。
看上去这个虚表属于父类?并不是,因为a对象本身内部并没有一个A类对象,而微观上的A类似乎会造成一种a对象有一个A类对象的假象,实则虚表和所谓的A类他们同属于a对象,他们都在a的内部。
以上是单继承的情况,那多继承嘞?
切,我知道跟继承一样每个类的虚函数都有自己的一个虚表,划分很清晰,在子类对象中放到各自的虚表。事实上确实是这样的
class A {
public:
virtual void show() {
cout << "A::show" << endl;
}
virtual void fun1(){
cout << "A::fun" << endl;
}
};
class B {
public:
virtual void show() {
cout << "B::show" << endl;
}
virtual void fun1() {
cout << "B::fun" << endl;
}
};
class D:public A,public B{
public:
virtual void show() {
cout << "D::show" << endl;
}
virtual void show1() {
cout <<" D::show1 "<< endl;
}
private:
int _m;
};
typedef void (*p_t)();
void main() {
D a;
A s;
p_t v=(p_t)*((int *)*(int*)&a+0);
cout << v << endl;
v();
p_t q = (p_t)*((int*)*(int*)&a+1);
cout << q << endl;
q();
p_t w = (p_t)*((int*)*(int*)&a + 2);
cout << w << endl;
w();
p_t t = (p_t) * ((int*)*(int*)&a + 3);
cout << t << endl;
}
但是也有例外,子类中的show1没有重写,他被放到了第一个虚表的末尾。
🟠父类子类对象的虚表
3️⃣重载、重写、重定义
这三兄弟看名字雀氏有点难区分,实际上他们大相径庭。
但从作用域来看,重载只能是同一个类中,同名函数的其他实现。而重写重定义都是发生在继承体系中,重写是子类对父类的同名函数,相同参数列表、返回值的实现,而重定义则是同名隐藏。
重 载 | 相同的范围(在同一个类中) 函数名字相同 参数不同 virtual 关键字可有可无 |
---|---|
重 写 | 不同的范围(分别位于派生类与基类) 函数名字相同 参数相同 基类函数必须有 virtual 关键字 |
重定义 | 不同的范围(分别位于派生类与基类) 函数名相同 |
4️⃣三同戒律和特例
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)(我称为三同戒律),称子类的虚函数重写了基类的虚函数。
不过虚函数有两个特例可以打破这个三同戒律。
🟢协变(基类与派生类虚函数返回值类型不同)
在继承关系中,派生类的返回类型可以是基类返回类型的子类型。在协变中,类型的兼容性是指派生类的类型可以隐式转换为基类的类型。
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引
用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
🟡析构函数的重写(基类与派生类析构函数的名字不同)
类似
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
5️⃣抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class A {
public:
virtual void fun() = 0;
};
class B :public A {
virtual void fun() {
cout << "fun" << endl;
}
};
6️⃣final和override
final:修饰虚函数,表示该虚函数不能再被继承
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
或许设计者那个年代编译器没现在那么智能,C++11设计者设置了这么两个关键字来提醒程序员,上下文那些函数要重写,那些不能重写。在当时的话,对于记性不好的朋友或者是当前项目极及其庞大的情况,这两个关键字还是很好用的。
总结:
多态跟继承,多态的出现其实就是为了解决继承中的同名隐藏问题,在多态这个概念中,只需要给相应的父函数加上virtual,便可实现多态,致使父类的其他同名函数不会失效,除此之外,多态还可以实现子类父用的功能。
下一篇见