定义
智能指针(Smart Pointers)是C++中的一种高级特性,它提供了一种自动管理动态分配内存的机制。通过智能指针,开发者可以避免手动管理内存所带来的问题,如内存泄漏和悬挂指针等。智能指针的主要目的是确保当对象不再需要时,其内存能够被自动释放。
C++标准库提供了几种智能指针类型,包括std::unique_ptr
、std::shared_ptr
、std::weak_ptr
和std::auto_ptr
(注意,std::auto_ptr
在C++11中已被弃用,并在C++17中被移除)。
示例
std::unique_ptr
std::unique_ptr
是一个独占所有权的智能指针。它拥有它所指向的对象,并且该对象在同一时间只能被一个unique_ptr
拥有。当unique_ptr
被销毁(例如离开其作用域)时,它所指向的对象也会被自动删除。
#include <memory>
void foo() {
//1
std::unique_ptr<int> ptr(new int(42)); // 分配内存
// 使用 ptr...
// 当 ptr 离开作用域时,它所指向的内存会被自动释放
}
常用API:
- 构造函数:用于创建
unique_ptr
实例。 reset()
:释放当前管理的对象,并将unique_ptr
置为空。release()
:返回裸指针并放弃所有权,unique_ptr
变为空。get()
:返回裸指针,但不放弃所有权。operator->()
和operator*()
:重载了箭头运算符和解引用运算符,用于访问指向的对象。
std::shared_ptr
std::shared_ptr
允许多个智能指针共享同一个对象的所有权。内部使用引用计数来追踪共享对象的所有权。当最后一个shared_ptr
被销毁时,它所指向的对象才会被删除。
#include <memory>
void bar() {
std::shared_ptr<int> sptr1(new int(42)); // 分配内存
std::shared_ptr<int> sptr2 = sptr1; // 复制构造,增加引用计数
// 当 sptr1 和 sptr2 都离开作用域时,内存才会被释放
}
常用API:
- 构造函数:用于创建
shared_ptr
实例。 reset()
:释放当前管理的对象,并将shared_ptr
置为空。get()
:返回裸指针,但不放弃所有权。use_count()
:返回当前共享对象的shared_ptr
实例数。unique()
:检查是否唯一拥有对象。operator->()
和operator*()
:重载了箭头运算符和解引用运算符。
std::weak_ptr
std::weak_ptr
是 C++11 引入的一个智能指针,它是对 std::shared_ptr
的一个补充。std::weak_ptr
不控制所指向对象的使用寿命,也就是说,它不会增加所指向对象的引用计数。其主要用途是观察一个由 std::shared_ptr
所管理的对象,而不会在对象的生命周期中持有所有权。
std::weak_ptr
的主要特性和用途:
- 不拥有对象:与
std::shared_ptr
不同,std::weak_ptr
不拥有其指向的对象。这意味着,当最后一个std::shared_ptr
被销毁或重置时,即使还有std::weak_ptr
指向该对象,对象也会被销毁。 - 防止循环引用:当两个或更多的
std::shared_ptr
相互引用时,它们会形成一个循环引用,导致它们所指向的对象无法被正确销毁。通过使用std::weak_ptr
来打破这种循环引用,可以确保对象在不再需要时被正确销毁。 - 转换为
std::shared_ptr
:尽管std::weak_ptr
不拥有对象,但它可以安全地转换为std::shared_ptr
(如果对象仍然存在)。这种转换会增加对象的引用计数,确保在转换后的std::shared_ptr
生命周期内对象不会被销毁。 - 检查对象是否存在:可以使用
std::weak_ptr
的expired()
成员函数来检查它所指向的对象是否仍然存在。
#include <iostream>
#include <memory>
class Parent;
class Child {
public:
std::weak_ptr<Parent> parent;
~Child() {
std::cout << "Child destroyed" << std::endl;
}
};
class Parent {
public:
std::shared_ptr<Child> child;
~Parent() {
std::cout << "Parent destroyed" << std::endl;
}
};
int main() {
{
auto parent = std::make_shared<Parent>();
auto child = std::make_shared<Child>();
parent->child = child;
child->parent = parent;
// 由于 Parent 和 Child 之间是 weak_ptr 和 shared_ptr 的关系,
// 所以当 parent 和 child 离开作用域时,它们所指向的对象都会被正确销毁。
}
return 0;
}
编译运行:
在这个例子中,Parent
类持有一个指向 Child
的 std::shared_ptr
,而 Child
类持有一个指向 Parent
的 std::weak_ptr
。当 parent
和 child
离开作用域时,由于 Child
中的 parent
是一个 std::weak_ptr
,它不会阻止 Parent
对象的销毁。同样,当 Parent
对象被销毁时,由于 Parent
中的 child
是一个 std::shared_ptr
,它会递减 Child
对象的引用计数,并最终导致 Child
对象也被销毁。这就避免了循环引用导致的内存泄漏问题
假如不使用weak_ptr
#include <iostream>
#include <memory>
class Parent;
class Child {
public:
std::shared_ptr<Parent> parent;
~Child() {
std::cout << "Child destroyed" << std::endl;
}
};
class Parent {
public:
std::shared_ptr<Child> child;
~Parent() {
std::cout << "Parent destroyed" << std::endl;
}
};
int main() {
{
auto parent = std::make_shared<Parent>();
auto child = std::make_shared<Child>();
parent->child = child;
child->parent = parent;
// 由于 Parent 和 Child 之间是 weak_ptr 和 shared_ptr 的关系,
// 所以当 parent 和 child 离开作用域时,它们所指向的对象都会被正确销毁。
}
return 0;
}
编译运行
查看log文件
可以看到是有内存泄漏的,原因:
Parent
和 Child
类都使用了 std::shared_ptr
来相互引用,这导致了一个典型的循环引用问题。当 parent
和 child
离开作用域时,它们的析构函数会被调用,但由于循环引用,它们的引用计数都不会降到0,因此它们所指向的 Parent
和 Child
对象都不会被销毁。
std::shared_ptr
的工作方式是,每当一个新的 std::shared_ptr
指向一个对象时,该对象的引用计数就会增加;每当一个 std::shared_ptr
被销毁(例如离开其作用域)或重置为指向另一个对象时,引用计数就会减少。如果引用计数减少到0,那么对象就会被销毁。
parent
指向 child
,同时 child
指向 parent
。当 parent
和 child
离开作用域时,它们的析构函数被调用,但是它们的引用计数仍然是1(因为另一个 std::shared_ptr
仍然指向它们所指向的对象)。因此,这两个对象都不会被销毁,内存泄漏发生了。
常用API:
- 构造函数:通常通过
shared_ptr
或另一个weak_ptr
来构造。 expired()
:检查所观察的对象是否已经被删除。lock()
:尝试获取一个指向对象的shared_ptr
。如果对象仍然存在,则返回一个指向它的shared_ptr
;否则返回一个空的shared_ptr
。get()
:返回裸指针,但不参与对象的生命周期管理。reset()
:将weak_ptr
置为空。
std::auto_ptr(已弃用)
std::auto_ptr
是一个早期的智能指针实现,它在C++98中被引入,但在C++11中被标记为弃用,并在C++17中被移除。auto_ptr
有一个所有权转移语义,这意味着当auto_ptr
被复制时,所有权会从源auto_ptr
转移到目标auto_ptr
,源auto_ptr
会失去所有权并变为空。由于这个所有权转移语义,auto_ptr
在使用上容易导致混淆和错误,因此被unique_ptr
和shared_ptr
所取代。
初始化方法
使用 std::make_unique
(C++14 及以后版本)
std::make_unique
是一个函数模板,它接受与 new
相同的参数,并返回一个对应的 unique_ptr
。这是推荐的方式,因为它更安全,更不容易出错。
#include <memory>
std::unique_ptr<int> a = std::make_unique<int>(42); // 初始化一个值为 42 的 int
使用 new
运算符
你可以使用 new
运算符直接分配内存,并将返回的原始指针传递给 unique_ptr
的构造函数。
#include <memory>
std::unique_ptr<int> a(new int(42)); // 初始化一个值为 42 的 int
初始化为 nullptr
如果你不想立即初始化 unique_ptr
所指向的对象,你可以将其初始化为 nullptr
。
#include <memory>
std::unique_ptr<int> a(nullptr); // 初始化为 nullptr
然后,你可以在需要的时候使用 reset
方法或重新赋值来分配内存。
a.reset(new int(100)); // 使用 reset 方法分配内存
// 或者
a = std::make_unique<int>(100); // 重新赋值
自定义智能指针的删除器
#include <iostream>
#include <fstream>
#include <memory>
// 自定义文件删除器 仿函数
class FileCloser {
public:
void operator()(FILE* fp) const {
if (fp != nullptr) {
std::fclose(fp);
std::cout << "my fcloase obj"<<std::endl;
}
}
};
//一般函数
void myClose(FILE* fp)
{
if (fp != nullptr) {
std::fclose(fp);
std::cout << "myClose obj"<<std::endl;
}
}
// 使用自定义删除器的 unique_ptr 来管理文件
typedef std::unique_ptr<FILE, FileCloser> UniqueFilePtr;
// typedef std::unique_ptr<FILE, decltype(&myClose)> UniqueFilePtr;
int main() {
// 打开文件
// UniqueFilePtr file(std::fopen("example.txt", "r"),&myClose);
UniqueFilePtr file(std::fopen("example.txt", "r"));
if (!file) {
std::cerr << "Failed to open file." << std::endl;
return 1;
}
// 使用文件...
// 例如,读取文件内容
char buffer[128];
while (std::fgets(buffer, sizeof(buffer), file.get()) != nullptr) {
std::cout << buffer<<std::endl;
}
// 当 file 离开作用域时,FileCloser 会被调用,从而关闭文件
return 0;
}
编译运行
智能指针的优缺点
自动内存管理:
{
std::unique_ptr<int> ptr(new int(10)); // 创建一个智能指针
// 当离开这个作用域时,ptr 被销毁,分配的内存自动释放
}
防止内存泄漏:
void function() {
std::unique_ptr<int> safePtr(new int(5));
// 函数结束时,safePtr 被销毁,防止内存泄漏
}
防止野指针:
{
std::shared_ptr<int> ptr1(new int(20));
std::shared_ptr<int> ptr2 = ptr1; // ptr2 和 ptr1 共享同一个资源
ptr1.reset(); // 释放 ptr1 的资源,但资源本身不会被删除
// ptr2 仍然有效,指向原始资源
}
防止重复释放:
{
std::shared_ptr<int> ptr1(new int(30));
std::shared_ptr<int> ptr2 = ptr1;
ptr1.reset(); // 引用计数减少,但资源不会被释放
ptr2.reset(); // 引用计数再次减少,资源被释放
// 不会因为多次调用 reset() 而导致未定义行为
}
支持资源共享:
{
std::shared_ptr<int> sharedPtr(new int(40));
std::shared_ptr<int> anotherPtr = sharedPtr; // 另一个智能指针共享资源
// 两个智能指针共享同一个 int 的所有权
}
异常安全:
std::shared_ptr<int> createInt() {
return std::shared_ptr<int>(new int(50));
}
void useInt() {
std::shared_ptr<int> ptr = createInt();
// 如果在这之后抛出异常,ptr 会自动释放内存
throw std::runtime_error("Error");
}
简化代码:
void process() {
std::vector<std::unique_ptr<int>> vec;
vec.emplace_back(std::make_unique<int>(60));
// 不需要手动释放内存
}
所有权和生命周期管理:
class Resource {
public:
~Resource() { std::cout << "Resource destroyed\n"; }
};
{
std::unique_ptr<Resource> resource(new Resource);
// 资源的生命周期由 unique_ptr 管理
}
// resource 被销毁,Resource 的析构函数被调用
支持自定义删除器:
void customDeleter(int* p) {
std::cout << "Custom deleter called\n";
delete p;
}
{
std::unique_ptr<int, decltype(&customDeleter)> ptr(new int(70), customDeleter);
// 使用自定义删除器
}
与标准库容器和算法的兼容性:
std::vector<std::shared_ptr<int>> vec;
vec.push_back(std::make_shared<int>(80));
vec.push_back(std::make_shared<int>(90));
// 使用标准库算法处理包含智能指针的容器
for (const auto& item : vec) {
std::cout << *item << std::endl;
}
总结:
- 自动管理内存,减少内存泄漏和悬挂指针的风险。
- 简化内存管理代码,提高代码的可读性和可维护性。
- 提供了多种所有权模型,适应不同的使用场景。
缺点:
- 智能指针的使用可能会引入额外的性能开销(尽管通常这些开销是微不足道的)。
- 需要正确理解和使用智能指针的所有权模型,以避免出现意外的行为。
总的来说,智能指针是C++中一个非常重要的特性,它帮助开发者更加安全和高效地管理动态内存。在选择使用哪种智能指针时,应该根据具体的使用场景和需求来做出决策。
注意事项
- 避免裸指针:尽可能使用智能指针代替裸指针来管理动态内存。
- 避免混用:尽量不要混用不同类型的智能指针,以避免意外的行为。
- 自定义删除器:如之前所述,可以为智能指针提供自定义的删除器,以处理非标准资源的释放。