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了,这块内存就被标记为“已释放”并归还给了操作系统。此时再调用析构函数去访问对象内部的成员,就相当于在访问一块已经释放的非法内存(野指针),会导致程序崩溃或未定义行为。 - 结论:必须先利用对象还“活着”的时候,通过析构函数把该清理的清理干净,最后再把对象的“躯壳”(内存)释放掉。
