智能指针 相关概念

1.悬挂指针和野指针

悬挂指针:指向已经被释放的内存区域或者指向无效的内存区域

野指针: 指向未知内存区域,或者指针本身未分配内存地址

悬挂指针和野指针都会导致程序出现错误,产生的原因不一样,是两个概念

1.1 悬挂指针

悬挂指针通常由于下面几个原因产生

  • 指针指向的内存被释放后,未将指针置空(使用delete或者free等释放内存后,没有将指针设置为NULL), 此时如果尝试通过该指针访问已经释放的内存,将会导致未定义行为,如程序崩溃或数据损坏
  • 指针指向函数内部的局部变量(栈上变量),该函数执行完,局部变量所占用内存会被回收。如果外部仍有指针指向这个局部变量,这个指针就会变成悬挂指针

悬挂指针可能会导致严重的错误和未定义的行为。当尝试通过悬挂指针解引用或访问其指向的内存时,可能会访问到无效的内存区域,导致程序崩溃、数据损坏或其他不可预测的行为

情况一:

#include <iostream>
using namespace std;
 
int main()
{
    int *p = new int(5);
    cout<<"*p = "<<*p<<endl;
    cout<<"p 地址:"<<p<<endl;
    free(p);  // p 在释放后成为悬空指针
    cout<<"p 地址:"<<p<<endl;
    cout<<"*p = "<<*p<<endl;
    p = NULL; // 非悬空指针
    return 0;
}

输出结果:

linuxy@linuxy:~/wildPointer$ ./main 
*p = 5
p 地址:0x55a885ef6eb0
p 地址:0x55a885ef6eb0
*p = 0
linuxy@linuxy:~/wildPointer$ 

p 指针在被 free 后,成为悬空指针,被 NULL 赋值后不再是悬空指针。

注意:这里 free 掉的是 p 的内存空间,并不是变量 p

free 前后 p 的地址是不变的,free 释放的是 p 指向的内存空间,释放后表示该快内存可以重新分配了,至于 free 后 *p 的值,视不同编译器情况而不同

情况二:

#include <iostream>
using namespace std;
 
int* getVal() {
    int tmp = 10;
    return &tmp;
}
 
int main()
{
    int *p = getVal(); //悬空指针
    cout<<"*p = "<<*p<<endl;
    return 0;
}

在函数 getVal 执行完后,局部变量的内存空间会被释放,而这里 p 指向了函数内的局部变量,p 便成为了悬空指针,可以将 tmp 变为 static 的

1.2 野指针

一般是指针没有初始化

#include <iostream>
using namespace std;
 
int main()
{
    int *p;     // 野指针
    int *q = NULL; // 非野指针
    p = new int(5);  // p 现在不再是野指针
    q = new int(10); 
    cout<<"*p = "<<*p<<endl;
    cout<<"*q = "<<*q<<endl;
    free(p);
    free(q);
    return 0;
}

代码出自下面
原文链接:https://blog.csdn.net/nyist_zxp/article/details/119478944

2. 内存泄漏

内存泄露的是分配的内存空间在不再使用时未被正确释放,导致这些内存无法再被程序访问到,同时仍然占用系统资源。内存泄露的本质是对内存的浪费

防止办法

  • new和delete, malloc和free成对出现
  • 智能指针
  • 内存泄漏检测工具

3. 智能指针

由于原始指针忘记删除,或者释放内存没有将其置空,导致悬空指针,会产生程序崩溃或者其他不良后果。特别是函数内部有异常,delete的语句执行不到,这种情况就比较复杂

智能指针就可以方便我们控制指针对象的生命周期。在智能指针中,一个对象什么情况下被析构或被删除,是由指针本身决定的,并不需要用户进行手动管理,自动释放new出来的内存,从而避免内存泄漏

  • 智能指针解决的问题
  1. 自动内存管理:智能指针(如 std::unique_ptr、std::shared_ptr 和 std::weak_ptr)在析构时会自动释放所管理的内存,从而防止内存泄漏。
  2. 异常安全:当异常发生时,智能指针能够确保即使在构造函数或析构函数抛出异常的情况下,也能正确释放内存。
  3. 提供共享和独占的语义:std::unique_ptr 提供独占所有权的语义,即同一时间只能有一个智能指针指向同一个对象。std::shared_ptr 提供共享所有权的语义,允许多个智能指针共享同一个对象的所有权,并能够跟踪引用计数,自动释放内存。
  4. 与标准容器兼容:智能指针可以与标准容器(如 std::vector、std::list 等)一起使用,使得容器中的对象也可以自动管理内存
  • 智能指针是什么?

智能指针是一个具有生命周期的对象,每种智能指针都是通过类模板的方式实现的, 就是对普通指针进行封装,通过重载 * 和 ->两个运算符,使得智能指针表现的就像普通指针一样

  • RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。本质是使用栈对象管理资源,对象离开作用域时,会自动调用析构函数

实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  1. 不需要显式地释放资源(对象在析构时会自动释放所管理的内存)
  2. 采用这种方式,对象所需的资源在其生命期内始终保持有效

智能指针只能管理动态开辟的堆区上的内存(delete)

3.1 unique_ptr

是一个不共享的智能指针, 独占对象的所有权,两个指针不能指向同一资源。由于没有引用计数,性能较好。

  1. 基于排他所有权模式:两个指针不能指向同一个资源
  2. 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
  3. 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
  4. 在容器中保存指针是安全的

unique_ptr 是 C++ 11 提供的用于防止内存泄漏的智能指针中的一种实现,独享被管理对象指针所有权,同一个时间只允许一个智能指针管理动态开辟的内存

unique_ptr对象包装一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中删除关联的原始指针。

#include <memory>

//unique_ptr 定义

// 单个对象  non-specialized

template <class T, class D = default<T>> class unique_ptr;

// 数组 array specialization

template <class T, class D> class unique_ptr<T[], D>;

Unique_ptr创建和使用

1. 使用构造函数初始化
std::unique_ptr<T> ptr(new T);

std::unique_ptr<int> ptr(new int())
#include <iostream>
#include <memory>
using namespace std;

int mian()
{
    unique_ptr<T> ptr(new T);        //指针指向T类型,开辟一个T类大小的空间
	unique_ptr<int> p;               // 可以指向int对象的一个空智能指针
	unique_ptr<int> p(new int(105)); // p指向一个值为105的int对象
	auto p(new int(105));            // 不能简写为auto,这里auto推断出是普通指针

    unique_ptr<int> p1 = unique_ptr<int>(new int()); //临时对象

    int *p = new int;
    unique_ptr<int> p1(p);           //这样会导致两个指针指向同一空间出错

}
struct Test1 {  
    int x;  
    double y;
};  

struct Test2 {  

    virtual void GetT(int* T) = 0;  //纯虚函数
  
};  

  
int main() {  
    // 使用std::unique_ptr动态分配Test1对象  
    std::unique_ptr<Test1> ptr(new Test1{22, 2.22});  
  
    // 使用ptr  
    std::cout << ptr->x << ", " << ptr->y << std::endl; 


    std::unique_ptr<Test2> ptr1 = nullptr; 

    ptr1->GetT(5);

  

}

unique_ptr不允许复制,  但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它原本就不再拥有原来指针的所有权了

unique_ptr<T> ptr(new T); // 正确
unique_ptr<T> ptr1 = std::move(ptr); // 正确,运行结果ptr指向空,ptr1指向T
unique_ptr<T> ptr2 = ptr; // 报错,不能复制
2. 使用make_unique()函数模板

C++14才有的make_unique,  make_unique不支持定制删除器

auto upw1(std::make_unique<Widget>()); // with make func

std::unique_ptr<T> ptr = make_unique<T>();  //不传入参数,创建一个指向一块T类型的指针空间

std::unique_ptr<T []> ptr = make_unique<T []>(5); //传入参数5,创建一个指向T类型的连续空间,5表示对应的个数




make_unique()的优点

不传入参数时,使用第一种方法(直接使用构造函数)创建的unique_ptr是指向nullptr的,不能直接赋值改变指向,而make_unique()不传入参数时,会分配一块地址给指针,并不是nullptr

提高了代码的安全性

3.2 shared_ptr

有时候多线程处理时需要对资源的共享,多个指针需要指向同一地址,unique_ptr就不适用于这种情况

shared_ptr的思路是记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数加1,当智能指针析构时,引用计数减1,如果计数为零,代表已经没有指针指向这块内存,那么就释放

1.使用构造函数初始化

#include <iostream>
#include <memory>
 
class T {};
 
int main()
{
  std::shared_ptr<T> f(new T());              // 裸指针直接初始化

2. 移动构造和拷贝构造

不同于unique_ptr,shared_ptr可以使用拷贝构造和拷贝赋值,unique_ptr不可以

#include <iostream>
#include <memory>
 
class T {};
 
int main()
{
  std::shared_ptr<T> ptr(new T());              // 裸指针直接初始化
  //std::shared_ptr<T> ptr1 = new T();            // Error,explicit禁止隐式初始化
  std::shared_ptr<T> ptr2(ptr);                       // 拷贝构造函数
  std::shared_ptr<T> ptr3 = ptr;                      // 拷贝构造函数
  ptr2 = ptr;                                             // copy赋值运算符重载
  std::cout << ptr3.use_count() << " " << ptr3.unique() << std::endl;
 
  std::shared_ptr<T> ptr4(std::move(new T()));        // 移动构造函数
  //std::shared_ptr<T> ptr5 = std::move(new T());       // Error,explicit禁止隐式初始化
  std::shared_ptr<T> ptr6(std::move(ptr4));                 // 移动构造函数
  std::shared_ptr<T> ptr7 = std::move(ptr6);                // 移动构造函数
  std::cout << ptr7.use_count() << " " << ptr7.unique() << std::endl;
 
  std::shared_ptr<T[]> ptr8(new T[10]());             // Error,管理动态数组时,需要指定删除器
  std::shared_ptr<T> ptr9(new T[10](), std::default_delete<T[]>());
 

 
  return 0;
}
3. 通过make_shared()构造

shared_ptr很好地包装了new表达式,但过多的显式new操作符也是个问题,至少使用两次new,并且内存不连续,make_shared会一次性申请一整块内存,一块内存给引用计数块使用,一块内存给源结构体使用

make_shared优点

用make_shared()内部仅分配一次内存,减少了堆内存的申请操作,性能有所提升,同时申请的内存是连续的,连续访问引用计数块和原结构体时,由于局部性原理,性能也会有提升

make_shared()函数可以接受最多10个参数,然后把它们传递给类型T的构造函数,创建一个shared_ptr<T>的对象并返回。

1.demo_01.cpp
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
 
class StructA{
public:
  int i;
  string str;
  //StructA(): i(100/*int()*/){//初始化列表
  StructA(int i){
    this->i = i;
    cout<< "StructA(int), line = "<< __LINE__<< endl;
  };
 
  StructA(string str){
    this->str = str;
    cout<< "StructA(string), line = "<< __LINE__<< endl;
  };
  
  ~StructA(){ cout <<"~StructA "<< endl;};
  
  void show(){
    cout << "show() is Called. line = "<< __LINE__<< endl;
  }
} ;
 
int main(int argc, const char * argv[]){
  //make_shared自动申请对象及内存,自动释放,不用手动执行new和delete
  shared_ptr<StructA> pA = make_shared<StructA>(123);
  cout << "pA->i = " << pA->i << endl;
 
  shared_ptr<StructA> pB = make_shared<StructA>("Hello Kitty!");
  cout << "pB->str = " << pB->str << endl;
  pB->show();
 
  auto t = make_shared<StructA>("Number One!");
  cout << "t->str = " << t->str << endl;
  t->show();
  
  return 0;
}
 
2.demo_02.cpp
#include <iostream>
#include <vector>
#include <map> 
#include <memory>
using namespace std;
 
class StructA{
public:
  int i;
  //StructA(): i(100/*int()*/){//初始化列表
  StructA(int i){
    this->i = i;
    cout<< "StructA(int), line = "<< __LINE__<< endl;
  };
  
  ~StructA(){ cout <<"~StructA "<< endl;};
  
  void show(){
    cout << "show() is Called. line = "<< __LINE__<< endl;
  }
  template<class T>
  T add(T x){
    return x;
  }
} ;
 
int main(){ 
  map<int, shared_ptr<StructA>> mStrA;
  //插入map映射key-value数据
  mStrA[0] = make_shared<StructA>(123);
  mStrA[1] = make_shared<StructA>(456);
  mStrA.insert(pair<int, shared_ptr<StructA>>(2,make_shared<StructA>(789)));
  mStrA.insert(pair<int, shared_ptr<StructA>>(3,make_shared<StructA>(333)));
  
  mStrA[0]->show();
  //遍历方式1
  for(auto &mA : mStrA){
    cout << mA.first << " " << mA.second->i << endl;
    //mA.second->show();
  }
  cout << endl;
  
  //遍历方式2
  map<int, shared_ptr<StructA>>::iterator iter;
  for(iter = mStrA.begin(); iter != mStrA.end(); iter++){
    cout << iter->first << " "<<iter->second->i<<endl;
    //iter->second->show();
  }
  return 0;
}
 
4.make系列函数的局限:
  • 如果想要创建的对象没有公有的构造函数,make_shared就无法使用
  • 所有的make系列函数都不允许自定义删除器;
  • make系列函数创建对象时,不能接受{}初始化列表(这是因为完美转发的转发函数是个模板函数,它利用模板类型进行推导。因此无法将{}推导为initializer_list)。换言之,make系列只能将圆括号内的形参完美转发;
  • 自定义内存管理的类(如重载了operator new和operator delete),不建议使用make_shared来创建。因为:重载operator new和operator delete时,往往用来分配和释放该类精确尺寸(sizeof(T))的内存块;而make_shared创建的shared_ptr,是一个自定义了分配器(std::allocate_shared)和删除器的智能指针,由allocate_shared分配的内存大小也不等于上述的尺寸,而是在此基础上加上控制块的大小;
  • 对象的内存可能无法及时回收。因为:make_shared只分配一次内存,减少了内存分配的开销,使得控制块和托管对象在同一内存块上分配。而控制块是由shared_ptr和weak_ptr共享的,因此两者共同管理着这个内存块(托管对象+控制块)。当强引用计数为0时,托管对象被析构(即析构函数被调用),但内存块并未被回收,只有等到最后一个weak_ptr离开作用域时,弱引用也减为0才会释放这块内存块。原本强引用减为0时就可以释放的内存, 现在变为了强引用和弱引用都减为0时才能释放, 意外的延迟了内存释放的时间。这对于内存要求高的场景来说, 是一个需要注意的问题。

3.3 weak_ptr

1.循环引用

在shared_ptr的使用过程中,当强引用计数为0是,就会释放所指向的堆内存。那么问题来了,如果和死锁一样,当两个shared_ptr互相引用,那么它们就永远无法被释放

2. 原理

weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。从这个角度看,weak_ptr更像是shared_ptr的一个助手而不是智能指针。

3.初始化

  1. 通过构造函数直接初始化,也可以通过隐式转换
  2. 移动构造和拷贝构造
#include <iostream>
#include <memory>

class Frame {};

int main()
{
  std::shared_ptr<Frame> f(new Frame());
  std::weak_ptr<Frame> f1(f);                     // shared_ptr直接构造
  std::weak_ptr<Frame> f2 = f;                    // 隐式转换
  std::weak_ptr<Frame> f3(f1);                    // 拷贝构造函数
  std::weak_ptr<Frame> f4 = f1;                   // 拷贝构造函数
  std::weak_ptr<Frame> f5;
  f5 = f;                                         // 拷贝赋值函数
  f5 = f2;                                        // 拷贝赋值函数
  std::cout << f.use_count() << std::endl;        // 1

  return 0;
}

如何判断weak_ptr指向对象是否存在?

既然weak_ptr并不改变其所共享的shared_ptr实例的引用计数,那就可能存在weak_ptr指向的对象被释放掉这种情况。这时,就不能使用weak_ptr直接访问对象。那么如何判断weak_ptr指向对象是否存在呢?C++中提供了lock函数来实现该功能。如果对象存在,lock()函数返回一个指向共享对象的shared_ptr(引用计数会增1),否则返回一个空shared_ptr。weak_ptr还提供了expired()函数来判断所指对象是否已经被销毁。

由于weak_ptr并没有重载operator ->和operator *操作符因此不可直接通过weak_ptr使用对象,同时也没有提供get函数直接获取裸指针。典型的用法是调用其lock函数来获得shared_ptr示例,进而访问原始对象

代码和部分内容出自下面

C++ make_shared() shared_ptr()用法(七)

【C++】shared_ptr共享型智能指针详解
【C++】weak_ptr弱引用智能指针详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值