多态与虚函数(理论)

📅 2026/7/13 15:44:16 👁️ 阅读次数 📝 编程学习
多态与虚函数(理论)

多态与虚函数(理论)

一、多态概述

多态是面向对象编程的三大特性之一(封装、继承、多态),指同一接口,不同实现

// 同一接口 speak(),不同实现 Animal* p1 = new Dog(); Animal* p2 = new Cat(); p1->speak(); // 输出: 汪汪 p2->speak(); // 输出: 喵喵

1.1 多态的两种类型

多态类型实现方式绑定时机示例
编译时多态函数重载、运算符重载、模板编译期void func(int)void func(double)
运行时多态继承 + 虚函数 + 指针/引用运行期基类指针调用派生类重写的函数

1.2 多态的目的

建立通用的程序:编写面向基类的代码,可以处理所有派生类对象,无需为每个派生类单独编写代码。

二、编译时多态(静态多态)

编译时多态在编译阶段就确定了函数调用,编译器根据参数类型、个数选择匹配的函数。

2.1 函数重载

#include<iostream>usingnamespacestd;// 函数名相同,参数不同 → 函数重载voidprint(intx){cout<<"int: "<<x<<endl;}voidprint(doublex){cout<<"double: "<<x<<endl;}voidprint(constchar*s){cout<<"string: "<<s<<endl;}intmain(){print(10);// 调用 print(int)print(3.14);// 调用 print(double)print("hello");// 调用 print(const char*)return0;}

编译时确定:编译器根据实参类型,在编译时直接生成调用对应函数的指令。

2.2 运算符重载

classComplex{doublereal,imag;public:Complex(doubler=0,doublei=0):real(r),imag(i){}// 运算符重载Complexoperator+(constComplex&other)const{returnComplex(real+other.real,imag+other.imag);}};intmain(){Complexa(1,2),b(3,4);Complex c=a+b;// 编译时确定为 a.operator+(b)return0;}

2.3 函数模板

template<typenameT>TmaxValue(T a,T b){returna>b?a:b;}intmain(){intx=maxValue(10,20);// 实例化 int maxValue(int, int)doubley=maxValue(3.14,2.71);// 实例化 double maxValue(double, double)return0;}
编译时的多态: 通过函数的重载和运算符的重载来实现 运行时的多态: 指在程序执行前,无法根据函数和参数来确定该调用哪一个函数,必须在程序执行过程中, 根据执行的具体情况来动态的确定。 它是通过类继承关系 public 和 虚函数来实现的。 目的也是建立一种通用的程序。

三、运行时多态(动态多态)

运行时多态在程序执行时才确定调用哪个函数,通过继承 + 虚函数 + 基类指针/引用实现。

运行时多态的设计思想:
对于相关的类型,确定它们之间的一些共同特征(属性和方法),将共同特征被转移到基类中,然后在基类中,把这些共同的函数 或 方法声明为公有虚函数接口
然后使用派生类继承基类,并且在派生类中重写这些虚函数,已完成具体的功能

​ 客户端代码(操作函数)通过基类的引用 或 指针来指向这些派生类型对象,对虚函数的调用会自动绑定到派生类对象重写的虚函数

3.1 运行时多态的三个必要条件

1. 公有继承(public inheritance) └─ 派生类 "is-a" 基类 2. 虚函数(virtual function) └─ 基类中声明为 virtual,派生类中重写 3. 基类指针 或 引用指向派生类对象 └─ Animal* p = new Dog(); └─ Animal& r = cat;
classAnimal{public:virtualvoidspeak(){cout<<"Animal speak"<<endl;}};classDog:publicAnimal{// 1. 公有继承public:voidspeak()override{cout<<"Dog: 汪汪"<<endl;}// 2. 重写虚函数};intmain(){Animal*p=newDog();// 3. 基类指针指向派生类p->speak();// 4. 调用虚函数 → 输出 "Dog: 汪汪"return0;}

四、虚函数的定义与语法

4.1 虚函数的定义

是一个类的成员函数: virtual 返回类型 函数名(参数表)

关键字 virtual 指明该成员函数为虚函数。只能将类的成员函数定义为虚函数
当某一个类的成员函数被定义为虚函数,则由该类派生出来的所派生类中,函数始终保持虚函数的特征

classAnimal{public:// 虚函数:使用 virtual 关键字声明virtualvoideat(){cout<<"Animal eat"<<endl;}virtualvoidspeak(){cout<<"Animal speak"<<endl;}// 纯虚函数:没有函数体,=0virtualvoidmove()=0;// 使 Animal 成为抽象类};classDog:publicAnimal{public:// 重写虚函数(override 关键字可选,推荐使用)voideat()override{cout<<"Dog eat bone"<<endl;}voidspeak()override{cout<<"Dog:汪汪"<<endl;}voidmove()override{cout<<"Dog run"<<endl;}};

4.2 虚函数的重写规则

规则说明
函数名必须相同派生类中的虚函数必须与基类同名
参数列表必须相同参数类型、个数、顺序必须一致
返回类型必须相同或者协变返回类型(派生类指针/引用)
const 修饰必须相同基类有 const,派生类也必须有 const
class Base { public: virtual void func1() { } virtual void func2(int x) { } virtual void func3() const { } virtual Base* func4() { return this; } }; class Derived : public Base { public: // 正确:完全匹配 void func1() override { } // 错误:参数不同(这是隐藏,不是重写) // void func2(double x) { } // 错误:缺少 const // void func3() { } // 正确:协变返回类型(返回派生类指针) Derived* func4() override { return this; } };

4.3 不能作为虚函数的情况

情况原因
构造函数虚表指针尚未初始化
静态成员函数属于类,不属于对象,没有 this
友元函数不是成员函数
内联函数虚函数通常不会被内联(通过指针调用时)
全局函数不是成员函数
classBase{public:// 错误:构造函数不能是虚函数// virtual Base() { }// 错误:静态函数不能是虚函数// virtual static void func() { }// 错误:友元不能是虚函数// friend virtual void friendFunc();};

4.4 虚函数规则总结

规则说明示例
只能是非静态成员函数虚函数必须属于某个类的实例virtual void func();
构造函数不能是虚函数构造时虚表未建立×virtual ClassName();
析构函数可以是虚函数用于正确释放派生类资源virtual ~ClassName();
静态成员函数不能是虚函数静态函数属于类,不属于对象,没有this×static virtual void func();
友元函数不能是虚函数友元不是成员函数×friend virtual void func();
内联函数可以是虚函数但多态调用时内联可能失效virtual inline void func();
类外定义不能加 virtual只在声明处加 virtual见下方示例
classObject{public:virtualvoidfunc();// 声明处加 virtual};// 错误:定义处不能再加 virtual// virtual void Object::func() { }// 正确:定义处不加 virtualvoidObject::func(){}

五、虚函数的调用机制(动态联编 与 静态联编)

classObject{public:virtualvoidfunc(){cout<<"Object::func"<<endl;}voidnormal(){cout<<"Object::normal"<<endl;}};classBase:publicObject{public:virtualvoidfunc()override{cout<<"Base::func"<<endl;}voidnormal(){cout<<"Base::normal"<<endl;}};intmain(){Base base;Object*p=&base;// 基类指针指向派生类对象Object&r=base;// 基类引用绑定派生类对象Object obj=base;// 对象切片(不是多态)// 动态联编(运行时确定):通过指针或引用调用虚函数p->func();// 输出 "Base::func" ← 多态r.func();// 输出 "Base::func" ← 多态// 静态联编(编译时确定):通过对象调用虚函数obj.func();// 输出 "Object::func" ← 对象切片,不是多态// 非虚函数:始终静态联编p->normal();// 输出 "Object::normal"(由指针类型决定)return0;}

总结

调用方式联编类型结果
基类指针调用虚函数动态联编调用派生类版本
基类引用调用虚函数动态联编调用派生类版本
对象调用虚函数静态联编调用该类版本
任何方式调用非虚函数静态联编由调用者类型决定

六、虚函数表(vtable)机制

七、虚析构函数

7.1 为什么需要虚析构函数?

classObject{public:~Object(){cout<<"Object 析构"<<endl;}// 非虚析构};classBase:publicObject{public:int*data;Base():data(newint[100]){}~Base(){delete[]data;cout<<"Base 析构"<<endl;}};intmain(){Object*p=newBase();deletep;// × 只调用 Object::~Object(),不会调用 Base::~Base()// 内存泄漏!data 数组未被释放return0;}

7.2 正确做法:虚析构函数

classObject{public:virtual~Object(){cout<<"Object 析构"<<endl;}// 虚析构};classBase:publicObject{public:int*data;Base():data(newint[100]){}~Base()override{delete[]data;cout<<"Base 析构"endl;}};intmain(){Object*p=newBase();deletep;// 先调用 Base::~Base(),再调用 Object::~Object()return0;}

输出

Base 析构 Object 析构

八、纯虚函数与抽象类

包含纯虚函数的类称为抽象类,不能实例化

classShape{public:// 纯虚函数:没有函数体,=0virtualdoublearea()const=0;virtualdoubleperimeter()const=0;virtual~Shape(){}// 抽象类也应有虚析构};// 派生类必须实现所有纯虚函数,否则也是抽象类classCircle:publicShape{doubleradius;public:Circle(doubler):radius(r){}doublearea()constoverride{return3.14159*radius*radius;}doubleperimeter()constoverride{return2*3.14159*radius;}};intmain(){// Shape s; // 错误:不能实例化抽象类Shape*p=newCircle(5.0);// 可以定义指针cout<<p->area()<<endl;// 多态调用deletep;return0;}

九、常见问题与陷阱

问题错误示例正确做法
对象切片Animal a = dog;使用指针或引用
忘记虚析构~Base() {}virtual ~Base() {}
重写时参数不同void func(int x)vsvoid func(double x)参数必须相同
类外定义加 virtualvirtual void func() {}只在声明处加
构造函数虚函数virtual ClassName() {}不允许

十、总结

概念说明
虚函数virtual修饰的成员函数,支持动态绑定
纯虚函数virtual void f() = 0;,使类成为抽象类
抽象类包含纯虚函数的类,不能实例化
重写(override)派生类重新定义基类的虚函数
协变返回类型派生类重写时可返回派生类指针/引用
虚析构函数确保通过基类指针删除派生类对象时正确释放资源
动态联编运行时确定函数调用,通过 vptr/vtable 实现
静态联编编译时确定函数调用,效率更高