目录
code 2 使用基类指针数组(*p[elementCnt])存放子类对象
多态的定义
如果基类中某个函数使用virtual关键字修饰,则该函数被定义为虚函数;派生类中与该函数原型一致的函数也将变成虚函数,并形成对基类版本的覆盖。此时,使用指向子类对象的基类指针或引用调用该虚函数,实际执行的是子类对象的覆盖该版本,而非基类对象的原始版本。
三个基本条件
virtual关键字修饰,存在继承关系、基类指针或引用指向子类对象
几点注意事项
- 类的构造函数、全局函数和静态函数不允许定义成虚函数;子类对象在构造时要首先调基类的构造函数才调用自己的构造函数;全局函数只能被重载,在编译时完成绑定;静态函数为类的所有对象所共有,没有this指针,就无法完成this à vptr à vtable àvirtual function的传递关系。
- 派生类的虚函数必须有派生类具有相同的函数签名,即相同的函数名、常属性和参数表。
- 可以允许返回值不一样,但此时派生类版本返回的应是类型本身的指针或引用,即类型协变。
- 使用子类对象为基类对象赋值不会形成多态
code 1 返回具有继承关系的父子类
|
#include <cstdio> #include <iostream> using namespace std; class general { int i; }; class base { public: base(){} virtual base* get() { cout << "get an obj" << endl; base b; b.i = 10; return &b; } int i; }; class sub : public base { public: sub(int i) { this->i = i; } virtual sub* get() { return this; }
}; void howToGet(base* obj) { cout << obj->i << endl; } int main() {
cout << "----------虚函数返回具有父子对象----------" << endl; sub* s4 = new sub(14); howToGet(s4); return 0; } |
code 2 使用基类指针数组(*p[elementCnt])存放子类对象
|
class shape { public: shape(int x, int y) : m_x(x), m_y(y){} virtual void draw() { cout << "绘制图形" << m_x << m_y << endl; } virtual ~shape() { cout << "基类shape的析构函数" << endl; } int m_x; int m_y; }; class rect : public shape { public: rect(int x, int y, int width, int height) :shape(x,y), m_width(width), m_height(height) {} virtual void draw() { cout << "绘制矩形" << m_x << m_y << endl; } virtual ~rect() { cout << "子类rect的析构函数" << endl; } int m_width; int m_height; };
class circle : public shape { public: circle(int x, int y, int radius) :shape(x, y), m_radius(radius) {} virtual void draw() { cout << "绘制圆形" << m_x << m_y << endl; } virtual ~circle() { cout << "子类circle的析构函数" << endl; } int m_radius; };
void drawShapes(shape* sh[]) { for (int i = 0; sh[i]; i++) { sh[i]->draw(); } }
void deleteShape(shape* sh[]) { for (int i = 0; sh[i]; i++) { delete[] sh[i]; } }
int main() { cout << "----------使用父类数据存储子类对象----------" << endl; shape* sh[4] = { NULL }; sh[0] = new rect(10, 20, 6, 8); sh[1] = new circle(10, 10, 5); drawShapes(sh); deleteShape(sh);
} |
code3 子类为基类赋值不会形成多态
|
rect rc(10,12,13,13); shape sh1 = rc; sh1.draw(); |
code 4 基类的成员函数调用具有覆盖关系的虚函数
简单工厂模式可以通过这种手段实现
|
在shape中添加如下代码: void func() { //此时this指针成为指向子类对象的基类指针,this的类型是当前类型(即基类类型),实际调用对象是子类对象 draw(); } 在main函数中添加如下代码: rect rc(10,12,13,13); rc.func(); |
多态的实现机制
理论基础:多态函数调用的本质是函数指针做函数参数,是设计模式的基础。
所有定义virtual关键字的类都定义了一个虚函数表,用来存放每个虚函数的地址;同时还隐式的声明了一个vtpr指针,指向该虚函数表。虚函数覆盖的本质是编译器看到虚函数被调用时,在编译阶段,不会直接调用该函数,而是生成一段代码替换该语句;在运行阶段,这段代码执行如下操作:首先会确定调用该函数的目标对象的实际类型,并找到其vtpr指针;根据vtpr指针找到对应的虚函数表中记录的该函数的入口地址,然后执行虚函数代码。
可以认为这里边存在一个三级指针:指向派生类对象的基类指针à对象中的虚函数指针(指向该对象的虚函数表的地址)à虚函数表记录对象虚函数的地址
因此,指针或引用的静态类型和动态类型不一致是C++语言支持多态的根本所在。
code 1 证明虚函数表存在的代码示例
|
#include <cstdio> #include <iostream> using namespace std; class general { int i; }; class base { public: base(){} private: int i; }; class sub : public base { }; int main() { cout << "---------证明虚指针是存在------------------" << endl; cout << "sizeof(general): " << sizeof(general) << endl; cout << "sizeof(base): " << sizeof(base) << endl; cout << "sizeof(sub): " << sizeof(sub) << endl; return 0; }
|
cede 2 构造函数中调用虚函数
|
#include <cstdio> #include <iostream> using namespace std; class general { int i; }; class base { public: base() { print(); } virtual void print() { cout << "I'am Father" << endl; } ~base() { cout << "I'm base's xigou Function" << endl; } private: int i; }; class sub : public base { public: virtual void print() { cout << "I'm son" << endl; } ~sub() { cout << "I'm sub's xigou Function" << endl; } };
void howToPrint(base* obj) { obj->print(); } int main() { cout << "---------基类构造函数调用虚函数的情形--------------" << endl; //在构造时,先调用父类的构造函数,并先指向父类的虚函数表,调用基类版本的虚函数;然后在指向子类的虚函数表,调用子类版本的虚函数 sub s1; howToPrint(&s1); return 0; } |
code 3 虚析构函数
当在函数内通过指向子类的基类指针或引用,释放子类对象时,只能调用基类的析构函数而无法调用子类的析构函数;此时需要将基类的析构函数定义为虚析构函数,才能保证子类的析构函数被正常调用。
构造函数和析构函数的调用顺序为:基类构造à子类构造à子类析构à基类析构
当基类的析构函数被声明为虚函数时,子类的析构函数自然也成为虚函数,会形成对基类析构函数的有效覆盖。在使用指向子类的父类指针或引用释放对象时,会直接调用子类的析构版本,完成对子类的对象的析构,之后在对父类进行释放。
|
#include <cstdio> #include <iostream> using namespace std; class general { int i; }; class base { public: base() { print(); } virtual void print() { cout << "I'am Father" << endl; } ~base() { cout << "I'm base's xigou Function" << endl; } private: int i; }; class sub : public base { public: virtual void print() { cout << "I'm son" << endl; } ~sub() { cout << "I'm sub's xigou Function" << endl; } };
void howToDelete(base* obj) { delete obj; } int main() { cout << "---------虚析构函数--------------" << endl; sub *s2 = new sub(); s2->print(); delete s2; cout << "---------虚析构函数异常情形--------------" << endl; sub *s3 = new sub(); s3->print(); howToDelete(s3); //子类的析构函数不能被执行,要想回到正常情况,只有添加virtual关键字 return 0; } |
两种函数调用及编译模式
(1)普通函数或非虚函数调用,在编译时进行绑定;函数调用将在编译时绑定到该对象所属的函数版本上。此时指针或引用的静态类型和动态类型是一致的,绑定在编译时确定。
(2)通过指针或引用调用虚函数,在运行时才解析该调用;在编译时无法确定实际作用于该函数的对象是基类对象还是父类对象,只有在运行时才能获得绑定该函数的真实对象。此时,指针或引用的静态类型与动态类型不一致,延迟编译发生,引发多态。
抽象类
纯虚函数:只有函数声明,而没有函数实现的virtual修饰的函数。
|
virtual void funcName(void) = 0; |
抽象类:包含纯虚函数的类,抽象类无法初始化对象。
子类如果不对基类的所有纯虚函数形成有效覆盖时,该子类也会变成抽象类,也将无法初始化对象。
纯抽象类:除构造函数外,其余所有函数均为纯虚函数,也称接口。
弊端
动态绑定会增加内存和时间开销,影响程序性能
虚函数无法进行内联优化
在没有多态语法要求的情况下,应尽量避免使用虚函数
个人微信公众号,会不定时分享编程、历史和一些实用的小工具,欢迎有兴趣者关注。