C++ 多态详解(Polymorphism)
目录
1. 多态的简介
多态(Polymorphism)指的是“同一种行为,在不同对象下产生不同结果”。
C++中的多态主要分为:
-
编译时多态(静态多态)
-
运行时多态(动态多态)
编译时多态(静态多态)
编译阶段就已经确定调用哪个函数。
常见方式:
-
函数重载
-
函数模板
例如:
#include <iostream>
using namespace std;
void Func(int x)
{
cout << "int" << endl;
}
void Func(double x)
{
cout << "double" << endl;
}
int main()
{
Func(10);
Func(3.14);
return 0;
}
运行结果:
int
double
因为参数类型不同,编译器在编译阶段就确定了调用哪个函数。
运行时多态(动态多态)
运行时多态指:
同一个函数调用,在程序运行过程中,根据对象不同,调用不同函数。
例如:
-
普通人买票 → 全价
-
学生买票 → 打折
-
军人买票 → 优先
2. 多态的实现条件
C++实现运行时多态必须满足两个条件:
(1)必须通过基类指针或引用调用虚函数
成员函数前加 virtual 就是虚函数。
只有成员函数才能是虚函数
普通函数不能加 virtual
(2)被调用的函数必须是虚函数,并且派生类完成重写
虚函数重写(覆盖)
派生类中:函数名相同,参数相同,返回值相同
则构成重写(Override)
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
void Func(Person& ptr)
{
// 这里可以看到虽然都是Person指针Ptr在调用BuyTicket
// 但是跟ptr没关系,而是由ptr指向的对象决定的。
ptr.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
调用父类的原因是:继承中规定,子类可以赋值给父类(切割),这样,不管传父类还是子类,都可以实现,指针指向谁,就调用谁的函数
运行结果:
买票-全价
买票-打折
如果把父类的virtual去掉,那么都不管传父类还是子类,都会调用父类的函数,因为此时不构成重写虚函数
如果不传指针和引用,也会都调用父类函数
运行结果:
买票-全价
买票-全价
在重写父类虚函数时,子类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承
后父类的虚函数被继承下来了在子类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使⽤,不过在考试选择题中,经常会故意埋这个坑,让你判断是否构成多态
3. 多态经典面试题
class A
{
public:
virtual void func(int val = 1)
{
cout << "A->" << val << endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
void func(int val = 0)
{
cout << "B->" << val << endl;
}
};
int main()
{
B* p = new B;
p->test();
return 0;
}
输出:
B->1
原因:
func() 是虚函数 ,首先b在自己类中找test,如果有,就隐藏A中test,如果没有,就去A中找test,A中test的this指针是A* ,所以这里参数是父类指针或引用,就构成了多态,这里是B对象调用func,所以调用B中函数,但是出题人很恶心,你如果答案是B->0,那你就被出题人秒杀了,重写的规定是重写函数题,函数声明不会重写,所以缺省值还是1,所以答案是B->1
4. 析构函数建议写成虚函数
看代码:
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
~B()
{
cout << "~B()" << endl;
delete[] _p;
}
private:
int* _p = new int[10];
};
int main()
{
A* p = new B;
delete p;
return 0;
}
如果基类析构函数不是虚函数,那么这里就调用A析构,因为虽然new的是B,但是不构成多态
则:
delete p;
只会调用:
~A()
不会调用:
~B()
导致:资源泄漏,内存泄漏
所以:
基类析构函数一般都建议写成虚函数。
在这种场景下,为了构成多态,所以编译器会被析构函数最后都处理成destructor,这里就解释了为什么在继承部分,析构构成了隐藏关系
5. override 与 final
override
用于检查是否真正完成重写。
class Car
{
public:
virtual void Drive()
{
}
};
class Benz : public Car
{
public:
virtual void Drive() override
{
cout << "Benz" << endl;
}
};
如果函数名写错:
Dirve()
编译器会直接报错。
final
禁止派生类继续重写。
class Car
{
public:
virtual void Drive() final
{
}
};
此时子类不能再重写 Drive。
6. 纯虚函数和抽象类
纯虚函数格式:在虚函数后面加 =0
virtual 函数 = 0;
例如:
class Car
{
public:
virtual void Drive() = 0;
};
这种函数叫:纯虚函数
包含纯虚函数的类叫抽象类
抽象类特点
(1)不能实例化对象
错误:
Car c;
(2)派生类必须重写纯虚函数
否则派生类仍然是抽象类。
7. 虚函数表(虚表)
含有虚函数的类中,
对象内部会存在一个:
vfptr
即:虚函数表指针
对象大小问题
class Base
{
public:
virtual void Func1()
{
}
private:
int _a;
char _b;
};
32位下:
sizeof(Base)
结果:
12
原因:
| 成员 | 大小 |
|---|---|
| int | 4 |
| char | 1 |
| 对齐 | 3 |
| 虚表指针 | 4 |
总计:
12 bytes
多态是如何实现的
满足多态条件后:
ptr->BuyTicket();
不再是编译时确定函数地址。
而是运行时去对象中的虚表寻找真正函数地址。
动态绑定与静态绑定
静态绑定
编译阶段确定函数地址。
例如:普通函数,非虚函数
动态绑定
运行时通过虚表确定函数地址。
满足条件:基类指针/引用,调用虚函数
虚函数表特点
(1)每个有虚函数的类都有虚表
(2)同类型对象共用同一张虚表
(3)派生类会覆盖虚表中的虚函数地址
例如:
Base::func1
会被:
Derive::func1
覆盖。
更多推荐
所有评论(0)