C++从入门到放弃
多态(Polymorphic)
1. 虚函数覆盖(函数重写)和多态概念
- 如果将基类中某个成员函数声明为虚函数,那么其子类中具有相同函数签名的成员函数也就是虚函数,并且对基类中的版本形成覆盖,即函数重写
- 满足虚函数覆盖以后,在通过指向子类对象的基类指针或引用子类对象的基类引用,调用虚函数,实际被执行的将是子类中的覆盖版本,而不是基类中的原始版本,这种语法现象就是多态
#include <iostream>
using namespace std;
class Shape {
public:
Shape(int x = 0, int y = 0) : m_x(x), m_y(y) {
}
//1. 基类中将draw声明为虚函数,在子类中重写此函数
virtual void draw() {
cout << "绘制图形:" << m_x << "," << m_y << endl;
}
protected:
int m_x, m_y;
};
class Rect : public Shape {
public:
Rect(int x, int y, int w, int h) :
Shape(x, y), m_w(w), m_h(h) {
}
void draw() {
cout << "绘制矩形:" << m_x << "," << m_y
<< "," << m_w << "," << m_h << endl;
}
protected:
int m_w, m_h;
};
class Circle : public Shape {
public:
Circle(int x, int y, int r) : Shape(x, y), m_r(r) {}
void draw() {
cout << "绘制圆形:" << m_x << "," << m_y << "," << m_r << endl;
}
protected:
int m_r;
};
void render(Shape *buf[]) {
for (int i = 0; buf[i] != nullptr; ++i) {
//调用时,将会根据调用对象的实际类型来进行调用,子类调用时会根据子类的实际类型来调用子类中重写的Draw函数
buf[i]->draw();
}
}
int main() {
Shape *buf[1024] = {nullptr};
buf[0] = new Rect(1, 3, 2, 4);
buf[1] = new Circle(5, 6, 8);
buf[2] = new Rect(11, 32, 21, 41);
buf[3] = new Rect(21, 34, 22, 44);
buf[4] = new Circle(15, 22, 12);
buf[5] = new Circle(33, 66, 23);
render(buf);
return 0;
}
2. 虚函数覆盖的条件
- 只有类中的成员函数才能声明为虚函数,而全局函数,静态成员函数和构造函数都不能为虚函数
注:析构函数可以是虚函数即虚析构函数
delete
指向子类对象的基类指针时,会调用子类对象的虚析构函数,从而避免内存泄漏的风险.(如果不使用虚析构函数会导致只执行基类的析构函数,而子类的析构函数执行不到) - 只有基类中以
virtual
关键字修饰的成员函数才能作为虚函数,被子类覆盖,而与子类中函数有无virtual
关键字无关 - 虚函数在子类中的覆盖版本和基类中的原始版本要具有相同的函数签名,即函数名,参数表(参数类型和参数个数),常属性
- 如果基类中的虚函数返回的是基本类型的数据,那么该函数的覆盖版本返回值必须是相同类型的数据
- 如果基类中的虚函数返回类类型的指针或引用,那么允许子类的覆盖版本返回其子类类型的指针或引用(协变覆盖)
在C++中,只要原来的返回类型是指向类的指针或引用,新的返回类型是指向派生类的指针或引用,覆盖的方法就可以改变返回类型。这样的类型称为协变返回类型(Covariant returns type). - 如果在基类中的版本中有异常说明,那么该函数在子类中的覆盖版本不能比基类版本抛出更多的异常,可以少于或等于基类版本抛出的异常
3. 形成多态条件
- 多态的语法特性除了需要满足虚函数覆盖的条件,还必须使用通过指针或引用调用虚函数,才能将多态特性表现出来
- 调用虚函数的指针也可以是
this
指针,当通过子类对象调用基类中的成员函数时,this
将是一个指向子类对象的基类指针,再通过它调用虚函数,同样可以表现多态的语法特性
4. 多态的原理
所谓的虚函数覆盖不是覆盖虚函数的代码段,实际覆盖的是虚函数表中的虚函数指针
通过虚函数表和动态绑定来实现
动态绑定:运行时根据虚函数表调用虚函数
- 虚函数表会增加内存的开销
- 动态绑定会增加时间的开销
- 虚函数不能内联优化
总结:实际开发中如果没有多态的语法要求,不要滥用虚函数
(((void(***)(void))&a))();
#include <iostream>
using namespace std;
class Base {
public:
virtual void func(void){
cout << "func"<<endl;
}
virtual void func2(void){
cout << "func2"<<endl;
}
int m_data;
};
int main() {
cout<<sizeof(Base)<<endl;//4->虚函数表指针大小
Base a;
(*(*(void(***)(void))&a))();//三级指针
(*(*(void(***)(void))&a+1))();//函数指针数组的下一个元素
/*一级:虚函数指针,保存虚函数地址
二级:虚函数指针数组:保存虚函数指针数组名
三级:虚表指针,指向虚表(虚函数指针数组)*/
//第一次×运算是取得函数指针数组
//第二次×运算是取得函数指针
return 0;
}
5. 纯虚函数,抽象类,纯抽象类
- 纯虚函数
virtual 返回类型 函数名 (形参表) = 0;
以等于零结尾,没有任何定义的虚函数就是纯虚函数 - 抽象类
如果类中包含了纯虚函数,那么这个类就是一个抽象类
注:
1>抽象类不能创建对象
2>如果子类没有覆盖基类中的纯虚函数,那么子类也是一个抽象类 - 纯抽象类(接口类)
如果类中所有的成员函数(除了构造函数,拷贝构造和析构函数)都是纯虚函数,那么这个类就是纯抽象类(接口类)
6. 工厂方法模式
#include <iostream>
using namespace std;
class PDFParser {//纯虚类
public:
void parse(const char *psfFile) {
cout << "解析出一个矩形" << endl;
onRect();
cout << "解析出一张图片" << endl;
onImage();
cout << "解析出一行文本" << endl;
onText();
}
private:
virtual void onRect() = 0;
virtual void onImage() = 0;
virtual void onText() = 0;
};
class PDFRender:public PDFParser{
private:
void onRect(){
cout<<"显示矩形"<<endl;
}
void onImage(){
cout<<"显示图片"<<endl;
}
void onText(){
cout<<"显示文本"<<endl;
}
};
int main() {
PDFRender render;
render.parse("demo.pdf");
return 0;
}
/home/panda/WorkSpace/CPP/cmake-build-debug/CPP
解析出一个矩形
显示矩形
解析出一张图片
显示图片
解析出一行文本
显示文本
进程已结束,退出代码 0
7. 虚析构函数
- 基类的析构函数不能调用子类的析构函数,因此
delete
一个指向子类对象的基类指针,实际被执行的将是基类的析构函数,而子类的析构函数不会被执行,有内存泄漏的风险 - 使用虚析构函数解决
可以将基类的析构函数声明为虚函数(虚析构函数),那么子类的析构函数也是成为了虚函数,并且可以对基类的虚析构函数形成了覆盖,也可以体现多态的语法特性;这时再delete
一个指向子类的基类指针,实际被执行的是子类的析构函数,而子类的析构函数又会自动调用基类的析构函数,避免了内存泄漏