温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

C++智能指针的示例分析

发布时间:2021-08-17 11:13:18 来源:亿速云 阅读:117 作者:小新 栏目:开发技术

这篇文章将为大家详细讲解有关C++智能指针的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。

    为什么要有智能指针?

    因为普通的指针存在以下几个问题:

    • 资源泄露

    • 野指针

      •  未初始化

      • 多个指针指向同一块内存,某个指针将内存释放,别的指针不知道

    • 异常安全问题

    • 如果在 malloc和free 或者 new和delete 之间如果存在抛异常,那么也会导致内存泄漏。

    资源泄漏示例代码:

    int main(){
    	int *p = new int;
    	*p = 1;
    	p = new int; // 未释放之前申请的资源,导致内存泄漏 
    	delete p;
    	
    	return 0;
    }

    野指针示例代码:

    int main(){
    	int *p1 = new int;
    	int *p2 = p1;
    	delete p1; 
    	*p2 = 1; // 申请的内存已经被释放掉了,  
    	
    	return 0;
    }
    int main(){
    	int *p;
    	*p = 1; // 程序直接报错, 使用了未初始化的变量
    	return 0;
    }

    解决方法:智能指针

    智能指针的使用及原理

    • 具有RALL 特性

    • 重载了 operator* 和 operator ->,使其具有了指针一样的行为

    RALL

    RALL(Resource Acquistion Is Initialization)是一种利用对象生命周期来控制程序资源(如内存,文件句柄,网络连接,互斥量等)的简单技术。

    在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。相当于利用 对象 管理了一份资源。这样的优势在于

    1.不需要显式的释放资源(对象析构时,自动释放资源)

    2.采用这种方式,对象所需的资源在其生命周期内始终保持有效。

    智能指针就是一个实例出来的对象

    C++98版本的库中就提供了auto_ptr的智能指针。但是 auto_ptr存在当对象拷贝或者赋值之后,前面的对象就悬空了。

    C++11 提供更靠谱的并且支持拷贝的 shared_ptr

    shared_ptr :

    通过引用计数的方式实现多个shared_ptr 对象之间共享资源。

    shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了

    unique_ptr :

    确保一个对象同一时刻只能被一个智能指针引用,可以转移所有权(可以从一个智能指针转移到另一个智能指针)

    auto_ptr :

    C++11 已弃用, 与unique_ptr 类似

    使用时,需包含头文件

     #include <memory>

    shared_ptr的使用注意事项

    创建
    1. 
    shared_ptr<int> ptr{new int(3)};
    2.
    shared_ptr<int> ptr;
    ptr.reset(new int(3));
    3.
    shared_ptr<int> ptr = make_shared<int>(3);

    shared_ptr 支持使用比较运算符,使用时,会调用共享指针内部封装的原始指针的比较运算符。

    支持

    ==、!=、<、<=、>、>=

    使用 比较运算符 的前提 必须是 同类型

    示例:

    shared_ptr<int> p1 = make_shared<int>(1);
    shared_ptr<int> p2 = make_shared<int>(2);
    shared_ptr<int> p3;
    shared_ptr<double> p4 = make_shared<double>(1);
    
    bool b1 = p1 < p2; 		// true
    bool b2 = p1 > p3;		// true, 非NULL 指针与 NULL 指针相比 ,都是大于
    bool b3 = p3 == p3;		// true
    bool b4 = p4 < p2		// 编译失败,类型不一致

    shared_ptr 可以使用强制类型转换,但是不能使用普通的强制类型转换符

    1.shared_ptr 强制类型转换符 允许将其中包含的指针强制转换为其它类型

    2.不能使用普通的强制类型转换运算符,否则会导致未定义行为

    3.shared_ptr 的强制类型转换运算符包括
    static_pointer_cast
    dynamic_pointer_cast
    const_pointer_cast

    示例:

    shared_ptr<void> p(new int);	// 内部保留 void* 指针
    static_pointer_cast<int*>(p);	// 正确的 强制类型转换方式
    shared_ptr<int> p1(static_cast<int*>(p.get()));	// 错误的强制类型转换方式,未定义错误
    多个 shared_ptr 不能拥有同一个对象

    利用代码理解

    示例:

    class Mytest{
    public:
    	Mytest(const string& str)
    	:_str(str){}
    	~Mytest(){
    		std::cout << _str << "destory" << std::endl;
    	}
    private:
    	string _str;
    };
    
    int main(){
    	Mytest* p = new Mytest("shared_test");
    	shared_ptr<Mytest> p1(p); 	// 该对象可以正常析构
    	shared_ptr<Mytest> p2(p); // 对象销毁时,错误,读取位置 0xDDDDDDDD 时发生访问冲突。
    	return 0;
    }

    上述代码, 共享指针 p1 对象在程序 结束时,调用析构,释放了p 所指向的空间, 当 p2 进行析构的时候,又释放p所指向的空间, 但是由于已经释放过了, 重复释放已经释放过的内存,导致段错误。

    可以使用 shared_from_this 避免这种问题

    改进代码:

    class Mytest:public enable_shared_from_this<Mytest> {
    public:
        Mytest(const string& str)
            :_str(str) {}
        ~Mytest() {
            std::cout << _str << "destory" << std::endl;
        }
        shared_ptr<Mytest> GetSharedptr() {
            return shared_from_this();
        }
    private:
        string _str;
    };
    int main() {
        Mytest* p = new Mytest("shared_test");
        shared_ptr<Mytest> p1(p);
        shared_ptr<Mytest> p2 = p->GetSharedptr(); // 正确做法
        
        return 0;
    }
    shared_ptr 的销毁

    shared_ptr 在初始化的时候,可以定义删除器,删除器可以定义为 普通函数、匿名函数、函数指针等符合要求的可调用对象

    示例代码:

    void delFun(string* p) {
        std::cout << "Fun delete " << *p << endl;
        delete p;
    }
    int main() {
    
        std::cout << "begin" << std::endl;
        shared_ptr<string> p1;
        {
            shared_ptr<string> p2(new string("p1"), [](string* p) {
                std::cout << "Lamda delete " << *p << std::endl;
                delete p;
            });
            p1 = p2;
            shared_ptr<string> p3(new string("p3"), delFun);
        }
        std::cout << "end" << std::endl;
        return 0;
    }

    执行结果:

    begin
    Fun delete p3
    end
    Lamda deletep1

    分析结果:

    首先 ,p3在{ }作用域内 ,生命周期最先结束,调用delFun作为删除器

    其次,p2 也在{ } 作用域内,生命周期也结束了,但是因为 p1 和 p2 指向了同一个对象,所以p2 销毁只是将其 对象 引用计数 -1。

    最后,程序运行结束,p1销毁,其对象引用计数-1 变为0,调用 删除器,销毁对象。

    shared_ptr<char> p(new char[10]); // 编译能够通过,但是会造成资源泄漏
    // 正确做法
    shared_ptr<char> p(new char[10], [](char* p){
    	delete p[];
    	});
    // 正确做法
    shared_ptr<char> p(new char[10], default_delete<char[]>());
    • 可以为数组创建一个shared_ptr ,但是这样会造成资源泄露。因为 shared_ptr 提供默认的删除调用的是 delete,而不是 delete[]

    • 可以使用自定义删除器,删除器中使用 delete[]

    • 可以使用 default_delete 作为删除器,因为它使用 delete[]

    shared_ptr 存在的问题:

    1.循环引用
    不同对象相互引用,形成环路

    2.想要共享但是不想拥有对象

    shared_ptr 的线程安全问题

    1. shared_ptr 对象中引用计数是多个shared_ptr对象共享的,两个线程中shared_ptr的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2 这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的。

    2.shared_ptr 管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

    // 1.因为线程安全问题是偶现性问题,main函数的n改大一些概率就变大了,就
    容易出现了。
    void SharePtrFunc(shared_ptr<Date>& sp, size_t n)
    {
    	cout << sp.Get() << endl;
    	for (size_t i = 0; i < n; ++i)
    	{
    		// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
    		shared_ptr<Date> copy(sp);
    		// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
    		copy->_year++;
    		copy->_month++;
    		copy->_day++;
    	}
    }
    int main()
    {
    	shared_ptr<Date> p(new Date);
    	cout << p.Get() << endl;
    	const size_t n = 100;
    	thread t1(SharePtrFunc, p, n);
    	thread t2(SharePtrFunc, p, n);
    	t1.join();
    	t2.join();
    	cout << p->_year << endl;
    	cout << p->_month << endl;
    	cout << p->_day << endl;
    	return 0;
    }
    shared_ptr 的循环引用
    struct ListNode
    {
    int _data;
    shared_ptr<ListNode> _prev;
    shared_ptr<ListNode> _next;
    ~ListNode(){ cout << "~ListNode()" << endl; }
    };
    int main()
    {
    shared_ptr<ListNode> node1(new ListNode);
    shared_ptr<ListNode> node2(new ListNode);
    cout << node1.use_count() << endl;
    cout << node2.use_count() << endl;
    node1->_next = node2;
    node2->_prev = node1;
    cout << node1.use_count() << endl;
    cout << node2.use_count() << endl;
    return 0;
    }

    循环引用代码分析:

    node1和node2两个智能指针对象指向两个节点,引用计数变成1,不需要手动delete。

    node1的_next指向node2,node2的_prev指向node1,引用计数变成2。

    node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。

    也就是说_next析构了,node2就释放了。

    也就是说_prev析构了,node1就释放了。

    但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。

    C++智能指针的示例分析

    解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了

    原理:

    node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加

    node1和node2的引用计数。

    struct ListNode
    {
    	int _data;
    	weak_ptr<ListNode> _prev;
    	weak_ptr<ListNode> _next;
    	~ListNode(){ cout << "~ListNode()" << endl; }
    };
    int main()
    {
    	shared_ptr<ListNode> node1(new ListNode);
    	shared_ptr<ListNode> node2(new ListNode);
    	cout << node1.use_count() << endl;
    	cout << node2.use_count() << endl;
    	node1->_next = node2;
    	node2->_prev = node1;
    	cout << node1.use_count() << endl;
    	cout << node2.use_count() << endl;
    	return 0;
    }

    unique_ptr

    • 同一个对象,只能有唯一的一个 unique_ptr 指向它

    • 继承了自动指针 auto_ptr,

    • 有助于避免发生异常时导致的资源泄漏

    unique_ptr的使用

    unique_ptr 定义了*、-> 运算符,没有定义 ++ 之类的指针算法

    unique_ptr 不允许使用赋值语法进行初始化,必须使用普通指针直接初始化

    unique_ptr 可以为 空

    unique_ptr 不能使用普通的复制语义赋值, 可以使用 C++11 的 move() 函数

    unique_ptr 获得新对象时,会销毁之前的对象

    unique_ptr 防止拷贝的原理:

    // C++98防拷贝的方式:只声明不实现+声明成私有
    UniquePtr(UniquePtr<T> const &);
    UniquePtr & operator=(UniquePtr<T> const &);
    // C++11防拷贝的方式:delete
    UniquePtr(UniquePtr<T> const &) = delete;
    UniquePtr & operator=(UniquePtr<T> const &) = delete;

    关于“C++智能指针的示例分析”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,使各位可以学到更多知识,如果觉得文章不错,请把它分享出去让更多的人看到。

    向AI问一下细节

    免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

    c++
    AI