在传统 C++ 中,我们使用 new
和 delete
手动管理堆内存。这非常容易出错:
- 内存泄漏 :忘了
delete
。导致内存被无效占用。 - 悬空指针 :提前
delete
了还在使用的内存,指针还在,指向的内存无效。 - 双重释放 :对同一块内存
delete
两次,可能导致程序崩溃。
智能指针其核心思想是:
谁最后一个使用这片内存,谁就负责释放它
C++11 在 <memory>
头文件中提供了三种主要的智能指针:
1. std::unique_ptr
(唯一指针)
同一时间只能有一个 unique_ptr
拥有某个资源。无法被复制(copy
),只能被移动(move
),如果被move了,
原来的unique_ptr
就没有这片资源了。离开作用域时,会自动释放这片空间。 当你不需要共享资源所有权时,优先使用它。
示例:
cpp
#include <memory>
int main() {
// 为该对象创建了一片内存空间,并独家占有它 (创建一个独占指针)
std::unique_ptr<int> myBalloon = std::make_unique<int>(42);
// 你可以使用该对象
*myBalloon = 100;
// 尝试复制?不行!编译器会报错。
// std::unique_ptr<int> copy = myBalloon; // 错误!
// 但是你可以把它移动给其他指针
std::unique_ptr<int> friendBalloon = std::move(myBalloon); // 所有权转移
// 现在 myBalloon 是空的了,你不能再用它了
// *myBalloon = 200; // 错误!崩溃
// 其他指针现在拥有该对象
*friendBalloon = 200; // 没问题
return 0;
} // 函数结束,friendBalloon 离开作用域,其所对应的内存自动释放
// myBalloon 离开作用域,但它是空的,所以啥也不做
2. std::shared_ptr
(共享指针)
多个 shared_ptr
可以共同拥有同一个对象。它使用引用计数 来跟踪有多少个 shared_ptr
,指向同一对象。当一个新的
shared_ptr
指向该资源时,引用计数增加。当一个 shared_ptr
被销毁或重置时,引用计数减少。当引用计数变为零时,管理的资源
空间被自动删除。
使用场景 :
当多个部分都需要使用同一个对象,并且没有明确的单一所有者时。例如:放在容器中的对象、多个类实例共享同一个公共资源、缓存等。
示例:
cpp
#include <iostream>
#include <memory>
class Resource {
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main() {
// 1. 创建 shared_ptr (推荐使用 std::make_shared)
std::shared_ptr<Resource> sp1 = std::make_shared<Resource>();
std::cout << "Use count: " << sp1.use_count() << "\n"; // 输出: 1
{
// 2. 复制:共享所有权,引用计数增加
std::shared_ptr<Resource> sp2 = sp1;
std::cout << "Use count: " << sp1.use_count() << "\n"; // 输出: 2
// sp2 离开作用域,析构,引用计数减 1
}
std::cout << "Use count: " << sp1.use_count() << "\n"; // 输出: 1
// 3. 原始指针陷阱:不要用同一个原始指针创建多个独立的 shared_ptr
Resource* raw_ptr = new Resource();
std::shared_ptr<Resource> sp3(raw_ptr);
// std::shared_ptr<Resource> sp4(raw_ptr); // 灾难!两个独立的 sp3 和 sp4
// 会尝试删除同一块内存,导致未定义行为。
// 正确做法:如果要再创建一个,应该复制 sp3
std::shared_ptr<Resource> sp4 = sp3; // 正确!引用计数增加。
return 0;
} // sp1, sp3, sp4 离开作用域,引用计数降为0,资源空间被销毁。
// 输出:
// Resource acquired (sp1)
// Use count: 1
// Use count: 2
// Use count: 1
// Resource acquired (raw_ptr)
// Resource destroyed (sp3/sp4)
// Resource destroyed (sp1)
3. std::weak_ptr
(弱指针)
用于解决 shared_ptr
的循环引用问题。
什么是循环引用?
两个或多个对象使用 shared_ptr 互相引用,形成一个闭环。导致它们的引用计数永远无法降到零,形成了一个死圈。当你设计类关系,
发现两个类需要互相指向对方时,就要高度警惕循环引用。通常,应该将其中"不属于核心所有权"的那个指针改为 weak_ptr。
cpp
#include <iostream>
#include <memory>
class SnakeB; // 前向声明
class SnakeA {
public:
std::shared_ptr<SnakeB> b_ptr; // 蛇A的肚子里装着蛇B
~SnakeA() { std::cout << "SnakeA 被销毁了!太好了!\n"; }
};
class SnakeB {
public:
std::shared_ptr<SnakeA> a_ptr; // 蛇B的肚子里装着蛇A
~SnakeB() { std::cout << "SnakeB 被销毁了!太好了!\n"; }
};
int main() {
std::cout << "游戏开始!创建两条蛇...\n";
// 创建两条蛇的智能指针
auto a = std::make_shared<SnakeA>();
auto b = std::make_shared<SnakeB>();
std::cout << "a 的引用计数: " << a.use_count() << std::endl; // 1
std::cout << "b 的引用计数: " << b.use_count() << std::endl; // 1
std::cout << "现在让它们互相引用...\n";
a->b_ptr = b; // 蛇A 吃掉 蛇B -> b 的引用计数 +1
b->a_ptr = a; // 蛇B 反过来也吃掉 蛇A -> a 的引用计数 +1
std::cout << "a 的引用计数: " << a.use_count() << std::endl; // 2
std::cout << "b 的引用计数: " << b.use_count() << std::endl; // 2
std::cout << "main函数结束了,该清理内存了...\n";
return 0;
// 此时,局部变量 a 和 b 的生命周期结束,它们应该被销毁。
// a 被销毁:蛇A 的引用计数从 2 -> 1
// b 被销毁:蛇B 的引用计数从 2 -> 1
// 问题来了:现在还有谁在引用蛇A?是蛇B肚子里的 shared_ptr!
// 现在还有谁在引用蛇B?是蛇A肚子里的 shared_ptr!
// 因为它们的引用计数都是1,而不是0,所以系统永远不会去调用它们的析构函数。
// 两条蛇永远僵持在那里,谁也无法销毁谁。内存泄漏发生了!
}
现在,我们派 weak_ptr 上场。它的核心思想是:"我只观察,我不拥有"。
我们修改一下代码,把其中一条蛇肚子里的 shared_ptr 改成 weak_ptr。
cpp
#include <iostream>
#include <memory>
class SnakeB;
class SnakeA {
public:
// 蛇A的肚子里改成"弱指针"指向蛇B。这意味着:我知道蛇B在哪,但我不会"拥有"它。
std::weak_ptr<SnakeB> b_weak_ptr; // 关键修改:shared_ptr -> weak_ptr
~SnakeA() { std::cout << "SnakeA 被销毁了!太好了!\n"; }
};
class SnakeB {
public:
std::shared_ptr<SnakeA> a_ptr;
~SnakeB() { std::cout << "SnakeB 被销毁了!太好了!\n"; }
};
int main() {
auto a = std::make_shared<SnakeA>();
auto b = std::make_shared<SnakeB>();
// 建立关系
a->b_weak_ptr = b; // 弱指针赋值,不会增加 b 的引用计数!
b->a_ptr = a; // shared_ptr 赋值,会增加 a 的引用计数。
std::cout << "a 的引用计数: " << a.use_count() << std::endl; // 2 (被 b->a_ptr 引用)
std::cout << "b 的引用计数: " << b.use_count() << std::endl; // 1 (a->b_weak_ptr 是弱引用,不增加计数!)
std::cout << "main函数结束了...\n";
return 0;
// 1. 局部变量 b 被销毁:蛇B 的引用计数从 1 -> 0。
// 因为计数为0,系统立即销毁 SnakeB 对象。打印 "SnakeB 被销毁了!太好了!"
// 2. 因为 SnakeB 被销毁了,它内部的成员 a_ptr 也随之被销毁。
// 这意味着指向 SnakeA 的引用减少了1。
// 3. 局部变量 a 被销毁:蛇A 的引用计数从 2 -> 1 -> (因为上一步) 现在变成了 0?
// 让我们仔细算:a 最初的计数是1,被 b->a_ptr 引用后变成2。
// b 被销毁时,b->a_ptr 这个引用没了,所以 a 的计数从2变回1。
// 然后局部变量 a 被销毁,a 的计数从1变为0。所以 SnakeA 也被销毁!打印 "SnakeA 被销毁了!太好了!"
}
运行修改后的代码,你会看到两条蛇的析构消息都成功打印了! 内存泄漏被解决了。
黄金法则:
- 默认使用
std::unique_ptr
:除非明确需要共享所有权,否则这是最安全、最快速的选择。 - 使用
std::make_unique
和std::make_shared
:它们更高效(一次分配内存,同时存放对象和控制块)。更安全
(避免了因为异常导致的内存泄漏)。make_unique
是 C++14 引入的,C++11 可以自己简单实现一个。 - 避免使用原始指针进行内存管理 :将所有
new
的结果立即放入智能指针中。 - 不要用同一个原始指针初始化多个智能指针:这是导致双重释放的常见原因。
- 使用
weak_ptr
来解决或避免循环引用:当你需要"观察"但不"拥有"资源时。
通过遵循这些原则,你可以极大地减少 C++ 程序中的内存管理错误,写出更安全、更清晰的代码。