凡是面向对象的语言,都有三大特性,继承,封装和多态,但并不是只有这三个特性,是因为者三个特性是最重要的特性,那今天我们一起来看多态!
目录
1.多态的概念
1.1虚函数
1.2虚函数的重写
1.3虚函数重写的两个例外
1.子类的虚函数可以不加virtual
2. 协变(基类与派生类虚函数返回值类型不同)
1.4如何实现一个不能被继承的类
2. 多态的定义及实现
2.1多态调用
2.2普通调用:
2.3析构函数建议加virtual吗?
2.4抽象类
2.5接口继承和实现继承
3.多态的实现原理
3.1.虚表(虚函数表)
3.2多态的实现
3.3静态多态和动态多态
3.4单继承的多态实现
3.5多继承的多态实现
4.一些常考的多态的问题
总结:
多态,就是不同对象去完成某一种行为时,产生的不同状态。
举例说明:日常生活中,我们去买票,尤其买火车票时,总会有不同的结果。当成人买的时候,就是原价,学生就是半价,军人就是优先买票,这都体现了多态。不同对象完成某一行为,产生不同状态。
那实现多态前,我们首先得清楚一些概念:
class A
{
public:
virtual void func()
{
cout << "A" << endl;
}
};
我们之前在继承的时候,学习过重定义(隐藏),现在在多态这一节,又出现了重写,那到底是什么呢?我们来看一看
重定义(隐藏):首先在两个不同的类中(父类,子类)(构成继承),只要函数名相同,就会构成重定义(隐藏)。
重写(覆盖):在重定义的基础上,除了函数名要相同,还有返回值,参数都得相同,这才构成重写。
举个例子:
class A
{
public:
virtual void func()
{
cout << "A" << endl;
}
};
class B:public A
{
public:
virtual void func() //重写
{
cout << "B" << endl;
}
};
总结就是:虚函数的重写条件:子类和父类都是虚函数,且函数名,返回值,参数都必须相同(三同),这才能构成虚函数的重写。
因为是继承关系,父类的虚函数也被继承下来,所以子类的可以不加virtual。(建议还是都写上)
class A
{
public:
virtual void func()
{
cout << "A" << endl;
}
};
class B:public A
{
public:
void func()
{
cout << "B" << endl;
}
};
意思是:三同中,返回值可以不同,但要求返回值必须是父子类关系的指针或引用。(其他父子类关系的指针或者引用也可以)
class person
{
public:
virtual person* func() //本父子类指针或引用
{
return this;
}
};
class student:public person
{
public:
virtual student* func() //本父子类指针或引用
{
return this;
}
};
其他父子类关系的指针或者引用也可以:
class A
{};
class B:public A
{};
class person
{
public:
virtual A* func()
{
return nullptr;
}
};
class student:public person
{
public:
virtual A* func()
{
return nullptr;
}
};
但是父类的返回值不可以为子类的指针。
方法一:是需要把它的构造函数写为私有即可,无法构造,就不可能被继承;
方法二:类定义时,加final(c++11),最终类,不能被继承
class A final {}; class B:public A {};
若final给虚函数,虚函数则不能被重写
class A { virtual void func()final {} }; class B:public A { virtual void func() {} };
override是来判断是否已经重写(检查重写)
class A { virtual void func(int) {} }; class B:public A { virtual void func()override {} };
首先多态实现的前提必须是继承!
多态实现的两个条件:
1.必须使用父类(基类)的指针或者引用调用虚函数;
2.被调用的函数必须是虚函数,且子类(派生类)必须对虚函数进行重写;
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了Person 。 Person 对象买票全价, Student 对象买票半价。2.1多态调用
class Person { public: virtual void Buyticket() { cout << "Person:全价" << endl; } }; class Student:public Person { public: virtual void Buyticket() { cout << "Student:半价" << endl; } }; void func(Person& p) //切片 { p.Buyticket(); } int main() { Person p; Student s; func(p); func(s); }
2.2普通调用:
不符合多态条件即可:
void func(Person p)//不是指针或者引用,就是对象 { p.Buyticket(); } int main() { Person p; Student s; func(p); func(s); }
那么我们可以发现:
普通调用跟调用对象的类型有关;
多态调用必须是父类的指针或者引用,无论是是哪个对象传,他都会指向该对象中父类的那一部分(切片),进而调用该对象中的虚函数。一句话,多态调用跟 指针/引用 指向的对象有关
我们看一个例子:
class Person { public: ~Person() { cout << "Person delete" << endl; delete _p; } protected: int* _p = new int[10]; }; class Student :public Person { public: ~Student() { cout << "Student delete" << endl; delete _s; } protected: int* _s = new int[20]; }; int main() { //Person p; //Student s; Person* ptr1 = new Person; Person* ptr2 = new Student; delete ptr1; delete ptr2; }
我们都知道,析构函数自动调用,在继承中,子类会先析构,调用子类的析构函数以后,自动再调用父类的析构函数。
但这用情况还适用吗?
先看一下结果:
我们发现,居然调用了两次父类的析构函数 !!!
这种情况就会造成子类对象中的成员变量没有释放,导致内存泄露!!
我们知道:
delete有两种行为:1.使用指针调用析构函数;2.operator delete(ptr)
所以使用指针调用析构函数是普通调用(不满足多态调用的条件),普通调用是跟调用的对象类型有关,类型都是Person,所以只会调用person的析构函数
但此时我们更希望的是多态调用,所以建议加virtual,指针指向的对象是哪个,就调用哪个的析构函数。但此时我们会想,析构函数名字都不一样,这能构成重写吗?当然可以,那是因为编译器会自动把父类子类的析构函数名字换成一样的:ptr->destructor()。
那么就可以实现我们预期的效果:
所以我们建议:再写析构函数时,可以无脑给父类的析构函数加virtual,防止出现上面的情况,导致内存泄露 。
普通调用时,时普通调用;父类的指针或者引用调用时,时多态调用,互不影响!
class Car
{
public:
virtual void Drive() = 0;
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
Car* pBMW = new BMW;
pBMW->Drive();
}
int main()
{
Test();
}
总结:有些类不需要类的对象,可以在写成纯虚函数。
接口继承针对虚函数;实现继承针对普通函数。
class A
{
public:
virtual void func(int val = 1){ std::cout << "A->" << val << std::endl; }
virtual void test(){ func(); }
};
class B : public A
{
public:
void func(int val = 0){ std::cout << "B->" << val << std::endl;
};
int main(int argc, char* argv[])
{
B*p = new B;
p->test();
return 0;
}
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
若是ptr->func(),就是B类对象直接调用,就是普通调用,普通调用跟对象类型有关。
普通调用在编译时就会静态绑定,在编译时调用的函数以及函数的默认值就已经确定,子类调用子类自己的函数,跟父类没有任何关系,函数都是子类编译时就已经静态绑定的,所以缺省值依然是0。最终结果是B->0
答案选哪个??
首先我们了解的第一点是,继承父类的成员,会原封不动的继承到子类;
我们接下来看:创建了一个B对象的指针,指针来调用p->test(),这时候,会直接调用父类中的test,再this->func(),此时的this的类型是A*,因为test处于A类中,继承到B中,也会原封不动的继承过去,this依然是A*,所以父类的指针调用虚函数,满足多态的调用,多态调用是看指针指向的对象,又因为p调用的test,所以指针指向B对象,所以会调用B的重写的func虚函数,所以最终答案是B->1.(其实多态调用一直是调的父类的接口,再根据指向的对象去调用具体的实现,后面会详细讲到)
当B对象自己调用函数func时,当不是多态调用时,就会直接调用自己的func(),缺省值还是自己的val=0.
来先看一道题:
class Base1
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
};
class Base2
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
sizeof(Base1),sizeof(Base2),它所占的字节数是多少?
通过之前学习的内容,我们可以了解到,如果类中没有成员变量,只有成员函数,会留一个字节进行占位,因为成员函数在代码段,所以Base1的大小是1吗?
原来不是我们想象的那样子,是事实上,来看:
凡是有虚函数的,都会有一个虚函数表指针来存虚函数,简称虚表指针,存虚函数的表叫做虚函数表,简称虚表。
VFptr(全程vftable)是一个指针, 指向虚表,虚表中存的是虚函数的地址。
所以我们知道,原来只要有虚函数,就会有虚表指针,所以Base1的字节大小是,4字节;
Base2的字节大小是,加上内存对齐,_b占四字节,vtf占四字节,8字节。
对于同一类实例化出的不同的对象,他们的虚表是公用的:
class A
{
public:
virtual void func(){}
}
int main()
{
A b;
A c;
}
我们了解虚表和虚表指针以后,那么多态到底如何实现呢?
来看一段代码:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
char _ch;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
void Func3()
{
cout << "Derive::Func3()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
//普通调用
Base* ptr = &b;
ptr->Func3();
ptr = &d;
ptr->Func3();
//多态调用
ptr = &b;
ptr->Func1();
ptr = &d;
ptr->Func1();
}
多态调用:
ptr是父类的指针,无论指向哪个对象都只能看到该对象父类的部分(切片),那么多态调用怎么调用呢?通过虚表指针来调用虚函数,完成重写的虚函数会在虚表对应的位置进行覆盖,变成重写后的虚函数,进而调用。(一句话,我也不知道我调用谁,我指向谁,就调用谁的虚函数,进而完成动态绑定,完成多态调用)
静态绑定:编译时,通过类型就确定调用函数的地址,然后直接call完成调用
通过反汇编可以看到:
静态绑定,一步完成;动态绑定得很多步完成。
总结:多态调用就是依靠虚表实现,指向谁,就调用谁的虚函数
虚表是存在代码段中的。
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
int main()
{
Base b;
Derive d;
}
我们知道Base对象中的虚表有func1和func2,子类对虚函数进行重写,func1重写,func2不变:
那么子类自己的虚函数func3在不在虚表里面呢?
为了更方便观察,我们可以实现一个打印虚表的函数:
typedef void(*VFTptr)(); //函数指针,重命名必须写到里面
void Print(VFTptr VFT[]) //函数指针数组
{
int i = 0;
while (VFT[i]) //虚表中,vs默认以空结束。
{
printf("[%d]%p->", i, VFT[i]);
VFT[i]();
i++;
}
cout << endl;
}
int main()
{
Base b;
Derive d;
Print((VFTptr*)(*(int*)&b)); //先取地址,再强转VFTptr的地址,然后解引用取到地址,再强转为VFT*类型,进而传参调用
Print((VFTptr*)(*(void**)&d));
//换为void**原因是因为,机器若是32位,指针大小就是4字节,若是64位,就是8字节
//所以换为void**更普适,先取地址,再强转void**,void*解引用,那么这就根据机器的位数来决定指针的大小了
}
我们可以发现,虚函数func3也会存在虚表中。
typedef void(*VFTptr)();
void Print(VFTptr VFT[])
{
int i = 0;
while (VFT[i])
{
printf("[%d]%p->", i, VFT[i]);
VFT[i]();
i++;
}
cout << endl;
}
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
int main()
{
Base1 b1;
Base2 b2;
Print((VFTptr*)(*(void**)&b1));
Print((VFTptr*)(*(void**)&b2));
Derive d;
Print((VFTptr*)(*(void**)&d));//Base1的虚表
Print((VFTptr*)(*(void**)((char*)&d + sizeof(Base1))));//Base2的虚表
//Base2* ptr=&d;
//Print((VFTptr*)(*(void**)ptr)); //也可以这样找到虚表指针
}
我们知道多继承下多态的实现,子类继承多个父类,只有当父类有虚函数,多继承时才有虚表。
当子类也有虚函数时,这时子类的虚函数放到第一个继承的父类的虚表中,我们可以从上面代码结果看出。
再来练习题目:
下列输出的结果是什么?
class A{
public:
A(char *s) { cout << s << endl; }
~A(){}
};
class B :public A
{
public:
B(char *s1, char*s2) :A(s1) { cout << s2 << endl; }
};
class C : public A
{
public:
C(char *s1, char*s2) :A(s1) { cout << s2 << endl; }
};
class D :public C, public B
{
public:
D(char *s1, char *s2, char *s3, char *s4) : B(s1, s2), C(s1, s3),A(s1)
{
cout << s4 << endl;
}
};
int main() {
D *p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
A:class A class B class C class D
B:class D class B class C class A
C:class D class C class B class A
D:class A class C class B class D
答案是哪个呢?
首先D肯定是最后一个才被初始化的,构造函数先走初始化列表,B,C,A,那肯定是A先被初始化,因为B,C中都有A,A不初始化,B,C没办法初始化;其次要看继承的顺序,D先继承C,再继承B,所以先初始化C,再初始化B.最终答案就是D
第二题:
class Base1 {public: int _b1;};
class Base2 { public: int _b2; };
class Derive : public Base2, public Base1
{ public: int _d; };
int main(){
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
A:p1 == p2 == p3 B:p1 > p2 == p3 C:p1 == p3 != p2 D:p1 != p2 != p3
答案选哪个呢?
子类首先继承了Base2,再继承了Base1,所以模型应该是这样的:
所以没有答案,答案应该是:p3==p2 所以通过上面这两个例子,我们可以看的出,其实实现继承时,继承的顺序是非常重要的,有关谁先被创建。 这一节,我们完完整整把多态的全部内容都讲了一遍,当然途中大家肯有会有不懂的地方,因为这是难点,我在编写这边文章的时候,也是反反复复思考和学习,所以大家需要反复思考观看,不懂得可以在评论区回复,或者私信我哦! 大家加油!!! 我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=3euzu1e5f38kg
4.一些常考的多态的问题
总结: