虚函数表
c++重写父类的函数必须是虚函数吗?
- 如果父类的函数不是虚函数,子类也可以重新声明一个同名的函数,但这只是隐藏了父类的函数,而非重写。
- 重写是子类对父类的方法进行覆盖,子类继承父类的方法后可以根据需要进行重写;重载是在同一个类中定义多个方法,方法名相同但参数列表不同。
虚函数表是什么?
-
虚函数表
可以看作
是一个类的静态成员
,它存储了该类所有虚函数的地址。C++虚函数表为什么不设计成静态成员的形式? -
在C++中,虚函数是通过虚函数表(Virtual Function Table)来实现的。
-
当一个类中存在虚函数时,编译器会为该类生成一个虚函数表。每个
类对象
都包含一个指向虚函数表的指针
(通常被称为虚函数指针或vptr),这个指针存在于对象的内存布局中。当对象被创建时,vptr会被初始化为指向该类的虚函数表。
-
当通过对象调用虚函数时,编译器会通过对象的vptr找到对应的虚函数表,并根据虚函数的索引(通常是函数在虚函数表中的位置)来调用相应的虚函数。这就是动态绑定(Dynamic Binding)的机制,它能够在运行时确定对象的具体类型,并调用相应的虚函数,而不是在编译时就确定调用的函数。
-
注:内存结构类似金字塔的结构,地址越低越稳定,地址越高对程序的影响越大(栈区之上为linux系统内核空间)
c++如何通过虚函数表重写父类的虚函数?
- 类 B 继承于类 A,类 B 可以调用类 A 的函数,类B的虚函数表中会包含所有类A的地址。
- 如果类B重写了类A的虚函数,则类B的虚函数表中对应位置的虚函数地址会改变
c++ 中类对象的指针是如何知道自己的访问边界的?
-
在C++中,类对象的指针并不直接知道自己的访问边界。指针本身只存储了对象的内存地址,不包含关于对象大小或边界的信息。
-
类对象的访问边界是由编译器在编译时确定的,编译器会根据
类的定义计算对象的大小
,并在对象的内存布局中添加用于管理对象的内部操作的元数据。这些元数据包括虚函数表指针、虚基类指针等。 -
所以当我们使用以下代码时
D DrivObject;
B *p = & DrivObject;
p只能访问基础对象的所有成员变量,成员函数和虚表指针(所有虚函数被一个虚表指针代替了),虚表指针属于基类部分,所以可以用p访问D中重写的函数。
如果基类析构函数没有声明为虚函数会造成什么问题?
- 虚函数表实在对象构造之后才建立的,所以构造函数不可能是虚函数,且不能在构造函数内调用虚函数
若析构函数不是虚函数,delete 时,只有基类会被释放,而子类没有释放
,存在内存泄漏的隐患。(因为代码中没有子类的对象,只有基类的指针。) - 代码例子:
#include <iostream>
class Base {
public:
int baseValue;
Base(int value) : baseValue(value) {}
virtual ~Base(){std::cout << "~Base()"<< std::endl;}
virtual void ShowValue() {
std::cout << "Base Value: " << baseValue << std::endl;
}
};
class Derived : public Base {
public:
int derivedValue;
Derived(int base, int derived) : Base(base), derivedValue(derived) {}
~Derived(){std::cout << "~Derived()"<< std::endl;}
void ShowValue() {
std::cout << "Derived Value: " << derivedValue << std::endl;
}
};
int main() {
// Derived derivedObj(1, 100);
Base* basePtr = new Derived(1, 100); // 子类指针对象赋值给父类指针
basePtr->ShowValue(); // 调用的是父类的 ShowValue(),不是子类的版本 , Base Value: 1
delete basePtr;
return 0;
}
在c++的多态中,为什么虚析构函数会逐层调用而其他重写的函数不会?
这是因为类的创建和虚构是“逐层”进行的:
-
在类的创建过程中,如果该类继承自其他类,则会先调用父类的构造函数,以确保父类的属性和状态也被正确初始化。这个过程会一直进行下去,直到达到类层次结构的顶层。
-
类的虚构与类的创建相反,它是在销毁一个类的实例时进行的。当一个类的实例不再被使用时,会调用该类的析构函数来释放对象所占用的资源。析构函数会按照与构造函数相反的顺序逐层调用,先调用子类的析构函数,然后再调用父类的析构函数,直到达到类层次结构的顶层。