多态与虚函数
- 这是一个经典陷阱。C++ 的访问控制检查在编译期基于指针的静态类型**(基类)。只要基类中该虚函数是
public,即使派生类里把它override成了private,通过基类指针调用时编译器仍然允许,运行时动态绑定到派生类的私有版本——完全可以调用。
- 封装——类的抽象和设计/可见性
- 继承——基类派生派生类/代码继承
- 多态——函数重载/?
虚函数
带 virtual 关键字的成员函数
- 只用在类定义的函数声明中,写函数体不用
- 静态成员函数不能是虚函数
- 基类函数声明,派生类自动虚函数(同名函数)
class base{
virtual int get();
//int virtual get(); //二者等价
};
int base::get(){
...
}
多态表现形式
表现形式1
派生类的指针可以赋给基类指针
通过基类指针调用基类和派生类中的同名虚函数时
若该指针 指向的是一个基类的对象,被调用的是基类的虚函数(派生类则派生类的虚函数)
CBase * r= &ODerived; r.VirtualFunction(); // 根据 指针/引用指向的对象类型,来决定调用的函数

表现形式2
派生类的对象可以赋给基类引用
通过基类引用调用基类和派生类中的同名虚函数时
若该引用 引用的是一个基类的对象,被调用的是基类的虚函数(派生类则派生类的虚函数)
CBase & r=ODerived; r.VirtualFunction(); // 根据 指针/引用指向的对象类型,来决定调用的函数

一个小例子
例子一:
#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; }
};
// 修改后的示例代码(派生类 Print 不写 virtual,效果相同)
/*
class A {
public:
virtual void Print() { cout << "A::Print" << endl; }
};
class B : public A {
public:
void Print() { cout << "B::Print" << endl; }
};
class D : public A {
public:
void Print() { cout << "D::Print" << endl; }
};
class E : public B {
public:
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(); // 输出: A::Print
pa = pb;
pa->Print(); // 输出: B::Print
pa = pd;
pa->Print(); // 输出: D::Print
pa = pe;
pa->Print(); // 输出: E::Print
return 0;
}
代码说明
- 多态核心:
A类中virtual,派生类重写该函数后,通过基类指针pa调用时,会根据指针实际指向的对象类型,自动调用对应类的- virtual 继承特性:基类函数声明为
virtual后,所有派生类中同名函数自动保持虚函数特性,即使不写virtual关键字,多态效果也完全一致- 继承关系:
E继承自B,B和D继承自A,因此E也间接继承自A,可以被A*指针指向。
例子二
#include <iostream>
using namespace std;
// 基类 myclass
class myclass {
public:
void reset();
// virtual 关键字可写在返回值前或后
virtual void print() {
cout << "myclass::print\n";
}
};
// 基类普通成员函数 reset,内部调用虚函数 print
void myclass::reset() {
print(); // 等价于 this->print(); this 是基类指针,会触发多态
}
// 派生类 derived,继承自 myclass
class derived : public myclass {
public:
// 重写基类虚函数 print
virtual void print() {
cout << "derived::print\n";
}
};
int main() {
myclass b;
derived d;
b.reset(); // 调用 myclass::reset(),内部调用 myclass::print()
d.reset(); // 调用 myclass::reset(),内部调用 derived::print()(多态)
return 0;
}
运行结果
myclass::print
derived::print
多态的价值
场景 手动写 A::多态(virtual) 适用范围 只在你明确知道类型时能用 哪怕不知道对象类型,也能正确调用 扩展性 新增派生类时,要到处改代码 新增派生类时,原有代码完全不用改 核心价值 语法糖 实现开闭原则:对扩展开放,对修改关闭 增强可扩充性
要改代码少
多态的作用
多态的实质
- 父类定义共同接口,子类不同实现
- 通过父类以相同方式操作不同子类行为
多态的实现实例
实例1
游戏:6只怪兽、每只怪兽可以攻击、被击后的扣生命值、被击后的后摇(每只怪兽可能不同)
非多态反面案例
基本思路
- 为每个怪兽编写
attack,fightback,hurted成员函数 - Attack:
- 被攻击怪兽Hurted
- 被攻击怪兽Fightback
- Hurted:自身生命值减少
- FightBack:反击并调用被反击对象的Hurted
传统的实现方式:
- 怪兽A:攻击其他5个怪兽的函数、被其他5个怪兽攻击后摇(扣生命值)的5个函数
- 怪兽B:……
- ……
若增加一个怪兽,则每个怪兽都要修改——平方型代码量增加,代码KPI轻松完成
多态做法
基类派生
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
基类:
class CCreature{
protected:
int m_nLiveValue,m_nPower;
public:
virtual void Attack(CCreature *pCreature);
virtual void Hurted(int nPower);
virtual void FrightBack(CCreature *pCreature);
};
派生:
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);
}
于是,新怪兽只需要编写新类,不需要专门增加传入新怪物的成员函数
具体使用的时候:
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实现代码:
#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]; //用来存放各种几何形体, 假设不超过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:
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是基类指针,触发动态联编,调用派生类虚函数
多态的实现原理
面向对象编程的三个基本概念
- 数据抽象 ↔ 用类进行数据抽象,封装
- 继承 ↔ 派生类继承基类成员
- 动态绑定 ↔ 多态,编译器在运行时决定调用基类/派生类函数
动态联编
一条函数调用语句在编译时无法确定调用哪个函数,运行到该语句时才确定调用哪个函数,这种机制叫动态联编
为什么需要动态联编?
class A { public: virtual void Get(); };
class B : public A { public: virtual void Get(); };
void MyFunction( A * pa ) {
pa->Get();
}
编译时无法确定
pa指向A/B对象,只能运行时判断调用哪个Get()
多态的实现解析
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;
}
Base::fun1()
Derived::fun2()
补充说明:
调用次序:Base::fun1() -> Derived::fun2()fun1()中fun2()等价于this->fun2(),fun2是虚函数+基类指针,触发动态联编;运行时this指向派生类对象,调用派生类函数。
稍加修改的例子
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,直接调用自身版本
虚函数表(多态的核心实现)
思考例子
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 - 程序调用派生类函数
虚函数的访问权限
访问权限检查是根据指针/引用的类型,而非运行时指向的对象类型。
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是私有成员,语法检查直接报错
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(); // 编译通过,运行调用Derived::fun2()
return 0;
}
编译通过:基类函数公有→语法检查通过;运行动态绑定派生类私有函数
构造函数和析构函数中调用虚函数
⚠️规则:构造/析构函数中调用虚函数为静态联编,仅调用当前类/基类函数;普通成员函数中才是动态联编。
例子
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;
}
输出
hello from son
constructing grandson
hello from grandson
destructing grandson
bye from myclass
原因
- 构造时:先调用基类构造,派生类未初始化,对象身份为基类,调用基类虚函数
- 普通调用:对象构造完成,动态绑定派生类函数
- 析构时:先析构派生类,对象身份回到基类,调用基类函数
测一测
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();
//64位程序用long long
long * p1 = (long * ) & a;
long * p2 = (long * ) pa;
* p2 = * p1;
//把 A 的 vptr 复制过去,覆盖 B 的 vptr
pa->Func();
return 0;
}
输出结果:B::Func 、A::Func
解析:
- 第一次调用:指向B对象→调用B::Func
- 赋值操作:将A的虚表指针覆盖B的虚表指针
- 第二次调用:通过A的虚表→调用A::Func
long * p1 = (long *) &a; // p1 指向 A 对象的首地址(把它当成 8 字节整数来看) long * p2 = (long *) pa; // p2 指向 B 对象的首地址(也当成 8 字节整数) * p2 = * p1; // 只复制了这 8 个字节!和对象赋值完全不是一回事,把 A 对象的前 8 个字节(恰好是它的 vptr)复制到 B 对象的前 8 个字节位置,覆盖了 B 的 vptr。
B 对象的其他部分——如果它有数据成员的话——完全没被碰。
虚析构函数
问题背景
⚠️⚠️通过基类指针删除派生类对象,仅调用基类析构函数,派生类资源未释放,造成内存泄漏。
例子
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。
通过基类指针删除对象时,先调用派生类析构,再调用基类析构。
例子
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;
class A {
private: int a;
public:
virtual void Print( ) = 0 ; //纯虚函数
void fun() { cout << "fun"; }
};
抽象类
包含纯虚函数的类为抽象类,特性:
- 不能创建抽象类对象,只能作为基类派生
- 抽象类指针/引用可指向派生类对象
- 成员函数可调用纯虚函数;构造/析构函数不能调用
- 派生类必须实现所有纯虚函数,否则仍为抽象类
示例
class A {
public:
virtual void f() = 0; //纯虚函数
A( ){ }
void g( ) { f( ) ; } //等价this->f(),合法
//A(){ f(); } 错误:构造函数不能调用纯虚函数
};
class B : public A{
public:
void f(){ cout<<"B:f()"<<endl ; }
};
int main(){
B b;
b.g( ); //输出B:f()
return 0;
}
注意:构造函数调用纯虚函数,虚表无实现,程序崩溃
抽象类不可以生成对象, 都包含纯虚函数,抽象类的纯虚函数可以有函数体,派生类可以调用抽象类的带函数体的纯虚函数,需要实例化纯虚函数后才可以生成对象,但是派生类还是要自己实现纯虚函数
访问机制对比(隐藏 vs 虚函数)
隐藏
由指针/引用的类型决定调用:
- 派生类指针→调用派生类同名函数
- 基类指针→调用基类同名函数(即使指向派生类)
虚函数
由指针/引用指向的对象类型决定调用:
- 指向基类→调用基类虚函数
- 指向派生类→派生类虚函数
总结
- 虚函数和多态:基类定义接口,派生类实现;通过基类指针/引用实现动态调用
- 多态的作用:提升程序扩展性,新增功能无需修改原有执行代码
- 多态的实现:依赖虚函数表+虚表指针,运行时动态查找函数
- 访问权限:语法检查基于指针类型,基类虚函数建议设为public
- 纯虚函数与抽象类:纯虚函数=0;抽象类不能实例化,派生类必须实现所有纯虚函数
- 虚函数调用:普通函数→动态联编;构造/析构→静态联编
- 虚析构函数:(如果一个类有任何虚函数,或者意图作为多态基类被继承)含虚函数的类必须定义虚析构,避免内存泄漏;构造函数不能为虚函数
补充:函数指针与qsort库函数
函数指针-基本概念
函数名是函数的入口地址;指向函数的指针称为函数指针,可通过指针调用函数。
⚠️函数指针-定义形式
类型名(*指针变量名)(参数类型1, 参数类型2, ...);
- 类型名:函数返回值类型
- 括号内:函数参数类型
⚠️示例**:int (*pf)(int, char);
函数指针-使用方法
- 用函数名给函数指针赋值
指针名(实参)调用函数
示例
#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); //输出4
return 0;
}
函数指针和qsort库函数
C快速排序函数,可排序任意类型数组:
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使用示例
按数字个位数排序:
#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]); //输出10 11 123 4 8
return 0;
}
