C++ 动态内存和智能指针


前言

对象有着严格的定义的生存期:全局对象在程序启动时分配,在程序结束时销毁;局部自动对象,当进入其定义所在的程序块时创建,离开程序块时销毁;局部static对象在第一次使用前分配,在程序结束时销毁。

上述对象外,C++还支持动态内存分配对象,动态分配的对象生存期与在哪里创建无关,只有显示地释放才会被销毁。

动态对象的释放是编程中极其容易出错的地方,为了更安全的使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动的释放它。


一、动态内存

静态内存(全局区)用来保存static对象、类static数据成员以及函数之外的变量(全局变量和全局常量);栈内存用来保存定义在函数内的非static对象和局部变量,分配在静态和栈内存中得对象由编译器自动创建和销毁,栈对象,仅仅在程序块运行时才存在,static对象在使用前分配,程序结束时销毁。

每个程序还拥有一个内存池,叫做堆,用来存储动态分配得对象,也就是程序运行时分配的对象,动态对象需要程序员显示的销毁。

C++动态内存创建(初始化)通过运算符:new实现,在动态内存中为对象分配空间并返回一个指向改对象的指针(P.S. 这个地方返回的是指针,需要用指针变量接收)。
C++动态内存的销毁通过 delete 实现,delete 接收一个动态对象的指针,销毁该对象并释放其内存。

动态内存的释放很容易出问题,有时会忘记释放导致内存泄漏;有时在有指针引用内存的时候释放会产生引用非法内存的指针。

C++ 标准库提供了两种智能指针管理动态对象,智能指针的行为类似常规指针,重要区别是它负责自动释放所指向的对象。两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象,unique_ptr独占所指向的对象,标准库定义了一个名为weak_ptr的伴随类(相关联的意思),是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在 memory 头文件中。

二、unique_ptr

unique_ptr 实现独占式拥有或严格拥有概念,保证同⼀时间内只有⼀个智能指针可以指向该对象。它对于避免资源泄露特别有⽤。

unique_ptr<string> p3 (new string (auto));//#4
unique_ptr<string> p4;//#5
p4 = p3;//此时会报错

注意
1、C++11中用来替代auto_ptr
2、拷贝构造和赋值运算符被禁用,不能进行拷贝构造和赋值运算
3、虽然禁用了拷贝构造和赋值运算符,但unique_ptr可以作为返回值,用于从某个函数中返回动态申请内存的所有权,即 通过调用release或reset将指针所有权从一个(非const)unique_ptr转移给另一个unique。

三、shared_ptr

shared_ptr 实现共享式拥有概念,多个智能指针可以指向相同对象,该对象和其相关资源会在 “最后⼀个引⽤被销毁” 时候释放。
注意
1、多个指针可以指向相同的对象,调用release()计数-1,计数0时资源释放
2、use_count()查计数
3、reset()放弃内部所有权
4、share_ptr多次引用同一数据会导致内存多次释放
5、循环引用会导致死锁,
6、引用计数不是原子操作。

shared_ptr 有两个数据成员,一个是指向 对象的指针 ptr,另一个是 ref_count 指针(包含vptr、use_count、weak_count、ptr等)。
在这里插入图片描述
y=x 涉及两个成员的复制,这两步拷贝不会同时(原子)发生,中间步骤 1,复制 ptr 指针,中间步骤 2,复制 ref_count 指针,导致引用计数加 1。因为是两步,如果没有 mutex 保护,那么在多线程里就有数据竞争。多线程读写同一个 shared_ptr 必须加锁。
在这里插入图片描述

同时,复制行为不恰当使用会导致两个指针指向同一个对象,很容易产生 野指针 问题。

#include<iostream>
#include<memory>
int main()
{
	第一种情况:
    auto sp = std::make_shared<std::string>("wechat:shouwangxiansheng");
    std::string *p = sp.get();
    std::shared_ptr<std::string> sp2(p);/*不要这样做!!*/
    delete p;/*不要这样做*/
    return 0;

	第二种情况
	int a = 10;
	int *p = &a;
	std::make_shared<int> sp1(p);
	std::make_shared<int> sp2(p);//当sp1退出时,sp2就会变成野指针,这个时候最后使用 make_shared 来初始化
}

shared_ptr还会产生 内存泄漏问题

processWidget(std::shared_ptr<Widget>(new Widget),  //潜在的资源泄露 
              computePriority());

编译器可能会把上面的代码片段优化成下面的执行顺序,如果这样的代码被产生出来,并且在运行期,computePriority产生了一个异常,则在第一步动态分配的Widget就会泄露了,因为它永远不会被存放到在第三步才开始管理它的std::shared_ptr中。

	1、执行“new Widget”。
	2、执行computePriority。
	3、执行std::shared_ptr的构造函数。

这时最好使用 make_shared 来对 shared_ptr进行初始化,通过这个方法可以提高初始化的效率,并且保证内存安全。这个方法的主要区别可以看下面的两幅图,第一幅是通过 new 进行初始化,第二幅是通过 make_shared 初始化

通过 new 初始化:
通过new初始化
通过 make_shared 初始化:
在这里插入图片描述
shared_ptr的循环引用会导致引用计数不正常,这个时候就引入了weak_ptr。

class Monster
{  
    std::shared_ptr<Monster> m_father;  
    std::shared_ptr<Monster> m_son;

public:  
    void setFather(std::shared_ptr<Monster>& father);      
    void setSon(std::shared_ptr<Monster>& son);    
    ~Monster(){std::cout << "A monster die!";}       
};

void runGame()
{
     std::shared_ptr<Monster> father = new Monster();
     std::shared_ptr<Monster> son = new Monster();
     father->setSon(son);
     son->setFather(father);
}

函数退出时栈的shared_ptr对象陆续释放后的情形:
(1)一开始:father,son指向的堆对象 shared计数都是为2;
(2)son智能指针退出栈:son指向的堆对象 计数减为1,father指向的堆对象 计数仍为2。
(3)father智能指针退出栈:father指向的堆对象 计数减为1 , son指向的堆对象 计数仍为1。
(4)函数结束:所有计数都没有变0,也就是说中途没有释放任何堆对象。

智能指针陷阱:

  1. 不使用相同的内置指针值初始化(或reset)多个智能指针。
  2. 不delete get()返回的指针
  3. 不使用get()初始化或reset另一个智能指针
  4. 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
  5. 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器

三、weak_ptr

weak_ptr是为了辅助shared_ptr的存在,它只提供了对管理对象的一个访问手段,同时也可以实时动态地知道指向的对象是否存活。只有某个对象的访问权,而没有它的生命控制权 即是 弱引用,所以weak_ptr是一种弱引用型指针。

  • weak_ptr可以由一个shared_ptr或者另一个weak_ptr构造。
  • weak_ptr的构造和析构不会引起shared_count的增加或减少,只会引起weak_count的增加或减少。
  • 被管理资源的释放只取决于shared计数,当shared计数为0,才会释放被管理资源,也就是说weak_ptr不控制资源的生命周期。
  • 计数区域的释放却取决于shared计数和weak计数,当两者均为0时,才会释放计数区域。
  • weak_ptr没有重载 * 和 -> ,所以并不能直接使用资源。 但可以使用lock()获得一个可用的shared_ptr对象,如果对象已经死了,lock()会失败,返回一个空的shared_ptr。

解决两个share_ptr互相引用产生死锁,计数永远降不到0,没办法进行资源释放,造成内存泄漏的问题。使用时配合share_ptr使用,把其中一个share_ptr更换为weak_ptr。

class Monster
{  
    std::weak_ptr<Monster> m_father;  
    std::weak_ptr<Monster> m_son;

public:  
    void setFather(std::shared_ptr<Monster>& father);      
    void setSon(std::shared_ptr<Monster>& son);    
    ~Monster(){std::cout << "A monster die!";}       
};

void runGame()
{
     std::shared_ptr<Monster> father = new Monster();
     std::shared_ptr<Monster> son = new Monster();
     father->setSon(son);
     son->setFather(father);
}

(1)father指向的堆对象 shared计数为1,weak计数为1;son指向的堆对象 shared计数为1,weak计数为1;
(2)son智能指针退出栈:son指向的堆对象 shared计数减为0,weak计数为1,释放son的堆对象;father指向的堆对象 shared计数为1,weak计数减为0;
(3)father智能指针退出栈:father指向的堆对象 shared计数减为0,weak计数为0;释放father的堆对象和father的计数区域;son指向的堆对象 shared计数为0,weak计数减为0;释放son的计数区域。
(4)函数结束,释放行为正确。


总结

参考文章:
C++智能指针总结
share_ptr内存泄漏
使用C++11解决内存泄露的问题
C++11 make_shared
运行程序报错:请检查是否存在数组、列表等越界非法访问,内存非法访问等情况
C++内存泄漏的常规问题和解决办法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值