1.虚函数的底层原理
虚函数的底层原理主要涉及虚函数表(Virtual Table,简称V-Table)和虚函数指针(vptr)的概念。以下是虚函数底层原理的详细解释:
-
虚函数表(V-Table)
-
虚函数表是一个类的虚函数的地址表,用于索引类本身以及父类的虚函数的地址。
-
每一个含有虚函数的类(无论是其本身的,还是继承而来的)都至少有一个与之对应的虚函数表。
-
虚函数表中存放着该类所有的虚函数对应的函数指针。子类对象的虚函数表中既包含继承自基类的虚函数指针,也包含子类新增或重写的虚函数指针。
-
在多重继承的情况下,子类对象中将包含多个虚函数表的指针,分别指向对应不同基类的虚函数表。
-
-
虚函数指针(vptr)
-
为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个私有指针(通常命名为
__vptr
),用来指向虚表。 -
当类的对象在创建时,这个指针的值会自动被设置为指向类的虚表。
-
只有拥有虚函数的类才会拥有虚函数指针,所以拥有虚函数的类的所有对象都会因为虚函数产生额外的开销。
-
-
虚函数调用过程
-
当通过父类指针或引用来调用虚函数时,程序会首先查找该对象的虚函数指针(vptr)。
-
然后通过虚函数指针找到对应的虚函数表(V-Table)。
-
在虚函数表中,根据函数的偏移量找到对应的函数指针。
-
最后,通过这个函数指针来调用实际的函数。
-
-
动态联编
-
虚函数的调用是通过动态联编实现的。与普通函数的静态联编不同,动态联编在运行时确定调用哪个函数,而静态联编在编译时就确定了函数的地址。
-
动态联编使得父类指针或引用能够调用子类重写的虚函数,从而实现多态性。
-
-
注意事项
-
虚函数表通常放在对象的开始地址处,以提高访问效率。
-
虚函数表的大小取决于类中虚函数的数量。
-
虚函数表是类的一部分,而不是对象的一部分,但每个对象都包含指向其类虚函数表的指针。
-
总结来说,虚函数的底层原理是通过虚函数表和虚函数指针来实现的。编译器为每个含有虚函数的类创建一个虚函数表,并在每个对象中添加一个指向该表的虚函数指针。当通过父类指针或引用来调用虚函数时,程序会根据虚函数指针找到对应的虚函数表,并在表中根据偏移量找到要调用的函数。这种机制使得子类能够重写父类的虚函数,并在运行时根据对象的实际类型来调用相应的函数,从而实现多态性。
2.静态联编和动态联编的区别
动态联编(Dynamic Binding) 和 静态联编(Static Binding) 是面向对象编程中两个重要的概念,它们决定了在程序执行过程中如何确定要调用的函数或方法。
静态联编
静态联编(也称为早期联编或编译时联编)是在编译时确定函数调用或方法调用的目标。编译器在编译阶段根据函数或方法的名称和参数类型,直接确定要调用的函数或方法的地址,并将其嵌入到生成的代码中。因此,在程序运行时,函数或方法的调用是确定的,不会根据实际对象类型的不同而发生变化。
静态联编通常用于非虚函数的调用,因为非虚函数的调用在编译时就可以确定其目标。
动态联编
动态联编(也称为晚期联编或运行时联编)是在程序运行时确定函数调用或方法调用的目标。编译器在编译时只知道函数或方法的名称和参数类型,但不知道具体要调用哪个函数或方法,因为这可能取决于实际对象的类型。因此,编译器会生成一些额外的代码来在运行时确定要调用的函数或方法的地址。
动态联编通常用于虚函数的调用。在面向对象编程中,子类可以重写父类的虚函数。当通过父类指针或引用来调用虚函数时,编译器会生成额外的代码来在运行时根据实际对象的类型来确定要调用的函数。这就是多态性的实现基础。
总结
静态联编和动态联编的主要区别在于函数调用或方法调用的确定时机。静态联编在编译时确定,而动态联编在运行时确定。动态联编是实现面向对象编程中多态性的关键机制之一。
3.C++ 虚函数表是属于类的还是属于对象的
虚函数表属于类,同一个类的多个对象共享同一张虚函数表。
4.为什么构造函数不能为虚函数?
虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数——构造函数了。
5.为什么析构函数可以为虚函数,如果不设为虚函数可能会存在什么问题?
首先析构函数可以为虚函数,而且当要使用基类指针或引用调用子类时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。 举例说明: class Animal { public: Animal(){} ~Animal(){} virtual void makeSound() { printf("The animal makes a sound"); } } class Dog :public Animal { public: Dog(){} ~Dog(){} void makeSound() { printf("The dog barks"); } } // 使用示例 Animal *myAnimal = new Dog(); myAnimal.makeSound(); // 输出 "The dog barks" 子类Dog继承自基类Animal;Animal *myAnimal = new Dog(); delete myAnimal;
1) 此时,如果类Animal的析构函数不是虚函数,那么delete myAnimal;将会仅仅调用Animal的析构函数,只释放了Dog对象中的Animal部分,而派生出的新的部分未释放掉。
2) 如果类Animal的析构函数是虚函数,delete myAnimal; 将会先调用Dog的析构函数,再调用Animal的析构函数,释放Dog对象的所有空间。 补充: Dog *p = new Dog; delete p;时也是先调用Dog的析构函数,再调用Animal的析构函数。
注意事项
1)每个类都有虚指针和虚表; 2)如果不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时)。有多少个虚函数,虚表里面的项就会有多少。多重继承时,可能存在多个的基类虚表与虚指针; 3)如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,多重继承时虚基表与虚基表指针有且只有一份。
6.虚析构函数的作用
虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的.
多重继承会有多个虚函数表,几重继承,就会有几个虚函数表。这些表按照派生的顺序依次排列,如果子类改写了父类的虚函数,那么就会用子类自己的虚函数覆盖虚函数表的相应的位置,如果子类有新的虚函数,那么就添加到第一个虚函数表的末尾。
7.虚函数表存放在哪里
虚函数表vtable在Linux/Unix中存放在可执行文件的只读数据段中(rodata),这与微软的编译器将虚函数表存放在常量段存在一些差别。
(存放在全局数据区)
1.虚函数表是全局共享的元素,即全局仅有一个.
2.虚函数表类似一个数组,类对象中存储vptr指针,指向虚函数表.即虚函数表不是函数,不是程序代码,不肯能存储在代码段.
3.虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函数表的大小可以确定,即大小是在编译时期确定的,不必动态分配内存空间存储虚函数表,所以不再堆中.
根据以上特征,虚函数表类似于类中静态成员变量.静态成员变量也是全局共享,大小确定.
8.多重继承的虚函数表布局
多重继承允许一个派生类继承多个基类,从而可以在一个类中获得多个不同基类的特性。
当一个类使用多重继承时,每个基类都有自己的虚函数表。派生类会根据基类的顺序,在其对象布局中分别保存每个基类的虚函数表指针。
虚函数表的布局与多重继承的顺序有关。对于单一继承,派生类的虚函数表只包含其直接基类的虚函数表指针和虚函数;对于多重继承,派生类的虚函数表中按照基类的继承顺序依次包含每个基类的虚函数表指针和虚函数。
9.C++中的override关键字
在C++中,override
是一个关键字,用于显式地指示派生类中的成员函数覆盖(override)了基类中的虚函数。当使用override
关键字时,编译器会检查派生类中声明的成员函数是否与基类中的虚函数具有相同的签名。如果不同,编译器将发出错误。
10.C++中可以用子类指针指向父类对象吗
不可以。
当我们使用一个子类指针指向一个父类对象时,由于子类可能包含父类没有的成员变量或方法,子类指针无法访问这些在父类中不存在的成员。因此,编译器会报错。
但是可以用父类指针指向子类对象,这也是C++中多态的实现方式。当我们声明一个基类指针指向一个派生类对象时,如果这个指针调用了一个虚函数,那么实际执行的是派生类中的虚函数,而不是基类中的虚函数。这就是多态性的一种表现。
11.使用父类指针指向子类对象,父类指针的地址和子类this指针地址一样吗
一样的。
1.父类指针的地址:当你声明一个父类指针并让它指向一个子类对象时,这个指针实际上存储的是子类对象在内存中的地址。但是,由于这个指针是父类类型的,它只能通过这个地址访问到父类部分的成员(包括属性和方法)。
class Parent {
// ...
};
class Child : public Parent {
// ...
};
int main() {
Child child;
Parent* parentPtr = &child; // parentPtr 存储的是 child 对象的地址
// ...
}
2.子类对象的this指针:每个对象都有一个隐式的this
指针,它指向对象自身。对于子类对象来说,this
指针指向的是子类对象在内存中的起始地址。这个地址可以用来访问子类对象的所有成员,包括从父类继承来的成员和子类自己定义的成员。
3.语义和用途:虽然父类指针和子类this
指针在物理上可能指向同一个地址(当父类指针指向子类对象时),但它们的语义和用途是不同的。父类指针只能用来访问父类的成员,而子类this
指针可以用来访问子类的所有成员。