1.类创建对象创建与初始化流程:
-
对象和变量是在
编译时
就确定内存大小
的,但同时也在编译期间确定该类没有虚函数,若没有,则会进行内存优化
,不实际分配虚函数指针的内存空间。 -
运行时
,对象是先创建的,然后再分配内存。系统为对象的非静态成员变量与vptr(虚函数指针)分配内存,并且在内存空间中,会先安放vptr(虚函数指针),但是此时的虚函数指针并未赋值
, -
当
调用构造函数
,在构造函数执行过程中,会将vptr指向正确的虚函数表,从而绑定对象和其虚函数。 -
对于没有虚函数的类,vptr可以为null或指向一个共享的空虚函数表。虽然在分配对象内存时考虑了vptr指针,但对于无虚函数的类,这部分内存也不会真正占用,它的大小被优化掉了。这是一种编译优化技术。
【此时插一嘴,C语言结构体是不能为空的,而C++中类可为空,struct在C中是结构体关键字,而在C++中是类的关键字,空类内存大小为1个字节】
2.编译器在编译阶段就会确定C++类对象的内存布局,主要根据以下几个因素来确定:
-
类中声明的成员变量的大小和类型
编译器需要为每个成员变量在对象内存中分配足够的空间。
-
类是否有虚函数
如果有虚函数,需要在对象开头预留一个虚函数表指针vptr。
-
数据成员的对齐需要
根据数据类型选择适当的对齐方式来优化内存访问速度。
-
编译器优化策略
如尾部优化可以减少内存碎片。
所以在编译阶段
,编译器就会根据这些因素来确定类的对象模型,计算出对象的大小
,并在程序运行时为对象在堆或栈上正确分配内存
。
这就是C++中类内存布局的编译期确定性的重要原因。
3.C++编译期间确定对象或者变量分配的内存有什么好处?
- 编译器在编译期间确定变量的内存空间,并进行内存优化处理,是因为这可以提高程序的性能。
- 通过在编译期间确定变量的内存空间,编译器可以避免在运行时分配和释放内存。这可以减少程序的运行时间,并提高程序的性能。
4.对象创建时,构造函数和虚指针的关系:
在C++中,创建对象时的过程大致如下:
- 调用对象的构造函数前,会先分配对象所需的内存空间
- 在内存空间中,会先安放虚函数表指针vptr。
- 然后调用构造函数初始化对象其他成员。
- 对于没有虚函数的类,vptr可以为null或指向一个共享的空虚函数表。
- 对于有虚函数的类,构造函数会初始化vptr,使它指向正确的虚函数表。
所以对象在调用构造函数前,内存是提前分配的,里面会有一个vptr指针。但这个指针可能最初没有明确指向正确的虚函数表。在构造函数执行过程中,会将vptr指向正确的虚函数表,从而绑定对象和其虚函数。
这是C++中通过vptr实现多态的一种技术手段。所以对象创建时vptr的确定是在构造函数执行过程中完成的
。
5.C++中,虚函数是在编译时就确定内存了,那虚函数表是在什么时候创建的?
虚函数表
是在编译时创建
的。当编译器遇到虚函数时,它会在编译时创建一个虚函数表,该表包含指向每个虚函数的指针。当程序运行时,虚函数表会被加载到内存中,这样程序就可以在运行时调用虚函数。
虚函数指针
是在调用构造函数时被赋值
的。当构造函数被调用时,虚函数指针会被指向虚函数表中的相应条目。
专业名词:
虚指针(vptr)
虚基表/虚函数表(vtable)
虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。
6.构造函数为什么不能为虚函数?
-
首先虚函数指针是在编译时就确定了内存的,但是并未赋值,需要等到对象创建调用构造函数时才会被正确赋值,也就是说如果构造函数是虚函数,那么对象创建后是找不到虚构造函数的地址的调用的。
-
构造函数是虚函数并没有实际意义,虚函数的目的是运行时实现多态,即通过父类的指针或者引用调用子类的成员函数,而构造函数是在创建对象时自动调用的,不能通过父类或者引用去调用。
7.析构函数为什么要虚函数?
C++中基类采用virtual虚析构函数是为了防止内存泄漏,即动态多态时能够调用子类的析构函数
如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。
8.那动态多态时,父类指针或引用调用子类的对象时,虚函数指针和虚函数表会有什么变化?
在动态多态时,父类指针或引用调用子类的对象时,虚函数指针和虚函数表会发生变化。虚函数指针会指向子类的虚函数表,而虚函数表会包含子类的虚函数。这使得父类指针或引用可以调用子类的虚函数。
9.C++中虚函数表的作用
虚函数表的作用如下:
实现运行时多态
: C++中的多态性是面向对象编程的重要特性之一。通过将基类的函数声明为虚函数,派生类可以在其内部重新定义该虚函数(即覆盖)。当通过基类指针或引用调用虚函数时,实际调用的是派生类中对应的虚函数实现,而不是基类的实现。这样可以实现动态绑定,即在运行时根据对象的实际类型来决定调用哪个类的函数。
允许基类指针或引用引用派生类对象
: 虚函数表的存在使得基类指针或引用可以用于引用派生类的对象,从而实现了多态性的特性。这种方式支持基类指针或引用多态性,可以将不同派生类的对象存储在同一个容器中,而在运行时通过虚函数表来调用各自的虚函数。
支持动态继承
:C++中支持动态继承(Dynamic Inheritance),即在运行时动态地切换对象的类型。通过虚函数表的机制,可以在运行时动态改变基类指针或引用的指向,从而在运行时切换对象的类型。
节省内存空间
: 虚函数表是按照类的层次结构存储的,而不是每个对象都存储一份虚函数表。这样可以节省内存空间,因为所有同一类的对象共享同一个虚函数表。
总的来说,虚函数表是实现C++中多态性的重要机制,它使得通过基类指针或引用调用虚函数时能够在运行时根据对象的实际类型来确定调用哪个类的虚函数实现。这样使得C++的继承和多态特性成为可能,使得面向对象编程更加灵活和强大。
10.创建对象是先创建对象,那内存分配和构造函数调用的先后顺序是怎么样的
C++中先有对象,后分配内存,再调用构造
11.C++中构造函数能调用虚函数吗?
不能,虽然调用构造前,对象已经创建好了并分配了内存,但是虚函数指针并没有被赋值,虚函数指针需要在构造函数中被赋予正确的指向,如果在构造函数中调用虚函数很可能会发生未定义的行为。