多态与虚函数(理论)
多态与虚函数(理论)一、多态概述多态是面向对象编程的三大特性之一封装、继承、多态指同一接口不同实现。// 同一接口 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 函数重载#includeiostreamusingnamespacestd;// 函数名相同参数不同 → 函数重载voidprint(intx){coutint: xendl;}voidprint(doublex){coutdouble: xendl;}voidprint(constchar*s){coutstring: sendl;}intmain(){print(10);// 调用 print(int)print(3.14);// 调用 print(double)print(hello);// 调用 print(const char*)return0;}编译时确定编译器根据实参类型在编译时直接生成调用对应函数的指令。2.2 运算符重载classComplex{doublereal,imag;public:Complex(doubler0,doublei0):real(r),imag(i){}// 运算符重载Complexoperator(constComplexother)const{returnComplex(realother.real,imagother.imag);}};intmain(){Complexa(1,2),b(3,4);Complex cab;// 编译时确定为 a.operator(b)return0;}2.3 函数模板templatetypenameTTmaxValue(T a,T b){returnab?a:b;}intmain(){intxmaxValue(10,20);// 实例化 int maxValue(int, int)doubleymaxValue(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(){coutAnimal speakendl;}};classDog:publicAnimal{// 1. 公有继承public:voidspeak()override{coutDog: 汪汪endl;}// 2. 重写虚函数};intmain(){Animal*pnewDog();// 3. 基类指针指向派生类p-speak();// 4. 调用虚函数 → 输出 Dog: 汪汪return0;}四、虚函数的定义与语法4.1 虚函数的定义是一个类的成员函数: virtual 返回类型 函数名参数表关键字 virtual 指明该成员函数为虚函数。只能将类的成员函数定义为虚函数当某一个类的成员函数被定义为虚函数则由该类派生出来的所派生类中函数始终保持虚函数的特征classAnimal{public:// 虚函数使用 virtual 关键字声明virtualvoideat(){coutAnimal eatendl;}virtualvoidspeak(){coutAnimal speakendl;}// 纯虚函数没有函数体0virtualvoidmove()0;// 使 Animal 成为抽象类};classDog:publicAnimal{public:// 重写虚函数override 关键字可选推荐使用voideat()override{coutDog eat boneendl;}voidspeak()override{coutDog:汪汪endl;}voidmove()override{coutDog runendl;}};4.2 虚函数的重写规则规则说明函数名必须相同派生类中的虚函数必须与基类同名参数列表必须相同参数类型、个数、顺序必须一致返回类型必须相同或者协变返回类型派生类指针/引用const 修饰必须相同基类有 const派生类也必须有 constclass 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(){coutObject::funcendl;}voidnormal(){coutObject::normalendl;}};classBase:publicObject{public:virtualvoidfunc()override{coutBase::funcendl;}voidnormal(){coutBase::normalendl;}};intmain(){Base base;Object*pbase;// 基类指针指向派生类对象Objectrbase;// 基类引用绑定派生类对象Object objbase;// 对象切片不是多态// 动态联编运行时确定通过指针或引用调用虚函数p-func();// 输出 Base::func ← 多态r.func();// 输出 Base::func ← 多态// 静态联编编译时确定通过对象调用虚函数obj.func();// 输出 Object::func ← 对象切片不是多态// 非虚函数始终静态联编p-normal();// 输出 Object::normal由指针类型决定return0;}总结调用方式联编类型结果基类指针调用虚函数动态联编调用派生类版本基类引用调用虚函数动态联编调用派生类版本对象调用虚函数静态联编调用该类版本任何方式调用非虚函数静态联编由调用者类型决定六、虚函数表vtable机制七、虚析构函数7.1 为什么需要虚析构函数classObject{public:~Object(){coutObject 析构endl;}// 非虚析构};classBase:publicObject{public:int*data;Base():data(newint[100]){}~Base(){delete[]data;coutBase 析构endl;}};intmain(){Object*pnewBase();deletep;// × 只调用 Object::~Object()不会调用 Base::~Base()// 内存泄漏data 数组未被释放return0;}7.2 正确做法虚析构函数classObject{public:virtual~Object(){coutObject 析构endl;}// 虚析构};classBase:publicObject{public:int*data;Base():data(newint[100]){}~Base()override{delete[]data;coutBase 析构endl;}};intmain(){Object*pnewBase();deletep;// 先调用 Base::~Base()再调用 Object::~Object()return0;}输出Base 析构 Object 析构八、纯虚函数与抽象类包含纯虚函数的类称为抽象类不能实例化classShape{public:// 纯虚函数没有函数体0virtualdoublearea()const0;virtualdoubleperimeter()const0;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*pnewCircle(5.0);// 可以定义指针coutp-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 实现静态联编编译时确定函数调用效率更高