从深拷贝、浅拷贝到引用计数和循环引用,再到强指针和弱指针的实现和分析

深拷贝和浅拷贝

对内存资源使用的讨论,必然要先从深拷贝浅拷贝说起。
深拷贝的优点:每一个的对象(哪怕是通过拷贝构造函数实例化的对象)的指针都有指向的内存空间,而不是共享,所以在对象析构的时候就不存在重复释放或内存泄露的问题了。
深拷贝的缺点:内存开销大。
浅拷贝优点:通过拷贝构造函数实例化的对象的指针数据变量指向的共享的内存空间,因此内存开销较小。
浅拷贝缺点:对象析构的时候就可能会重复释放或造成内存泄露。

鉴于深拷贝和浅拷贝的优缺点,可采用引用计数技术,既减小了内存开销,又避免了堆的重复释放或内存泄露问题。
浅拷贝实例:

#include <iostream>

using std::string;
using std::cout;
using std::endl;

class Student
{
public:
    Student(string* name)
    {
        m_name = name;
    }

    Student(Student& obj)
    {
        m_name = obj.m_name;
    }

    ~Student()
    {
        if (m_name != nullptr)
        {
            delete m_name;
            m_name = nullptr;
        }
    }

    Student& operator=(Student& obj)
    {
        m_name = obj.m_name;
        return *this;
    }

    void Show()
    {
        cout << "name: " << *m_name << endl;
    }

private:
    string* m_name;
};

void test01()
{
    Student stu1 = new string("zhang san");
    Student stu2 = new string("li si");
    Student stu3 = stu2;

    stu1.Show();                //name: zhang san
    stu2.Show();                //name: li si
    stu3.Show();                //name: li si
}

int main(int argc, char const *argv[])
{
    test01();                   //stu2和stu3析构的时候会重复释放

    return 0;
}

浅拷贝虽然节省了内存开销,但是有一个致命的问题:一旦其中一个对象释放了资源,那么所有的其他对象的资源也被释放了。
解决办法:增加一个变量,记录资源使用次数。

引用计数

我们把刚才的实例改写下:

#include <iostream>

using std::string;
using std::cout;
using std::endl;

class Student
{
public:
    Student(string* name)
    {
        m_name = name;
        m_count = new int(1);
    }

    Student(Student& obj)
    {
        m_name = obj.m_name;
        m_count = obj.m_count;
        ++(*m_count);
    }

    ~Student()
    {
        if (--(*m_count) == 0)
        {
            //最后一个使用的对象
            delete m_name;
            m_name = nullptr;
            delete m_count;
            m_count = nullptr;
        }
    }

    Student& operator=(Student& obj)
    {
        if (obj.m_name == m_name)
        {
            return *this;
        }

        if (--(*m_count) == 0)
        {
            delete m_name;
            m_name = nullptr;
            delete m_count;
            m_count = nullptr;
        }

        m_name = obj.m_name;
        m_count = obj.m_count;
        ++(*m_count);
        return *this;
    }

    void Show()
    {
        cout << "name: " << *m_name << endl;
        cout << "count: " << *m_count << endl;
    }

private:
    string* m_name;
    int* m_count;
};

void test01()
{
    Student stu1 = new string("zhang san");
    stu1.Show();                    //name: zhang san   count: 1
    Student stu2 = new string("li si");  
    stu2.Show();                    //name: li si       count: 1
    Student stu3 = stu2;
    stu3.Show();                    //name: li si       count: 2
}

int main(int argc, char const *argv[])
{
    //引用计数方式不会重复释放,用户使用的时候不需要关心和手动释放
    test01();                       

    return 0;
}

我们将该引用计数做一个简易的封装,把引用计数作为一个新的类来使用。

#include <iostream>

using std::string;
using std::cout;
using std::endl;

class RefValue
{
public:
    RefValue(string* name)
    {
        m_name = name;
        m_count = 1;
    }

    void Release()
    {
        if (--m_count == 0)
        {
            delete this;
        }
    }

    ~RefValue()
    {
        if (m_name != nullptr)
        {
            delete m_name;
            m_name = nullptr;
        }
    }

    void AddRef()
    {
        ++m_count;
    }

    void Show()
    {
        cout << "name: " << *m_name << endl;
        cout << "count: " << m_count << endl;
    }

private:
    string* m_name;
    int m_count;
};

class Student
{
public:
    Student(string* name)
    {
        m_pValue = new RefValue(name);
    }

    Student(Student& obj)
    {
        m_pValue = obj.m_pValue;
        m_pValue->AddRef();
    }

    void Release()
    {
        m_pValue->Release();
    }

    ~Student()
    {
        Release();
    }

    Student& operator=(Student& obj)
    {
        if (obj.m_pValue == m_pValue)
        {
            return *this;
        }

        m_pValue->Release();
        m_pValue = obj.m_pValue;
        m_pValue->AddRef();
        return *this;
    }

    void Show()
    {
        m_pValue->Show();
    }
    
private:
    RefValue* m_pValue;
};

void test01()
{
    Student stu1 = new string("zhang san");
    stu1.Show();                    //name: zhang san   count: 1
    Student stu2 = new string("li si");  
    stu2.Show();                    //name: li si       count: 1
    Student stu3 = stu2;
    stu3.Show();                    //name: li si       count: 2
}

int main(int argc, char const *argv[])
{
    test01();

    return 0;
}

上面的做法能在一定程度上解决资源多次重复申请的浪费,但是仍然存在两个核心的问题。
1.如果对其中某一个类对象中的资源进行了修改,那么所有引用该资源的对象全部会被修改,这显然是错误的。
2.当前的计数器作用于Student类,在使用时候,需要强行加上引用计数类,这样复用性不好,使用不方便。

写时拷贝

为了解决上述问题1,我们引入写时拷贝的思想。在使用引用计数时,当发生共享资源值改变的时候,需要对其资源进行重新的拷贝,这样改变的时拷贝的值,而不影响原有的对象中的共享资源。

#include <iostream>

using std::string;
using std::cout;
using std::endl;

class RefValue
{
public:
    RefValue(string* name)
    {
        m_name = name;
        m_count = 1;
    }

    void Release()
    {
        if (--m_count == 0)
        {
            delete this;
        }
    }

    ~RefValue()
    {
        if (m_name != nullptr)
        {
            delete m_name;
            m_name = nullptr;
        }
    }

    void AddRef()
    {
        ++m_count;
    }

    void Show()
    {
        cout << "name: " << *m_name << endl;
        cout << "count: " << m_count << endl;
    }

private:
    string* m_name;
    int m_count;
};

class Student
{
public:
    Student(string* name)
    {
        m_pValue = new RefValue(name);
    }

    Student(Student& obj)
    {
        m_pValue = obj.m_pValue;
        m_pValue->AddRef();
    }

    ~Student()
    {
        m_pValue->Release();
    }

    void SetName(string* name)
    {
        m_pValue->Release();
        m_pValue = new RefValue(name);
    }

    Student& operator=(Student& obj)
    {
        if (obj.m_pValue == m_pValue)
        {
            return *this;
        }

        m_pValue->Release();
        m_pValue = obj.m_pValue;
        m_pValue->AddRef();
        return *this;
    }

    void Show()
    {
        m_pValue->Show();
    }
    
private:
    RefValue* m_pValue;
};

void test01()
{
    Student stu1 = new string("zhang san");
    stu1.Show();                                    //name: zhang san   count: 1
    Student stu2 = new string("li si");  
    stu2.Show();                                    //name: li si       count: 1
    Student stu3 = stu2;
    stu3.Show();                                    //name: li si       count: 2
    stu2.SetName(new string("li si2"));
    stu2.Show();                                    //name: li si2      count: 1
    stu3.Show();                                    //name: li si       count: 1
}

int main(int argc, char const *argv[])
{
    test01();

    return 0;
}   

简易版智能指针

我们可以利用模板技术来解决上述第二个问题,使我们自己定义的智能指针更加通用。

#include <iostream>

using std::string;
using std::cout;
using std::endl;

template <class T>
class RefValue
{
public:
    RefValue(T* name)
    {
        m_name = name;
        m_count = 1;
    }

    void Release()
    {
        if (--m_count == 0)
        {
            delete this;
        }
    }

    ~RefValue()
    {
        if (m_name != nullptr)
        {
            delete m_name;
            m_name = nullptr;
        }
    }

    void AddRef()
    {
        ++m_count;
    }

    T* m_name;

private:
    int m_count;
};

template <class T>
class SmartPtr
{
public:
    SmartPtr()
    {
        m_pValue = nullptr;
    }

    SmartPtr(T* name)
    {
        m_pValue = new RefValue<T>(name);
    }

    SmartPtr(SmartPtr& obj)
    {
        m_pValue = obj.m_pValue;
        m_pValue->AddRef();
    }

    ~SmartPtr()
    {
        m_pValue->Release();
    }

    SmartPtr& operator=(SmartPtr& obj)
    {
        if (obj.m_pValue == m_pValue)
        {
            return *this;
        }

        if (m_pValue != nullptr)
        {
            m_pValue->Release();
        }
        m_pValue = obj.m_pValue;
        m_pValue->AddRef();
        return *this;
    }

    T* operator->()
    {
        return m_pValue->m_name;
    }
    
private:
    RefValue<T>* m_pValue;
};

class Student
{
public:
    Student() = default;
    Student(string name, int age) : m_name(name), m_age(age)
    {}

    Student(Student& stu)
    {
        m_name = stu.m_name;
        m_age = stu.m_age;
    }

    void Show()
    {
        cout << "name: " << m_name << "\tage:" << m_age << endl;
    }

    ~Student()
    {}

private:
    string m_name;
    int m_age;
};


void test01()
{
    SmartPtr<Student> p1 = new Student("zhang san", 20);
    p1->Show();                                             //name: zhang san	age:20
    SmartPtr<Student> p2 = new Student("li si", 18);
    p2->Show();                                             //name: li si	age:18
    SmartPtr<Student> p3 = p2;
    p3->Show();                                             //name: li si	age:18
   
    {
        SmartPtr<Student> p4;
        p4 = p1;
        p4 = p2;
    }
    p1->Show();                                             //name: zhang san	age:20
    p2->Show();                                            //name: li si	age:18
    p3->Show();                                            //name: li si	age:18
}

int main(int argc, char const *argv[])
{
    test01();

    return 0;
}

至此,我们已经实现了一个简易版的智能指针。通过以上的学习对C++中std::shared_ptr的实现和使用有了一定的认知,但是增加了引用计数后就没有别的问题了吗?

循环引用

我们看下面这段代码:

#include <iostream>
#include <memory>

using std::cout;
using std::endl;

class Son;
class Parent;

class Son
{
public:
    Son()
    {}

    ~Son()
    {}

    void Set(std::shared_ptr<Parent> parent)
    {
        m_pParent = parent;
    }

    std::shared_ptr<Parent> m_pParent;
};

class Parent
{
public:
    Parent()
    {}

    ~Parent()
    {}

    void Set(std::shared_ptr<Son> pSon)
    {
        m_pSon = pSon;
    }

    std::shared_ptr<Son> m_pSon;
};

void testShared()
{
    Son *pSon = new Son();
    Parent *pParent = new Parent();
    {
        std::shared_ptr<Parent> shared_Parent(pParent);
        std::shared_ptr<Son> shared_Son(pSon);

        shared_Parent->Set(shared_Son);
        shared_Son->Set(shared_Parent);

        cout << "shared_Parent.use_count: " << shared_Parent.use_count() << endl;           //shared_Parent.use_count: 2
        cout << "shared_Son.use_count: " << shared_Son.use_count() << endl;                 //shared_Son.use_count: 2
    }
    //通过gdb调试,在这个位置可以看到shared_Parent.use_count和shared_Son.use_count均为1,两个对象均未被销毁
    /*最后两者的引用计数均为1,原因是出了块作用域之后,两个shared_parent和shared_son均会析构,
    在这两个智能指针的内部,均会先去判断对应的内部指针是否-1是否为0,显然这里相互引用的情况下,
    引用计数初值为2,减1后值为1,所以两个指针均不会被释放。这里,其实只需要一个释放了,
    另外一个也能跟着释放。*/
}

int main(int argc, char const *argv[])
{

    testShared();

    return 0;
}

放上我用gdb调试的过程

[root@root-centos8 c++11,14,17]$ gdb ./20_循环引用 
GNU gdb (GDB) Red Hat Enterprise Linux 8.2-6.el8
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./20_循环引用...done.
(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 2 at 0x400dde: file 20_循环引用.cpp, line 62.
Starting program: /home/wyl/work/c++11,14,17/20_循环引用 
warning: Loadable section ".note.gnu.property" outside of ELF segments
warning: Loadable section ".note.gnu.property" outside of ELF segments

Temporary breakpoint 2, main (argc=1, argv=0x7fffffffe0a8)
    at 20_循环引用.cpp:62
62	    testShared();
(gdb) s
testShared () at 20_循环引用.cpp:46
46	    Son *pSon = new Son();
(gdb) n
47	    Parent *pParent = new Parent();
(gdb) 
49	        std::shared_ptr<Parent> shared_Parent(pParent);
(gdb) 
50	        std::shared_ptr<Son> shared_Son(pSon);
(gdb) 
52	        shared_Parent->Set(shared_Son);
(gdb) 
53	        shared_Son->Set(shared_Parent);
(gdb) 
55	        cout << "shared_Parent.use_count: " << shared_Parent.use_count() << endl;           //shared_Parent.use_count: 2
(gdb) whatis shared_Parent
type = std::shared_ptr<Parent>
(gdb) whatis shared_Son
type = std::shared_ptr<Son>
(gdb) p shared_Parent
$5 = std::shared_ptr<Parent> (use count 2, weak count 0) = {get() = 0x615e90}
(gdb) p shared_Son
$6 = std::shared_ptr<Son> (use count 2, weak count 0) = {get() = 0x615e70}
(gdb) n
shared_Parent.use_count: 2
56	        cout << "shared_Son.use_count: " << shared_Son.use_count() << endl;                 //shared_Son.use_count: 2
(gdb) 
shared_Son.use_count: 2
50	        std::shared_ptr<Son> shared_Son(pSon);
(gdb) 
49	        std::shared_ptr<Parent> shared_Parent(pParent);
(gdb) 
58	}
(gdb) p *(Parent*)0x615e90
$7 = {m_pSon = std::shared_ptr<Son> (use count 1, weak count 0) = {
    get() = 0x615e70}}
(gdb) p *(Son*)0x615e70
$8 = {m_pParent = std::shared_ptr<Parent> (use count 1, weak count 0) = {
    get() = 0x615e90}}

可以看到,代码最后有内存泄漏的问题。
相互引用
实际上,如上图所示,a,b 内部的pointer 同时又引用了a,b,这使得a,b 的引用计数均变为了2,而离开作用域时,a,b 智能指针被析构,却只能造成这块区域的引用计数减一,这样就导致了a,b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露。
在这里插入图片描述
解决办法也很简单,就是采用弱引用。弱引用不会引起引用计数增加,当换用弱引用时候,最终的释放流程如上图。

更多C++新特性

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值