C++里的异同点

发布于:2024-07-02 ⋅ 阅读:(18) ⋅ 点赞:(0)

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

  • 作用:让编译器在函数调用点上展开函数,可以避免函数调用的开销。
  • 场景:适用于简单且频繁调用的函数。
  • 缺点:
    • 可能造成代码膨胀,尤其是递归函数,导致可执行文件太大,造成大量内存开销。
    • 不方便调试
    • 每次修改内联函数的实现或调用内联函数的地方时,编译器重新生成大量的代码,增加编译时间