C++知识点复习(面向面试5)
我们开始今天的复习21.什么情况下会发生内存泄露22.主函数之前可以执行哪些函数23.类的大小由什么决定24.虚函数和纯虚函数的区别25.既然有了 malloc 和 free 为什么还要有 new 和 delete好的我们从第21条开始。21.什么情况下会发生内存泄露1. 在函数内申请堆区内存函数结束未释放底层逻辑这是最经典的“野指针/内存丢失”场景。在函数内定义的指针变量如int* p本身是局部变量存储在栈区。当函数结束时这个指针变量p会被自动销毁栈帧弹出但p指向的那块堆区内存通过new或malloc申请并不会被自动回收。由于指针变量p已经被销毁程序彻底失去了这块堆内存的地址导致它变成了无法访问也无法释放的“无主之地”。解决之道严格遵守“谁申请谁释放”的原则。在现代 C 中最推荐的做法是使用智能指针如std::unique_ptr来接管这块内存。智能指针本身就是栈对象当函数结束时智能指针被销毁它的析构函数会自动帮你把堆内存释放掉RAII 机制。2. 父类指针指向子类对象父类析构函数非虚底层逻辑我们在之前详细聊过这个点。当通过基类指针去delete一个派生类对象时如果基类的析构函数没有加virtual关键字编译器会执行静态绑定只调用基类的析构函数而派生类特有的资源比如派生类里new出来的成员变量就永远得不到释放从而引发内存泄漏。解决之道只要一个类打算作为基类被继承并且会通过基类指针来操作派生类对象必须将基类的析构函数声明为虚析构函数virtual ~Base()。3. 指针重新赋值导致原内存地址丢失底层逻辑这也是极易被忽视的泄漏点。比如你先int* p new int(10);紧接着在没有delete p的情况下又执行了p new int(20);或者p other_variable;。此时指针p指向了新的地址而第一次new出来的那块内存的地址就彻底丢失了再也无法被释放。解决之道在给指针赋予新地址之前一定要先检查并释放它当前指向的旧内存。当然使用智能指针同样能完美规避这个问题。4. 智能指针的循环引用问题shared_ptr的死锁底层逻辑这是std::shared_ptr独有的缺陷。shared_ptr依靠内部的引用计数来管理内存当计数归零时才会释放对象。如果两个对象比如 A 和 B内部各持有一个指向对方的shared_ptr就会形成“你中有我我中有你”的死循环。当外部作用域结束时A 和 B 的引用计数依然为 1因为对方还强引用着自己导致引用计数永远无法归零两个对象都无法被析构造成内存泄漏。解决之道引入std::weak_ptr来打破循环。第一步画出“死锁”现场画两个方框代表对象 A 和 B然后画出它们互相指向的箭头讲解话术“面试官您看A 和 B 互相持有对方的shared_ptr强引用。当外部指针销毁后它们各自的引用计数都还剩下 1谁也释放不了谁这就形成了循环引用的死锁。”第二步画出“破局”方案把其中一条箭头比如 B 指向 A 的那条改成虚线代表weak_ptr讲解话术“解决办法就是把其中一方的强引用shared_ptr换成弱引用weak_ptr。weak_ptr只是静静地观察对象不会增加引用计数。这样一来当外部的强引用销毁后A 和 B 的引用计数就能顺利归零从而被正常析构释放。”22.主函数之前可以执行哪些函数静态成员变量的构造函数类内部的static成员变量以及函数内部的static局部变量它们的初始化如果是复杂对象需要调用构造函数也会在这个阶段完成。全局对象的构造函数在所有函数包括main之外定义的对象其构造函数会在此阶段被调用。23.类的大小由什么决定●非静态成员变量的大小静态成员变量不占有对象的内存sizeof是计算类创建的对 象占多大内存●内存对齐方式可以了解下为什么会有内存对齐以及对齐规则●是否有虚函数●是否虚继承注空类或空结构体的大小为1函数不影响对象的内存单独存放在代码区24.虚函数和纯虚函数的区别●虚函数有具体的实现纯虚函数没有具体的实现●子类可以选择性的覆盖父类的虚函数纯虚函数它的目的是强制子类必须实现该函数以确保特定的行为在子类中得到定义。含有纯虚函数的类被称为抽象类不能创建该类的对象子类必须重写父类的所有纯虚函数子类才可以创建对象而虚函数则没有该要求。●虚函数在虚表中存放的是函数地址纯虚函数在虚表中存放的是0。25.既然有了 malloc 和 free 为什么还要有 new 和 delete这个问题我们之前探讨过十分深入的答案。因为C语言没有对象而C是面向对象语言C中类或结构体创建的对象在释放时需 要调用析构函数去释放对象内部的指针指向的堆区内存在创建对象时需要调用构造函数给成 员变量赋值。而malloc不会调用构造函数free不会调用析构函数。new会先调用malloc申请 内存然后再调用构造函数赋值初始化是初始化参数列表delete会先调用析构函数释放 对象内的成员变量指向的堆区内存再调用 free。这里要注意为啥不是先调用free再调用析构。delete在释放数组时要注意加中括号。C 语言的malloc和free仅仅是内存管理函数它们只负责从堆上申请和释放一块原始的内存空间。而 C 的new和delete是运算符它们不仅管理内存更重要的是管理对象的生命周期new 的两步操作底层调用operator new本质是调用malloc分配足够的原始内存。在这块内存上自动调用构造函数完成对象的初始化包括成员变量赋值、内部资源的申请等。delete 的两步操作先自动调用析构函数清理对象内部占用的资源比如类内部new出来的指针、打开的文件句柄等。底层调用operator delete本质是调用free将这块内存归还给系统。为什么 delete 必须先调用析构再调用 free原因析构函数本身也是一段代码它需要访问对象内部的成员变量比如delete掉类内部的某个指针成员。如果先free了这块内存就被标记为“已释放”并归还给了操作系统。此时再调用析构函数去访问对象内部的成员就相当于在访问一块已经释放的非法内存野指针会导致程序崩溃或未定义行为。结论必须先利用对象还“活着”的时候通过析构函数把该清理的清理干净最后再把对象的“躯壳”内存释放掉。