目录
前言
多态也是面向对象程序设计的重要特性,它都是配合继承来使用,多态可以增加程序设计的灵活性;本文将深入探讨C++中的多态性,剖析其原理和实现方式;
1. 多态的概念
多态是指多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态;
举个例子感受一下多态,比如:
买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票同样是买票,不同的用户买票服务有所不同,这就是一种多态行为;
2. 多态的定义及实现
开始时提到,多态都是配合继承来使用,这是因为继承是实现多态的前提,如果对继承不太了解可以先去阅读:
多态是在继承体现中,不同的类对象调用同一函数,产生了不同的行为;比如Student继承了
Person。Person对象买票全价,Student对象买票半价;
在继承体系中,要构成多态需要添加两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
2.1 虚函数
虚函数:被virtual修饰的类成员函数称为虚函数;
2.2 虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数
注意:虚函数重写的是函数的实现!!
简单介绍完我们来上手用一用:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
//派生类重写的虚函数可以不加virtual(不加依然可以构成多态),但最好是加上;
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
// 基类的引用或做指针调用,父类对象就无法构成多态
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
2.3 虚函数重写的两个特例
- 协变(基类与派生类虚函数返回值类型不同)
在虚函数重写时,返回类型可以不同,但前提是,基类的虚函数返回基类对象的指针或引用,派生类的虚函数返回派生类对象的指针或引用
class A{};
class B : public A {};
class C
{
public:
virtual A* f() {return new A;}
};
class D : public C
{
public:
virtual B* f() {return new B;}
};
- 析构函数(函数名不同)
在继承里边我们就提到,派生类的析构和基类的析构会构成隐藏,为什么?这是因为析构函数在调用时,编译器对析构函数的名称做了特殊处理,函数名会被统一处理成destructor;
虚函数重写时在基类的析构函数前加上virtual修饰,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然函数名不同,但最终会被统一处理成destructor;
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 没有virtual调用析构时不会区分子类、父类
// 加上virtual可构成多态
int main()
{
Person* p1 = new Person;//父类指针指向父类对象
Person* p2 = new Student;//父类指针指向子类对象
// 析构是虚函数,才能正确调用析构函数
// p->destrutor() + operator delete(p)
delete p1;
delete p2;
return 0;
}
这个场景析构函数必须要使用多态;如果不是多态(去掉虚函数的virtual),调用析构时就会发现它不会调用派生类的析构函数,两次都调用了基类的析构函数(正常的应该是基类对象调基类析构,派生类对象调基类析构和自己的析构);C++为了让析构函数能够正确调用(构成多态),对析构函数做了特殊处理,将析构函数统一处理成destructor;
3. 关于多态的两个关键字
在C++11中新添加了两个关键字override 和 final,其目的是为了防止重写时出现函数名错误的问题,帮助我们检查是否重写;
- final:修饰虚函数,表示该虚函数不能被重写
它也可以修饰类,修饰类时,表示该类不能被继承;
- override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
4. 多态的底层分析
4.1 虚函数表
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
Base b;
cout << sizeof(b);
return 0;
}
这里sizeof (b)的大小是多少?如果你认为它是4那就错了,在32位机器下是8;在64位机器下是16;通过监视窗口我们会发现b里边除了有_b之外还有一个_vfptr的指针:
这个指针也叫做虚函数表指针vfptr ( virtual function ptr );它是一个二级指针,准确的说它是一个虚函数指针 数组;一个含有虚函数的类中都至少都有一个虚函数表指针,虚函数的地址要被放到虚函数表中,虚函数表也简称虚表;
4.2 多态的原理
调试下面这段代码,通过监视窗口观察_vfptr:
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;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
通过监视窗口我们发现:
- 基类对象和派生类对象内部_vfptr地址不同
- 派生类对Func1进行了重写,在派生类的需表中Func1的地址发生了变化,Func2的地址没变
得出以下结论:
- 基类和派生类它们用的不是同一个虚表
- 派生类对象的虚表是基类对象虚表的拷贝,当派生类对基类虚函数重写时,会替换原来虚表中的Func1(函数地址发生改变)
那它们是怎么实现多态的? p对象和s对象里的1和2是给的初始值,可以不用管;
原理其实也很简单:
- p指向父类对象时,在父类对象中拿到父类的_vfptr(虚函数表指针),通过虚表调用父类的虚函数;
- p指向子类对象时(会发生切片),在子类对象中拿到子类的_vfptr,通过虚表调用子类的虚函数;
总结就是通过各自的虚表去调用各自的虚函数;
拓展结论:同一个类的不同对象,它们共用一个虚表;
建议:私下想自己调试验证的话,建议在32位环境下,内存查看时显示4位,这样更为清晰
5. 抽象类
概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)
抽象类有以下特性:
- 抽象类不能实例化出对象
- 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象
纯虚函数规范了派生类必须重写;
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
注意:不管有没有多态体系,只要有虚函数就会有虚函数表;
拓展阅读:
多态的原理及底层较为复杂,联系到多继承,菱形继承更为复杂,感兴趣的可以去研读下面的文章;
总结
继承和多态在校招笔试中考察的非常多,面试官很喜欢从继承和多态这里考察学生对C++的了解程度,所以这部分要耐心的去学习;好了以上便是本文的全部内容,希望可以对你有所帮助,最后感谢阅读!