尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

【C++】多态——为什么父类指针能调用子类的函数?

【C++】多态——为什么父类指针能调用子类的函数? 目录多态的概念概念满足多态的三要素虚函数的重写/覆盖多态失效场景经典面试题协变析构函数重写纯虚函数和抽象类多态的原理核心总结表多态的概念概念同一个行为函数调用传不同的对象做不同的事。生活场景买票你去火车站买票你是谁买票结果普通人全价 100 元学生半价 50 元凭学生证军人优先购票 优惠同一个行为“买票”不同身份的人执行结果完全不同。这就是多态同一个接口买票多种形态全价/半价/优先。满足多态的三要素要素说明① 继承必须有基类和派生类② 虚函数重写派生类重写基类的虚函数③ 基类指针/引用调用时用基类指针或引用指向派生类对象满足这三条编译器就在运行时根据对象实际类型决定调用哪个函数。虚函数的重写/覆盖派生类虚函数和基类的虚函数的返回值函数名参数列表完全一样注意派生类重写基类虚函数时可以不加virtual因为基类的virtual属性会被“遗传”下来。但这不是好习惯实际开发中建议加上override关键字来明确表示重写。class Person { public: virtual void BuyTicket() { cout 全价买票 endl; } //void BuyTicket() { cout 全价买票 endl; }写成这样就不行 }; class Student : public Person { public: virtual void BuyTicket() { cout 半价买票 endl; } //派生类的virtual可以不写也可以写 }; class Soldier : public Person { public: virtual void BuyTicket() { cout 优先买票 endl; } }; // 关键用一个基类指针传不同对象表现不同行为 void Buy(Person* p) { p-BuyTicket(); // 同一个调用不同表现 } int main() { Person p; Student s; Soldier sol; Buy(p); // 输出全价买票——p是person类型的调用person的函数 Buy(s); // 输出半价买票——p是student类型的调用student的函数 Buy(sol); // 输出优先买票——p是Soldier类型的调用Soldier的函数 return 0; }多态失效场景场景一基类没有virtualclass Person { public: void BuyTicket() { cout 全价买票 endl; } // 没有 virtual }; class Student : public Person { public: void BuyTicket() { cout 半价买票 endl; } // 不是重写是隐藏 }; class Soldier : public Person { public: void BuyTicket() { cout 优先买票 endl; } // 不是重写是隐藏 }; void Buy(Person* p) { p-BuyTicket(); // 编译时就已经确定调用 Person::BuyTicket() } int main() { Person p; Student s; Soldier sol; Buy(p); // 输出全价买票 Buy(s); // 输出全价买票 ← 多态失效 Buy(sol); // 输出全价买票 ← 多态失效 return 0; }场景二不是指针和引用的调用class Person { public: virtual void BuyTicket() { cout 全价 endl; } }; class Student : public Person { public: virtual void BuyTicket() { cout 半价 endl; } }; int main() { Student s; Person p s; // 切片用对象接收 p.BuyTicket(); // 输出全价多态失效 Person r s; // 用引用 r.BuyTicket(); // 输出半价多态生效 Person* ptr s; // 用指针 ptr-BuyTicket(); // 输出半价多态生效 return 0; }原因用对象调用 → 编译时就确定了调用哪个函数用指针/引用 → 运行时到虚表中查找Person p s;还会发生切片派生类部分被切掉场景3函数名/参数/返回值不匹配不是重写是隐藏class Person { public: virtual void BuyTicket() { cout 全价 endl; } }; class Student : public Person { public: // 参数不同 → 不是重写是隐藏 virtual void BuyTicket(int discount) { cout 半价 discount endl; } }; int main() { Student s; Person* p s; p-BuyTicket(); // 输出全价多态失效 // p-BuyTicket(5); // 编译报错Person 没有 BuyTicket(int) return 0; }场景4在构造函数或析构函数中调用虚函数class Person { public: Person() { BuyTicket(); // 在构造函数中调用虚函数 } virtual void BuyTicket() { cout 全价 endl; } }; class Student : public Person { public: virtual void BuyTicket() { cout 半价 endl; } }; int main() { Student s; // 输出全价不是半价多态失效 return 0; }原因构造时先构造基类 → 派生类还没构造完成 → 对象类型还是基类 → 调用基类版本析构时先析构派生类 → 基类析构时派生类已销毁 → 对象类型变成基类 → 调用基类版本核心构造/析构期间对象的动态类型就是当前正在构造/析构的类场景5基类指针指向基类对象class Person { public: virtual void BuyTicket() { cout 全价 endl; } }; class Student : public Person { public: virtual void BuyTicket() { cout 半价 endl; } }; int main() { Person p; // 基类对象 Person* ptr p; // 指向基类对象 ptr-BuyTicket(); // 输出全价不是多态失效而是本来就是基类对象 return 0; }场景6普通函数非成员函数不能是虚函数// 编译报错非成员函数不能加 virtual virtual void func() { cout hello endl; }原因virtual是成员函数专属依赖this指针和虚表。场景7静态函数不能是虚函数class Person { public: // 编译报错静态函数不能是 virtual virtual static void func() { cout hello endl; } };原因静态函数属于类不属于对象不需要this无法使用虚表。经典面试题#include iostream using namespace std; class A { public: virtual void func(int val 1) { cout A- val endl; } virtual void test() { func(); } }; class B : public A { public: void func(int val 0) { cout B- val endl; } }; int main() { B* p new B; p-test(); // 输出B-1 p-func(); // 输出B-0正常情况不是多态 return 0; }第1步B* p new B;创建一个B对象p指向这个对象第2步p-test();B类没有重写test()所以调用的是从A继承来的A::test()进入A::test()函数第3步A::test()内部执行func();func()是虚函数this是p指向B对象虚函数机制会去B的虚表中找func→ 找到B::func所以调用的是B::func第4步调用B::func时默认参数是哪个关键规则虚函数的默认参数值是静态绑定的编译时确定而不是动态绑定的运行时确定。func()的调用写在A::test()中编译器在编译A::test()时看到func()的声明来自A类所以编译器从A::func的声明中取默认参数 1虽然运行时实际调用的是B::func但默认参数已经在编译时被固定为1了第5步输出调用B::func(int val)val 1输出B-1协变定义基类虚函数返回基类指针/引用派生类重写时允许返回派生类指针/引用。class A {}; class B : public A {}; class Person { public: virtual A* GetObj() { // 基类返回 A* cout Person::GetObj endl; return nullptr; } }; class Student : public Person { public: virtual B* GetObj() { // 派生类返回 B*协变 cout Student::GetObj endl; return nullptr; } }; int main() { Person* p1 new Person; Person* p2 new Student; A* a1 p1-GetObj(); // 调用 Person::GetObj返回 A* A* a2 p2-GetObj(); // 调用 Student::GetObj返回 B*隐式转成 A* return 0; }析构函数重写定义基类析构函数加virtual后派生类析构函数无论是否加virtual都与基类析构函数构成重写。编译器对析构函数名做了特殊处理所有析构函数统一被处理成destructor()。因此他们的名字编译器是看成是一样的。class A { public: virtual ~A() { } // 析构函数名~A }; class B : public A { public: ~B() { } // 析构函数名~B看起来不同,实际上编译器把他们看成了一个destructor };class A { public: virtual ~A() { cout ~A() endl; } // 加 virtual }; class B : public A { public: ~B() { cout ~B() endl; } // 构成重写不需要加 virtual }; int main() { A* p new B; delete p; // 输出~B() → ~A() ← 先调 B 的再调 A 的 return 0; //如果~A不加virtual多态失效就不会调用B的析构相当于B的对象给A切片了 }纯虚函数和抽象类纯虚函数 只声明不实现的函数相当于“空接口”——虚函数后面加0抽象类 含有纯虚函数的类相当于“设计图纸”不能直接拿来住人// 抽象类汽车设计图纸 class Car { public: // 纯虚函数只声明不实现 virtual void Drive() 0; // 0 表示纯虚函数 }; // 正确派生类实现纯虚函数后可以实例化 class Benz : public Car { public: virtual void Drive() override { cout Benz 舒适驾驶 endl; // 实现了纯虚函数 } }; class BMW : public Car { public: virtual void Drive() override { cout BMW 操控驾驶 endl; // 实现了纯虚函数 } }; int main() { // Car c; // 不能实例化 Car Benz b; // Benz 实现了纯虚函数可以实例化 BMW bmw; // BMW 实现了纯虚函数可以实例化 Car* p1 new Benz; // 基类指针指向派生类对象 Car* p2 new BMW; p1-Drive(); // 输出Benz 舒适驾驶 p2-Drive(); // 输出BMW 操控驾驶 return 0; }多态的原理多态的底层 虚函数表指针(_vfptr)虚函数表vftable 动态绑定运行时查表#include iostream using namespace std; 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 _a 1; }; int main() { Base b; cout sizeof(b) endl; // 8_a__vfptr return 0; }b对象实例化后真正的内存大小是_a和一个虚表指针。只要有虚函数对象就会多一个隐藏指针 _vfptr(虚函数表指针这个虚表指针在对象内存的最前面不同的编译器可能不同VS在最前面非func不占用对象内存函数代码在代码段#include iostream using namespace std; class Base { public: virtual void func1() { cout Base::func1 endl; } virtual void func2() { cout Base::func2 endl; } }; class Derive : public Base { public: virtual void func1() override { cout Derive::func1 endl; } virtual void func3() { cout Derive::func3 endl; } }; void Call(Base* p) { p-func1(); // 这里到底调用谁的 func1 } int main() { Base b; Derive d; Call(b); // 输出Base::func1 Call(d); // 输出Derive::func1 return 0; } 第1步调用 Call(d) │ ▼ 第2步进入 Call 函数p 指向 d 对象 │ ▼ 第3步执行 p-func1() │ │ 编译器看到 │ - p 是 Base* 类型 │ - func1 是虚函数 │ - 所以不能直接确定地址要去虚表查 ▼ 第4步通过 p 找到指向的对象d 对象 │ ▼ 第5步从 d 对象中取出 __vfptr │ ▼ 第6步从虚表中取出 func1 对应的函数地址 │ │ 因为 d 是 Derive 类型虚表中 func1 存的是 Derive::func1 ▼ 第7步调用 Derive::func1() │ ▼ 输出Derive::func1核心总结表概念说明位置__vfptr虚函数表指针对象内部隐藏指针对象内存中栈/堆vftable虚函数表存放虚函数地址的数组代码段/常量区虚函数带virtual的成员函数代码段动态绑定运行时到虚表查地址调用多态调用时静态绑定编译时确定地址直接调用非虚函数调用时
返回列表