C++智能指针

本文介绍了C++中的智能指针(如unique_ptr、shared_ptr和weak_ptr)的概念、作用、使用场景以及它们在防止内存泄漏和悬垂指针问题上的优势。通过实例详细解释了如何使用这些智能指针来管理和释放动态内存,提升代码的可靠性和安全性。
摘要由CSDN通过智能技术生成

1. 什么是智能指针

C++智能指针是一种用于管理动态分配的内存的特殊类型的指针。智能指针是通过类来实现的,它提供了自动管理内存的功能。

C++中常见的智能指针包括std::unique_ptr、std::shared_ptr和std::weak_ptr

头文件: #include <memory>

auto_ptr

c++98智能指针,已在C++11中弃用。

auto_ptr具有一些限制和缺陷,例如不能在容器中存储auto_ptr对象,不能进行拷贝操作等,因此建议在实际项目中尽量避免使用auto_ptr,而是使用更安全和功能更强大的智能指针类如unique_ptrshared_ptr等。

std::unique_ptr

是一种独占所有权的智能指针,同一时间只能有一个std::unique_ptr指向同一块内存。

当unique_ptr被销毁或转移所有权时,它所指向的内存会被自动释放。

std::shared_ptr

是一种共享所有权的智能指针,可以让多个std::shared_ptr指向同一块内存。

内部通过引用计数来管理内存的释放,当最后一个shared_ptr销毁时,会释放所指向的内存。

std::weak_ptr

是一种弱引用智能指针,它不增加所指对象的引用计数。

std::weak_ptr主要用于解决std::shared_ptr的循环引用问题。

2. 为什么要用智能指针

使用智能指针可以帮助我们管理动态分配的内存,避免了手动管理内存时可能出现的内存泄漏和悬垂指针的问题,可以大大提高代码的可靠性和安全性。

2.1 背景

使用指针,如果没有释放,就会造成内存泄露。但是使用普通对象却不会。

如果让有生命周期的对象来管理分配的动态内存,那么在对象过期时,它的析构函数删除指向的内存,就可以解决指针忘记释放的问题。

基于这个原理,C++98 提供了 auto_ptr 模板的解决方案,C++11 增加unique_ptr、shared_ptr 和weak_ptr。

2.2 内存泄漏

分配了内存,但是没释放就会导致内存泄露。导致系统长时间运行后,可用内存越来越少。

C/C++中常见的几种内存泄露例子

1. 函数结束时,没有释放分配的内存

void MemoryLeak( )
{
  int *data = new int;
  *data = 15;
}

2. 在内存释放前结束函数 

void MemoryLeak() {
    int* ptr = new int;
    // do something which may throw an exception
    // we never get here if an exception is thrown
    delete ptr;
}

3. 在释放分配的内存a之前,给a赋其他值作为地址,导致原先分配的地址丢失,产生内存泄露

void MemoryLeak() {
    int * a = malloc(sizeof(int)); 
    a = 0;
}

 4. 不能使用delete去释放malloc分配的内存

void MemoryLeak() {
    char *s = (char*) malloc(5); 
    delete s;   
}

 5. 使用已经释放的指针

void MemoryLeak() {
    delete ptr;
    ptr = nullptr;

    ptr->show();
}

6. 释放形参中的指针

void MemoryLeak(char* c) {
    char* temp = new char [ strlen (c) + 1 ];
    strcpy ( temp, c );
    delete[] c; //错误,不应该删除形参的指针
    return temp;//错误,返回局域变量的指针
}

 7. 使用没有分配内存的指针

void MemoryLeak() {
    MyWindow* window;
    window->show();
}

8. 释放非堆上的指针

void MemoryLeak() {
    MyWindow window;
    delete window;
}

内存泄露的定位方法:https://www.cnblogs.com/skynet/archive/2011/02/20/1959162.html

2.3 悬垂指针

悬垂指针是指指针在指向的内存释放或者销毁后,依然保留着指向该内存地址的情况。当程序中存在悬垂指针时,如果再次使用这个指针访问该内存地址,可能会导致程序崩溃或者产生未定义的行为。

悬垂指针通常出现在以下情况下:

  1. 指针指向的内存被释放或者销毁,但指针没有被置为nullptr或者被赋予新的有效地址。
  2. 指针指向的对象在其生命周期内已经被销毁,但指针继续被使用。

例如:

  1. 重复释放,指针释放后,没有赋值为nullptr

void MemoryLeak() {
    char* pStr = (char*) malloc(20); 
    free(pStr); 
    free(pStr); // results in an invalid deallocation
}

2. 指针指向的对象被销毁:

int* ptr; 
{ 
    int num = 10; 
    ptr = &num; 
} 

在这里,指针ptr指向的对象num已经被销毁,ptr成为悬垂指针

3. 返回局部变量的地址

int* getLocalPtr() {
    int num = 20;
    return &num;
}
int* ptr = getLocalPtr();

函数调用结束后,局部变量num被销毁,ptr成为悬垂指针

使用智能指针可以有效地避免悬垂指针的问题,因为智能指针会在其所指向的对象不再需要时自动释放内存。

3. 如何用智能指针

3.1 unique_ptr

unique_ptr独享所有权,一个非空的unique_ptr总是拥有它所指向的资源。

转移一个unique_ptr将会把所有权也从源指针转移给目标指针(源指针被置空)。

无法拷贝,也无法用于需要副本的任何标准模板库 (STL) 算法。

 1. 创建一个unique_ptr实例

int main()
{
    unique_ptr<int> pInt(new int(5));
    cout << *pInt;
}

2. 移动,转移所有权

int main() 
{
    unique_ptr<int> pInt(new int(5));
    unique_ptr<int> pInt2 = std::move(pInt);    // 转移所有权
    //cout << *pInt << endl; // 出错,pInt为空
    cout << *pInt2 << endl;
    unique_ptr<int> pInt3(std::move(pInt2));
}

 3. 从函数中返回unique_ptr

unique_ptr<int> clone(int p)
{
    unique_ptr<int> pInt(new int(p));
    return pInt;    // 返回unique_ptr
}

int main() {
    int p = 5;
    unique_ptr<int> ret = clone(p);
    cout << *ret << endl;
}

4. 不能赋值构造和赋值

int main() 
{
    unique_ptr<int> pInt(new int(5));
    unique_ptr<int> pInt2(pInt);    // 报错
    unique_ptr<int> pInt3 = pInt;   // 报错
}

5. 在容器中保存指针(要用move放到容器中)

int main() 
{
    vector<unique_ptr<int>> vec;
    unique_ptr<int> p(new int(5));
    vec.push_back(std::move(p));    // 使用移动语义
}

6. 用unique_ptr管理动态数组

int main() 
{
    unique_ptr<int[]> p(new int[5] {1, 2, 3, 4, 5});
    p[0] = 0;   // 重载了operator[]
}

3.2 shared_ptr

shared_ptr采用引用计数的方式来管理内存,即每当有一个shared_ptr指向某段内存时,该内存的引用计数就会加1,当所有shared_ptr都不再指向这段内存时,引用计数就会减1。当引用计数减为0时,内存会自动释放。

shared_ptr内部的引用计数是安全的,但是对象的读取需要加锁。

1. 创建一个shared_ptr实例

使用make_shared创建shared_ptr

#include <iostream>
#include <memory>

// 定义一个结构体
struct Person {
    std::string name;
    int age;
    
    Person(const std::string& n, int a) : name(n), age(a) {}
};

int main() {
    // 使用std::make_shared创建一个指向Person结构体的shared_ptr
    std::shared_ptr<Person> personPtr = std::make_shared<Person>("Alice", 25);
    
    // 访问结构体成员变量
    std::cout << "Name: " << personPtr->name << std::endl;
    std::cout << "Age: " << personPtr->age << std::endl;
    
    
    //使用std::make_shared创建一个指向int的shared_ptr
    std::shared_ptr<int> ptr1 = std::make_shared<int>(10); 

    // 多个shared_ptr指向同一块内存
    std::shared_ptr<int> ptr2 = ptr1; 
    
    std::cout << *ptr1 << std::endl; // 输出:10
    std::cout << *ptr2 << std::endl; // 输出:10    

    return 0;
}

2. 避免循环引用

下面的示例中,我们创建了两个Node对象,然后将它们的next指针互相指向,造成循环引用。

当main函数结束时,由于循环引用导致两个Node对象无法释放,最终会造成内存泄漏。要解决这个问题,可以使用weak_ptr来打破循环引用。 

#include <iostream>
#include <memory>

class Node {
public:
    int value;
    std::shared_ptr<Node> next;

    Node(int v) : value(v), next(nullptr) {
        std::cout << "Node " << value << " created\n";
    }

    ~Node() {
        std::cout << "Node " << value << " destroyed\n";
    }
};

int main() {
    std::shared_ptr<Node> node1 = std::make_shared<Node>(1);
    std::shared_ptr<Node> node2 = std::make_shared<Node>(2);

    node1->next = node2;
    node2->next = node1; // 会导致循环引用

    return 0;
}

3. 减少计数

使用reset方法,可以将shared_ptr指向另一个新的Test对象。这将导致原来的Test对象的引用计数减少,当引用计数减少到0时,原来的Test对象将被删除。

注意,reset方法不仅会减少原来对象的引用计数,还会增加新对象的引用计数。因此不需要显式地删除原来的Test对象。

这个示例也展示了shared_ptr的一个重要特性,即它可以自动管理内存,无需程序员显式地调用delete。

#include <iostream>
#include <memory>

class Test {
public:
   Test() {
       std::cout << "Test Constructor" << std::endl;
   }

   ~Test() {
       std::cout << "Test Destructor" << std::endl;
   }
};

int main() {
   std::shared_ptr<Test> p1(new Test());
   p1.reset(new Test());

   return 0;
}

3.3 weak_ptr

weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。从而避免了循环引用的问题。

1. 创建weak_ptr、检测对象是否可用

首先创建了一个shared_ptr指向一个Test对象,然后我们创建了一个weak_ptr,它指向同一个Test对象。

然后,使用lock方法,尝试获取一个shared_ptr,指向weak_ptr所指向的对象。如果weak_ptr所指向的对象仍然存在,lock方法将返回一个shared_ptr,否则,lock方法将返回一个空的shared_ptr。

这个示例展示了weak_ptr的一个重要特性,即它可以安全地访问shared_ptr所指向的对象,而不会增加对象的引用计数。在if语句中使用auto关键字,自动推断shared_ptr的类型。

另外,这个示例也展示了weak_ptr的另一个重要特性,即它可以检查shared_ptr所指向的对象是否仍然存在。这就是为什么需要使用if语句,来检查lock方法返回的shared_ptr是否为空。

   #include <iostream>
    #include <memory>
    
    class Test {
    public:
       Test() {
           std::cout << "Test Constructor" << std::endl;
       }
    
       ~Test() {
           std::cout << "Test Destructor" << std::endl;
       }
    };
    
    int main() {
       std::shared_ptr<Test> p1(new Test());
       std::weak_ptr<Test> wp1(p1);
    
       if (auto sp1 = wp1.lock()) {
           std::cout << "Test object is still alive" << std::endl;
       } else {
           std::cout << "Test object has been destroyed" << std::endl;
       }
    
       return 0;
    }

 
   

  • 35
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值