【C++修炼】面向对象—多态

发布时间:2026/11/3 10:08:35
【C++修炼】面向对象—多态 多态1. 多态的概念多态 (polymorphism)通俗来说就是多种形态。多态分为静态多态(编译时多态) 和动态多态(运行时多态)。重点是运⾏时多态。静态多态例如函数重载和函数模板就是静态多态通过参数使函数达到多种形态。函数重载靠形参模板靠模板参数根据不同类型的参数调⽤不同的重载或模板实例。之所以叫编译时多态是因为参数的类型匹配是在编译时完成的而编译时通常归为静态运⾏时通常归为动态。动态多态传不同的对象就会完成不同的⾏为 (函数)。⽐如买票普通⼈要全价买票学⽣优惠半价买票军⼈免排队优先买票。再⽐如动物的叫声传cat类对象过去就是喵喵叫传dog类对象过去就是汪汪叫。2. 多态的定义及实现2.1 多态的构成条件多态是继承关系下不同类对象调⽤同⼀函数产⽣了不同的⾏为。⽐如Student继承父类PersonPerson对象原价买票Student对象半价买票。(重载或模板本质是根据不同参数类型实例化不同函数进行调用前者由用户进行后者交给编译器)。实现多态的两个重要条件是被调⽤的函数必须是虚函数且子类完成了虚函数的重写/覆盖。完成重写才可以实现不同行为达到多种形态的效果。必须是父类的指针或引⽤调⽤虚函数。因为只有父类的指针或引⽤才能既指向父类对象⼜指向子类对象。2.2 虚函数在前面继承章节我们提到了虚继承和菱形虚继承。所谓虚继承就是定义子类时在继承方式前加上virtual。虚函数同样用到virtual关键字但它们是两回事只是恰好用同一个关键字而言一个用于继承关系一个用于函数。类的成员函数前加virtual修饰该成员函数称为虚函数。强调⾮成员函数不行。代码示例 2.2.1class Person { public: // 在成员函数前加 virtual 成为虚函数注意必须是成员函数 virtual void BuyTicket() { cout 买票全价 endl; } };2.3 虚函数的重写/覆盖在子类中实现⼀个跟父类有“三同”的虚函数则称子类的虚函数重写了父类的虚函数。(三同返回值类型、函数名、参数列表完全相同)注意重写父类的虚函数时子类的虚函数可以不加virtual关键字也可以构成重写。我们可以理解成父类的虚函数先被子类继承下来了所以在子类中已经具有虚函数属性再由子类进行重写。不过这种写法并不规范不建议这样使⽤但在一些笔试题中却经常会故意埋这个坑让人判断是否构成多态。总结一下通过父类的指针或引用调用虚函数父类对象都不可以。子类要完成重写即实现和父类相同的虚函数函数实现可以不同但返回值类型/函数名/参数列表必须相同。代码示例 2.3.1#include iostream #include string using namespace std; class Person { public: virtual void BuyTicket() { // 原价买票 cout Person: Buy at full price. endl; } }; class Student : public Person { public: // 在子类中完成重写 virtual void BuyTicket() { // 半价买票 cout Student: Buy at half price. endl; } }; // 必须是父类 Person 的指针或引用 void Func(Person* ptr) { // 这⾥可以看到虽然都是 Person 指针 Ptr 在调用 BuyTicket // 但是跟 ptr 没关系而是由 ptr 指向的对象决定的 ptr-BuyTicket(); } int main() { // 定义子类对象 st 和父类对象 ps Person ps; Student st; // 父类对象调用 Func Func(ps); // 子类对象调用 Func Func(st); // 子类可以赋值给父类会切割子类 return 0; }运行结果代码示例 2.3.2#include iostream #include string using namespace std; class Person { public: // 不满足多态BuyTicket 不是虚函数 void BuyTicket() { // 原价买票 cout Person: Buy at full price. endl; } }; class Student : public Person { public: // 不满足多态子类不写 virtual 可以继承父类不可以 virtual void BuyTicket() { // 半价买票 cout Student: Buy at half price. endl; } }; // 改成引用 Person ptr 也满足多态但改成 Person 对象就不满足必须指针或引用 // 改成子类也不可以子类对象/指针/引用都可以切割赋值给父类对象/指针/引用反过来不行 // 除非父类的指针/引用本就指向子类那父类的指针/引用就可以赋值给子类的指针/引用 void Func(Person* ptr) { // 满足多态ptr 指向谁调用谁 // 不满足多态ptr 指针的类型是什么就调用什么如 Person* 则调用 Person::BuyTicket ptr-BuyTicket(); } int main() { Person ps; Student st; Func(ps); // 如果 ptr 是 Student* 就只能指向子类对象不能指向父类对象 Func(st); return 0; }运行结果相比重载和模板多态实现复用的关键不是函数本身实现在于父类指针或引用具体指向的对象。可以看到始终都是同一函数Func但ptr指向的对象不同Func产生的结果不同这便是多态。重载和模板则是对函数本身Func本身进行复用实现了多种参数类型的Func进行个性化处理。利用多态Func函数中其他代码逻辑可以被复用但关键行为结点如虚函数ptr-BuyTicket()则会根据父类和子类中的不同实现进行不同行为Func得到不同结果。除了父类和子类也可能同时存在多个子类子类和子类之间完成不同重写ptr指向不同子类对象实现子类之间的多态子类的指针/引用无法指向其他子类可见这也是必须用父类的指针/引用的原因之一。代码示例 2.3.3#include iostream using namespace std; // 基础动物类 class Animal { public: virtual void talk() const { // 动物叫声 // ... } }; // 狗 class Dog : public Animal { public: virtual void talk() const { std::cout Dog: Woof!Woof! std::endl; } }; // 猫 class Cat : public Animal { public: virtual void talk() const { std::cout Cat: Meow~Mewo~ std::endl; } }; // 父类指针指向不同子类对象 Cat/Dog void Hear(const Animal animal) { animal.talk(); } int main() { Cat cat; Dog dog; Hear(cat); Hear(dog); return 0; }运行结果2.4 多态笔试题练习下面程序的输出结果是 ( ) AA-0 BB-1 CA-1 DB-0 E编译出错 F以上都不正确代码示例 2.4.1#include iostream using namespace std; class A { public: virtual void func(int a 1) { std::cout A- a std::endl; } virtual void test() { func(); } }; class B : public A { public: void func(int b 0) { std::cout B- b std::endl; } }; int main() { B* p new B; p-test(); return 0; }解析首先有几点信息B和A是继承关系子类对func函数完成了重写virtual虚函数属性可以从父类中继承返回值类型同为void函数名同为func形参都为int变量。注意形参名称虽然不同但和重载那一块一样我们通常关注的是形参的类型而非名称包括缺省值虽然不同也不影响。子类没有重写test虚函数可以不重写但无法构成多态。根据上面这些信息我们来分析主要代码逻辑。用父类指针可以指向父类对象、子类对象以及其他子类对象所以它是构成多态的必要条件之一。对于p-test()test是成员函数第一个参数永远是隐式的this指针成员函数内通过this指针访问成员p是B*指针只能指向B对象那就只能给this传参B对象所以p只能调用B中成员。这里又有一个问题test是子类继承的那它的this指针是什么类型A*还是B*实际上继承只是“形象地说法”并不会真的从父类拷贝一份到子类。继承影响的是搜索规则先在子类搜索找不到再去父类如果同名成员构成隐藏则屏蔽父类作用域。所以子类的test还是父类中的同一个this指针仍然是A*因此调用func是构成多态的this-func()的this是父类指针结果取决于this指向的对象是谁。this只能指向B对象那就只能调用子类中重写的func。到这里我们只能算行百里者半九十。如果选 D 那就功亏一篑了。因为重写的本质是对函数的实现进行重写。绝对不应该重新定义继承而来的成员函数形参的缺省值。什么意思我们可以理解为子类对func的重写实际是把花括号{}内的代码给替换掉了。也就是A 进行了函数的声明 virtual void func(int a 1)但B 进行了函数定义的重写 { std::cout B- b std::endl; }。B中void func(int b 0)形参b的缺省值是0但真正由A进行函数声明形参的缺省值其实是1所以形参b被初始化为1。正确答案应该是 BB-1。我们再来进行一下简单理解。关于“三同”为什么有这3点因为子类对虚函数重写函数如何声明根本不重要不管是返回值类型、函数名、形参或者缺省值全部都在父类中声明过了重写只会覆盖函数的具体定义。所以函数声明照抄跟父类保持相同表明身份即可反正不重要。代码示例 2.4.2#include iostream using namespace std; 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() { B* p new B; p-test(); // p 是子类指针不满足多态 // 也没有 test 中的 this 父类指针来构成多态 // 所以这里完全不可能涉及多态这就是一个普通调用不需要考虑重写 // p 只能指向 B 对象那就原原本本的调用 B 的 void func(int val 0) p-func(); // 结果B-0 return 0; }运行结果可以看到明明p始终指向B对象结果却不一致不属于多态、不属于重载、不属于模板三不沾妥妥的大坑Bug而且隐藏的极深。再次强调对于从父类继承而来的函数子类重写时千万不要重写定义形参的缺省值。2.5 虚函数重写的⼀些其他问题协变了解子类重写时返回值类型在部分情况可以不同如协变父类的虚函数返回父类对象的指针/引⽤子类的虚函数返回子类对象的指针/引⽤。实际意义不⼤了解⼀下即可。注返回父类/子类对象的指针或引用这里的父子不一定是自己也可以是别的父子。例如Person中虚函数返回值类型恰好是父类Animal*子类Student/Teacher进行重写时允许返回值类型不同未必要相同返回Animal*可以返回Animal的子类的指针/引用例如Dog*或者Cat。代码示例 2.5.1class A { // ... }; class B : public A { // ... }; class Person { public: virtual A* BuyTicket() { cout Person: Buy at full price. endl; return nullptr; } }; class Student : public Person { public: // 部分情况下重写允许返回值类型不同但必须是对应的父子关系返回指针/引用 virtual B* BuyTicket() { cout Student: Buy at half price. endl; return nullptr; } };如果返回值类型不同但不是父子关系或者不是指针/引用或者父子关系反了 (Person中返回B*Student中返回A*)都不满足协变要求。报错析构函数的重写 (需要注意)析构函数的重写也很容易踩坑需要特别注意一些笔试面试题中比较喜欢出。回顾一下前面继承章节的部分原文编译器会将析构的函数名称特殊处理成destructor()例如~Person和~Student。所以在不加virtual的情况下子类析构和父类析构满足隐藏。(注多态场景下析构函数需要构成重写满足重写的条件之⼀就是函数名相同后面多态章节会讲解)。因此尽管子类析构名称和父类析构名称不同但底层是一致的都是destructor()。所以父类析构为虚函数子类析构只要定义就是重写(不管子类写不写virtual子类析构都会继承父类析构的虚函数属性)。代码示例 2.5.2#include iostream using namespace std; class A { public: // 父类析构为虚函数 virtual ~A() { cout ~A() endl; } }; class B : public A { public: // 子类析构只要定义就是重写~A 和 ~B 底层都叫 destructor ~B() { cout ~B()-delete: _p endl; delete _p; // 释放动态数组空间 } protected: int* _p new int[10]; // 申请十个整型元素大小的数组空间 }; // 父类析构为虚函数Student 必须定义了析构才会满足重写 // 父类指针 p1/p2 调用析构才能构成多态才能保证根据 p1/p2 真正指向的对象正确调⽤析构 // 如果不满足多态由于 p1/p2 都是 A*指针是什么类型就调用谁的两个都会调用 ~A() int main() { A* p1 new A; A* p2 new B; // delete 会调用析构释放资源 delete p1; // 调用析构 p1-~destructor() delete p2; // 调用析构 p2-~destructor() return 0; }运行结果p1指向A对象delete p1调⽤~A()p2指向B对象delete p2调⽤~B()前面继承章节说过子类对象有3部分内置类型、自定义类型和父类部分。父类部分当作整体作为一个父类对象进行任何处理又为了保证析构是先子后父所以子类析构结束后会自动调用父类析构~A()。假如~A()不加virtual不构成多态那么delete p2调⽤的就是~A()而不是~B()。B对象中的资源得不到释放导致内存泄漏。这都是为了解决父类的指针或引用的问题父类的指针/引用既可以指向父类对象也可以指向子类对象但父类和子类实现不同有不同的资源情况而p1和p2都是A*delete p1和delete p2只会调用~A()对于指向子类对象的p2来说是不行的。所以析构必须为多态编译器才会对析构的函数名称进行特殊处理。总结如果存在继承关系那么父类析构和子类析构必须是虚函数这样子类才能完成析构的重写才能构成多态。2.6 override 和 final 关键字C对虚函数重写的要求⽐较严格有些情况下容易疏忽⽐如返回值写错、函数名写错、参数写错等都⽆法构成重写。是否完成重写这种错误是不会编译报错的只有自己运行程序后没有得到预期结果才会发现。因此C11提供了两个关键字override可以帮助用户检测虚函数是否有重写。final让父类的虚函数无法被子类重写 (final在前面继承章节出现过被final修饰成为最终类就无法被子类继承。这里修饰的是虚函数)。代码示例 2.6.1#include iostream using namespace std; class Car { public: // 函数名故意写错正确应该是 Drive virtual void Dirve() { // ... } }; class Benz : public Car { public: // 在子类重写的虚函数后面加个 override 关键字 // override 会判断各种条件看是否正确重写 virtual void Drive() override { cout 奔驰——舒适 endl; } }; int main() { // ... return 0; }编译报错函数名不同不构成重写代码示例 2.6.2#include iostream using namespace std; class Car { public: // 不希望父类的虚函数被重写在函数后面加 final 关键字 virtual void Drive() final { // ... } }; class Benz : public Car { public: // 无法重写虚函数被 final 修饰编译报错提醒 virtual void Drive() { cout 奔驰——舒适 endl; } }; int main() { // ... return 0; }无法重写被 final 修饰的虚函数编译报错2.7 重载/重写/隐藏的对⽐拿这3个进行对比是因为它们具有一定的相似性都是两个函数名相同的函数之间的关系。函数名相同的两个函数不在同一作用域就不是重载。重载不看返回值只看参数列表中参数的类型和个数。分别在父类和子类中只有函数名相同那就是隐藏。分别在父类和子类中函数名、参数列表、返回值类型都相同且父类的那个加了virtual那就是重写。返回值部分情况可以不同例如协变。3. 纯虚函数和抽象类纯虚函数无需函数定义只需要函数声明等待子类进行函数定义的重写。在虚函数的后⾯写上0。纯虚函数不需要定义实现实现没意义因为要被子类重写但是语法上可以实现。抽象类只要这个类有纯虚函数那就是抽象类。抽象类不能实例化出对象。如果子类不重写纯虚函数那么子类也是抽象类。抽象事物是没有实体的所以抽象类这个名字其实就是在说明它不会有实体。纯虚函数某种程度上强制了子类必须完成虚函数的重写因为子类不重写纯虚函数它就得继承下来子类就变成了抽象类就无法实例化出对象 (override不同它是直接语法规则限制。如果不实例化抽象类对象那就无所谓重不重写也就不存在所谓的强制了)。代码示例 3.1#include iostream using namespace std; // 动物 class Animal { public: // 作为基础类单纯的“动物”是没有具体叫声的talk 的实现没有意义 // 因此可以定义为虚函数函数后面加个等于0即可 // 只需函数声明无需函数定义 virtual void talk() const 0; // 纯虚函数 virtual void talk() const 0 可以实现但没有意义 //virtual void talk() const 0 //{ // // 没有意义 //} }; // 狗 class Dog : public Animal { public: // 子类不重写纯虚函数子类也是抽象类因为子类会继承得到父类的纯虚函数 }; // 猫 class Cat : public Animal { public: // 重写纯虚函数子类就不是抽象类了 virtual void talk() const { std::cout Cat: Meow~Mewo~ std::endl; } }; int main() { // Animal 类中有纯虚函数则为抽象类抽象类不能实例化对象 // 报错C2259: “Animal”: 无法实例化抽象类 // Animal* pA new Animal; // 子类 Dog 继承得到纯虚函数同样是抽象类 // 报错C2259: “Dog”: 无法实例化抽象类 // Animal* pD new Dog; // Cat 重写了纯虚函数不是抽象类可以成功实例化 Animal* pC new Cat; // Animal 是抽象类但可以用抽象类的指针或引用 pC-talk(); return 0; }运行结果4. 多态的原理4.1 虚函数表指针下⾯编译为32位程序的运⾏结果是什么 ( ) A、编译报错 B、运⾏报错 C、8 D、12代码示例 4.1.1#include iostream using namespace std; class Base { public: virtual void Func1() { cout Func1() endl; } protected: int _b 1; char _ch x; }; int main() { Base b; cout sizeof(b) endl; return 0; }运行结果我们知道对象中只存储成员变量。整型变量占4个字节字符变量再占1个字节最后内存对齐一下答案应该是8。但最终答案是12字节。实际上是因为多了一个指针__vfptr32位下指针为4字节。对象中的这个指针叫做虚函数表指针(v即virtualf即functionptr即指针)。__vfptr被放在对象的前⾯ (有些平台可能会放到对象的最后⾯)。一个含有虚函数的类编译器会为其生成一张虚函数表存放所有虚函数的地址。该类每一个对象都会包含一个隐藏的虚表指针指向这个类的虚表。这里的表其实就是个数组存储了指向虚函数的函数指针严格来说虚函数表其实就是函数指针数组。再看上面的图片[0]其实就是数组下标为0的元素即1个函数指针存储了函数地址0x00a01546也就是Base::Func1(void)的地址。如果再多几个虚函数那就再多存储几个函数指针再多几个数组元素。但注意如果不是虚函数不会存入虚表。注虚函数表 virtual function table所以 __vfptr 应该是 __vftptr 会更完整准确一点少了个表 table 的缩写 t。4.2 原理4.2.1 多态是如何实现的代码示例 4.2.1.1#include iostream #include string using namespace std; // 普通人原价买票 class Person { public: virtual void BuyTicket() { cout Person: Buy at full price. endl; } private: string _name; }; // 学生半价买票 class Student : public Person { public: // 虚函数重写 virtual void BuyTicket() { cout Student: Buy at half price. endl; } private: string _id; }; // 军人优先买票 class Soldier : public Person { public: // 虚函数重写 virtual void BuyTicket() { cout Soldier: Buy tickets with priority. endl; } private: string _codename; }; void Func(Person* ptr) { // 由父类指针 ptr 指向的对象决定买票结果 ptr-BuyTicket(); } int main() { // 多态可以发生在父类和子类之间也可以发生在同一父类的子类之间 Person ps; Student st; Soldier sr; Func(ps); Func(st); Func(sr); return 0; }可以看到3个对象的__vfptr虚表指针地址都不一样说明虚表各个类自己单独有一份。并且每个虚表在[0]处分别存储了这3个类BuyTicket的地址函数重写后地址各不相同。问题是从底层的⻆度Func函数中的ptr-BuyTicket()为什么ptr指向Person对象就调⽤Person::BuyTicketptr指向Student对象就调⽤Student::BuyTicket具体如何实现我们知道文件经过预编译阶段与编译阶段会得到目标文件代码语句被翻译成汇编指令函数调用变成call [函数地址]如。编译包括多个小阶段例如语法分析和语义分析编译器会调用语法分析器和语义分析器检查语法语义。如果ptr-BuyTicket()满足多态则会去ptr指向的对象中查找函数地址。ptr的类型是Person*对于指针而言它指向的始终是Person对象。即使是子类ptr指向的也是子类对象中经过切割得到的父类对象。因此不管是父类还是子类编译器只需要去这个Person对象查找虚表指针即可。虚表指针可能在对象的前4个字节也可能在之后4个字节 (64位下指针为8 Bytes)。从父类对象取到虚表指针后就能找到这个类的虚表或者说函数指针数组。找到数组就能找到各个函数指针如果是父类存储的就是父类的虚函数的地址如果是子类存储的就是子类重写的虚函数的地址。满⾜多态条件后底层不再是“编译时通过调⽤对象确定函数地址”⽽是“程序运行时到指针指向的对象中找这个类的虚表通过虚表确定虚函数的地址”。这就是所谓的动态多态或者说运行时多态。4.2.2 动态绑定与静态绑定不满⾜多态父类的指针/引⽤调⽤虚函数函数调⽤call [函数地址]是在编译时确定函数地址的这个过程叫做静态绑定。满⾜多态函数调⽤是在运⾏时完成绑定的也就是运⾏时去指向的对象的虚表中找到函数地址这个过程叫做动态绑定。汇编代码的部分截取// 满足多态ptr 是父类指针BuyTicket 是虚函数且完成重写 // 因此此处函数调用是动态绑定 // 运行时去 ptr 指向的对象找虚表指针由虚表指针找这个类的虚表通过虚表确定函数地址 ptr-BuyTicket(); 00EF2001 mov eax,dword ptr [ptr] 00EF2004 mov edx,dword ptr [eax] 00EF2006 mov esi,esp 00EF2008 mov ecx,dword ptr [ptr] 00EF200B mov eax,dword ptr [edx] 00EF200D call eax // 如果 BuyTicket 不是虚函数不满足多态这里就是静态绑定 // 编译器直接根据 ptr 的类型 Person*找到 Person 确定函数地址 ptr-BuyTicket(); 00EA2C91 mov ecx,dword ptr [ptr] 00EA2C94 call Student::Student (0EA153Ch)从运行效率的角度来说静态绑定的效率会高一点只需要两句指令动态绑定则需要运行时再去找。4.2.3 虚函数表同类型对象虚表共用不同类型对象虚表各自独立。因此父类和子类的虚表不同。父类存父类的虚函数子类存子类的虚函数。子类重写了父类的虚函数子类虚表中原本是父类虚表中的虚函数地址则被覆盖成子类重写的虚函数的地址 (逻辑上可以理解为子类先拷贝了一份父类虚表以此为基础进行覆盖)。子类如果不重写就不会覆盖地址即该虚函数在父类和子类的虚表中地址是相同的。子类虚表包括三部分1. 父类的虚函数地址 (未重写覆盖)2. 子类重写覆盖后的虚函数地址3. 子类⾃⼰新定义的虚函数的地址。子类有两部分父类部分和子类成员。子类对象继承的父类部分包括虚表指针子类对象无需再⽣成。但注意子类对象中的父类对象是子类对象调用构造初始化时调用父类构造定义初始化的虚表指针其实也是新创建的并指向了子类虚表。就像定义了两个普通的父类对象虚表是父类对象共有的但虚表指针是对象各自独有的变量。虚函数表本质是⼀个存储虚函数指针的指针数组。通常这个数组最后⾯放了⼀个0x00000000标记(C没有规定标准各编译器⾃⾏定义Visual Studio 系列编译器会在后⾯放0x00000000标记g系列编译器不会放)。就像字符串一样字符数组存储字符串要求最后1个元素必须存储\0当然和这里没关系只是相似。虚表这里是一种标记而且不同编译器的实现也不同像g系列就不放算不上语法规则。虚函数存储在哪和普通函数一样虚函数编译后就是⼀句句汇编指令都存在代码段只是虚函数的地址还会单独放一下存储到虚表/函数指针数组中存到虚表的是指针是虚函数的地址而不是虚函数。虚表存储在哪或者说函数指针数组存在哪严格来说没有标准答案C标准没有规定但VS是存在常量区的。注代码区存储可执行代码 (机器指令)常量区存储常量数据如字符串常量等静态区存储全局变量和静态变量栈区存储局部变量等堆区存储动态分配的内存空间。代码示例 4.2.3.1#include iostream using namespace std; class Base { public: virtual void func1() { cout Base::func1 endl; } virtual void func2() { cout Base::func2 endl; } // func5 不是虚函数不存到虚表 void func5() { cout Base::func5 endl; } protected: int a 1; }; class Derive : public Base { public: // func2 虚函数不重写 // 重写父类的 func1 virtual void func1() { cout Derive::func1 endl; } // 子类自己的虚函数 func3 virtual void func3() { cout Derive::func1 endl; } // 子类自己的普通函数 func4不存到虚表 void func4() { cout Derive::func4 endl; } protected: int b 2; }; int main() { // 看看栈、堆、静态区、常量区的地址大概是多少 int i 0; // i 为局部变量栈区 static int j 1; // j 为静态变量静态区 int* p1 new int; // p1 指针变量本身是局部变量栈区p1 指向的是动态内存空间堆区 const char* p2 xxxxxxx; // p2 指针变量本身是局部变量栈区p2 指向的是字符串常量常量区 printf(栈:%p\n, i); // 打印变量 i 的地址 printf(静态区:%p\n, j); // 打印变量 j 的地址 printf(堆:%p\n, p1); // 打印 p1 指针指向的地址 printf(常量区:%p\n, p2); // 打印 p2 指针指向的地址 // 看看虚表地址是多少比对一下 Base b; Derive d; Base* p3 b; // 父类指针 p3指针变量在栈上指向的父类对象也在栈上 Derive* p4 d; // 子类指针 p4指针变量在栈上指向的子类对象也在栈上 // 如何拿到虚表的地址VS下32位下拿到对象的前4个字节即可取得虚表指针虚表指针存储了虚表的地址 // int 变量4个字节所以 int* 指针解引用后也取4个字节 // p3 存储了父类对象的首地址Base* 解引用取地址整个父类对象的字节大小 // p3 强制类型转换变成 int*int* 解引用则只取父类对象的前4个字节 // p4 同理只取子类对象的前4个字节 // (int*)p3 和 (int*)p4 可分别获取父类的虚表指针和子类的虚表指针 // 还需要对虚表指针进行解引用获取指针存储的值即虚表的地址 printf(Base 虚表地址:%p\n, *(int*)p3); printf(Derive 虚表地址:%p\n, *(int*)p4); // 看看代码区的地址虚函数和普通函数都存储在代码区只是指向虚函数地址的指针被存储在了虚表中 printf(虚函数地址:%p\n, Base::func1); // 函数名即函数首地址 printf(普通函数地址:%p\n, Base::func5); // 函数名即函数首地址 return 0; }运行结果可以看到两个虚表地址都是00E0AB常量区也是00E0AB它们是最接近的。也就是说在Visual Studio下虚表或者说函数指针数组被认为是常量。放到常量区是合理的类被定义虚表就会存在定义类对象时则是创建虚表指针指向虚表所以虚表应该是整个程序始终全局存在的而且运行时是只读不可修改。如果放到堆也可以全局存在只不过需要系统动态申请再由系统自己释放。如果放到静态区可以像全局变量全局存在也可以像静态变量只读而且静态区的变量随程序结束自动释放。如果放到栈上就不是很合理因为都是局部变量函数调用结束就销毁定义两个对象其中一个在普通函数内部函数结束对象被销毁虚表被销毁这是不合理的。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询