C++智能指针

1. 前言

    这依然是面试的一大考点。(好吧,怎么又是面试呢?我说就不能说点对技术有用的东西吗?)

    不能,开玩笑的,其实智能指针真的很方便了C++开发人员。

    好吧,说到智能指针,还是先从c++常见的内存问题说起吧。    


2. 内存问题

    (1) 缓冲区溢出问题

    说的简单一点,就是你申明了一个64长度的数组,却要访问并修改第65位,虽然有时候是不会出现问题,但是实际上,这个地址根本不知道里面放着什么东西,可能是函数的返回值?可能是某些重要的数据?可能是下面需要用到的数组?总之,这种情况造成的后果是不可估量的。

    (2) 空悬指针/野指针

    空悬指针/野指针是什么东西呢?举个简单的例子,比如有两个指针,都指向堆上相同的一个对象Object,然后这个两个指针分别位于不同的线程中,这样的话第一个线程肯定不知道第二个线程做了什么?第二个线程也不知道第一个线程有没有对该堆上数据做什么修改?

    假如现在p1释放了这个堆上的内存,那么p2就成了野指针/空悬指针,因为它所指向的东西是不存在的。样例图如下:


p1,p2同时指向堆中Object对象


p1释放了资源,p2就成了空悬指针/野指针

    这就是空悬指针/野指针

    (3) 重复释放

    这个问题和上面那个很像,只是当p2指向的东西被p1释放的时候,p2再去释放一次。

    (4) 内存泄漏

    这个问题是最常见的问题,当然,也不怎么好解决。但并不是那么严重的问题,毕竟比起缓冲区溢出,重复释放这种会导致系统奔溃的问题而言,内存泄漏其实并不算大问题,但也是一个常见的问题,毕竟随着使用的时间的增加,内存的增加也成线性,一定时间时候服务器的内存就会被占满,这不符合使用服务器的使用。

    下面举个比较常见的例子:

char* IntToString(int n)
{
    char *ch = new char[33];
    int flag = 1,num = 0;

    if(n < 0 )
    {
        flag = 0;
        num++;
        n = -n;
    }

    while(n)
    {
        ch[num++] = n%10 + '0';
        n = n/10;
    }

    for(int i=0;i

    上面的代码是一个整数变成字符串的例子,很容易理解,乍一看,觉得好像并不存在BUG,这和内存泄露有什么关系,但是仔细想一想,如果有那个客户端的小伙伴不小心使用了以下的代码:    

cout << IntToString(100) << endl;

    是不是就出事了,咦,创建的ch指针里面的内存去哪了?没错,这个时候无论怎么做,都没法释放你之前申请的内存。(这里可以使用string模板类来解决或者使用智能指针了来解决。)    

    (5) 不匹配的new[] / delete

    这个问题,感觉没什么意义!在《Linux多线程服务器编程 使用moduo C++网络库》中说道,尽量不要自己使用delete,delete应该是交给资源管理者而不是申请资源的人。当然,也不是一定不能用。那会出现什么问题呢?我先简单说一下,就是new[]的时候你分配的是一个数组,这个数组包含了数组的大小以及数组里面的元素空间,使用delete的时候,delete执行的步骤是释放掉数组的大小以及数组的第一个元素,如果你数组原来存放的是类似String这样的东西,除了第一个,后面的析构函数是不会调用的,也就是你申请的内存统统都不会回收,这实际上也是一种内存泄漏的问题。

    (6) 内存碎片

    这是个操作系统的问题,绝不是C++的问题,当然,任何一种语言都需要在内存中申请内存,这种问题实际上并不是我可以避免的。举个例子吧(假设内存是采用分页存储的):

内存的存储情况

编号

进程

1

A

2

A

3

A

4

-

    原本内存是这样存储的,当然实际的内存不可能这么小。A占据了内存的上个页,假设这个时候有一个B进程进来,申请3个内存单元,由于只剩下一个,不得不换出A进程,但是第4个页框一直没被使用,这个就是内存碎片。


3. 内存问题的解决方案

    (1) 缓冲区溢出

    这个一定要记得缓冲区的长度,仔细检查代码,保证不让自己的程序有一点访问到越出长度的结点。当然,这个是不实际的。(你要是看过那种3个循环嵌套的执行,找个BUG都需要理解思路的时候你会觉得很绝望的,什么哪里溢出啦?什么鬼哟?)这时候可以使用std::vector或者std::string或者自己编写Buffer Class来管理缓冲区,自动记住缓冲区的长度,并使用成员函数而不是裸指针来修改缓冲区。

    (2) 空悬指针和野指针

    这种就可以使用智能指针来解决了。等下再来解释一下怎么解决。

    (3) 重复释放

    这种呢?你可能会觉得怎么可能出现这个问题?是吧,但其实是会出现的,因为在多线程中,你永远不知道你的程序是怎么运行的,比如进程A释放了一个对象a,而B又刚好在使用完对象a之后接着释放,这就导致两次释放了a对象。这种问题也可以通过智能指针(scoped_ptr)(说明一下,这个库我并没有,所以先不对这个指针做太多的介绍)来解决,不需要自己delete,而是交给系统来处理什么时候需要delete。

    (4) 内存泄露

    同(3),都可以使用scoped_ptr,在对象析构的时候自动释放内存。

    (5) 不匹配的new[]/delete

    这个也可以使用智能指针,因为你都不需要自己delete,你还怕会遇到这种问题?

    (6) 内存碎片

    这个是真的没有办法,只能在操作系统层进行优化,而不能在C++代码中实现优化。

    

    有没有觉得智能指针会有用了?6个问题里面4个可以使用智能指针解决。


4. 智能指针的类型

    (1) auto_ptr:应该算智能指针的原型吧。(其实就是觉得C++越来越往Java垃圾回收机制那方面发展了)该智能指针是所有权占有,一旦所有权被分配给另一个,该智能指针也就失去了意义。(现在已经被遗弃了。)

auto_ptr a = auto_ptr(new int(5));

    (2) unique_ptr:auto_ptr的进化版本,这个智能指针只能允许对象所有权为一个对象所有,也就是一个智能指针不允许复制给另一个智能指针,当然,智能指针可以将一个临时右值赋值给它。

unique_ptr a = unique_ptr(new int(5));

    (3) shared_ptr:这个就不是所有权智能指针了,而是计数智能指针,什么意思呢?每一个所只有该指针的智能指针,都会在它的计数器上+1,直到计数器为0的时候才释放对象。

shared_ptr a = shared_ptr(new int(5));

    (4) weak_ptr:为了解决shared_ptr存在的问题而出来的。其实也不能说是智能指针,这东西连个*都没有,说真的,如果是指针,至少需要读取该内存里面的东西吧。

shared_ptr a = shared_ptr(new int(5));
weak_ptr b = a;

    注意,weak_ptr只能指向shared_ptr的指针,不能自己创建一个,它本身也是联系与shared_ptr的。

    此外,智能指针实际上也是指针,你平时怎么使用指针,就怎么使用智能指针,不同的地方在于这个指针能帮你管理内存。


5. 智能指针解决4种的问题

    (1) 空指针/野指针    

class C
{
public:
    void insert(int num)
    {
        int *k = new int(num);
        a.push_back(k);
    }

    void del(int id)
    {
        int *point = a[id];

        vector::iterator it = a.begin();
        for(int i=0;i::iterator it = a.begin();

        for(;it != a.end();it++)
            cout << *(*it) << endl;   // 可能输出的是野指针里面的内容
    }

private:
   vector a;
};

    说真的,我现在想不出还有什么可能导致这种情况出现的。实际上就是释放和申请不同步,也就是不在同一个代码块里的问题。这样,释放资源的行为永远存在不确定的因素,它被多少个线程使用在每一个线程中是不知道的,它们可能修改,读取,或者删除,这就使得释放问题难上加难。有人说加锁,可以啊!但是如果释放资源的函数先获得锁的话结果是不是一样?后面需要用到资源的依然会成为野指针。有人说那就让释放资源最后调用,但是这样有可能在途中某个函数就已经将该指针的地址指向其他的指针,又会导致内存泄露,而且也会引起重复释放的问题。

    改用智能指针:

class D
{
public:
    int num;
    D(int _num)
    {
        num = _num;
        cout << 1 << endl;
    }
    ~D()
    {
        cout << 2 << endl;
    }
};

class C
{
public:
    void insert(int num)
    {
        D *d = new D(num);
        shared_ptr k = shared_ptr(d);
        a.push_back(k);

        mut = PTHREAD_MUTEX_INITIALIZER;
    }

    void del(int id)
    {
        pthread_mutex_lock(&mut);
        vector >::iterator it = a.begin();
        for(int i=0;i >::iterator it = a.begin();

        for(;it != a.end();it++)
            cout << (*it)->num << endl;

        pthread_mutex_unlock(&mut);
    }

private:
    pthread_mutex_t mut;
    vector > a;
};

    D类用于测试,当然,问题虽然得到了缓解,毕竟现在已经没有野指针/空悬指针了。但是却存在另一个问题,程序有可能不是删除,而是将给智能指针赋值给一个对象的内部shared_ptr,这样导致的循环引用,shared_ptr是没办法解决的。但是却可以解决之前说的那两个问题,无论vector里面的指针怎么指,shared_ptr都可以将其释放,而不会导致什么内存泄露或者重复释放什么的。

    (2) 重复释放

    说真的,你都不需要自己delete了,也就不怕会重复释放了,当然,你要是特意要这么写,我也拿你没办法的。

int *a = new int(10);
shared_ptr sa(a);
delete a;

    (3) 内存泄露

    注意,使用shared_ptr还是会内存泄露的,比如下面这个代码:

class A
{
public:
    shared_ptr b;
    A()
    {
        cout << "1" << endl;
    }
    ~A()
    {
        cout << "2" << endl;
    }
};

class B
{
public:
    shared_ptr a;
    B()
    {
        cout << "3" << endl;
    }
    ~B()
    {
        cout << "4" << endl;
    }
};

void f()
{
    A *a = new A();
    B *b = new B();

    shared_ptr aa = shared_ptr(a);
    shared_ptr bb = shared_ptr(b);
    a->b = bb;
    b->a = aa;
}

    调用函数f的话,A,B申请的内存是永远不会释放的,因为函数的智能指针管理函数给予它的变量,而管不了对象内部的类。这个时候A,B类不能释放内存而导致内存泄露。这实际上也是python内存模型存在的问题。

    解决要不就在结尾加上:

a->b = NULL;
b->a = NULL;

    要不就改用weak_ptr。

    (4) 不匹配的new[]/delete

    这,你都不用delete了,也就不会导致这种问题的发生。


6. 智能指针的优缺点

    (1) auto_ptr:

    这东西都被摒弃了,算了,还是总结一下吧。看下面代码:

auto_ptr file[5] =
{
    auto_ptr(new string("123")),
    auto_ptr(new string("456")),
    auto_ptr(new string("789")),
    auto_ptr(new string("101112")),
    auto_ptr(new string("131415")),
};
auto_ptr mfile = file[3];
for(auto aptr:file)
    cout << *aptr << endl;

    想想运行结果是什么?没错,运行结果就是下面这样:


    咦?之后那几个呢?其实并不是没有,而是在file[3]的时候已经将所有权交出去,导致file[3]根本没有东西可言。

    (2) shared_ptr:

    将上面的代码替换这个就可一解决问题了。

shared_ptr file[5] =
{
    shared_ptr(new string("123")),
    shared_ptr(new string("456")),
    shared_ptr(new string("789")),
    shared_ptr(new string("101112")),
    shared_ptr(new string("131415")),
};
shared_ptr mfile = file[3];
for(auto aptr:file)
    cout << *aptr << endl;

    但是shared_ptr却存在另一种问题,就是循环应用,5(3)一样的问题,当智能指针存在对象内而不是函数内的时候,shared_ptr就会出现BUG,计数一直不为0.

    解决这个问题可以使用weak_ptr,其实这个东西本来就是用来解决shared_ptr存在的这个问题。代码如下:

class A
{
public:
    weak_ptr b;
    A()
    {
        cout << "1" << endl;
    }
    ~A()
    {
        cout << "2" << endl;
    }
};

class B
{
public:
    weak_ptr a;
    B()
    {
        cout << "3" << endl;
    }
    ~B()
    {
        cout << "4" << endl;
    }
};

void f()
{
    A *a = new A();
    B *b = new B();

    shared_ptr aa = shared_ptr(a);
    shared_ptr bb = shared_ptr(b);
    a->b = bb;
    b->a = aa;
}

    这样调用f就不会存在a,b指针没有释放的问题了,此外,weak_ptr还可以用于当提升为shared_ptr不存在的时候,判断shared_ptr是否为空,或者已经不存在。

    (3) unique_ptr

    这个也没什么好讲的,只是将auto_ptr指向别人的东西智能自己指,而不能将所有权赋值给别人。限制比较大。


7. 总结

    智能指针讲的快差不多了,是不是也该写个总结呢?其实智能指针很简单,但是和其他东西实现起来的时候就会很复杂。但是有时候遇到一个问题的时候,不应该只是像如何解决这个问题,而是为什么会产生这个问题,说真的,问题的本质永远是因为设计不合理或者其他什么原因导致,解决问题还是要从根源想,如何解决问题,而不是如何为了一个问题去解决这个问题。


(可能有些地方写的不怎么好不要见怪,如果错误希望能指出。)


参考书籍:

《C++ primer plus 第六版》

《Linux多线程服务器编程 使用muduo C++网络库》

 



          


你可能感兴趣的:(C++,面试题)