C++:多态
一、多态的概念
面向对象的三大特性:封装、继承、多态。多态即多种形态,具体的来说就是,针对同一种行为,不同的对象执行其会产生不同的结果。而之所以“不同的对象”会执行同一种行为,是因为这些对象具有继承关系。
- 静态多态:就是在编译时就已经确定的多态,编译器在编译时根据函数类型,推断出要调用那个函数。静态多态可以认为就是函数重载。
- 动态多态:父类对象的指针或引用去调用被重写的一个函数,能实现不同的结果。所谓动态是运行时确定的,根据调用对象的不同来确定调用那个函数。
二、多态的定义及实现
1.重写/覆盖 的要求
重写/覆盖: 子类中有一个跟父类完全相同的虚函数,子类的虚函数重写了基类的虚函数
即:子类父类都有这个虚函数 + 子类的虚函数与父类虚函数的 函数名/参数/返回值 都相同 -> 重写/覆盖 (注意:参数只看类型是否相同,不看缺省值)
2.多态两个要求
(1)被调用的函数必须是虚函数,子类对父类的虚函数进行重写 (重写:三同(函数名/参数/返回值)+虚函数)
(2)父类指针或者引用去调用虚函数。
3.多态的切片示意图
(1)示例1:给一个student的子类对象(临时对象也行),然后把这个对象赋给一个父类指针,通过这个父类指针就可以访问student子类的虚拟函数
(2)示例2:假设B是子类,A是父类,new一个B类的临时对象,然后把这个临时对象赋给一个父类指针A* p2,通过这个父类指针p2就可以访问子类B的虚拟函数func
class A { public: virtual void func(int val = 1){ std::cout << "A->" << val << std::endl; } virtual void test(){ func(); } }; class B : public A { public: void func(int val = 0){ std::cout << "B->" << val << std::endl; } }; int main(int argc, char* argv[]) { B*p1 = new B; //p1->test(); 这个是多态调用,下有讲解 二->6 p1->func(); //普通调用 A*p2 = new B; p2->func(); //多态调用 return 0; }
4.多态演示
class Person { public: Person(const char* name) :_name(name) {} // 虚函数 virtual void BuyTicket() { cout << _name << "Person:买票-全价 100¥" << endl; } protected: string _name; //int _id; }; class Student : public Person { public: Student(const char* name) :Person(name) {} // 虚函数 + 函数名/参数/返回值 -》 重写/覆盖 virtual void BuyTicket() { cout << _name << " Student:买票-半价 50 ¥" << endl; } }; void Pay(Person& ptr) { ptr.BuyTicket(); } int main() { string name; cin >> name; Student s(name.c_str()); Pay(s); }
买票场景下的多态 完整代码:
普通人 买票时,是全价买票; 学生 买票时,是半价买票; 军人 买票时是优先买票。
class Person { public: Person(const char* name) :_name(name) {} // 虚函数 virtual void BuyTicket() { cout << _name << "Person:买票-全价 100¥" << endl; } protected: string _name; //int _id; }; class Student : public Person { public: Student(const char* name) :Person(name) {} // 虚函数 + 函数名/参数/返回值 -》 重写/覆盖 virtual void BuyTicket() { cout << _name << " Student:买票-半价 50 ¥" << endl; } }; class Soldier : public Person { public: Soldier(const char* name) :Person(name) {} // 虚函数 + 函数名/参数/返回值 -》 重写/覆盖 virtual void BuyTicket() { cout << _name << " Soldier:优先买预留票-88折 88 ¥" << endl; } }; // 多态两个要求: // 1、子类虚函数重写的父类虚函数 (重写:三同(函数名/参数/返回值)+虚函数) // 2、父类指针或者引用去调用虚函数。 //void Pay(Person* ptr) //{ // ptr->BuyTicket(); //} void Pay(Person& ptr) { ptr.BuyTicket(); } // 不能构成多态 //void Pay(Person ptr) //{ // ptr.BuyTicket(); //} int main() { int option = 0; cout << "=======================================" << endl; do { cout << "请选择身份:"; cout << "1、普通人 2、学生 3、军人" << endl; cin >> option; cout << "请输入名字:"; string name; cin >> name; switch (option) { case 1: { Person p(name.c_str()); Pay(p); break; } case 2: { Student s(name.c_str()); Pay(s); break; } case 3: { Soldier s(name.c_str()); Pay(s); break; } default: cout << "输入错误,请重新输入" << endl; break; } cout << "=======================================" << endl; } while (option != -1); return 0; }
解释 2、父类指针或者引用去调用虚函数,传值调用不构成多态。
用子类也不行,必须用父类,比如你用个student,那么你的Person或者Soldier就传不进形参
void Pay(Person* ptr) //指针调用可以 { ptr->BuyTicket(); } void Pay(Person& ptr) //引用调用可以 { ptr.BuyTicket(); } // 不能构成多态 //void Pay(Person ptr) //传值调用不可以 //{ // ptr.BuyTicket(); //}
5.虚函数重写的例外
协变(父类与子类虚函数返回值类型不同)
子类重写父类虚函数时,与父类虚函数返回值类型不同 称为协变。
虚函数重写对返回值要求有一个例外:协变,协变是子类虚函数与父类虚函数返回值类型不同,但子类和父类的返回值类型也必须是父子关系指针和引用。
子类虚函数没有写virtual,f依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f() ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 ) ps:我们自己写的时候子类虚函数也写上virtual
class A{}; class B : public A {}; // 虚函数重写对返回值要求有一个例外:协变,父子关系指针和引用 // class Person { public: virtual A* f() { cout << "virtual A* Person::f()" << endl; return nullptr; } }; class Student : public Person { public: // 子类虚函数没有写virtual,f依旧时虚函数,因为先继承了父类函数接口声明 // 重写父类虚函数实现 // ps:我们自己写的时候子类虚函数也写上virtual // B& f() { virtual B* f() { cout << "virtual B* Student::f()" << endl; return nullptr; } }; int main() { Person p; Student s; Person* ptr = &p; ptr->f(); ptr = &s; ptr->f(); return 0; }
6.接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
所以如果不实现多态,不要把函数定义成虚函数。
所以就有了 子类虚函数没有写virtual,依旧是虚函数;子类虚函数使用的是父类虚函数的缺省参数,只是重写了实现
多态的坑题目(考接口继承)
子类虚函数没有写virtual,func 依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f() ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 ) ps:我们自己写的时候子类虚函数也写上virtual
p->test(),调用test中的this指针类型是A*,但指向的是对象B* p中的内容,类B中继承的test函数中又调用func函数,func函数没有写virtual 但依旧是虚函数,只要是虚函数重写就是接口继承,子类先继承了父类函数接口声明(父类接口部分是virtual void func(int va1=1) ),重写是重写父类虚函数的实现部分( 即使用子类的函数的实现部分{}内容 ),所以缺省函数用的是父类的1,实现用的子类的函数实现,打印结果是 B->1
7.析构函数的重写
析构函数名统一会被处理成destructor()
函数名处理成destructor() 才能满足多态:
如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字,都与父类的析构函数构成重写,虽然父类与子类析构函数名字不同。虽然函数名不相同,
class Person { public: virtual ~Person() {cout << "~Person()" << endl;} }; class Student : public Person { public: virtual ~Student() { cout << "~Student()" << endl; } }; int main() { Person* p1 = new Person; Person* p2 = new Student; delete p1; delete p2; return 0; }
注意:期望delete ptr调用析构函数是一个多态调用, 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数
class Person { public: virtual ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: // Person析构函数加了virtual,关系就变了 // 重定义(隐藏)关系 -> 重写(覆盖)关系 virtual ~Student() //这里virtual加不加都行 { cout << "~Student()" << endl; delete[] _name; cout << "delete:" << (void*)_name << endl; } private: char* _name = new char[10]{ 'j','a','c','k' }; }; int main() { // 对于普通对象是没有影响的 //Person p; //Student s; // 期望delete ptr调用析构函数是一个多态调用 // 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数 Person* ptr = new Person; delete ptr; // ptr->destructor() + operator delete(ptr) ptr = new Student; delete ptr; // ptr->destructor() + operator delete(ptr) return 0; }
8.C++11 override 和 final

(2)override :override写在子类中,要求严格检查是否完成重写,如果没有完成重写就报错
override的作用时让编译器帮助用户检测是否派生类是否对基类总的某个虚函数进行重写,如 果重写成功,编译 通过,否则,编译失败,因此 override作用发生在编译时。
override只能修饰子类的虚函数
override修饰子类成员函数虚函数时,编译时编译器会自动检测是否对基类中那个成员函数进行重写。(在子类里面是可以自己增加 成员函数的,如果这个成员函数不是虚函数,就不可以进行修饰)
示例:如果父类没写virtual能检查出来并报错
9.重载、覆盖(重写)、隐藏(重定义)的对比
(只有重写要求原型相同,原型相同就是指 函数名/参数/返回值都相同)
函数重载:在同一个作用域中,两个函数的函数名相同,参数个数,参数类型,参数顺序至少有一个不同,函数返回值的类型可以相同,也可以不相同。
重定义(也叫做隐藏)是指在继承体系中,子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) ,此时子类的函数会屏蔽掉父类的那个同名函数。
重写(也叫做覆盖)是指在继承体系中子类定义了和父类函数名,函数参数,函数返回值完全相同的虚函数。此时构成多态,根据对象去调用对应的函数。
10.抽象类
(1)子类继承后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。
(2)父类的纯虚函数强制了派生类必须重写,才能实例化出对象(跟override异曲同工,override是放在子类虛函数,检查重写。功能有一些重叠和相似 )
另外纯虚函数更体现出了接口继承。
(3)纯虚函数也可以写实现{ },但没有意义,因为是接口继承,{ }中的实现会被重写;父类没有对象,所以无法调用纯虚函数
抽象类 -- 在现实一般没有具体对应实体 不能实例化出对象 间接功能:要求子类需要重写,才能实例化出对象 class Car { public: virtual void Drive() = 0; // // 实现没有价值,因为没有对象会调用他 // /*virtual void Drive() = 0 // { // cout << " Drive()" << endl; // }*/ }; class Benz :public Car { public: virtual void Drive() { cout << "Benz-舒适" << endl; } }; class BMW :public Car { public: virtual void Drive() { cout << "BMW-操控" << endl; } }; void Test() { Car* pBenz = new Benz; pBenz->Drive(); Car* pBMW = new BMW; pBMW->Drive(); }
三.多态的原理
1.虚函数介绍
被virtual修饰的成员函数称为虚函数,虚函数的作用是用来实现多态,只有在需要实现多态时,才需要将成员函数设置成虚函数,否则没有必要
2.虚函数表
和菱形虚拟继承的虚基表不一样,那个存的是偏移量
// 这里常考一道笔试题:sizeof(Base)是多少? class Base { public: virtual void Func1() { cout << "Func1()" << endl; } private: int _b = 1; };
通过观察测试我们发现b对象是8bytes,除了_b成员(int4个bytes),还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
那么派生类中这个表放了些什么呢?我们接着往下分析
3.虚表存储
class Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } virtual void Func2() { cout << "Base::Func2()" << endl; } void Func3() { cout << "Base::Func3()" << endl; } private: int _b = 1; }; class Derive : public Base { public: virtual void Func1() { cout << "Derive::Func1()" << endl; } void Func3() { cout << "Derive::Func3()" << endl; } private: int _d = 2; }; int main() { cout << sizeof(Base) << endl; Base b; cout << sizeof(Derive) << endl; Derive d; Base* p = &b; p->Func1(); p->Func3(); p = &d; p->Func1(); p->Func3(); // /*Base& r1 = b; 引用也是多态调用 // r1.Func1(); // r1.Func3(); // // Base& r2 = d; // r2.Func1(); // r2.Func3();*/ }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了