宁夏电建网站自己做的网站别人
- 作者: 五速梦信息网
- 时间: 2026年03月21日 10:16
当前位置: 首页 > news >正文
宁夏电建网站,自己做的网站别人,灵台门户网站建设,品牌官网搭建一、多态的概念 多态的概念#xff1a;通俗来说#xff0c;就是多种形态#xff0c;具体点就是去完成某种行为#xff0c;当不同的对象去完成时会产生出不同的状态。 我们可以举一个例子#xff1a; 比如买票这种行为#xff0c;当普通人买票时#xff0c;是全价买票通俗来说就是多种形态具体点就是去完成某种行为当不同的对象去完成时会产生出不同的状态。 我们可以举一个例子 比如买票这种行为当普通人买票时是全价买票学生买票时是半价买票军人买票时是优先买票。 再举一个例子 会最近为了争夺在线支付市场支付宝年底经常做诱人的扫红包-支付-给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块…而有人扫的红包都是1毛5毛….。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据比如你是新用户、比如你没有经常支付宝支付等等那么你需要被鼓励使用支付宝那么就你扫码金额 random()%99比如你经常使用支付宝支付或者支付宝账户中常年没钱那么就不需要太鼓励你去使用支付宝那么就你扫码金额 random()%1总结一下同样是扫码动作不同的用户扫得到的不一样的红包这也是一种多态行为。ps支付宝红包问题纯属瞎编大家仅供娱乐。 二、多态的定义与实现
2.1 虚函数
虚函数即被 virtual 修饰的类成员函数称为虚函数。
class Person
{
public:virtual void BuyTicket(){std::cout 买票-全价 std::endl;}
} 虚函数virtual function是面向对象编程OOP中的一个重要概念特别是在C等支持多态的语言中。虚函数允许在子类中重写覆盖父类中的函数达到动态绑定或运行时多态的效果。下面是虚函数的一些关键要点
2.1.1 虚函数的基本定义 虚函数是在基类中声明的函数其声明前使用关键字 virtual。虚函数的作用是允许在派生类中重新定义该函数并且在运行时程序会根据对象的实际类型来决定调用哪一个版本的函数而不是根据指针或者引用的类型来决定。
2.1.2 虚函数的语法
在基类中声明一个虚函数的基本语法如下
class Base {
public:virtual void show() {std::cout Base class show() std::endl;}
};
2.1.3 虚函数的作用 虚函数的主要作用是实现多态性即允许通过基类指针或引用来调用派生类的重写函数。
class Derived : public Base {
public:void show() override {std::cout Derived class show() std::endl;}
}; 如果你使用基类指针或引用指向派生类对象调用虚函数时实际执行的是派生类中的版本而不是基类中的版本
Base* basePtr;
Derived derivedObj;basePtr derivedObj;
basePtr-show(); // 输出Derived class show() 在这个例子中show()函数的调用会根据basePtr指向的实际对象derivedObj来决定使用派生类中的show()函数而不是基类中的show()。
2.1.4 虚函数与静态绑定/动态绑定
静态绑定即编译时决定调用哪个函数通常发生在没有使用虚函数的情况下。动态绑定即运行时决定调用哪个函数这就是虚函数的作用所在。 在虚函数的情况下程序在运行时通过动态绑定来决定实际调用的是基类的还是派生类的函数。
2.1.5 虚函数的析构函数 通常基类的析构函数也应该声明为虚函数。这是因为如果通过基类指针删除派生类对象虚析构函数可以确保派生类的析构函数被调用从而避免资源泄漏。
class Base {
public:virtual ~Base() {std::cout Base class destructor std::endl;}
};class Derived : public Base {
public:~Derived() override {std::cout Derived class destructor std::endl;}
}; 如果没有虚析构函数删除派生类对象时可能只会调用基类的析构函数导致派生类部分没有正确释放造成资源泄漏。
2.1.6 纯虚函数 当一个类中的虚函数没有具体的实现时称其为纯虚函数。纯虚函数在基类中只声明并且在函数声明的末尾加上 0。包含纯虚函数的类叫做抽象类抽象类不能直接实例化。
class Base {
public:virtual void show() 0; // 纯虚函数
}; Base类是一个抽象类不能直接实例化。必须通过继承并提供show()函数的实现才能创建派生类对象。
2.1.7 虚函数的性能开销 使用虚函数会有一定的性能开销因为系统需要在运行时查找函数的实际实现。这通常通过虚函数表VTable来实现。每个类中包含一个虚函数表表中存储了指向虚函数实现的指针。调用虚函数时程序会查找虚函数表并调用相应的函数。因此虚函数调用比普通函数调用稍慢但这种开销通常可以忽略不计除非在非常性能敏感的代码中。
2.1.8 总结 虚函数是面向对象编程中实现运行时多态性的重要工具。它使得通过基类指针或引用调用派生类重写的方法成为可能从而提高了程序的灵活性和扩展性。在实际应用中虚函数常用于需要多态行为的场景比如图形库中的形状对象如Circle、Rectangle等都可以通过基类指针调用draw()函数而不需要关心具体是哪一种形状。
2.2 多态的构成条件 多态是在不同继承关系的类对象去调用同一个函数产生了不同的行为。比如 Student 继承了 PersonPerson 对象买票全价Student 对象买票半价。 那么在继承中要构成多态还有两个条件 必须通过基类的指针或者引用调用虚函数被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写 2.3 虚函数的重写 在面向对象编程中虚函数重写是指在派生类中重新定义和实现基类中的虚函数。通过重写派生类可以改变或者扩展基类提供的功能。虚函数重写是实现运行时多态的核心机制。
2.3.1 虚函数重写的条件 虚函数的重写覆盖派生类中有一个跟基类完全相同的虚函数即派生类和基类虚函数的返回值类型、函数名字、参数列表完全相同称子类的虚函数重写了基类的虚函数。
class Person {
public:virtual void BuyTicket() { cout 买票-全价 endl; }
};class Student : public Person {
public:virtual void BuyTicket() { cout 买票-半价 endl; }/注意在重写基类虚函数时派生类的虚函数在不加virtual关键字时虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范不建议
这样使用//void BuyTicket() { cout 买票-半价 endl; }/
};void Func(Person p)
{ p.BuyTicket();
}int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
} 注意 在重写基类虚函数时派生类的虚函数在不加 virtual 关键字时虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范不建议这样使用但是如果基类虚函数没有写 virtual则不会构成多态。 建议两个虚函数都加上 virtual 2.3.2 虚函数重写的两个例外
2.3.2.1 协变基类与派生类虚函数返回值类型不同 派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用称为协变。
class A{};class B : public A {};class Person {
public:virtual A* f() {return new A;}
};class Student : public Person {
public:virtual B* f() {return new B;}
};
2.3.2.2 析构函数的重写基类与派生类析构函数的名字不同 如果基类的析构函数为虚函数此时派生类的析构函数只要定义无论是否加上 virtual 关键字都与基类的析构函数构成重写虽然基类与派生类函数的名字不同。虽然函数名不相同看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成 destructor。 下面这一份代码就是另外一种情况基类的析构函数没有进行加上 virtual这就没有构成多态当去析构子类的时候只会调用父类的析构函数造成内存泄露。 class Person {
public:virtual ~Person() {cout ~Person() endl;}
};class Student : public Person {
public:virtual ~Student() { cout ~Student() endl; }
};// 只有派生类Student的析构函数重写了Person的析构函数下面的delete对象调用析构函
数才能构成多态才能保证p1和p2指向的对象正确的调用析构函数。int main()
{Person* p1 new Person;Person* p2 new Student;delete p1;delete p2;return 0;
}
2.3.3 普通调用和多态调用 普通调用静态绑定调用和多态调用动态绑定调用是 C 中函数调用的两种基本方式他们有着本质的区别尤其体现在运行时函数选择的方式上。下面我们来详细解释他们的概念和区别
2.3.3.1 普通调用静态绑定调用 普通调用是指在编译时就确定了调用的函数编译器通过函数的名字、参数和作用域来确定该调用。普通调用发生在编译时也称为静态绑定或早绑定。 特点 编译时确定调用函数。适用于非虚函数。函数调用和具体的函数实现是静态绑定的编译器可以直接决定调用哪个函数。 class Base {
public:void display() { // 非虚函数std::cout Base display std::endl;}
};class Derived : public Base {
public:void display() { // 非虚函数std::cout Derived display std::endl;}
};int main() {Base b;Derived d;b.display(); // 普通调用调用的是 Base 的 displayd.display(); // 普通调用调用的是 Derived 的 displayreturn 0;
} 在上述代码中display()是普通的非虚函数调用。当b.display()和d.display()被调用时编译器在编译时根据对象的类型Base 或 Derived确定调用哪个函数这种调用方式称为静态绑定。
2.3.3.2 多态调用动态绑定调用 多态调用是指在程序运行时基于实际对象的类型来确定调用哪个函数而不是编译时就决定。这种机制依赖于虚函数和继承关系通常会使用虚函数来实现称为动态绑定或晚绑定。 特点 运行时决定调用的函数。适用于虚函数需要在基类中声明为virtual。通过对象的实际类型来决定调用哪个函数而不是通过声明时的类型来决定。 class Base {
public:virtual void display() { // 虚函数std::cout Base display std::endl;}
};class Derived : public Base {
public:void display() override { // 重写基类虚函数std::cout Derived display std::endl;}
};int main() {Base* b new Derived(); // 基类指针指向派生类对象b-display(); // 多态调用调用的是 Derived 的 displaydelete b;return 0;
} 在这个例子中Base类中的display()函数是虚函数。在main函数中基类指针b指向了Derived类的对象。调用b-display()时由于函数是虚函数编译器在运行时会根据对象的实际类型Derived来决定调用Derived类中的display()函数而不是基类Base中的display()函数。这种调用方式就是多态调用它依赖于运行时的动态绑定。
2.3.3.3 区别 2.3.3.4 怎么判断是普通调用还是多态调用 判断是普通调用还是多态调用的关键在于是否涉及到继承和方法重写或者接口实现。具体来说可以通过以下几个方面来判断
类的继承结构
普通调用通常是在同一个类中直接调用方法没有涉及继承关系。多态调用通常会涉及到父类和子类的继承关系在父类中定义了方法在子类中重写Override了该方法。
方法重写
普通调用在调用方法时如果没有涉及子类重写父类的方法调用的就是父类定义的普通方法。多态调用如果存在方法重写且在父类引用指向子类对象时实际调用的是子类重写的方法而不是父类的版本。
引用类型
普通调用调用的方法是根据引用类型例如A类的引用来确定的。多态调用调用的方法是根据实际对象的类型即运行时的类型来决定的而不是引用类型。例如当使用父类引用指向子类对象时运行时会调用子类中的方法。
是否使用 virtual 和 override
普通调用没有使用 virtual在父类方法上或没有使用 override在子类中。多态调用在父类方法上使用了 virtual并在子类中使用了 override。
方法调用时的实际对象模型
普通调用通常在调用方法时方法调用根据编译时确定的引用类型来决定。多态调用即使引用类型是父类类型实际调用的也是子类的重写方法这是基于对象的实际类型运行时类型来决定的。 当子类调用子类的虚函数时这依然属于普通调用因为这种情况是在同一个类中进行的且没有涉及到父类和子类之间的多态特性。
2.4 C11 的 override 和 final
实现一个类这个类不能被继承。
方法一父类构造函数私有化派送类实例不出来对象。方法二C11 中的 final 修饰的类为最终类不能被继承。
class A
{
private:A() {}
}// 方法二
class A final
{}
2.4.1 final
final 修饰虚函数表示该旭函数不能再被重写
class Car
{
public:virtual void Drive() final {}
};class Benz :public Car
{
public:virtual void Drive() {cout Benz-舒适 endl;}
};
2.4.2 override
override 检查派生类虚函数是否重写了基类某一个虚函数如果美欧重写编译报错
class Car{
public:virtual void Drive(){}
};class Benz :public Car {
public:virtual void Drive() override {cout Benz-舒适 endl;}
};
2.5 重载、覆盖重写、隐藏重定义的对比 2.5.1 重载 重载发生在同一个类中当一个类中存在多个方法名称相同但参数列表不同的方法时。重载是基于方法签名方法名称、参数类型、参数个数等来区分的。 特点 发生在同一个类中。方法名相同但参数不同参数个数或类型不同返回类型可以相同也可以不同但通常只看参数区分。编译时决定调用哪个方法。重载方法并不涉及继承和多态。 class MyClass {void print(int a) {System.out.println(Printing integer: a);}void print(double a) {System.out.println(Printing double: a);}void print(int a, double b) {System.out.println(Printing int and double: a and b);}
}
2.5.2 覆盖重写 覆盖发生在子类中当子类继承父类的方法并重新定义该方法时。重写是基于方法签名相同来实现的。 特点 发生在继承关系中即父类和子类之间。方法名、返回类型、参数列表相同仅仅是子类重新定义了父类方法的实现。动态绑定方法的调用是在运行时决定的依赖于对象的实际类型。在子类中使用 Override 注解标记Java 中可以避免方法签名不匹配时出现错误。 #include iostream
using namespace std;class Animal {
public:virtual void sound() { // 虚函数允许子类重写cout Animal makes a sound endl;}
};class Dog : public Animal {
public:void sound() override { // 重写父类的 sound 方法cout Dog barks endl;}
};int main() {Animal* animal new Dog();animal-sound(); // 调用的是 Dog 类中的 sound() 方法delete animal;return 0;
}
2.5.3 隐藏重定义 隐藏是指子类重新定义了父类中已经存在的方法、字段或属性但这种重新定义并不属于覆盖重写的范畴。对于字段来说隐藏是通过子类声明一个同名的字段来实现的对于方法来说它指的是子类声明一个与父类方法名称相同但签名不同的方法。 特点 字段隐藏子类中声明一个与父类字段名称相同的字段这样父类的字段在子类中就被隐藏了。方法隐藏子类中声明一个与父类方法名称相同的方法且方法签名不同实际上并没有实现覆盖重写。这种情况下父类的方法不会被动态调用而是根据引用的类型决定使用哪个方法。编译时决定方法和字段的隐藏会根据引用的类型而非实际对象类型来决定使用哪个成员。 2.5.3.1 字段隐藏
#include iostream
using namespace std;class Animal {
public:int numLegs 4; // 父类字段
};class Dog : public Animal {
public:int numLegs 3; // 子类字段隐藏父类字段
};int main() {Dog d;cout Dog has d.numLegs legs. endl; // 输出 3Animal a;cout Animal has a.numLegs legs. endl; // 输出 4return 0;
}
2.5.3.2 方法隐藏
#include iostream
using namespace std;class Animal {
public:void sound() {cout Animal makes a sound endl;}
};class Dog : public Animal {
public:void sound(int times) { // 方法隐藏参数不同for (int i 0; i times; i) {cout Dog barks endl;}}
};int main() {Dog d;d.sound(3); // 调用 Dog 类的 sound(int) 方法Animal a;a.sound(); // 调用 Animal 类的 sound() 方法return 0;
} 总结
重载同一类中多个方法名相同但参数不同编译时决定。覆盖重写子类重新定义父类的方法方法签名相同运行时动态绑定支持多态。隐藏重定义子类重新定义了父类的方法或字段方法签名不同或字段名相同编译时决定。
三、抽象类
3.1 概念 在虚函数的后面写上 0 则这个函数称为纯虚函数。包含纯虚函数的类叫做抽象类也叫做接口类抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出接口继承。
3.2 接口继承和实现继承 普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成多态继承的是接口所以如果不实现多态不要把函数定义为虚函数。
四、多态的原理
4.1 虚函数表 我们可以通过一道题目来引出虚函数表 答案为8 // 这里常考一道笔试题sizeof(Base)是多少
class Base
{
public:virtual void Func1(){cout Func1() endl;}
private:int _b 1;
}; 通过观察测试我们发现 b 对象是 8 bytes除了 _b 成员还多了一个 _vfptr 放在对象的前面注意有些平台可能会放到对象的最后面这个跟平台有关对象中的这个指针我们叫做虚函数表指针v 代表 virtualf 代表 function。一个含有虚函数的类中都至少有一个虚函数表指针因为虚函数的地址要被放到虚函数表中虚函数表也简称为虚表。那么派生类中这个表中放了些什么我们来继续分析 // 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
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;}
private:int _d 2;
};int main()
{Base b;Derive d;return 0;
} 通过观察和测试我们发现了以下几点问题
派生类对象 d 中也有一个虚表指针d 对象由两个部分构成一部分是父类继承下来的成员虚表指针也就是存在父类的和自己重写覆盖的另一部分是自己的成员。基类 b 对象和派生类 d 对象虚表是不一样的这里我们发现 Func1 完成了重写所以 d 的虚表中存的是重写的 Derive::Func1所以虚函数的重写叫做覆盖覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法覆盖是原理层的叫法。另外 Func2 继承下来后是虚函数所以放进了虚表Func3 也继承下来了但是不是虚函数所以不会放进虚表中。虚函数表本质是一个存虚函数指针的指针数组一般情况下这个数据最后放了一个 nullptr。总结一下派生类的虚表生成 先将基类中的虚表内容拷贝一份到派生类虚表中如果派生类重写了基类中的某一个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函数派生类自己新增加的虚函数按其在派生类中的声明义序增加到派生类虚表的最后这里还有一个容易混淆的问题虚函数存在哪里虚表存在哪里注意虚表存的是虚函数指针不是虚函数虚函数和普通函数一样的都是存在代码段中只是他的指针又存到了虚表中。另外对象中存的不是虚表存的是虚表指针。那么虚表存在哪的呢实际我们去验证一下会发现 vs 下是存在代码段的Linux中是什么呢
检验虚函数表在 vs 中的位置
class Base
{
public:Base():_b(2){//cout Base() endl;}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;}virtual void Func3(){cout Derive::Func3() endl;}
private:int _d 2;
};int main()
{Base b;Base b1;Base b2;Derive d;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* p3 b;Derive* p4 d;printf(Base虚表地址:%p\n, (int)p3);printf(Base虚表地址:%p\n, (int)p4);return 0;
} 检验虚函数表在 Linux 中的位置
#include iostream
using namespace std;class Base
{
public:Base(): _b(2){// cout Base() endl;}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;}virtual void Func3(){cout Derive::Func3() endl;}private:int _d 2;
};int main()
{Base b;Base b1;Base b2;Derive d;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 *p3 b;Derive *p4 d;printf(Base虚表地址:%p\n, *(int *)p3);printf(Base虚表地址:%p\n, *(int )p4);return 0;
} 4.2 多态的原理 上面分析了这个半天了那么多态的原理到底是什么还记得这里Func 函数传 Person 调用的 Person::BuyTicket 传 Student 调用的是 Student::BuyTicket class Person {
public:virtual void BuyTicket() { cout 买票-全价 endl; }
};class Student : public Person {
public:virtual void BuyTicket() { cout 买票-半价 endl; }
};void Func(Person p)
{p.BuyTicket();
}int main()
{Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
}
观察下图的红色箭头我们看到p 是指向 mike 对象时p-BuyTicket 在 mike 的虚表中找到虚函数是 Person::BuyTicket观察下图的蓝色箭头我们看到p 是指向 johnson 对象时p-BuyTicket 在 jonhson 的虚表中找到虚函数是 Student::BuyTicket这样就实现了不同对象去完成同一行为时展示出不同的形态反过来思考我们要达多态有两个条件一个是虚函数覆盖一个是对象的指针或者引用调用虚函数 4.3 动态绑定与静态绑定
静态绑定又称为前期绑定早绑定在程序编译期间确定了程序的行为也称为静态多态比如函数重载动态绑定又称后期绑定晚绑定是在程序运行期间根据具体拿到的类型确定程序的具体行为调用具体的函数也称为动态多态
4.4 虚函数表和虚表指针是什么时候初始化的 虚函数表VTable和虚函数表指针VPtr的初始化通常与对象的构造过程相关尤其是在对象的类型包含虚函数时。理解这两个概念对于深入理解 C 的面向对象机制以及多态的实现方式是非常重要的。以下是关于它们初始化时机的详细说明。
4.4.1 虚函数表 虚函数表是一个内部的数据结构用于存储类的虚函数地址。每个含有虚函数的类即包含至少一个虚函数的类都有一个与之关联的虚函数表。 在类加载时编译时虚函数表本身是由编译器在程序加载时生成的并且是与类本身关联的。虚函数表本质上是一个数组其中的每个元素是指向虚函数的指针。每个类有且仅有一个虚函数表它存储了该类的虚函数的地址。 在程序启动时初始化虚函数表通常在程序的启动阶段被加载到内存中例如由操作系统的加载器或运行时环境。每个类的虚函数表在程序启动时就已经初始化实际上是在静态存储区域中维护的并且对于所有的对象实例都是共享的。
4.4.2 虚函数表指针 虚函数表指针VPtr是每个对象实例中隐式存在的指针指向该对象所属类的虚函数表。它的初始化时机紧密地与对象的构造过程有关。 在对象构造时初始化虚函数表指针是由编译器自动生成的并且会在对象的构造函数中被初始化。每当一个对象实例被创建时虚函数表指针会被初始化为指向该对象所属类的虚函数表。 在构造函数中初始化虚函数表指针的初始化通常发生在对象构造过程中。在 C 中构造函数的执行顺序是首先执行基类的构造函数然后执行派生类的构造函数如果有。在基类的构造函数执行时虚函数表指针已经指向基类的虚函数表即使在派生类的构造函数执行之前虚函数表指针已经是有效的。如果对象是派生类的实例虚函数表指针会在构造过程中被更新为指向派生类的虚函数表具体取决于当前构造函数执行到的部分。 如果是动态分配的对象通过 new 创建虚函数表指针会在分配内存后、构造函数执行之前初始化确保对象的虚函数表指针正确地指向相应的虚函数表。
4.4.3 总结初始化时机
虚函数表VTable在程序加载时初始化由编译器生成。它是与类相关的静态数据结构。虚函数表指针VPtr在对象的构造函数中初始化。对于每个对象实例虚函数表指针会在对象构造期间被设置为指向相应类的虚函数表。对于基类的构造函数虚函数表指针指向基类的虚函数表对于派生类的构造函数虚函数表指针会在构造过程中更新为指向派生类的虚函数表。
#include iostreamclass Base {
public:Base() {std::cout Base constructor\n;// 虚函数表指针已经指向 Base 的虚函数表}virtual void foo() {std::cout Base foo\n;}
};class Derived : public Base {
public:Derived() {std::cout Derived constructor\n;// 虚函数表指针会在此时指向 Derived 的虚函数表}void foo() override {std::cout Derived foo\n;}
};int main() {Base b new Derived();b-foo(); // 会调用 Derived::foo展示了虚函数的多态性delete b;return 0;
}
执行流程
程序启动时虚函数表会被生成并加载到内存中。Base 类和 Derived 类分别拥有自己的虚函数表。当 Derived 对象被创建时Base 类的构造函数会首先执行。在此时虚函数表指针vptr会被设置为指向 Base 类的虚函数表。当 Derived 类的构造函数执行时虚函数表指针会被更新为指向 Derived 类的虚函数表。调用 foo() 时因为 vptr 已经指向 Derived 类的虚函数表最终调用的是 Derived 类的 foo() 函数。 因此虚函数表和虚函数表指针的初始化是与对象的构造过程密切相关的虚函数表是静态的早期就初始化虚函数表指针是动态的随着对象的构造过程逐步设置。
五、单继承和多继承关系的虚函数表 需要注意的是在单继承和多继承关系中下面我们去关注的是派生类对象的虚表模型因为基类的虚表模型没有什么特别研究的了。
5.1 单继承中的虚函数表 在单继承中一个派生类从一个基类继承并可能重写一些虚函数。虚函数表的结构比较简单每个类有一个虚函数表VTable该表包含指向该类的虚函数实现的指针。每个对象实例有一个虚函数表指针VPtr指向当前对象所属类的虚函数表。
#include iostream
using namespace std;class Base {
public:virtual void foo() {cout Base foo\n;}
};class Derived : public Base {
public:void foo() override {cout Derived foo\n;}
};int main() {Base* b new Derived();b-foo(); // 输出 Derived foodelete b;return 0;
}
虚函数表结构
Base 类有一个虚函数表VTable它存储指向 Base::foo 的指针。Derived 类有一个虚函数表VTable它存储指向 Derived::foo 的指针。对于 Derived 对象虚函数表指针VPtr指向 Derived 的虚函数表调用 foo() 时将调用 Derived::foo。
初始化过程
在 Base 类的构造函数中虚函数表指针指向 Base 类的虚函数表。在 Derived 类的构造函数中虚函数表指针会被更新为指向 Derived 类的虚函数表。 class Base {
public:virtual void func1() { cout Base::func1 endl; }virtual void func2() { cout Base::func2 endl; }
private:int a;
};
class Derive :public Base {
public:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }virtual void func4() { cout Derive::func4 endl; }
private:int b;
};int main()
{Base b;Derive d;return 0;
} 通过观察下图的监视窗口中我们发现看不见 func3 和 func4。这里是编译器的监视窗口故意隐藏了这两个函数也可以认为是他的一个小bug那么我们应该如何查看d的虚表呢下面我们使用代码打印出虚表中的函数。 代码打印虚表
typedef void(VF_PTR)();// 打印虚表本质打印指针(虚函数指针)数组
//void PrintVFT(VF_PTR vft[], int n)
//void PrintVFT(VF_PTR vft[])
void PrintVFT(VF_PTR vft)
{for (size_t i 0; vft[i] ! nullptr; i){printf([%d]:%p-, i, vft[i]);VF_PTR f vft[i];f();//(*f)();}cout endl endl;
}
5.2 多继承中的虚函数表
5.2.1 多继承中的虚函数表介绍 多继承涉及一个类从多个基类继承。在这种情况下每个基类都有一个自己的虚函数表因此在对象中需要存储多个虚函数表指针VPtr。每个基类的虚函数表指针将指向对应基类的虚函数表派生类的虚函数表指针指向派生类的虚函数表。
#include iostream
using namespace std;class Base1 {
public:virtual void foo() {cout Base1 foo\n;}
};class Base2 {
public:virtual void bar() {cout Base2 bar\n;}
};class Derived : public Base1, public Base2 {
public:void foo() override {cout Derived foo\n;}void bar() override {cout Derived bar\n;}
};int main() {Derived d;d.foo(); // 输出 Derived food.bar(); // 输出 Derived barreturn 0;
}
虚函数表结构
Base1 类有一个虚函数表VTable1存储指向 Base1::foo 的指针。Base2 类有一个虚函数表VTable2存储指向 Base2::bar 的指针。Derived 类有一个虚函数表VTable3它存储指向 Derived::foo 和 Derived::bar 的指针。Derived 对象中会有三个虚函数表指针VPtrs分别指向 Base1 的虚函数表VTable1Base2 的虚函数表VTable2Derived 的虚函数表VTable3
初始化过程
当 Derived 类对象被创建时虚函数表指针将指向 Base1、Base2 和 Derived 类的虚函数表。基类的构造函数Base1 和 Base2会负责初始化这些虚函数表指针。在构造过程中Derived 的虚函数表指针会被更新为指向 Derived 类的虚函数表。
多继承的虚函数表布局
Base1 类有一个虚函数表指针指向 Base1 的虚函数表。Base2 类有一个虚函数表指针指向 Base2 的虚函数表。Derived 类有一个虚函数表指针指向 Derived 的虚函数表。
5.2.2 多继承中虚函数表的具体细节 多继承时由于每个基类都有自己的虚函数表编译器需要在对象中为每个基类维护独立的虚函数表指针。如果派生类重写了某个基类的虚函数编译器会更新相应的虚函数表指针确保调用的是正确的虚函数。
当 Derived 类对象创建时 Base1 的虚函数表指针指向 Base1 的虚函数表。Base2 的虚函数表指针指向 Base2 的虚函数表。Derived 的虚函数表指针指向 Derived 的虚函数表包含 Derived 类重写的虚函数。
5.2.3 虚函数表指针的布局 对于多继承来说虚函数表指针的存储顺序通常是由继承的顺序决定的。例如如果一个类 D 继承了 B1 和 B2那么 D 的对象通常会首先存储一个指向 B1 的虚函数表指针然后存储指向 B2 的虚函数表指针最后是指向 D 自身虚函数表的指针。 在内存布局上一个对象的虚函数表指针数组的顺序与继承的顺序有关。例如Derived 对象的内存布局可能如下所示
| VPtr to Base1 | — | VPtr to Base2 | — | VPtr to Derived |
5.2.4 总结 单继承每个类有一个虚函数表虚函数表指针在对象构造过程中初始化并指向当前类的虚函数表。多继承每个基类有独立的虚函数表每个基类有一个虚函数表指针派生类的虚函数表指针会指向每个基类的虚函数表同时也会指向自己的虚函数表。内存布局多继承会在对象中存储多个虚函数表指针按继承顺序布局。 虚函数表机制的设计使得 C 在处理继承和多态时能够有效地实现动态绑定尽管多继承的情况下需要处理多个虚函数表指针的复杂情况。 为什么在多继承中子类有自己的虚函数指针不是使用父类的虚函数指针 在C的多继承中子类会有自己的虚函数指针而不是直接使用父类的虚函数指针这是由于以下几个关键原因 多继承中的虚函数指针独立性 在多继承的情况下子类不仅继承了多个父类的属性和方法还可能重写父类的虚函数。为了能够正确地支持动态多态和虚函数的调用编译器必须为每个基类维护独立的虚函数表指针VPtr。这意味着每个父类的虚函数表VTable仍然存在并且在派生类中每个父类都有一个指向其对应虚函数表的指针。这样每个父类的虚函数都可以独立地被调用而不会与其他父类的虚函数表发生冲突。 子类可能重写父类的虚函数 在多继承中子类可能会重写某些基类的虚函数。如果子类直接使用父类的虚函数指针那么在运行时就无法正确地调用到子类重写后的虚函数。 举个例子假设有两个基类 Base1 和 Base2并且它们各自有一个虚函数 foo()同时子类 Derived 重写了这两个虚函数。如果子类的虚函数表指针只是简单地继承父类的指针那么当通过父类指针调用 foo() 时可能会调用到父类的版本而不是子类的版本。为了确保动态多态正确性编译器需要给每个父类维护独立的虚函数表指针。 虚函数表指针的布局 在多继承中编译器会为每个基类创建独立的虚函数表并为每个类对象创建多个虚函数表指针。在派生类对象中每个基类的虚函数表指针指向对应基类的虚函数表派生类的虚函数表指针指向派生类自己的虚函数表。 这种做法确保了即使在多继承的情况下每个基类的虚函数能够正确地进行调用和重写。具体来说派生类的虚函数表会包含指向它自己的虚函数实现的指针而每个基类的虚函数表会包含指向基类的虚函数实现的指针。如果基类中的虚函数被子类重写了那么虚函数表中的指针会指向子类的重写版本。 子类和父类的虚函数表的不同 即使父类和子类都定义了虚函数子类的虚函数表通常会与父类的虚函数表不同。子类可能会在虚函数表中替换父类的虚函数指针指向子类自己的实现。 避免虚函数调用冲突 如果没有子类自己的虚函数表指针而是直接使用父类的虚函数指针那么会有潜在的冲突。例如如果父类有相同名称但不同实现的虚函数或者在派生类中有重写的虚函数直接使用父类的虚函数表指针就无法确保正确的动态绑定。每个类自己的虚函数表指针确保了正确的多态性避免了父类指针或虚函数表指针间的冲突。 在多继承中子类有自己的虚函数指针而不是简单地使用父类的虚函数指针主要是为了确保 动态多态能够正确地调用到子类重写的虚函数。每个基类的虚函数表指针可以独立维护避免了不同基类虚函数的冲突。确保每个基类在多继承中的虚函数都能够正确地调用不受到其他基类的影响。 多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。 5.3 菱形继承、菱形虚拟继承了解
相关文章
-
宁夏城乡和住房建设厅网站wordpress获取文章块
宁夏城乡和住房建设厅网站wordpress获取文章块
- 技术栈
- 2026年03月21日
-
宁晋seo网站优化排名凡客诚品购物
宁晋seo网站优化排名凡客诚品购物
- 技术栈
- 2026年03月21日
-
宁津做网站公司重庆网站建站模板
宁津做网站公司重庆网站建站模板
- 技术栈
- 2026年03月21日
-
宁夏固原建设网站企业网站的建设与应用开题报告
宁夏固原建设网站企业网站的建设与应用开题报告
- 技术栈
- 2026年03月21日
-
宁夏建设工程招标投标信息网站济南个人网站建设
宁夏建设工程招标投标信息网站济南个人网站建设
- 技术栈
- 2026年03月21日
-
宁夏建设管理局网站宠物网站怎么做
宁夏建设管理局网站宠物网站怎么做
- 技术栈
- 2026年03月21日






