多态与虚函数这是一个经典陷阱。C++ 的访问控制检查在 编译期基于指针的 静态类型**(基类)。只要基类中该虚函数是 public,即使派生类里把它 override 成了 private,通过基类指针调用时编译器仍然允许,运行时动态绑定到派生类的私有版本——完全可以调用 。封装——类的抽象和设计/可见性 继承——基类派生派生类/代码继承 多态——函数重载/? 虚函数带 virtual 关键字 的成员函数
只 用在类定义的函数声明 中,写函数体不用静态成员函数不能 是虚函数 基类函数声明,派生类自动虚函数(同名函数 ) 1 2 3 4 5 6 7 class base { virtual int get () ; }; int base::get () { ... }
多态表现形式 表现形式1派生类的指针 可以赋给基类指针
通过基类指针 调用基类和派生类中的同名虚函数 时
表现形式2派生类的对象 可以赋给基类引用
通过基类引用 调用基类和派生类中的同名虚函数 时
一个小例子例子一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <iostream> using namespace std;class A {public : virtual void Print () { cout << "A::Print" << endl; } }; class B : public A {public : virtual void Print () { cout << "B::Print" << endl; } }; class D : public A {public : virtual void Print () { cout << "D::Print" << endl; } }; class E : public B {public : virtual void Print () { cout << "E::Print" << endl; } }; int main () { A a; B b; E e; D d; A *pa = &a; B *pb = &b; D *pd = &d; E *pe = &e; pa->Print (); pa = pb; pa->Print (); pa = pd; pa->Print (); pa = pe; pa->Print (); return 0 ; }
代码说明
多态核心 :A 类中 Print 声明为 virtual,派生类重写该函数后,通过基类指针 pa 调用时,会根据指针实际指向的对象类型,自动调用对应类的 Print 函数,这就是运行时多态 。virtual 继承特性 :基类函数声明为 virtual 后,所有派生类中同名函数自动保持虚函数特性 ,即使不写 virtual 关键字,多态效果也完全一致继承关系 :E 继承自 B,B 和 D 继承自 A,因此 E 也间接继承自 A,可以被 A* 指针指向。例子二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <iostream> using namespace std;class myclass {public : void reset () ; virtual void print () { cout << "myclass::print\n" ; } }; void myclass::reset () { print (); } class derived : public myclass {public : virtual void print () { cout << "derived::print\n" ; } }; int main () { myclass b; derived d; b.reset (); d.reset (); return 0 ; }
运行结果
1 2 myclass::print derived::print
多态的价值场景 手动写 A:: 多态(virtual) 适用范围 只在你明确知道类型 时能用 哪怕不知道对象类型 ,也能正确调用 扩展性 新增派生类时,要到处改代码 新增派生类时,原有代码完全不用改 核心价值 语法糖 实现开闭原则 :对扩展开放,对修改关闭
增强可扩充性
要改代码少
多态的作用多态的实质
父类定义共同接口,子类不同实现 通过父类以相同方式 操作不同子类行为 多态的实现实例 实例1游戏:6只怪兽、每只怪兽可以攻击、被击后的扣生命值、被击后的后摇(每只怪兽可能不同)
非多态反面案例 基本思路为每个怪兽编写attack,fightback,hurted成员函数 Attack:被攻击怪兽Hurted 被攻击怪兽Fightback Hurted:自身生命值减少 FightBack:反击并调用被反击对象的Hurted 传统的实现方式:
怪兽A:攻击其他5个怪兽的函数、被其他5个怪兽攻击后摇(扣生命值)的5个函数 怪兽B:…… …… 若增加一个怪兽,则每个怪兽都要修改——平方型代码量增加,代码KPI轻松完成
多态做法 基类派生1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 graph TD %% 定义节点样式(可选,模拟原图效果) classDef baseNode fill:#c6e7ff,stroke:#66afe9,stroke-width:2px; classDef subNode fill:#ffffff,stroke:#999,stroke-width:1px; %% 父节点 CCreature["CCreature"]:::baseNode %% 子节点 CDragon["CDragon"]:::subNode CWolf["CWolf"]:::subNode CSoldier["CSoldier"]:::subNode CPhonex["CPhonex"]:::subNode CAngel["CAngel"]:::subNode %% 继承关系(父 -> 子) CCreature --> CDragon CCreature --> CWolf CCreature --> CSoldier CCreature --> CPhonex CCreature --> CAngel
基类:
1 2 3 4 5 6 7 8 class CCreature {protected : int m_nLiveValue,m_nPower; public : virtual void Attack (CCreature *pCreature) ; virtual void Hurted (int nPower) ; virtual void FrightBack (CCreature *pCreature) ; };
派生:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class CDragon : public CCreature{public : virtual void Attack (CCreature *pCreature) ; virtual void Hurted (int nPower) ; virtual void FrightBack (CCreature *pCreature) ; }; void CDragon::Attack (CCreature *p) { p->Hurted (m_nPower); p->FightBack (this ); } void CDragon::Hurted (int nPower) { m_nLifeValue-=nPower; } void CDragon::FightBack (CCreature *p) { p->Hurted (m_nPower/2 ); }
于是,新怪兽只需要编写新类,不需要 专门增加传入新怪物的成员函数
具体使用的时候:
1 2 3 4 5 6 7 CDragon Dragon; CWolf Wolf; CGhost Ghost; CThunderBird Brid; Dragon.Attack (&Wolf); ... CGhost::Hurted;
注意:以上采用传指针的方式实现多态 ,使用引用 传入亦可
实例2:几何形体处理程序需求:输入若干个几何形体的参数,要求按面积排序输出,输出时要指明形状。
输入:第一行是几何形体数目n (≤100),下面n行每行以字母c开头;c为’R’代表矩形,后跟宽和高;c为’C’代表圆,后跟半径;c为‘T’代表三角形,后跟三条边长度。
输出:按面积从小到大输出每个几何形体的种类及面积,格式为“形体名称: 面积”。
示例输入: 3 R 3 5 C 9 T 3 4 5
示例输出: Triangle: 6 Rectangle: 15 Circle: 254.34
实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include <iostream> #include <stdlib.h> #include <math.h> using namespace std;class CShape {public :virtual double Area ( ) = 0 ; virtual void PrintInfo ( ) = 0 ;}; class CRectangle : public CShape {public :int w, h;virtual double Area () ;virtual void PrintInfo () ;}; class CCircle : public CShape {public :int r;virtual double Area () ;virtual void PrintInfo () ;}; class CTriangle : public CShape {public :int a, b, c;virtual double Area () ;virtual void PrintInfo () ;}; double CRectangle::Area () {return w * h;} void CRectangle::PrintInfo () {cout << "Rectangle:" << Area () << endl; } double CCircle::Area () {return 3.14 * r * r ;} void CCircle::PrintInfo () {cout << "Circle:" << Area () << endl; } double CTriangle::Area () {double p = (a + b + c) / 2.0 ;return sqrt (p * (p - a)*(p - b)*(p - c));} void CTriangle::PrintInfo () {cout << "Triangle:" << Area () << endl; } CShape * pShapes[100 ]; int MyCompare (const void * s1, const void * s2) {CShape * * p1; CShape * * p2; p1 = ( CShape * * ) s1; p2 = ( CShape * * ) s2; double a1 = (*p1)->Area (); double a2 = (*p2)->Area ();if ( a1 < a2 ) return -1 ;else if ( a2 < a1 ) return 1 ;else return 0 ;} int main () {int i; int n;CRectangle * pr; CCircle * pc; CTriangle * pt; cin >> n; for ( i = 0 ; i < n; i ++ ) {char c;cin >> c; switch (c) {case 'R' :pr = new CRectangle (); cin >> pr->w >> pr->h; pShapes[i] = pr; break ;case 'C' :pc = new CCircle (); cin >> pc->r; pShapes[i] = pc; break ;case 'T' :pt = new CTriangle (); cin >> pt->a >> pt->b >> pt->c; pShapes[i] = pt; break ;} } qsort (pShapes, n, sizeof (CShape*), MyCompare);for ( i = 0 ; i <n; i ++ )pShapes[i]->PrintInfo (); return 0 ;}
代码说明
⚠️纯虚函数与抽象类 :CShape 包含纯虚函数,是抽象类,仅定义统一接口,不能实例化 多态调用 :基类指针数组存储派生类对象,调用Area()和PrintInfo()自动匹配对应类的实现排序逻辑 :通过qsort+自定义比较函数,利用多态计算面积并排序优势:添加新的几何形体(如五边形)时,只需从CShape派生出CPentagon类并重写虚函数,再在main的switch中增加一个case即可,其余代码无需修改。
核心技巧:用基类指针数组存放指向各种派生类对象的指针,然后遍历该数组对各个派生类对象做操作,是C++中使用多态的常用做法。
MORE:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Base {public :void fun1 () { fun2 (); }virtual void fun2 () { cout << "Base::fun2()" << endl; }}; class Derived :public Base {public :virtual void fun2 () { cout << "Derived:fun2()" << endl; }}; int main () {Derived d; Base * pBase = & d; pBase->fun1 (); return 0 ;}
输出结果:Derived::fun2() 解析:fun1()内部的fun2()等价于this->fun2(),this是基类指针,触发动态联编,调用派生类虚函数
多态的实现原理 面向对象编程的三个基本概念数据抽象 ↔ 用类进行数据抽象,封装 继承 ↔ 派生类继承基类成员 动态绑定 ↔ 多态,编译器在运行时决定调用基类/派生类函数 动态联编一条函数调用语句在编译时无法确定调用哪个函数,运行到该语句时才确定调用哪个函数 ,这种机制叫动态联编
为什么需要动态联编?
1 2 3 4 5 class A { public : virtual void Get () ; };class B : public A { public : virtual void Get () ; };void MyFunction ( A * pa ) {pa->Get (); }
编译时无法确定pa指向A/B对象,只能运行时判断调用哪个Get()
多态的实现解析1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Base {public :void fun1 () { cout << "Base::fun1()" <<endl; fun2 (); }virtual void fun2 () { cout << "Base::fun2()" << endl; }}; class Derived : public Base {public :virtual void fun1 () { cout << "Derived::fun1()" << endl; fun2 (); }void fun2 () { cout << "Derived::fun2()" << endl; }}; int main () {Derived d; Base * pBase = & d; pBase -> fun1 (); return 0 ;}
1 2 Base::fun1() Derived::fun2()
补充说明: 调用次序:Base::fun1() -> Derived::fun2()fun1()中fun2()等价于this->fun2(),fun2是虚函数+基类指针,触发动态联编;运行时this指向派生类对象,调用派生类函数。
稍加修改的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Base {public :virtual void fun1 () { cout << "Base::fun1()" <<endl; fun2 (); }void fun2 () { cout << "Base::fun2()" << endl; }}; class Derived : public Base {public :virtual void fun1 () { cout << "Derived::fun1()" << endl; fun2 (); }void fun2 () { cout << "Derived::fun2()" << endl; }}; int main () {Derived d; Base * pBase = & d; pBase -> fun1 (); return 0 ;}
输出:Derived::fun1() 、Derived::fun2() 解析:fun1是虚函数→动态联编;派生类fun1中调用普通函数fun2,直接调用自身版本
虚函数表(多态的核心实现)思考例子
1 2 3 4 5 6 7 8 9 10 11 12 13 class Base1 {public : int i;virtual void Print () { cout << "Base1:Print" ; }}; class Derived : public Base1{public : int n;virtual void Print () { cout <<"Derived:Print" << endl; }}; int main () {Derived d; cout << sizeof ( Base1 ) << ", " << sizeof ( Derived ) ; return 0 ;}
输出:8, 12 疑问:对象大小多出4字节?这与多态有何关联?
咦?
神奇的是对于8字节,会有pad8字节对齐
原理:多出的空间用于存储虚函数表指针
含虚函数的类/派生类,都有虚函数表 ,对象中存储虚函数表指针 (4/8字节) 虚函数表存储类的所有虚函数地址 基类指针调用虚函数时,通过对象的虚表指针找到对应函数,实现动态绑定 调用过程 :Base* pBase = &d; pBase->Print();
pBase指向Derived对象,虚表指针指向Derived类虚表 虚表中Print地址为Derived::Print 程序调用派生类函数 虚函数的访问权限访问权限检查是根据指针/引用的类型 ,而非运行时指向的对象类型。
1 2 3 4 5 6 7 8 9 10 11 12 class Base {private : virtual void fun2 () { cout << "Base::fun2()" << endl; }}; class Derived : public Base {public : virtual void fun2 () { cout << "Derived::fun2()" << endl; }}; int main () {Derived d; Base * pBase = & d; pBase -> fun2 (); return 0 ;}
编译出错:基类fun2是私有成员,语法检查直接报错
1 2 3 4 5 6 7 8 9 10 11 12 class Base {public : virtual void fun2 () { cout << "Base::fun2()" << endl; }}; class Derived :public Base {private : virtual void fun2 () { cout << "Derived::fun2()" << endl; }}; int main () {Derived d; Base*pBase=&d; pBase->fun2 (); return 0 ;}
编译通过:基类函数公有→语法检查通过;运行动态绑定派生类私有函数
构造函数和析构函数中调用虚函数⚠️规则 :构造/析构函数中 调用虚函数为静态联编 ,仅调用当前类/基类函数 ;普通成员函数中才是动态联编。
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class myclass {public :virtual void hello () { cout<<"hello from myclass" <<endl; };virtual void bye () { cout<<"bye from myclass" <<endl; };}; class son : public myclass{public :void hello () { cout<<"hello from son" <<endl; };son () { hello (); };~son () { bye (); }; }; class grandson : public son{public :void hello () { cout<<"hello from grandson" <<endl; };grandson () { cout<<"constructing grandson" <<endl; };~grandson () { cout<<"destructing grandson" <<endl; }; }; int main () {grandson gson; son *pson; pson=&gson; pson->hello (); return 0 ;}
输出
1 2 3 4 5 hello from son constructing grandson hello from grandson destructing grandson bye from myclass
原因
构造时:先调用基类构造,派生类未初始化,对象身份为基类,调用基类虚函数 普通调用:对象构造完成,动态绑定派生类函数 析构时:先析构派生类,对象身份回到基类 ,调用基类函数 测一测1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class A {public :virtual void Func () { cout << "A::Func" << endl; }}; class B :public A {public :virtual void Func () { cout << "B::Func" << endl; }}; int main () {A a; A * pa = new B (); pa->Func (); long * p1 = (long * ) & a;long * p2 = (long * ) pa;* p2 = * p1; pa->Func (); return 0 ;}
输出结果:B::Func 、A::Func 解析:
第一次调用:指向B对象→调用B::Func 赋值操作:将A的虚表指针覆盖B的虚表指针 第二次调用:通过A的虚表→调用A::Func 1 2 3 long * p1 = (long *) &a; long * p2 = (long *) pa; * p2 = * p1;
和对象赋值完全不是一回事 ,把 A 对象的前 8 个字节(恰好是它的 vptr)复制到 B 对象的前 8 个字节位置,覆盖了 B 的 vptr。
B 对象的其他部分——如果它有数据成员的话——完全没被碰 。
虚析构函数 问题背景⚠️⚠️通过基类指针删除派生类对象,仅调用基类析构函数,派生类资源未释放,造成内存泄漏。
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class son {public :~son () { cout<<"bye from son" <<endl; }; }; class grandson : public son{public :~grandson (){ cout<<"bye from grandson" <<endl; }; }; int main () {son *pson; pson=new grandson; delete pson;return 0 ;}
输出:bye from son 问题:未调用派生类析构函数,资源泄漏
解决办法将基类析构函数声明为virtual ,派生类析构函数可省略virtual。 通过基类指针删除对象时,先调用派生类析构,再调用基类析构。
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class son {public :virtual ~son () { cout<<"bye from son" <<endl; };}; class grandson : public son {public :~grandson (){ cout<<"bye from grandson" <<endl; }; }; int main () {son *pson; pson= new grandson; delete pson;return 0 ;}
输出: bye from grandson bye from son
通用规则 :类定义了虚函数,析构函数必须定义为虚函数。
构造函数能否声明为virtual?不能 。 ⚠️原因:构造函数负责初始化虚函数表指针 ,而虚函数调用依赖该指针;构造函数执行前指针未初始化,矛盾。
纯虚函数和抽象类 纯虚函数没有函数体的虚函数,声明格式:virtual 返回值 函数名(参数) = 0;
1 2 3 4 5 6 class A {private : int a;public :virtual void Print ( ) = 0 ; void fun () { cout << "fun" ; }};
抽象类包含纯虚函数的类为抽象类,特性:
不能创建抽象类对象,只能作为基类派生 抽象类指针/引用可指向派生类对象 成员函数可调用纯虚函数;构造/析构函数不能 调用 派生类必须实现所有纯虚函数,否则仍为抽象类 示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class A {public :virtual void f () = 0 ; A ( ){ }void g ( ) { f ( ) ; } }; class B : public A{public :void f () { cout<<"B:f()" <<endl ; }}; int main () {B b; b.g ( ); return 0 ;}
注意:构造函数调用纯虚函数,虚表无实现,程序崩溃
抽象类不可以生成对象, 都包含纯虚函数,抽象类的纯虚函数可以有函数体 ,派生类可以调用抽象类的带函数体的纯虚函数,需要实例化纯虚函数后才可以生成对象,但是派生类还是要自己实现纯虚函数
访问机制对比(隐藏 vs 虚函数) 隐藏由指针/引用的类型 决定调用:
派生类指针→调用派生类同名函数 基类指针→调用基类同名函数(即使指向派生类) 虚函数由指针/引用指向的对象类型 决定调用:
总结虚函数和多态 :基类定义接口,派生类实现;通过基类指针/引用实现动态调用多态的作用 :提升程序扩展性,新增功能无需修改原有执行代码多态的实现 :依赖虚函数表+虚表指针 ,运行时动态查找函数访问权限 :语法检查基于指针类型,基类虚函数建议设为public纯虚函数与抽象类 :纯虚函数=0;抽象类不能实例化,派生类必须实现所有纯虚函数虚函数调用 :普通函数→动态联编;构造/析构→静态联编虚析构函数 :(如果一个类有任何虚函数 ,或者意图作为多态基类被继承 )含虚函数的类必须定义虚析构,避免内存泄漏;构造函数不能为虚函数 补充:函数指针与qsort库函数 函数指针-基本概念函数名是函数的入口地址;指向函数的指针称为函数指针 ,可通过指针调用函数。
⚠️函数指针-定义形式1 类型名(*指针变量名)(参数类型1, 参数类型2, ...);
⚠️示例**:int (*pf)(int, char);
函数指针-使用方法用函数名给函数指针赋值 指针名(实参)调用函数示例
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> void PrintMin (int a, int b) {if ( a < b ) printf ("%d" , a);else printf ("%d" , b);} int main () {int x = 4 , y = 5 ;void (* pf)(int , int );pf = PrintMin; pf (x, y); return 0 ;}
函数指针和qsort库函数C快速排序函数,可排序任意类型数组:
1 2 void qsort (void *base, int nelem, unsigned int width, int (* pfCompare)(const void *, const void *)) ;
参数说明 :
base:数组首地址 nelem:元素个数 width:单个元素字节数 pfCompare:自定义比较函数指针 比较函数的要求原型:int 函数名(const void * elem1, const void * elem2); 返回值规则:
elem1排在前 → 返回负数 相等 → 返回0 elem1排在后 → 返回正数 qsort使用示例按数字个位数排序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> #include <stdlib.h> int MyCompare (const void * elem1, const void * elem2 ) {unsigned int * p1 = (unsigned int *) elem1;unsigned int * p2 = (unsigned int *) elem2;return (*p1 % 10 ) - (*p2 % 10 );} #define NUM 5 int main () {unsigned int an[NUM] = { 8 , 123 , 11 , 10 , 4 };qsort (an, NUM, sizeof (unsigned int ), MyCompare);for (int i = 0 ; i < NUM; i ++ )printf ("%d " , an[i]); return 0 ;}