引入
在传统的C++中,动态内存的管理是一个棘手的问题,完全依赖于程序员手动使用 new
和 delete
(或 new[]
和 delete[]
)。这种方式非常容易出错,是许多程序Bug的根源。
普通指针的问题
-
必须手动释放:
new
和new[]
分配的内存必须严格对应地使用delete
和delete[]
来释放。 -
容易忘记释放:在复杂的代码逻辑或异常发生时,很容易忘记释放已分配的内存。
-
不知何时释放:在多个函数或对象之间传递指针时,很难确定在哪个时间点、哪个作用域下应该释放内存。
这些问题会导致内存泄漏、重复释放或访问已释放内存等严重问题。
智能指针的优点
C++11引入了智能指针,它们位于 <memory>
头文件中,通过RAII(Resource Acquisition Is Initialization) 机制来管理动态分配的对象。其核心思想是:将内存资源与对象的生命周期绑定。当智能指针对象被销毁时(例如离开作用域),它的析构函数会自动释放其所管理的内存。
这带来了巨大的优势:
-
自动释放:无需手动调用
delete
,避免了忘记释放的问题。 -
异常安全:即使代码中抛出异常,智能指针也能保证内存被正确释放。
-
清晰的所有权语义:明确了谁拥有内存、谁负责释放。
C++11主要有三种智能指针:unique_ptr
, shared_ptr
和 weak_ptr
。auto_ptr
已被弃用,应避免使用。
C++11 智能指针类型及特点
1. std::unique_ptr
unique_ptr
独占其所指向对象的所有权。它不能被复制,但可以通过 std::move
转移所有权。
代码举例
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass(int value) : data(value) {
std::cout << "MyClass constructed with value: " << data << std::endl;
}
~MyClass() {
std::cout << "MyClass destroyed with value: " << data << std::endl;
}
void print() const { std::cout << "Value: " << data << std::endl; }
private:
int data;
};
int main() {
std::cout << "--- unique_ptr Demo ---" << std::endl;
{
// 创建一个 unique_ptr,管理一个 MyClass 对象
std::unique_ptr<MyClass> ptr1(new MyClass(42));
// 更推荐使用 std::make_unique (C++14)
// auto ptr1 = std::make_unique<MyClass>(42);
ptr1->print(); // 使用 -> 操作符访问成员
// 编译错误!unique_ptr 不能复制
// std::unique_ptr<MyClass> ptr2 = ptr1;
// 所有权转移:ptr1 将所有权交给 ptr2,ptr1 变为 nullptr
std::unique_ptr<MyClass> ptr2 = std::move(ptr1);
if (ptr1) {
ptr1->print(); // 这行不会执行,因为 ptr1 已是空指针
}
if (ptr2) {
ptr2->print(); // 这行会执行
}
} // 离开作用域,ptr2 被销毁,它管理的对象也随之自动销毁
std::cout << "--- End of scope ---" << std::endl;
return 0;
}
输出:
--- unique_ptr Demo ---
MyClass constructed with value: 42
Value: 42
Value: 42
MyClass destroyed with value: 42
--- End of scope ---
2. std::shared_ptr
shared_ptr
通过引用计数实现共享所有权。每当一个新的 shared_ptr
指向同一个对象时,引用计数增加。当一个 shared_ptr
被销毁或重置时,引用计数减少。当计数变为零时,管理的对象被自动删除。
代码举例
#include <iostream>
#include <memory>
void shared_ptr_demo() {
std::cout << "\n--- shared_ptr Demo ---" << std::endl;
{
// 创建 shared_ptr (推荐使用 std::make_shared)
auto ptr1 = std::make_shared<MyClass>(100);
std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl; // 输出:1
{
// 复制构造,引用计数增加
auto ptr2 = ptr1;
std::cout << "After ptr2 copy, use_count: " << ptr1.use_count() << std::endl; // 输出:2
ptr2->print();
} // ptr2 离开作用域被销毁,引用计数减为 1
std::cout << "After ptr2 destroyed, use_count: " << ptr1.use_count() << std::endl; // 输出:1
} // ptr1 离开作用域,引用计数减为 0,对象被销毁
std::cout << "--- End of shared_ptr scope ---" << std::endl;
}
int main() {
shared_ptr_demo();
return 0;
}
输出:
--- shared_ptr Demo ---
MyClass constructed with value: 100
ptr1 use_count: 1
After ptr2 copy, use_count: 2
Value: 100
After ptr2 destroyed, use_count: 1
MyClass destroyed with value: 100
--- End of shared_ptr scope ---
3. std::weak_ptr 与循环引用问题
weak_ptr
是为了解决 shared_ptr
最大的陷阱——循环引用而设计的。
循环引用问题
#include <iostream>
#include <memory>
struct Node {
~Node() { std::cout << "Node " << id << " destroyed." << std::endl; }
int id;
std::shared_ptr<Node> next; // 指向下一个节点的强引用
// std::weak_ptr<Node> next; // 正确的做法应该是使用 weak_ptr
};
void circular_reference_demo() {
std::cout << "\n--- Circular Reference Demo ---" << std::endl;
auto nodeA = std::make_shared<Node>();
auto nodeB = std::make_shared<Node>();
nodeA->id = 1;
nodeB->id = 2;
// 形成循环引用:nodeA->next 指向 nodeB,nodeB->next 指向 nodeA
nodeA->next = nodeB;
nodeB->next = nodeA; // 这里导致循环引用!
std::cout << "nodeA use_count: " << nodeA.use_count() << std::endl; // 输出:2
std::cout << "nodeB use_count: " << nodeB.use_count() << std::endl; // 输出:2
} // 函数结束,nodeA和nodeB的局部shared_ptr被销毁,但引用计数只减1,不为0,对象无法释放!内存泄漏!
int main() {
circular_reference_demo();
std::cout << "Program ended, but Nodes were not destroyed due to circular reference!" << std::endl;
return 0;
}
输出:
--- Circular Reference Demo ---
nodeA use_count: 2
nodeB use_count: 2
Program ended, but Nodes were not destroyed due to circular reference!
// 注意:没有看到 Node 的析构函数被调用!
使用 weak_ptr 解决循环引用
weak_ptr
只观察对象,而不拥有它。它不会增加引用计数。
#include <iostream>
#include <memory>
struct SafeNode {
~SafeNode() { std::cout << "SafeNode " << id << " destroyed." << std::endl; }
int id;
std::weak_ptr<SafeNode> next; // 使用弱引用打破循环
void printNext() {
// 使用 lock() 来获取一个临时的 shared_ptr 以访问对象
if (auto tempSharedPtr = next.lock()) {
std::cout << "Next node id: " << tempSharedPtr->id << std::endl;
} else {
std::cout << "Next node is expired." << std::endl;
}
}
};
void weak_ptr_demo() {
std::cout << "\n--- weak_ptr Solution Demo ---" << std::endl;
auto nodeA = std::make_shared<SafeNode>();
auto nodeB = std::make_shared<SafeNode>();
nodeA->id = 1;
nodeB->id = 2;
// 形成引用,但使用的是 weak_ptr,不会增加引用计数
nodeA->next = nodeB; // nodeB 的引用计数还是 1
nodeB->next = nodeA; // nodeA 的引用计数还是 1
nodeA->printNext();
std::cout << "nodeA use_count: " << nodeA.use_count() << std::endl; // 输出:1
std::cout << "nodeB use_count: " << nodeB.use_count() << std::endl; // 输出:1
} // 离开作用域,nodeA和nodeB的引用计数都减为0,对象被正确销毁
int main() {
weak_ptr_demo();
std::cout << "Program ended correctly." << std::endl;
return 0;
}
输出:
--- weak_ptr Solution Demo ---
Next node id: 2
nodeA use_count: 1
nodeB use_count: 1
SafeNode 2 destroyed.
SafeNode 1 destroyed.
Program ended correctly.
总结与最佳实践
-
首选
unique_ptr
:默认情况下使用unique_ptr
。它开销小,语义清晰,能满足大部分需求。 -
需要共享时再用
shared_ptr
:只有在确实需要多个指针共享同一个对象的所有权时,才使用shared_ptr
。 -
使用
make_unique
和make_shared
:优先使用std::make_unique<T>(args...)
(C++14) 和std::make_shared<T>(args...)
来创建智能指针。它们更安全(避免内存泄漏)、更高效(make_shared
可能将引用计数和对象分配在同一块内存)。 -
警惕循环引用:当设计可能形成循环引用的数据结构(如链表、树、图)时,使用
weak_ptr
来打破循环。 -
不要混合使用原始指针和智能指针:一旦将资源交给智能指针管理,就尽量全程使用智能指针,避免手动
delete
或用原始指针创建另一个智能指针,这可能导致重复释放。