C++ 智能指针

0. 内存泄漏

内存泄露 :因为错误或疏忽,造成程序未能释放某段不再使用的内存的情景。

内存泄漏的危害长期运行的程序出现内存泄露,如:操作系统等,会导致响应越来越慢,最终卡死。

在正式了解之前,需要明晰一个观念,否则容易把 智能指针的作用自定义类型生命周期结束时会自动调用析构函数 混淆:

智能指针的出现,是为了解决传统指针内存泄露的问题

智能指针的使用及原理

1. RAII

RAII(Resource Acquisition Is Initialization):资源获取即初始化利用对象生命周期来控制资源

  • 在构造对象时自动获取资源
  • 在对象析构时自动释放资源

实际上,我们把管理一份资源的责任委托给一个对象,这样做有两个好处:

  1. 不需要显式释放资源
cpp 复制代码
// 传统指针
int* pa = new int[10];
...
delete[] pa;

// 智能指针(包括接下来会介绍到的 unique_ptr、shared_ptr)
unque_ptr<int[]> pb(new int[10]);
// 智能指针对象析构时,会自动释放资源,不需要显式 delete
  1. 对象所需的资源在其生命周期内始终有效
cpp 复制代码
class MyClass {
public:
	MyClass(int data) : _data(data) {
		std::cout << "MyClass constructed with data: " << _data << std::endl;
	}

	~MyClass() {
		std::cout << "MyClass destructed." << std::endl;
	}

	void PrintData() const {
		std::cout << "Data: " << _data << std::endl;
	}

private:
	int _data;
};

int main() {
	// 使用 std::unique_ptr 创建一个 MyClass 对象
	std::unique_ptr<MyClass> my_instance = std::make_unique<MyClass>(42);

	// 可以像普通指针一样使用 my_instance
	my_instance->PrintData();

	// 当 my_instance 超出作用域时,其析构函数会自动调用,释放 MyClass 实例
	// 无需手动 delete
	return 0;
}
2. 智能指针的原理
  • RAII
cpp 复制代码
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};
  • 重载 operator*() operator->()
cpp 复制代码
  public:
  	T& operator*()
      {
          return *_ptr;
      }
  
      T* operator->()
      {
          return _ptr;
      }
3. unique_ptr
cpp 复制代码
template<class T>
class unique_ptr
{
public:
    unique_ptr(T* ptr)
        :_ptr(ptr)
    {}

    ~unique_ptr()
    {
        if (_ptr)
        {
            cout << "~unique_ptr()" << endl;
            delete _ptr;
            _ptr = nullptr;
        }
    }
	
    // 禁止拷贝
    unique_ptr(unique_ptr<T>&) = delete;
    unique_ptr<T>& operator=(unique_ptr<T>&) = delete;

    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }
private:
    T* _ptr;
};
4. shared_ptr shared_ptr

在 unique_ptr 基础上加入了引用计数,解决拷贝导致资源管理权转移的问题 ------ 一份资源对应一个引用计数;

引用计数 代表着 对应的资源被几个 shared_ptr 所共有;

在对象被销毁(析构)时,--(*_psize)

*_psize == 0 时,delete _ptr 释放资源。

cpp 复制代码
template<class T>
class shared_ptr
{
public:
    shared_ptr(T* ptr)
        :_ptr(ptr)
        ,_psize(new int(1))
    {}

    void release()
    {
        if (--(*_psize) == 0)
        {
            if (_ptr)
            {
                cout << "delete: " << _ptr << endl;
                delete _ptr;
                _ptr = nullptr;
            }
            delete _psize; // _psize 是在堆上开空间,需要 delete
        }
    }

    ~shared_ptr()
    {
        release();
    }

    shared_ptr(shared_ptr<T>& sp)
        :_ptr(sp._ptr)
        ,_psize(sp._psize)
    {
        ++(*_psize);
    }

    int use_count()
    {
        return *_psize;
    }

    shared_ptr<T>& operator=(shared_ptr<T>& sp)
    {
        if (_ptr != sp._ptr)
        {
            release();

            _ptr = sp._ptr;
            _psize = sp._psize;
            ++(*_psize);
        }
        return *this;
    }

    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }

    T* get()
    {
        return _ptr;
    }

private:
    T* _ptr;
    // 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源
    int* _psize;
};

shared_ptr 同样有设计缺陷:循环引用

if (--(*_psize) == 0) 才真正释放资源,循环引用 会导致资源无法被释放:

左边节点的释放,依赖于 delete 右边节点的 _prev;

delete 右边节点的 _prev,依赖于 右边节点的释放;

右边节点的释放,依赖于 delete 左边节点的 _next;

delete 左边节点的 _next,依赖于 左边节点的释放 ... ... (如此循环下去,两个节点的资源都无法被释放)

因此,需要引入 weak_ptr 。

weak_ptr 不会增加它所指向对象的引用计数,它不会影响 shared_ptr 所管理对象的生命周期,可以很好解决循环引用的问题。

cpp 复制代码
template<class T>
class weak_ptr
{
public:
    weak_ptr(T* ptr)
        :_ptr(ptr)
    {}

    weak_ptr(shared_ptr<T>& sp)
    {
        _ptr = sp.get();
    }

    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }
private:
    T* _ptr;
};
相关推荐
咖喱鱼蛋2 分钟前
Ubuntu安装Electron环境
linux·ubuntu·electron
ac.char6 分钟前
在 Ubuntu 系统上安装 npm 环境以及 nvm(Node Version Manager)
linux·ubuntu·npm
肖永威11 分钟前
CentOS环境上离线安装python3及相关包
linux·运维·机器学习·centos
tian2kong14 分钟前
Centos 7 修改YUM镜像源地址为阿里云镜像地址
linux·阿里云·centos
Code_Artist14 分钟前
使用Portainer来管理并编排Docker容器
docker·云原生·容器
布鲁格若门18 分钟前
CentOS 7 桌面版安装 cuda 12.4
linux·运维·centos·cuda
Eternal-Student23 分钟前
【docker 保存】将Docker镜像保存为一个离线的tar归档文件
运维·docker·容器
C-cat.25 分钟前
Linux|进程程序替换
linux·服务器·microsoft
dessler26 分钟前
云计算&虚拟化-kvm-扩缩容cpu
linux·运维·云计算
怀澈12227 分钟前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++