智能指针(Smart Pointers)

定义

智能指针(Smart Pointers)是C++中的一种高级特性,它提供了一种自动管理动态分配内存的机制。通过智能指针,开发者可以避免手动管理内存所带来的问题,如内存泄漏和悬挂指针等。智能指针的主要目的是确保当对象不再需要时,其内存能够被自动释放。

C++标准库提供了几种智能指针类型,包括std::unique_ptrstd::shared_ptrstd::weak_ptrstd::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 的主要特性和用途:

  1. 不拥有对象:与 std::shared_ptr 不同,std::weak_ptr 不拥有其指向的对象。这意味着,当最后一个 std::shared_ptr 被销毁或重置时,即使还有 std::weak_ptr 指向该对象,对象也会被销毁。
  2. 防止循环引用:当两个或更多的 std::shared_ptr 相互引用时,它们会形成一个循环引用,导致它们所指向的对象无法被正确销毁。通过使用 std::weak_ptr 来打破这种循环引用,可以确保对象在不再需要时被正确销毁。
  3. 转换为 std::shared_ptr:尽管 std::weak_ptr 不拥有对象,但它可以安全地转换为 std::shared_ptr(如果对象仍然存在)。这种转换会增加对象的引用计数,确保在转换后的 std::shared_ptr 生命周期内对象不会被销毁。
  4. 检查对象是否存在:可以使用 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_ptrshared_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++中一个非常重要的特性,它帮助开发者更加安全和高效地管理动态内存。在选择使用哪种智能指针时,应该根据具体的使用场景和需求来做出决策。

注意事项

  • 避免裸指针:尽可能使用智能指针代替裸指针来管理动态内存。
  • 避免混用:尽量不要混用不同类型的智能指针,以避免意外的行为。
  • 自定义删除器:如之前所述,可以为智能指针提供自定义的删除器,以处理非标准资源的释放。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值