多态与虚函数(理论)
多态与虚函数(理论)
一、多态概述
多态是面向对象编程的三大特性之一(封装、继承、多态),指同一接口,不同实现。
// 同一接口 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) | 参数必须相同 |
| 类外定义加 virtual | virtual void func() {} | 只在声明处加 |
| 构造函数虚函数 | virtual ClassName() {} | 不允许 |
十、总结
| 概念 | 说明 |
|---|---|
| 虚函数 | 用virtual修饰的成员函数,支持动态绑定 |
| 纯虚函数 | virtual void f() = 0;,使类成为抽象类 |
| 抽象类 | 包含纯虚函数的类,不能实例化 |
| 重写(override) | 派生类重新定义基类的虚函数 |
| 协变返回类型 | 派生类重写时可返回派生类指针/引用 |
| 虚析构函数 | 确保通过基类指针删除派生类对象时正确释放资源 |
| 动态联编 | 运行时确定函数调用,通过 vptr/vtable 实现 |
| 静态联编 | 编译时确定函数调用,效率更高 |
