网站建设系统分析,服务网站建设方案,软件开发文档编制规范,外发加工网磨字继承和多态是 C 的灵魂#xff0c;也是很多初学者的噩梦。你可能背过“父类指针指向子类对象”#xff0c;但你真的理解编译器背后做了什么吗#xff1f; 这篇文章不仅讲怎么用#xff0c;更讲为什么。 我们将从最基础的定义开始#xff0c;一层层剥开 C 的外衣#xff0…继承和多态是 C 的灵魂也是很多初学者的噩梦。你可能背过“父类指针指向子类对象”但你真的理解编译器背后做了什么吗 这篇文章不仅讲怎么用更讲为什么。 我们将从最基础的定义开始一层层剥开 C 的外衣直抵内存深处。目录一、继承的概念二、继承的定义2.1 定义格式2.2 继承方式与访问属性变化三、继承类模板四、基类和派生类间的转换五、继承中的作用域5.1 隐藏规则5.2 继承作用域相关的典型考点六、派生类的默认成员函数6.1 4个常见默认成员函数七、实现一个不能被继承的类7.1 C98 写法构造函数设为 private7.2 C11 写法final 关键字八、继承与友元九、继承与静态成员十、多继承及其菱形继承问题10.1 多继承与菱形继承10.2 虚继承10.3 多继承中的指针偏移问题10.4 IO 库中的菱形虚继承十一、继承和组合11.1 继承和组合11.2 继承与组合的例子十二、多态的概念十三、多态的构成条件十四、虚函数与重写14.1 虚函数14.2 虚函数的重写十五、默认参数与虚函数十六、协变十七、虚析构函数与多态销毁十八、override 和 final 关键字十九、重载、重写与隐藏对比二十、纯虚函数与抽象类二十一、多态的原理21.1 虚函数表指针21.2 虚函数表与动态绑定21.3 虚函数表一、继承的概念继承(inheritance)是 C 面向对象中实现“类级别代码复用”的关键机制。它允许你在一个已有类基类的基础上扩展出一个新类派生类复用原有成员再加上自己的成员。先看一个没用继承的写法同时表示学生和老师。class Student { public: void identity() { //身份认证逻辑 } void study() { //学习 } protected: std::string _name; //姓名 std::string _address; //地址 std::string _tel; //电话 int _age; //年龄 int _stuid; //学号 }; class Teacher { public: void identity() { //身份认证逻辑 } void teaching() { //授课 } protected: std::string _name; //姓名 int _age; //年龄 std::string _address; //地址 std::string _tel; //电话 std::string _title; //职称 };问题identity重复姓名/地址/电话/年龄等字段也重复一旦认证规则改了要改两处。下面我们公共的成员都放到Person类中Student和teacher都继承Person就可以复用这些成员就不需要重复定义了省去了很多麻烦class Person { public: void identity() { std::cout void identity() _name std::endl; } protected: std::string _name 张三; //姓名 std::string _address; //地址 std::string _tel; //电话 int _age 18; //年龄 }; class Student : public Person { public: void study() { //学习 } protected: int _stuid;//学号 }; class Teacher : public Person { public: void teaching() { //授课 } protected: std::string _title;//职称 }; int main() { Student s; Teacher t; s.identity(); t.identity(); return 0; }学生是人老师是人公共属性放在Person里派生类只扩展自己的部分。classDiagram class Person{ string _name string _address string _tel int _age identity() } class Student{ int _stuid study() } class Teacher{ string _title teaching() } Person -- Student Person -- Teacher二、继承的定义2.1 定义格式下面我们看到Person是基类也称作父类。Student是派生类也称作子类。(因为翻译的原因所以既叫基类/派生类也叫父类/子类)继承的基本语法class 派生类名 : 继承方式 基类名 { //新增成员 };例如class Student : public Person { public: int _stuid;//学号 int _major;//专业 };名词对应基类/父类Person派生类/子类Student继承方式public、protected、private。2.2 继承方式与访问属性变化三种继承方式会改变“基类成员在派生类中的访问级别”注意只影响访问级别不影响对象里是否有那块内存。基类成员原属性public 继承后protected 继承后private 继承后基类的 public 成员变成 public变成 protected变成 private基类的 protected 成员变成 protected变成 protected变成 private基类的 private 成员在派生类中不可见在派生类中不可见在派生类中不可见总结派生类中一个成员的最终访问级别 Min(该成员在基类中的访问限定符, 继承方式)排序规则public protected private。要点基类的private成员在派生类中语法不可访问但在对象里仍然有那块内存需要“类外不能访问但派生类能访问”的成员就应该放到protected里class默认继承方式是privatestruct默认是public实际编码中大多采用public 继承。三、继承类模板继承同样可以作用在类模板上比如基于std::vectorT封装一个简单的stacknamespace bit { templateclass T class stack : public std::vectorT { public: void push(const T x) { std::vectorT::push_back(x); } void pop() { std::vectorT::pop_back(); } const T top() { return std::vectorT::back(); } bool empty() { return std::vectorT::empty(); } }; } int main() { bit::stackint st; st.push(1); st.push(2); st.push(3); while (!st.empty()) { std::cout st.top() ; st.pop(); } return 0; }模板细节模板是按需实例化的基类是类模板时在派生类模板中访问基类成员编译器不一定能推断所以在模板中调用基类模板成员时最好写成std::vectorT::push_back(x)这种带类名限定的形式。四、基类和派生类间的转换class Person { protected: std::string _name;//姓名 std::string _sex;//性别 int _age;//年龄 }; class Student : public Person { public: int _No;//学号 }; int main() { Student sobj; //1.派生类对象可以转换为基类 Person* pp sobj;//向上转型 Person rp sobj;//向上转型 Person pobj sobj;//对象切片只拷贝Person那部分 //2.基类对象不能隐式转换为派生类对象 //sobj pobj;//错误派生类多出来的信息没法填 return 0; }规则public继承下派生类对象可以隐式转换为基类对象/指针/引用向上转型反过来不行因为基类不包含派生类扩展的那部分信息基类指针如果实际上指向派生类对象可以用强转转回来C 风格(Student*)pp很危险多态场景下应优先使用dynamic_castStudent*(pp)做运行时检查。五、继承中的作用域5.1 隐藏规则继承体系中有两个独立的作用域基类作用域和派生类作用域。同名成员会发生隐藏。规则基类和派生类的作用域彼此独立在派生类中定义了与基类同名的成员变量或者函数则派生类的同名成员会隐藏基类成员对函数来说只要名字相同就隐藏不看参数列表想在派生类中访问被隐藏的基类成员需要写成基类名::成员名实际编码中尽量避免在继承体系里大量使用同名成员容易搞混。示例class Person { protected: std::string _name 好评; //姓名 int _num 111; //身份证号 }; class Student : public Person { public: void Print() { std::cout 姓名: _name std::endl; std::cout 身份证号: Person::_num std::endl; std::cout 学号: _num std::endl; } protected: int _num 999;//学号 }; int main() { Student s1; s1.Print(); return 0; }这里Person::_num表示身份证号Student::_num表示学号在Student作用域中直接写_num访问的是学号身份证那一份被隐藏了。5.2 继承作用域相关的典型考点例1两个fun是什么关系class A { public: void fun() { std::cout func() std::endl; } }; class B : public A { public: void fun(int i) { std::cout func(int i) i std::endl; } };A::fun()和B::fun(int)不在同一作用域名字相同参数列表不同它们之间的关系是隐藏不是重载。例 2下面程序结果int main() { B b; b.fun(10); b.fun(); return 0; }答案编译错误。原因在B的作用域中A::fun被B::fun(int)隐藏了名字查找只看到B::fun(int)调用b.fun()时没有无参版本匹配编译失败。如果既想保留A::fun()又要在B中增加fun(int)可以class B : public A { public: using A::fun;//把A中的fun引入当前作用域 void fun(int i) { std::cout func(int i) i std::endl; } };六、派生类的默认成员函数派生类也有那几大默认函数构造、拷贝构造、赋值、析构等。在继承体系中它们有统一的调用顺序。6.1 4个常见默认成员函数6个默认成员函数默认的意思就是指我们不写编译器会变我们自动生成—个那么在派生类中这几个成员函数是如何生成的呢派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数则必须在派生类构造函数的初始化列表阶段显示调用。派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。派生类的operator必须要调用基类的operator完成基类的复制。需要注意的是派生类的operator隐藏了基类的operator所以显示调用基类的operator需要指定基类作用域派生类的析构函数会在被调用完成后用自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。派生类对象初始化先调用基类构造再调派生类构造。派生类对象析构清理先调用派生类析构再调基类的析构。因为多态中一些场景析构函数需要构成重写重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理处理成destructor()所以基类析构函数不加virtual的情况下派生类析构函数和基类析构函数构成隐藏关系。class Person { public: Person(const char* name peter) : _name(name) { std::cout Person() std::endl; } Person(const Person p) : _name(p._name) { std::cout Person(const Person p) std::endl; } Person operator(const Person p) { std::cout Person operator(const Person p) std::endl; if (this ! p) { _name p._name; } return *this; } ~Person() { std::cout ~Person() std::endl; } protected: std::string _name;//姓名 }; class Student : public Person { public: Student(const char* name, int num) : Person(name) , _num(num) { std::cout Student() std::endl; } Student(const Student s) : Person(s) , _num(s._num) { std::cout Student(const Student s) std::endl; } Student operator(const Student s) { std::cout Student operator(const Student s) std::endl; if (this ! s) { Person::operator(s);//显式调用基类的 _num s._num; } return *this; } ~Student() { std::cout ~Student() std::endl; } protected: int _num;//学号 }; int main() { Student s1(jack, 18); Student s2(s1); Student s3(rose, 17); s1 s3; return 0; }总结构造顺序先构造基类子对象再构造派生类自己的成员拷贝构造先调用基类拷贝构造再拷贝派生类成员赋值运算符先调用基类operator再赋值派生类成员析构顺序先析构派生类再析构基类栈式反向派生类的operator会隐藏基类的operator所以要显式写Person::operator(s)。七、实现一个不能被继承的类有些类希望“只能被使用不能被继承”例如工具类、单例类等。常见做法有两种。7.1 C98 写法构造函数设为 privateclass Base { private: Base() {} };派生类构造函数必须先调用基类构造但基类构造是private派生类调用不到语法上可以写class Derive : public Base {};但无法构造派生类对象。7.2 C11 写法final 关键字C11 提供了final关键字直接在类定义后标记class Base final { public: void func5() { std::cout Base::func5 std::endl; } protected: int a 1; }; class Derive : public Base { public: void func4() { std::cout Derive::func4 std::endl; } protected: int b 2; };从Base继承会直接编译失败因为Base被标记为final无法被继承。八、继承与友元友元关系不会自动继承。基类的友元不是派生类的友元。class Student; class Person { public: friend void Display(const Person p, const Student s); protected: std::string _name;//姓名 }; class Student : public Person { protected: int _stuNum;//学号 }; void Display(const Person p, const Student s) { std::cout p._name std::endl; std::cout s._stuNum std::endl; }想让Display既能访问Person的内部成员又能访问Student的内部成员规范的做法是在Person中声明friend void Display(const Person, const Student);在Student中也声明同样的friend。九、继承与静态成员静态成员在整个继承体系中只有一份是全类共享的。class Person { public: std::string _name; static int _count; }; int Person::_count 0; class Student : public Person { protected: int _stuNum; }; int main() { Person p; Student s; //访问静态成员的两种写法本质是同一块内存 Person::_count 10; Student::_count 20; std::cout Person::_count std::endl;//20 std::cout Student::_count std::endl;//20 return 0; }要点静态成员属于类而不是具体哪个对象在 public 继承下可以用Person::_count和Student::_count两种形式访问同一份静态成员。十、多继承及其菱形继承问题10.1 多继承与菱形继承多继承一个类同时继承多个直接基类。对象布局通常是“基类1子对象 基类2子对象 ... 派生类成员”。典型菱形结构class Person { public: std::string _name;//姓名 }; class Student : public Person { protected: int _num;//学号 }; class Teacher : public Person { protected: int _id;//职工编号 }; class Assistant : public Student, public Teacher { protected: std::string _majorCourse;//主修课程 };Assistant对象中会有两份Person子对象一份来自Student一份来自Teacher。int main() { Assistant a; //错误_name不明确既可以来自Student也可以来自Teacher //a._name peter; a.Student::_name peter; a.Teacher::_name jack; return 0; }问题数据冗余_name有两份使用有二义性a._name不明确必须带上类域。这就是绝大部分人不推荐菱形继承的原因。10.2 虚继承为了解决菱形继承中“同一基类多份”的问题C 提供了虚继承(virtual inheritance)。class Person { public: std::string _name;//姓名 }; //虚继承Person class Student : virtual public Person { protected: int _num;//学号 }; //虚继承Person class Teacher : virtual public Person { protected: int _id;//职工编号 }; class Assistant : public Student, public Teacher { protected: std::string _majorCourse;//主修课程 }; int main() { Assistant a; a._name peter;//现在只有一份Person子对象不再二义 return 0; }效果对每条虚继承链最终派生类对象中只有一份虚基类子对象避免数据冗余和访问二义性。代价对象布局更复杂构造函数初始化顺序更绕一般只在库级基础设施里使用业务代码中尽量避免。示例class Person { public: Person(const char* name) : _name(name) {} std::string _name;//姓名 }; class Student : virtual public Person { public: Student(const char* name, int num) : Person(name) , _num(num) {} protected: int _num;//学号 }; class Teacher : virtual public Person { public: Teacher(const char* name, int id) : Person(name) , _id(id) {} protected: int _id;//职工编号 }; class Assistant : public Student, public Teacher { public: Assistant(const char* name1, const char* name2, const char* name3) : Person(name3) , Student(name1, 1) , Teacher(name2, 2) {} protected: std::string _majorCourse;//主修课程 }; int main() { Assistant a(张三, 李四, 王五); //最终Person那一份名字会被构造成王五 return 0; }10.3 多继承中的指针偏移问题多继承下不同基类子对象在派生类对象中位置不同因此把派生类对象地址转成不同基类指针时会产生偏移。class Base1 { public: int _b1; }; class Base2 { public: int _b2; }; class Derive : public Base1, public Base2 { public: int _d; }; int main() { Derive d; Base1* p1 d; Base2* p2 d; Derive* p3 d; //通常p1 p3p2 p3 sizeof(Base1) return 0; }Base1子对象一般位于Derive对象开头所以p1和p3相等Base2子对象在Base1之后因此p2在地址上相对p3有偏移。10.4 IO 库中的菱形虚继承顶层有一个ios_basebasic_ios从ios_base继承basic_istream和basic_ostream都虚继承自basic_iosbasic_iostream多继承自basic_istream和basic_ostream因为是虚继承basic_iostream对象里只会有一份basic_ios子对象。十一、继承和组合11.1 继承和组合两种最常见的代码复用关系继承(inheritance)is-a关系Student是一种PersonBenz是一种Car常和多态一起出现。组合(composition)has-a关系Car里有 4 个Tirestack内部有一个vector做底层存储。从封装和耦合角度看继承属于白箱复用派生类能看到基类大部分实现细节基类实现变动会把派生类一起拖下水耦合高封装性相对差一些。组合属于黑箱复用只通过接口和被组合对象交互内部实现对外隐藏耦合更低更利于维护。总结能用组合解决的就尽量用组合只有在天然 is-a 且需要多态时再考虑继承。11.2 继承与组合的例子轮胎和车很明显是has-a关系。class Tire { protected: std::string _brand Michelin;//品牌 size_t _size 17;//尺寸 }; class Car { protected: std::string _colour 白色;//颜色 std::string _num 陕ABIT00;//车牌号 Tire _t1; Tire _t2; Tire _t3; Tire _t4; };具体车型和车很自然是is-a关系。class BMW : public Car { public: void Drive() { std::cout 好开-操控 std::endl; } }; class Benz : public Car { public: void Drive() { std::cout 好坐-舒适 std::endl; } };vector与stacktemplateclass T class vector {}; //is-a写法stack继承vector templateclass T class stack_is_a : public vectorT {}; //has-a写法stack里组合一个vector templateclass T class stack_has_a { public: vectorT _v; };从语义上讲stack 更像“内部用某种顺序容器实现”的抽象不是“某个特殊的 vector”所以组合更合适。十二、多态的概念多态(polymorphism)字面意思就是多种形态。从发生阶段看编译时多态静态多态函数重载、函数模板根据参数类型/个数不同在编译期决定调用哪个函数运行时多态动态多态虚函数基类指针/引用调用哪个版本在运行时决定。这里重点讲运行时多态。例子买票。每个人都执行BuyTicket()普通人全价学生打折军人优先窗口函数名一样行为随对象类型改变这就是多态。例子动物叫Animal定义接口talk()猫重写后输出“喵”狗重写后输出“汪”接口统一行为不同。十三、多态的构成条件要形成运行时多态必须满足三个条件存在继承关系例如Student继承Person基类中有虚函数派生类对其重写通过基类指针或引用调用虚函数。少任何一条都不是动态多态。示例买票。class Person { public: virtual void BuyTicket() { std::cout 买票-全价 std::endl; } }; class Student : public Person { public: virtual void BuyTicket() { std::cout 买票-打折 std::endl; } }; void Func(Person* ptr) { ptr-BuyTicket(); } int main() { Person ps; Student st; Func(ps);//买票-全价 Func(st);//买票-打折 return 0; }十四、虚函数与重写14.1 虚函数在成员函数前加virtual这个成员函数就变成虚函数class Person { public: virtual void BuyTicket() { std::cout 买票-全价 std::endl; } };注意只有成员函数可以是虚函数虚属性会被派生类继承即使派生类省略virtual关键字该函数仍然是虚函数。14.2 虚函数的重写重写(override)的条件基类中有虚函数virtual R f(Args...)派生类中也有R f(Args...)参数列表完全相同即重写。示例动物叫。class Animal { public: virtual void talk() const {} }; class Dog : public Animal { public: virtual void talk() const { std::cout 汪汪 std::endl; } }; class Cat : public Animal { public: virtual void talk() const { std::cout (^ω^)喵 std::endl; } }; void letsHear(const Animal animal) { animal.talk(); } int main() { Cat cat; Dog dog; letsHear(cat); letsHear(dog); return 0; }letsHear只知道拿到一个Animal调用talk()时通过虚函数机制根据实际对象类型选择对应版本。十五、默认参数与虚函数多态场景的选择题以下程序输出结果是什么A: A-0 B: B-1 C: A-1 D: B-0 E: 编译出错 F: 以上都不正确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(); delete p; return 0; }输出是B: B - 1。原因虚函数调用的动态绑定发生在运行时默认参数的选择发生在编译时依据的是“静态类型”。想象你去吃汉堡优惠券编译时的类型 A你手里拿着一张由A公司印发的优惠券代码里test()函数是在A类里写的。这张优惠券上写着一行小字“默认赠送1包番茄酱”。厨师运行时的对象 B真正给你做汉堡的厨师是B师傅代码里new B。B师傅平时的习惯是“默认赠送0包番茄酱”。冲突发生了 当你拿着 A公司的优惠券给 B师傅看时谁做汉堡当然是B师傅做因为是virtual虚函数看实际对象。给几包酱必须按优惠券上写的来默认参数是编译时决定的看纸上写了啥。结果B师傅做了汉堡打印B-但是被迫按优惠券给了 1 包酱打印1。十六、协变协变(covariant)返回类型指的是基类虚函数返回“基类指针/引用”派生类重写时返回“派生类指针/引用”。示例class A {}; class B : public A {}; class Person { public: virtual A* BuyTicket() { std::cout 买票-全价 std::endl; return nullptr; } }; class Student : public Person { public: virtual B* BuyTicket() { std::cout 买票-打折 std::endl; return nullptr; } };这种写法在语义上仍然算重写C 允许这种“协变”返回。实际业务中不常用了解就行。十七、虚析构函数与多态销毁多态最容易翻车的地方用基类指针delete派生类对象。class A { public: virtual ~A() { std::cout ~A() std::endl; } }; class B : public A { public: ~B() { std::cout ~B()-delete: _p std::endl; delete[] _p; } protected: int* _p new int[10]; }; int main() { A* p1 new A; A* p2 new B; delete p1;//调用A::~A delete p2;//先调用B::~B再调用A::~A return 0; }如果把A的析构函数上的virtual删掉那么A* p2 new B; delete p2;//只会调用A::~A不会调用B::~B数组泄漏因此凡是“打算当多态基类使用”的类都应该把析构函数声明为virtual确保delete 基类指针时能正确析构派生类对象。十八、override 和 final 关键字默认情况下如果派生类函数“看起来像是重写”实则签名不一致编译器不会报错只会当作隐藏。这很容易埋坑。C11 提供了两个关键字override表示“我就是要重写基类虚函数”如果没成功重写则编译报错final表示“这个虚函数到我这里为止”派生类不能再重写。示例一用override查拼写错误。class Car { public: virtual void Dirve()//故意拼错 {} }; class Benz : public Car { public: virtual void Drive() override { std::cout Benz-舒适 std::endl; } };编译器会报类似“带 override 却没有重写任何基类方法”的错误暴露出Dirve的拼写问题。示例二用final禁止重写。class Car { public: virtual void Drive() final {} }; class Benz : public Car { public: virtual void Drive() { std::cout Benz-舒适 std::endl; } };这会直接编译失败因为试图重写一个被final修饰的虚函数。推荐习惯只要是“故意要重写”的虚函数派生类都加上override某个虚函数不希望再被重写时加上final。十九、重载、重写与隐藏对比二十、纯虚函数与抽象类在虚函数声明后写 0就变成纯虚函数(pure virtual)。class Car { public: virtual void Drive() 0; };特性纯虚函数可以只声明不实现包含纯虚函数的类称为抽象类(abstract class)抽象类不能实例化对象派生类如果没有重写完所有纯虚函数它自己也是抽象类。例子class Car { public: virtual void Drive() 0; }; class Benz : public Car { public: virtual void Drive() { std::cout Benz-舒适 std::endl; } }; class BMW : public Car { public: virtual void Drive() { std::cout BMW-操控 std::endl; } }; int main() { //Car car;//错误抽象类不能实例化 Car* pBenz new Benz; pBenz-Drive(); Car* pBMW new BMW; pBMW-Drive(); delete pBenz; delete pBMW; return 0; }抽象类常用来做“接口类”提供一组纯虚函数规定“要做什么”具体“怎么做”由派生类去实现。二十一、多态的原理21.1 虚函数表指针下⾯编译为32位程序的运行结果是什么A. 编译报错 B. 运行报错 C. 8 D. 12class Base { public: virtual void Func1() { std::cout Func1() std::endl; } protected: int _b 1; char _ch x; }; int main() { Base b; std::cout sizeof(b) std::endl; return 0; }答案是D.12:一个int4 字节一个char加上对齐填充再加上一个隐藏的虚函数表指针 vptr4 字节。这个 vptr 就是多态的关键。21.2 虚函数表与动态绑定买票例子class Person { public: virtual void BuyTicket() { std::cout 买票-全价 std::endl; } private: std::string _name; }; class Student : public Person { public: virtual void BuyTicket() { std::cout 买票-打折 std::endl; } private: std::string _id; }; class Soldier : public Person { public: virtual void BuyTicket() { std::cout 买票-优先 std::endl; } private: std::string _codename; }; void Func(Person* ptr) { ptr-BuyTicket(); } int main() { Person ps; Student st; Soldier sr; Func(ps); Func(st); Func(sr); return 0; }底层大致这样每个含虚函数的类对应一张虚函数表(vtable)里面存放该类所有虚函数的地址每个对象中有一个 vptr 指向对应类型的虚表调用ptr-BuyTicket()时从对象中取出 vptr在虚表中按固定偏移找到BuyTicket对应的函数指针跳转到该函数地址执行。因此ptr指向Person对象→调用Person::BuyTicket指向Student对象→调用Student::BuyTicket指向Soldier对象→调用Soldier::BuyTicket。这就是动态绑定(dynamic binding)。21.3 虚函数表class Base { public: virtual void func1() { std::cout Base::func1 std::endl; } virtual void func2() { std::cout Base::func2 std::endl; } void func5() { std::cout Base::func5 std::endl; } protected: int a 1; }; class Derive : public Base { public: virtual void func1() { std::cout Derive::func1 std::endl; } virtual void func3() { std::cout Derive::func3 std::endl; } void func4() { std::cout Derive::func4 std::endl; } protected: int b 2; };通常Base的虚表类似[Base::func1, Base::func2]Derive的虚表类似[Derive::func1, Base::func2, Derive::func3]func4和func5因为不是虚函数不在虚表中。int main() { int i 0; static int j 1; int* p1 new int; const char* p2 xxxxxxxx; printf(栈:%p\n, i); printf(静态区:%p\n, j); printf(堆:%p\n, p1); printf(常量区:%p\n, p2); Base b; Derive d; Base* p3 b; Derive* p4 d; printf(Base虚表地址:%p\n, *(void**)p3); printf(Derive虚表地址:%p\n, *(void**)p4); printf(虚函数地址:%p\n, (void*)Base::func1); printf(普通函数地址:%p\n, (void*)Base::func5); delete p1; return 0; }可以观察到栈、堆、静态区、常量区的地址大致位于不同区间虚表指针*(void**)p3和*(void**)p4指向的区域通常与代码/常量区比较接近虚函数和普通函数的地址都落在代码段虚表本质上就是一个“函数指针数组”。完