文章目录
- 1. 可以在构造函数和析构函数中调用虚函数吗?
- 2. 类对象的内存模型(内存布局)
- 3. 菱形继承问题(钻石问题)如何解决?
- 4. 堆和栈内存区别
- 5. static_cast和dynamic_cast异同
- 6. 智能指针的实现机制
- 7. 移动构造函数和拷贝构造函数区别
- 8. 内联函数inline
1. 可以在构造函数和析构函数中调用虚函数吗?
- 可以,但没有动态绑定的效果。
- 因为当基类指针或引用指向子类对象,调用构造函数时,先构造父类,再构造子类。在构造父类时,由于子类还未被构造,无法下调子类的虚函数。同样,调用析构函数时,析构顺序和构造顺序相反,先析构子类。在析构父类时,由于子类已经被析构,所以也是无法触发多态。
2. 类对象的内存模型(内存布局)
- 有虚函数时,虚表指针存放在对象的首地址处;接着,按继承顺序的数据成员声明顺序布局(虚表指针》父类数据成员》子类数据成员)。
- 多继承时,有虚函数的父类会有自己的虚表,按照继承顺序的虚表指针和数据成员布局(父类A虚表指针》父类A数据成员》父类B虚表指针》父类B数据成员》子类数据成员)。另外,如果子类也有虚函数,就会在第一个虚表上增加该虚函数地址。
- 菱形继承,且采用了虚继承。内存布局顺序:各个父类、子类、公共基类。(父类A虚表指针》父类A数据成员》父类B虚表指针》父类B数据成员》子类数据成员》公共基类虚表指针》公共基类数据成员)。另外,由于虚继承,各个父类不再拷贝公共基类的数据成员。
3. 菱形继承问题(钻石问题)如何解决?
- 存在二义性问题,由于两个父类都会对公共基类的属性和方法进行拷贝,当子类访问公共基类的属性或方法时,不知道要访问哪个父类的属性或方法,导致编译错误。
- 解决:采用虚继承,即两个父类继承公共基类时用virtual修饰。这样保证只有一份公共基类的拷贝。
4. 堆和栈内存区别
- 堆内存需要手动管理,可能会造成内存泄漏问题;栈内存是由系统自动管理。
- 堆能分配的内存较大(32位系统下通常4G);栈能分配的内存较小(默认1M)。
- 在堆中分配和释放内存会产生内存碎片;栈不会产生内存碎片。
- 堆内存分配效率低;栈内存分配效率高。
- 堆地址从低向上;栈地址从高向下。
5. static_cast和dynamic_cast异同
- static_cast:用于基本数据类型间转换,能进行类层次间的向上和向下转换,但向下转换不安全,因为没有进行动态类型检查。
- dynamic_cast:用于多态对象间转换,将基类指针或引用转换为子类指针或引用(也可以向上转换),从而访问子类特有的成员函数。注意 ,引用转型失败会抛异常”bad_cast“;指针转型失败会返回一个空指针,如果漏写检查代码(assert/if语句)会导致安全隐患。
- 二者都会做类型安全检查,但处理阶段不一样。static_cast在编译期进行类型检查,dynamic_cast在运行期进行类型检查。
- dynamic_cast需要父类有虚函数,而static_cast不需要。
6. 智能指针的实现机制
- 智能指针是为了解决内存泄漏问题,它可以自动释放内存空间。因为它本身是一个类,由析构函数释放内存空间。
- unique_ptr:独占所指向的对象的所有权。底层使用了C++11的=delete,禁止直接构造和拷贝构造,确保了独一无二的特性。但是,可以使用移动语义来移动所有权。
- shared_ptr:允许多个指针共享同一个对象的所有权。底层使用了引用计数机制,当引用计数为0时,会自动释放堆内存。
- shared_ptr内存泄漏问题:shared_ptr相互引用会导致引用计数混乱,堆内存无法正确释放,造成内存泄漏。
- 解决:使用weak_ptr打破循环引用,因为它不会增加引用计数
- shared_ptr线程安全问题:shared_ptr的引用计数本身是安全且无锁,但它所指对象的读写则不是,因为shared_ptr有两个数据成员,读写操作不能原子化,所以shared_ptr不是线程安全。
- 场景:多个线程读写同一个shared_ptr对象,需要加锁。
- shared_ptr 具体实现:
- 构造函数:将指针指向该对象,引用计数置1;
- 拷贝构造函数:将指针指向该对象,引用计数+1;
- 赋值运算符:等号左边的shared_ptr引用计数-1,且若引用计数为0,还要释放指针所指对象的内存空间。等号右边的shared_ptr引用计数+1。
7. 移动构造函数和拷贝构造函数区别
- 移动构造函数需要传递一个右值引用,不分配新内存,而是接管传递对象的内存,并在移动之后将源对象销毁。
- 拷贝构造函数需要传递一个左值引用,可能会重新分配内存,性能更低。
8. 内联函数inline
- 作用:让编译器在函数调用点上展开函数,可以避免函数调用的开销。
- 场景:适用于简单且频繁调用的函数。
- 缺点:
- 可能造成代码膨胀,尤其是递归函数,导致可执行文件太大,造成大量内存开销。
- 不方便调试。
- 每次修改内联函数的实现或调用内联函数的地方时,编译器重新生成大量的代码,增加编译时间。