一、为什么 C++ 要重视内存管理?
C++ 和 Java、Python 这类语言不太一样,C++ 程序员需要更加关注内存的申请和释放。
在 C++ 中,如果手动申请了堆区内存,但是没有及时释放,就可能造成内存泄漏。如果释放之后继续使用指针,就可能出现悬空指针。如果指针没有初始化就直接使用,就可能出现野指针。
所以 C++ 面试中,内存管理是非常高频的问题。
二、栈区和堆区
C++ 程序中的内存大致可以分为栈区、堆区、全局区、代码区等。面试中最常问的是栈区和堆区。
1. 栈区
栈区一般用于存放局部变量、函数参数等。栈区内存由系统自动管理,函数执行结束后,局部变量会自动销毁。
#include <iostream>
using namespace std;
void test() {
// a 是局部变量,存放在栈区
int a = 10;
cout << "a = " << a << endl;
// 函数执行结束后,a 会自动销毁
}
int main() {
test();
return 0;
}
在这段代码中,a 是函数内部的局部变量,函数执行结束后,系统会自动释放它占用的内存。
2. 堆区
堆区一般用于动态申请内存,需要程序员手动申请和释放。
#include <iostream>
using namespace std;
int main() {
// 使用 new 在堆区申请一个 int 类型空间
int* p = new int(10);
cout << "*p = " << *p << endl;
// 使用 delete 释放堆区内存
delete p;
// 释放后把指针置空,避免悬空指针
p = nullptr;
return 0;
}
这里的 new int(10) 会在堆区申请一块内存,并初始化为 10。使用完之后,需要手动 delete 释放。
3. 栈和堆的区别
| 对比项 | 栈区 | 堆区 |
|---|---|---|
| 管理方式 | 系统自动管理 | 程序员手动管理 |
| 常见内容 | 局部变量、函数参数 | 动态申请的内存 |
| 生命周期 | 函数结束自动释放 | 手动 delete 才释放 |
| 使用方式 | 直接定义变量 | new/delete |
| 风险 | 空间较小,递归过深可能栈溢出 | 容易内存泄漏 |
面试时可以这样回答:
栈区内存由系统自动分配和释放,常用于局部变量和函数参数。堆区内存由程序员手动申请和释放,常用于动态创建对象。栈区使用方便但空间有限,堆区空间较大但需要手动管理,如果忘记释放就可能造成内存泄漏。
三、内存泄漏
内存泄漏指的是程序申请了内存,但是使用完之后没有释放,导致这块内存一直被占用。
1. 内存泄漏示例
#include <iostream>
using namespace std;
void test() {
// 在堆区申请内存
int* p = new int(10);
cout << "*p = " << *p << endl;
// 忘记 delete,函数结束后 p 变量销毁
// 但是 p 指向的堆区内存没有释放
}
int main() {
test();
return 0;
}
在上面的代码中,p 是一个指针变量,函数结束后 p 本身会被销毁。
但是 new int(10) 申请的堆区内存没有被释放,这就是内存泄漏。
2. 正确写法
#include <iostream>
using namespace std;
void test() {
int* p = new int(10);
cout << "*p = " << *p << endl;
// 使用完之后释放内存
delete p;
// 避免悬空指针
p = nullptr;
}
int main() {
test();
return 0;
}
3. 内存泄漏面试总结
面试时可以这样回答:
内存泄漏是指程序动态申请了内存,但是使用完之后没有释放,导致这块内存无法再次使用。C++ 中常见原因是 new 之后忘记 delete,或者异常提前返回导致释放代码没有执行。解决方法包括手动及时释放内存,或者使用 RAII 和智能指针自动管理资源。
四、野指针和悬空指针
1. 野指针
野指针是指没有初始化的指针,它里面可能保存的是一个随机地址。
#include <iostream>
using namespace std;
int main() {
int* p; // 没有初始化,p 是野指针
// *p = 10; // 错误,可能访问非法内存
return 0;
}
正确写法:
int* p = nullptr;
指针定义时最好初始化为 nullptr。
2. 悬空指针
悬空指针是指指针指向的内存已经被释放,但是指针本身还保存着原来的地址。
#include <iostream>
using namespace std;
int main() {
int* p = new int(10);
delete p;
// p 指向的内存已经被释放
// 但是 p 里面还保存着原来的地址
// 此时 p 就是悬空指针
p = nullptr;
return 0;
}
3. 野指针和悬空指针总结
野指针:指针没有初始化,指向随机地址。
悬空指针:指针指向的内存已经被释放,但指针还保存原地址。
面试时可以这样回答:
野指针通常是指没有初始化的指针,里面可能是随机地址。悬空指针是指内存已经释放,但是指针仍然指向原来的地址。为了避免这些问题,指针定义时要初始化为 nullptr,释放内存后也要置为 nullptr。
五、RAII 机制
RAII 是 C++ 中非常重要的资源管理思想,全称是:
Resource Acquisition Is Initialization
意思是:资源获取即初始化。
通俗理解就是:
在对象构造函数中申请资源。
在对象析构函数中释放资源。
这样对象创建时资源自动获取,对象销毁时资源自动释放。
1. 没有 RAII 的问题
#include <iostream>
using namespace std;
void test() {
int* p = new int(10);
// 如果中间逻辑很复杂,或者提前 return
// 就可能忘记 delete
if (*p == 10) {
return; // 这里直接返回,下面 delete 不会执行
}
delete p;
}
这段代码中,如果函数提前 return,delete p 就不会执行,导致内存泄漏。
2. 使用类实现 RAII
#include <iostream>
using namespace std;
class IntGuard {
private:
int* ptr;
public:
// 构造函数中申请资源
IntGuard(int value) {
ptr = new int(value);
cout << "申请内存" << endl;
}
// 析构函数中释放资源
~IntGuard() {
delete ptr;
cout << "释放内存" << endl;
}
// 提供一个访问资源的函数
int getValue() {
return *ptr;
}
};
void test() {
IntGuard guard(10);
cout << guard.getValue() << endl;
// 函数结束时,guard 对象会自动销毁
// 自动调用析构函数释放内存
}
int main() {
test();
return 0;
}
在这个例子中,IntGuard 对象创建时申请内存,函数结束时对象自动析构,内存也会自动释放。
3. RAII 的常见应用
RAII 不仅可以管理内存,还可以管理很多资源,例如:
1. 动态内存
2. 文件句柄
3. 互斥锁
4. 数据库连接
5. 网络连接
例如 C++ 中的 lock_guard 就是 RAII 思想的典型应用。
#include <iostream>
#include <mutex>
using namespace std;
mutex mtx;
void test() {
// lock_guard 创建时自动加锁
lock_guard<mutex> lock(mtx);
// 这里写需要保护的共享数据操作
cout << "正在访问共享资源" << endl;
// 函数结束时,lock 自动析构
// 析构时自动解锁
}
4. RAII 面试总结
面试时可以这样回答:
RAII 是 C++ 中一种资源管理思想,核心是把资源的申请和释放绑定到对象的生命周期中。对象构造时获取资源,对象析构时释放资源。这样可以避免手动释放资源带来的问题,即使函数提前返回或者发生异常,局部对象也会自动析构,从而释放资源。智能指针和 lock_guard 都是 RAII 的典型应用。
六、智能指针
智能指针是 C++11 引入的重要工具,用来自动管理动态内存,减少内存泄漏风险。
使用智能指针需要包含头文件:
#include <memory>
常见智能指针有:
unique_ptr
shared_ptr
weak_ptr
七、unique_ptr
unique_ptr 表示独占所有权。
也就是说,同一块资源只能由一个 unique_ptr 管理。
1. unique_ptr 基本使用
#include <iostream>
#include <memory>
using namespace std;
int main() {
// 创建一个 unique_ptr,管理一个 int 对象
unique_ptr<int> p = make_unique<int>(10);
cout << "*p = " << *p << endl;
// 不需要手动 delete
// p 离开作用域时会自动释放内存
return 0;
}
这里不需要写:
delete p;
因为 unique_ptr 析构时会自动释放它管理的资源。
2. unique_ptr 不能拷贝
#include <iostream>
#include <memory>
using namespace std;
int main() {
unique_ptr<int> p1 = make_unique<int>(10);
// 错误:unique_ptr 不允许拷贝
// unique_ptr<int> p2 = p1;
return 0;
}
因为 unique_ptr 是独占所有权,如果允许拷贝,就会出现两个智能指针管理同一块内存的问题,可能导致重复释放。
3. unique_ptr 可以移动
#include <iostream>
#include <memory>
using namespace std;
int main() {
unique_ptr<int> p1 = make_unique<int>(10);
// 使用 move 转移所有权
unique_ptr<int> p2 = move(p1);
// 此时 p1 不再拥有资源
if (p1 == nullptr) {
cout << "p1 已经为空" << endl;
}
cout << "*p2 = " << *p2 << endl;
return 0;
}
move(p1) 表示把 p1 管理的资源转移给 p2。
4. unique_ptr 面试总结
面试时可以这样回答:
unique_ptr 表示独占所有权,同一块资源只能由一个 unique_ptr 管理。它不能拷贝,只能通过 std::move 转移所有权。unique_ptr 离开作用域时会自动释放资源,适合表达资源只属于一个对象的场景。
八、shared_ptr
shared_ptr 表示共享所有权。
多个 shared_ptr 可以共同管理同一块资源。它内部使用引用计数,当引用计数变为 0 时,资源会自动释放。
1. shared_ptr 基本使用
#include <iostream>
#include <memory>
using namespace std;
int main() {
// 创建 shared_ptr,引用计数为 1
shared_ptr<int> p1 = make_shared<int>(10);
cout << "p1.use_count() = " << p1.use_count() << endl;
{
// p2 和 p1 共同管理同一块资源
shared_ptr<int> p2 = p1;
cout << "p1.use_count() = " << p1.use_count() << endl;
cout << "p2.use_count() = " << p2.use_count() << endl;
}
// p2 离开作用域后,引用计数减少
cout << "p1.use_count() = " << p1.use_count() << endl;
return 0;
}
输出中可以看到引用计数的变化。
2. shared_ptr 的释放时机
当最后一个 shared_ptr 被销毁时,引用计数变为 0,资源自动释放。
也就是说,只要还有一个 shared_ptr 管理资源,资源就不会释放。
3. shared_ptr 面试总结
面试时可以这样回答:
shared_ptr 表示共享所有权,多个 shared_ptr 可以管理同一块资源。它通过引用计数记录当前有多少个 shared_ptr 正在管理该资源。当引用计数变为 0 时,资源会自动释放。shared_ptr 使用方便,但有一定引用计数开销,并且可能出现循环引用问题。
九、weak_ptr
weak_ptr 是为了解决 shared_ptr 循环引用问题而引入的。
它不增加引用计数,只是弱引用一个由 shared_ptr 管理的对象。
1. shared_ptr 循环引用问题
#include <iostream>
#include <memory>
using namespace std;
class B;
class A {
public:
shared_ptr<B> bptr;
~A() {
cout << "A 析构" << endl;
}
};
class B {
public:
shared_ptr<A> aptr;
~B() {
cout << "B 析构" << endl;
}
};
int main() {
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
// A 中保存 B
a->bptr = b;
// B 中保存 A
b->aptr = a;
return 0;
}
这段代码中,A 和 B 互相使用 shared_ptr 指向对方。
结果是:
A 的引用计数不为 0
B 的引用计数不为 0
两个对象都无法释放
这就是循环引用。
2. 使用 weak_ptr 解决循环引用
#include <iostream>
#include <memory>
using namespace std;
class B;
class A {
public:
shared_ptr<B> bptr;
~A() {
cout << "A 析构" << endl;
}
};
class B {
public:
// 使用 weak_ptr,不增加 A 的引用计数
weak_ptr<A> aptr;
~B() {
cout << "B 析构" << endl;
}
};
int main() {
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
a->bptr = b;
b->aptr = a;
return 0;
}
这时 B 中的 aptr 是 weak_ptr,不会增加 A 的引用计数,因此对象可以正常释放。
3. weak_ptr 如何访问对象?
weak_ptr 不能直接使用 * 或 -> 访问对象,需要先调用 lock() 转成 shared_ptr。
#include <iostream>
#include <memory>
using namespace std;
int main() {
shared_ptr<int> sp = make_shared<int>(10);
// weak_ptr 弱引用 sp 管理的对象
weak_ptr<int> wp = sp;
// 使用 lock 尝试获取 shared_ptr
shared_ptr<int> temp = wp.lock();
if (temp) {
cout << "*temp = " << *temp << endl;
} else {
cout << "对象已经释放" << endl;
}
return 0;
}
如果对象还存在,lock() 返回有效的 shared_ptr。
如果对象已经释放,lock() 返回空指针。
4. weak_ptr 面试总结
面试时可以这样回答:
weak_ptr 是一种弱引用智能指针,它不会增加引用计数,主要用于解决 shared_ptr 的循环引用问题。weak_ptr 不能直接访问对象,需要通过 lock() 转换成 shared_ptr 后再使用。如果对象已经被释放,lock() 会返回空指针。
十、unique_ptr、shared_ptr、weak_ptr 对比
| 智能指针 | 所有权特点 | 是否增加引用计数 | 主要用途 |
|---|---|---|---|
| unique_ptr | 独占所有权 | 不使用引用计数 | 一个对象只由一个指针管理 |
| shared_ptr | 共享所有权 | 增加引用计数 | 多个对象共享同一资源 |
| weak_ptr | 弱引用 | 不增加引用计数 | 解决 shared_ptr 循环引用 |
简单记忆:
unique_ptr:独占资源,不能拷贝,可以移动。
shared_ptr:共享资源,引用计数控制生命周期。
weak_ptr:弱引用资源,不增加引用计数,解决循环引用。
十一、面试高频问题整理
1. 栈和堆有什么区别?
栈由系统自动管理,主要存放局部变量和函数参数,函数结束后自动释放。堆由程序员手动管理,使用 new/delete 申请和释放,适合动态创建对象。如果堆内存申请后忘记释放,就会造成内存泄漏。
2. 什么是内存泄漏?
内存泄漏是指程序申请了堆区内存,但是使用完之后没有释放,导致这块内存一直被占用。常见原因是 new 后忘记 delete,或者函数提前返回导致释放代码没有执行。解决方法是及时释放资源,或者使用 RAII 和智能指针。
3. 什么是 RAII?
RAII 是 C++ 的资源管理思想,核心是把资源绑定到对象生命周期中。对象构造时获取资源,对象析构时释放资源。这样可以避免手动释放资源导致的问题。智能指针、lock_guard 都是 RAII 的典型应用。
4. unique_ptr 和 shared_ptr 有什么区别?
unique_ptr 是独占所有权,同一块资源只能由一个 unique_ptr 管理,不能拷贝,只能移动。
shared_ptr 是共享所有权,多个 shared_ptr 可以管理同一块资源,通过引用计数控制资源释放。
如果资源只属于一个对象,优先使用 unique_ptr。如果资源需要被多个对象共享,可以使用 shared_ptr。
5. shared_ptr 有什么缺点?
shared_ptr 使用引用计数管理资源,使用方便,但会带来一定性能开销。另外,如果两个对象互相持有 shared_ptr,可能出现循环引用,导致引用计数无法归零,资源无法释放。这个问题可以使用 weak_ptr 解决。
6. weak_ptr 的作用是什么?
weak_ptr 是弱引用智能指针,不增加引用计数,主要用来解决 shared_ptr 的循环引用问题。weak_ptr 不能直接访问对象,需要调用 lock() 转成 shared_ptr 后再使用。
7. 为什么推荐使用 make_unique 和 make_shared?
推荐使用 make_unique 和 make_shared 创建智能指针,因为写法更简洁,也更安全。
unique_ptr<int> p1 = make_unique<int>(10);
shared_ptr<int> p2 = make_shared<int>(20);
相比直接写 new,这种方式可以减少手动管理内存的风险。
十二、总结
本文主要整理了 C++ 面试中内存管理相关的高频知识点,包括栈区和堆区、内存泄漏、野指针、悬空指针、RAII 机制以及智能指针。
栈区内存由系统自动管理,函数结束后局部变量会自动销毁。堆区内存由程序员手动管理,使用 new/delete 申请和释放,如果忘记释放就可能造成内存泄漏。
野指针是没有初始化的指针,悬空指针是指向已经释放内存的指针。为了避免这些问题,指针定义时建议初始化为 nullptr,释放后也要置为 nullptr。
RAII 是 C++ 中非常重要的资源管理思想,它把资源的申请和释放绑定到对象生命周期中。对象构造时获取资源,对象析构时释放资源。智能指针和 lock_guard 都是 RAII 的典型应用。
智能指针可以帮助我们自动管理动态内存。unique_ptr 表示独占所有权,不能拷贝,只能移动。shared_ptr 表示共享所有权,通过引用计数管理资源。weak_ptr 是弱引用,不增加引用计数,主要用于解决 shared_ptr 循环引用问题。
简单记忆:
栈:系统自动管理。
堆:程序员手动管理。
内存泄漏:申请了内存但没有释放。
野指针:没有初始化的指针。
悬空指针:指向已经释放内存的指针。
RAII:构造时获取资源,析构时释放资源。
unique_ptr:独占所有权。
shared_ptr:共享所有权。
weak_ptr:弱引用,不增加引用计数。
面试中回答内存管理相关问题时,最好围绕三个方面展开:
1. 资源在哪里申请?
2. 资源什么时候释放?
3. 如何避免忘记释放?
如果能结合 RAII 和智能指针回答,面试效果会更好。