施磊老师高级c++(二)

智能指针

基础–实现简单的智能指针

  1. 裸指针是什么?
    裸指针(Raw Pointer) 是 C++ 中的一种基本指针类型,它直接存储内存地址,不附带任何额外的管理功能(如自动内存管理或生命周期管理)。裸指针是 C++ 中最原始的指针形式,通常用于直接操作内存。
    没有自动内存管理功能,容易导致悬空指针、内存泄漏等问题。 需要手动释放

  2. 裸指针缺点:

    1. 忘记释放资源,导致资源泄露(常发生内存泄漏问题)
    2. 同一资源释放多次,导致释放野指针,程序崩溃
    3. 明明代码的后面写了释放资源的代码,但是由于程序逻辑满足条件,从中间
    return掉了,导致释放资源的代码未被执行到,懵
    4. 代码运行过程中发生异常,随着异常栈展开,导致释放资源的代码未被执行到,
    懵
    
  3. 智能指针,智能指针的智能二字,主要体现在用户可以不关注资源的释放,因为智能指针
    会帮你完全管理资源的释放,它会保证无论程序逻辑怎么跑,正常执行或者产生异
    常,资源在到期的情况下,一定会进行释放

  4. 实现一个简单的 智能指针

    #include <iostream>
    #include <cstring>
    using namespace std;
    
    template<typename T>
    class CSmartPtr
    {
    public:
        CSmartPtr(T* ptr = nullptr) : mptr(ptr) {}
        ~CSmartPtr() { delete mptr; }
    
        T& operator* () { return *mptr; }  // 必须引用, 否则不能修改
    
        T* operator->() { return mptr; }
    private:
        T* mptr;
    };
    
    int main()
    {
        // int *p = new int;
        CSmartPtr<int> ptr1(new int);
       
        *ptr1 = 20;   // 虽然 ptr1 本身是普通对象, 但是里面是指针,想要访问指针, 除了写访问函数, 还能用 *重载 
    
        class Test
        {
        public:
            void test() { cout << "test" << endl; }
        };
    
        CSmartPtr<Test> ptr2(new Test()); 
        (*ptr2).test();  // 好好理解,为什么要用*,
        ptr2->test(); 
         return 0;
    }
    
    

    利用栈上的 对象, 出作用域 自动析构的特征, 做到资源的 自动释放!

2.不带引用计数的智能指针

问题

  1. 上一节代码里, 加入做下面这个:

     CSmartPtr<int> ptr1(new int);
     CSmartPtr<int> ptr2(ptr1);
    

    默认拷贝构造是一个浅拷贝

    1. 浅拷贝
      • 仅复制指针的值(内存地址)。
      • 多个对象共享同一块动态内存。
      • 适用于没有动态内存或资源的对象。
    2. 深拷贝
      • 复制指针所指向的实际数据。
      • 每个对象拥有独立的动态内存副本。
      • 适用于有动态内存或资源的对象。

    会导致 两次析构 释放同一个值, 造成野指针问题

    野指针(Wild Pointer) 是指未初始化或指向已释放内存的指针。野指针的行为是未定义的,访问或修改野指针指向的内存可能导致程序崩溃、数据损坏或安全漏洞。

解决

  1. 使用 自定义的 深拷贝 构造函数, 可以解决这个问题
  2. 不带引用计数的智能指针 解决

不带引用计数的智能指针汇总

那些独占所有权的智能指针
禁止拷贝和赋值重载,auto_ptr例外
自动释放

auto_ptr – #include < memory > – 不推荐

它在 C++11 中被弃用,并在 C++17 中被移除。 why?

//c++库里

int main()
{
    auto_ptr<int>  ptr1(new int);   
    auto_ptr<int>  ptr2(ptr1);  // auto_ptr 源码显示, 在拷贝构造时, 会把原来的ptr1的指针置空

    *ptr2 = 20;
    cout << *ptr1 << endl;  // 错误
     return 0;
}

也就是说, 拷贝构造会使得 原来的 所有的 指针 都会 置空, 不能再访问这块内存

std::auto_ptr 采用独占所有权模型,但在拷贝或赋值时会发生所有权转移。

这意味着,当一个 auto_ptr 被拷贝或赋值给另一个 auto_ptr 时,原指针会失去所有权,变为 nullptr。

这种行为容易导致意外的所有权转移和悬空指针问题。
scoped_ptr — 不推荐

scoped_ptr 是 Boost 库中提供的一种智能指针,用于在特定作用域内管理动态分配的内存。
主要特点:禁止拷贝和赋值,从而避免所有权转移的问题
直接把 里面的 拷贝构造=delete; operator=() = delete

unique_ptr – 推荐 – 右值引用–move

直接把 里面的 左值引用拷贝构造=delete; operator=() = delete

但是, 提供了 右值 引用 拷贝构造 和 右值引用 operator=, 因此 需要移动语义

虽然同样是 所有权, 但是 好处是, 可以清楚地 看出来, 是在转移 资源

int main()
{
    unique_ptr<int>  ptr1(new int);
    unique_ptr<int>  ptr2(move(ptr1)); // 需要移动语义, 好处, 清晰可见

    *ptr2 = 20;
    cout << *ptr1 << endl;  // 也是错误的, 
     return 0;
}

3.带引用计数的智能指针

好处: 多个指针 可以管理同一个资源

给每一个对象资源, 匹配一个引用计数

  1. 实现简单的 引用计数 智能指针----还是比较麻烦的
#include <iostream>

// 引用计数类
class RefCnt {
public:
    RefCnt(void* ptr = nullptr) : ptr_(ptr), count_(1) {}

    // 增加引用计数
    void addRef() {
        ++count_;
    }

    // 减少引用计数,如果计数为 0 则释放资源
    void release() {
        --count_;
        if (count_ == 0) {
            delete static_cast<int*>(ptr_);  // 假设资源是 int 类型
            delete this;  // 释放引用计数对象本身
        }
    }

    int getCount() const {
        return count_;
    }

private:
    void* ptr_;      // 指向资源的指针
    int count_;      // 引用计数
};

// 智能指针类
template<typename T>
class CSmartPtr {
public:
    // 构造函数
    CSmartPtr(T* ptr = nullptr) : mptr(ptr) {
        if (mptr) {
            mpRefCnt = new RefCnt(mptr);
        } else {
            mpRefCnt = nullptr;
        }
    }

    // 析构函数
    ~CSmartPtr() {
        if (mpRefCnt) {
            mpRefCnt->release();
        }
    }

    // 拷贝构造函数
    CSmartPtr(const CSmartPtr<T>& src) : mptr(src.mptr), mpRefCnt(src.mpRefCnt) {
        if (mpRefCnt) {
            mpRefCnt->addRef();
        }
    }

    // 拷贝赋值运算符
    CSmartPtr<T>& operator=(const CSmartPtr<T>& src) {
        if (this != &src) {
            // 释放当前资源
            if (mpRefCnt) {
                mpRefCnt->release();
            }

            // 复制新资源
            mptr = src.mptr;
            mpRefCnt = src.mpRefCnt;

            // 增加引用计数
            if (mpRefCnt) {
                mpRefCnt->addRef();
            }
        }
        return *this;
    }

    // 解引用运算符
    T& operator*() const {
        return *mptr;
    }

    // 箭头运算符
    T* operator->() const {
        return mptr;
    }

    // 获取引用计数
    int use_count() const {
        return mpRefCnt ? mpRefCnt->getCount() : 0;
    }

private:
    T* mptr;            // 指向资源的指针
    RefCnt* mpRefCnt;   // 指向引用计数对象的指针
};

// 测试代码
int main() {
    CSmartPtr<int> ptr1(new int(10));
    std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl;  // 输出 1

    {
        CSmartPtr<int> ptr2 = ptr1;  // 拷贝构造
        std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl;  // 输出 2
        std::cout << "ptr2 use count: " << ptr2.use_count() << std::endl;  // 输出 2
    }  // ptr2 离开作用域,引用计数减 1

    std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl;  // 输出 1

    return 0;
}
  1. 多线程下 是不安全的, 因为 引用计数的修改 不是原子操作

    std::atomic<int> count_;         // 原子引用计数
    
    

4.shared_ptr 交叉(循环)引用问题

使用 .use_count() 获得 指针计数

代码示例

  1. shared_ptr 是强智能指针, weak_ptr 是弱智能指针

    强的 能改变资源的 引用计数, 弱的 不行

  2. 强智能指针, 交叉引用问题?
    造成 new 出来的 资源 无法释放

    #include <iostream>
    #include <memory>
    
    using namespace std;
    
    class B;  // 前向声明
    
    class A {
    public:
        A() { cout << "A()" << endl; }
        ~A() { cout << "~A()" << endl; }
        shared_ptr<B> __ptrb;  // 指向 B 的 shared_ptr
    };
    
    class B {
    public:
        B() { cout << "B()" << endl; }
        ~B() { cout << "~B()" << endl; }
        // 如果 B 也持有 A 的 shared_ptr,会导致循环引用
         shared_ptr<A> __ptra;
    };
    
    int main() {
    
            shared_ptr<A> pa(new A()) ;
            shared_ptr<B> pb(new B());
            pa->__ptrb = pb;  // 出作用域之前, 是2 ,达不到析构
            pb->__ptra = pa;
    
            cout << pa.use_count() << endl;
            cout << pb.use_count() << endl;
    
    
        return 0;
    }
    

整体过程

初始状态(创建对象)

  1. 创建 A 对象
    • shared_ptr<A> pa(new A());
    • A 对象的引用计数为 1(来自 pa)。
    • B 对象尚未创建。
+-------------------+
|       A           |
|-------------------|
| shared_ptr<B> __ptrb | -> nullptr
| ref_count: 1       |
+-------------------+
  1. 创建 B 对象
    • shared_ptr<B> pb(new B());
    • B 对象的引用计数为 1(来自 pb)。
    • A 对象的引用计数仍为 1
+-------------------+          +-------------------+
|       A           |          |       B           |
|-------------------|          |-------------------|
| shared_ptr<B> __ptrb | -> nullptr | shared_ptr<A> __ptra | -> nullptr
| ref_count: 1       |          | ref_count: 1       |
+-------------------+          +-------------------+

步骤 1:A 持有 Bpa->__ptrb = pb

  • A 对象的 __ptrb 指向 B 对象。
  • B 对象的引用计数从 1 变为 2(来自 pbpa->__ptrb)。
  • A 对象的引用计数仍为 1
+-------------------+          +-------------------+
|       A           |          |       B           |
|-------------------|          |-------------------|
| shared_ptr<B> __ptrb | ----> | shared_ptr<A> __ptra | -> nullptr
| ref_count: 1       |          | ref_count: 2       |
+-------------------+          +-------------------+

步骤 2:B 持有 Apb->__ptra = pa

  • B 对象的 __ptra 指向 A 对象。
  • A 对象的引用计数从 1 变为 2(来自 papb->__ptra)。
  • B 对象的引用计数仍为 2
+-------------------+          +-------------------+
|       A           |          |       B           |
|-------------------|          |-------------------|
| shared_ptr<B> __ptrb | ----> | shared_ptr<A> __ptra | 
| ref_count: 2       |          | ref_count: 2       |
+-------------------+          +-------------------+
      ^                             |
      |                             |
      +-----------------------------+

最终状态(循环引用)

  • AB 对象的引用计数均为 2
  • 由于互相持有 shared_ptr,引用计数无法归零,导致内存泄漏。

解决办法–强弱混用

定义对象的地方 使用 强智能指针, 引用对象的 地方 使用 弱智能指针

#include <iostream>
#include <memory>

using namespace std;

class B;  // 前向声明

class A {
public:
    A() { cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }
    weak_ptr<B> __ptrb;  // 修改
};

class B {
public:
    B() { cout << "B()" << endl; }
    ~B() { cout << "~B()" << endl; }
    
     weak_ptr<A> __ptra;  // 修改
};

int main() {

        shared_ptr<A> pa(new A()) ;
        shared_ptr<B> pb(new B());
        pa->__ptrb = pb;  // 出作用域之前, 是2 ,达不到析构
        pb->__ptra = pa;

        cout << pa.use_count() << endl;
        cout << pb.use_count() << endl;
    return 0;
}

弱智能指针 - 不能使用资源

只观察资源, 不能使用资源

需要转换为 std::shared_ptr 才能访问对象

  • 通过 weak_ptr::lock 方法,可以将 std::weak_ptr 转换为 std::shared_ptr,从而安全地访问对象。 shared_ptr<A> ps = __ptra.lock(); .lock()
  • 如果对象已被释放,lock 返回一个空的 std::shared_ptr
#include <iostream>
#include <memory>

using namespace std;

class B;  // 前向声明

class A {
public:
    A() { cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }
    void test() { cout << "test" << endl; }
    weak_ptr<B> __ptrb; 
};

class B {
public:
    B() { cout << "B()" << endl; }
    ~B() { cout << "~B()" << endl; }
    void func()
    {
        //__ptra->test(); // 弱智能指针不能 访问对象
        shared_ptr<A> ps = __ptra.lock();
        ps->test();
    }
     weak_ptr<A> __ptra;
};

int main() {

        shared_ptr<A> pa(new A()) ;
        shared_ptr<B> pb(new B());
        pa->__ptrb = pb;  
        pb->__ptra = pa;
        pb->func();

        cout << pa.use_count() << endl;
        cout << pb.use_count() << endl;


    return 0;
}

5.多线程访问共享对象的线程安全问题

非常著名的 开源网络库 muduo 库

  1. 该源码中对于智能指针的应用非常优秀,其中借助shared_ptr和
    weak_ptr解决了这样一个问题,多线程访问共享对象的线程安全问题,解释如下:

    线程A和线程B访问一个共享的对象,如果线程A正在析构这个对象的时候,线程B又
    要调用该共享对象的成员方法,此时可能线程A已经把对象析构完了,线程B再去访问
    该对象,就会发生不可预期的错误。
    
  2. 代码如下:

    #include <iostream>
    #include <thread>
    using namespace std;
    
    class Test
    {
    public:
    	// 构造Test对象,_ptr指向一块int堆内存,初始值是20
    	Test() :_ptr(new int(20)) 
    	{
    		cout << "Test()" << endl;
    	}
    	// 析构Test对象,释放_ptr指向的堆内存
    	~Test()
    	{
    		delete _ptr;
    		_ptr = nullptr;
    		cout << "~Test()" << endl;
    	}
    	// 该show会在另外一个线程中被执行
    	void show()
    	{
    		cout << *_ptr << endl;
    	}
    private:
    	int *volatile _ptr;
    };
    void threadProc(Test *p)
    {
    	// 睡眠两秒,此时main主线程已经把Test对象给delete析构掉了
    	std::this_thread::sleep_for(std::chrono::seconds(2));
    	/* 
    	此时当前线程访问了main线程已经析构的共享对象,结果未知,隐含bug。
    	此时通过p指针想访问Test对象,需要判断Test对象是否存活,如果Test对象
    	存活,调用show方法没有问题;如果Test对象已经析构,调用show有问题!
    	*/
    	p->show();
    }
    int main()
    {
    	// 在堆上定义共享对象
    	Test *p = new Test();
    	// 使用C++11的线程类,开启一个新线程,并传入共享对象的地址p
    	std::thread t1(threadProc, p);
    	// 在main线程中析构Test共享对象
    	delete p;
    	// 等待子线程运行结束
    	t1.join();
    	return 0;
    }
    

    运行上面的代码,发现在main主线程已经delete析构Test对象以后,子线程threadProc再去访问Test对象的show方法无法打印出*_ptr的值20。可以用shared_ptr和weak_ptr来解决多线程访问共享对象的线程安全问题,上面代码修改如下:

    #include <iostream>
    #include <thread>
    #include <memory>
    using namespace std;
    
    class Test
    {
    public:
    	// 构造Test对象,_ptr指向一块int堆内存,初始值是20
    	Test() :_ptr(new int(20)) 
    	{
    		cout << "Test()" << endl;
    	}
    	// 析构Test对象,释放_ptr指向的堆内存
    	~Test()
    	{
    		delete _ptr;
    		_ptr = nullptr;
    		cout << "~Test()" << endl;
    	}
    	// 该show会在另外一个线程中被执行
    	void show()
    	{
    		cout << *_ptr << endl;
    	}
    private:
    	int *volatile _ptr;
    };
    void threadProc(weak_ptr<Test> pw) // 通过弱智能指针观察强智能指针
    {
    	// 睡眠两秒
    	std::this_thread::sleep_for(std::chrono::seconds(2));
    	/* 
    	如果想访问对象的方法,先通过pw的lock方法进行提升操作,把weak_ptr提升
    	为shared_ptr强智能指针,提升过程中,是通过检测它所观察的强智能指针保存
    	的Test对象的引用计数,来判定Test对象是否存活,ps如果为nullptr,说明Test对象
    	已经析构,不能再访问;如果ps!=nullptr,则可以正常访问Test对象的方法。
    	*/
    	shared_ptr<Test> ps = pw.lock();
    	if (ps != nullptr)
    	{
    		ps->show();
    	}
    }
    int main()
    {
    	// 在堆上定义共享对象 ,  智能释放
    	shared_ptr<Test> p(new Test);
    	// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针
    	std::thread t1(threadProc, weak_ptr<Test>(p));
    	// 在main线程中析构Test共享对象
        
    	// 阻塞等待子线程运行结束
    	t1.join();
    	return 0;
    }
    

    运行上面的代码,show方法可以打印出20,因为main线程调用了t1.join()方法等待子线程结束,此时pw通过lock提升为ps成功,见上面代码示例。

    如果设置t1为分离线程,让main主线程结束,p智能指针析构,进而把Test对象析构,此时show方法已经不会被调用,因为在threadProc方法中,pw提升到ps时,lock方法判定Test对象已经析构,提升失败!main函数代码可以如下修改测试:

    int main()
    {
    	// 在堆上定义共享对象
    	shared_ptr<Test> p(new Test);
    	// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针
    	std::thread t1(threadProc, weak_ptr<Test>(p));
    	// 在main线程中析构Test共享对象
    	// 设置子线程分离 主线程不等待子线程
    	t1.detach();
    	return 0;
    }
    

    该main函数运行后,最终的threadProc中,show方法不会被执行到。以上是在多线程中访问共享对象时,对shared_ptr和weak_ptr的一个典型应用

6.自定义删除器

什么是自定义删除器

我们经常用智能指针管理的资源是堆内存,当智能指针出作用域的时候,在其析构函数中会delete释放堆内存资源,但是除了堆内存资源,智能指针还可以管理其它资源,比如打开的文件,此时对于文件指针的关闭,就不能用delete了,这时我们需要自定义智能指针释放资源的方式,先看看unique_ptr智能指针的析构函数代码,如下:

~unique_ptr() noexcept
{	// destroy the object
if (get() != pointer())
	{
	this->get_deleter()(get()); // 这里获取底层的删除器,进行函数对象的调用
	}
}

从unique_ptr的析构函数可以看到,如果要实现一个自定义的删除器,实际上就是定义一个函数对象而已,示例代码如下:

_EXPORT_STD template <class _Ty, class _Dx /* = default_delete<_Ty> */>
class unique_ptr { // non-copyable pointer to an object
    {}   
    

这是unique的源码, 可以看到, 模板里的第二个参数, 使用 默认的 删除器, 这个是可选的, 因此 自定义删除类的 传入接口 就在这里

#include <iostream>
#include <thread>
#include <memory>
using namespace std;

template<typename T>
class MyDeletor
{
public:
    void operator() (T* ptr) const
    {
        cout << "call MyDeletor.operator()" << endl;
        delete[] ptr;
    }
};

int main()
{
	unique_ptr<int, MyDeletor<int>> ptr1(new int[100]);
	
	return 0;
}

文件的自定义删除器 – 不推荐写类

class FileDeleter
{
public:
	// 删除器负责删除资源的函数
	void operator()(FILE *pf)
	{
		fclose(pf);
	}
};
int main()
{
    // 由于用智能指针管理文件资源,因此传入自定义的删除器类型FileDeleter
	unique_ptr<FILE, FileDeleter> filePtr(fopen("data.txt", "w"));
	return 0;
}

当然这种方式需要定义额外的函数对象类型,不推荐,

function和lambda结合的删除器-----推荐

function下一节会讲到

自定义删除器 一般指出现在 指定语句中, 写类 就显得繁杂了

可以用C++11提供的函数对象function和lambda表达式更好的处理自定义删除器,代码如下:

#include <iostream>
#include <memory>
#include <cstdio>  // 包含 fopen 和 fclose 的头文件
#include <functional>
using namespace std;

int main()
{
    // 自定义智能指针删除器,关闭文件资源
    unique_ptr<FILE, function<void(FILE*)>>
        filePtr(fopen("data.txt", "w"), [](FILE* pf)->void {cout << "call lambda release file" << endl;fclose(pf); });

    // 自定义智能指针删除器,释放数组资源
    unique_ptr<int, function<void(int*)>>
        arrayPtr(new int[100], [](int* ptr)->void {cout << "call lambda release int []" << endl;delete[]ptr; });

    return 0;
}

如果想进一步了解智能指针,可以查看智能指针的源码实现,或者看muduo网络库的源码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值