前言

无论是在C还是C++中,指针都是在使用的时候需要非常谨慎的一个点,而在C++中,我们引入一个智能指针的概念,以此来规避在使用指针时可能出现的问题。

 

智能指针的引入

我们以之前的一个程序为例子,也就是Person类,如下是Person类的代码:

 

class Person {

public:

    Person() 
    {
        cout <<"Pserson()"<    }

    ~Person()
    {
        cout << "~Person()"<    }
    void printInfo(void)
    {
        cout<<"just a test function"<    }
};

 

基于此,我们来编写一个测试函数:

 

void test_func(void)
{
    Person *p = new Person();
    p->printInfo();
}

 

可以看到在测试函数里,我们定义了一个指针变量,但是,这里需要注意的是,这个指针变量并没有delete操作,紧接着,我们来编写main函数,代码如下所示:

 

int main(int argc, char **argv)
{    
    int i;

    for (i = 0; i < 2; i++)
        test_func();
    return 0;
}

 

这样的程序存在一个什么隐患呢?如果在main函数中的i的最大值是是一个很大的数,那么程序就会调用很多次test_func函数,但是由于test_func函数里没有delete操作,那么这个时候由new获得的内存就会一直不能得到释放,最终导致程序崩溃。

 

我们将test_func函数进行一些更改,更改如下所示:

 

void test_func(void)
{
    Person per;
    per.printInfo();
}

 

main函数不变,这个时候如下i的最大值是一个很大的数,那么会导致程序崩溃么,答案是否定的,因为在这里,在test_func函数里定义的是一个局部变量,局部变量是存放在栈里的,也就是说每当test_func执行完局部变量就会出栈,其所占用的空间自然也就释放了。

 

智能指针

所以,这给我们一个启发,如果将指针和局部变量相联系起来,是不是就能解决使用指针所带来的隐患呢?我们来看下面这样一个代码(Person类的代码不变)

 

class sp
{
private:
    Person *p;

public:
    sp() : p(0) {}

    sp(Person *other)
    {
        cout << "sp(Person *other)" << endl;
        p = other;
    }

    ~sp()
    {
        cout << "~sp()" << endl;
        if (p)
            delete p;
    }

    Person *operator->()  /* -> 被重载,是为了使得 sp 实例化的对象能够访问到 person 类的成员函数*/
    {
        return p;
    }
};

 

基于此,我们来编写test_func函数:

 

void test_func(void)
{
    sp s = new Person();
    s->printInfo();
}

 

同样的main函数不变,在这种情况下,test_func的执行就不会导致程序崩溃,因为此时实际上是定义了一个局部变量,在函数执行完毕之后,局部变量也就会自动地释放掉。

 

我们继续完善代码,我们在sp类中增加一个拷贝构造函数,增加的代码如下所示:

 

class sp
{
private:
    Person *p;

public:
    /*省略前面已有的代码*/
    sp(sp &other)
    {
        cout << "sp(sp &other)" << endl;
        p = other.p
    }
};

 

在增加了拷贝构造函数的基础上,我们编写main函数:

 

int main(int argc, char** argv)
{
    sp other = new Person();

    return 0;
}

 

我们编译代码,编译结果如下所示:

 

image-20210228172543467

 

上述错误的提示是说,不能将非常亮的引用与临时变量绑定,到底是什么意思呢,我们来看下面的分析,我们看主函数的这条语句:

 

sp other = new person();

 

这条语句实际上可以等同于如下这几条语句:

 

Person *p = new Person();
sp tmp(p); ==> sp(Person *p)  /*tmp 表示的是临时变量*/
sp other(tmp); ==> sp(sp &other2)  

 

那为什么会报错呢?这是因为第三条语句,我们将第三条语句进行以下剖析,第三条语句实际上是相当于下面这条语句:

 

sp &other2 = tmp;

 

那这条语句是为什么会出错呢,这是因为tmp当前是一个临时变量,而临时变量是不能够赋值给非常量引用的。

 

临时变量没有名字,自然不能够赋值给非常量引用

 

而解决方法,也很简单,那就改成常量引用就好了,因此,我们将拷贝构造函数改为如下的形式:

 

class sp
{
private:
    Person *p;

public:
    /*省略前面已有的代码*/
    sp(const sp &other)
    {
        cout << "sp(sp &other)" << endl;
        p = other.p
    }
};

 

这样一来就解决这个问题了。

 

我们继续更改代码,将test_func代码改为如下的形式:

 

void test_func(sp &other)
{
    sp s = other;

    s->printInfo();
}

 

然后,基于此,我们在主函数里测试test_func函数,测试代码如下所示:

 

int main(int argc, char **argv)
{
    int i;
    sp other = new Person();

    for (i = 0; i < 2; i++)
        test_func(other);

    return 0;
}

 

编译,运行代码,结果如下所示:

 

image-20210228201922544

 

上述运行的结果提示是当前被释放了两次,这是为什么呢?我们来仔细分析一下,下面是程序执行的一个流程图:

 

image-20210228203637110

 

因此,这也就解释了上述出错的原因,那么可以采取什么方法来解决这个错误呢?原理也是简单的,只要不让它销毁两次就行,那我们采取的方法是,定义一个变量,这个变量能够记录指向Person对象的个数,只有当前指向这个Person对象的个数为0的时候,才执行销毁操作,否则就不执行销毁操作。

 

下面我们来编写代码,首先是Person类的代码:

 

class Person
{
private:
    int count;

public:
    void incStrong { count++; }
    void decStrong { count--; }
    void getStrongCount { return count; } /* 因为当前 count 属于是私有数据成员,自然编写这些访问接口是很有必要了 */

    Person() : count(0)
    {
        cout << "Person()" << endl;
    }

    ~Person()
    {
        cout << "~Person()" << endl;
    }

    void printInfo(void)
    {
        cout << "just a test function" << endl;
    }
};

 

上述代码中,我们在Person类中定义了私有数据成员,并且定义了其访问的接口,同时,我们在Person的构造函数中,初始化了count变量。

 

紧接着,我们来编写sp类的代码,注意:我们在讲述原理的时候,提到了定义一个能够记录指向Person类次数的变量,那么在接下来的代码中,只要涉及指向Person类的操作的时候,就需要将count加一,下面是sp类的代码:

 

class sp
{
private:
    Person *p;

public:
    sp() : p(0) {}

    sp(Person *other)
    {
        cout << "sp(Person *other)" << endl;
        p = other;
        p->incStrong();
    }

    sp(const sp &other)
    {
        cout << "sp(const sp &other)" << endl;
        p = other.p;
        p->incStrong();
    }

    ~sp()
    {
        cout << "~sp()" << endl;

        if (p)
        {
            p->decStrong();
            if (p->getStrongCount() == 0)
            {
                delete p;
                p = NULL;
            }
        }
    }

    Person* operator->()
    {
        return p;
    }
};

 

为了更好地观察代码的运行,我们增加一些打印信息用于观察,首先是test_func里的,增加的代码如下所示:

 

void test_func(sp &other)
{
    sp s = other;

    cout<<"In test_func: "<getStrongCount()<
    s->printInfo(); 
}

 

然后,我们继续来编写main函数里面的代码:

 

int main(int argc, char **argv)
{    
    int i;

    sp other = new Person();

    cout<<"Before call test_func: "<getStrongCount()<
    for (i = 0; i < 2; i++)
    {
        test_func(other);
        cout<<"After call test_func: "<getStrongCount()<    }
    return 0;
}

 

编译,执行,下面是代码执行的结果:

 

image-20210228210842670

 

对照着代码,我们可以看到Person对象被指向的次数,而且在更改之后的基础上运行,代码就没有出现错误了。

 

现在来小结一下,在使用了智能指针之后,在遇到需要定义指针型变量的时候,我们也更加倾向于使用下面的方式:

 

少用Person*,而是用sp来替代Person*

 

对于 Person*来说,有两种操作:per->XXX或者是(*per).XXX

 

那么对于sp来说,也应该有这两种操作:sp->XXX或者是(*sp).XXX

 

为了实现(*sp).XXX,那么我们还需要额外补充一点,就是关于*运算符的重载,重载的代码如下:

 

class sp
{
private:
    Person *p;

public:
    /* 省略相关代码 */
    Person& operator*()
    {
        return *p;
    }
};

 

另外需要注意的一点就是上述中使用&而不是直接返回值的原因是为了提高效率,因为如果是返回值的话就需要调用构造函数,而如果是返回引用的话就不需要。

 

改进

那么到目前为止,我们的代码还能不能再进行完善呢?我们来看Person类的代码,关于count相关的代码,实际上只要涉及到构造一个智能指针,那么就会用的到,而这个时候,可以把这部分代码单独分离出来,然后,Person类可以从这个分离出来的类继承,这样就更加具有普适性,比如,我们如果想要构造一个其他的智能指针,所需要的类就可以从这个分离出来的类中继承。我们来看具体的代码:

 

class RefBase {
private:
    int count;

public:
    RefBase() : count(0) {}
    void incStrong(){ count++; }    
    void decStrong(){ count--; }    
    int getStrongCount(){ return count;}
};

 

上述就是我们分离出来的类,然后Person类从这个类中继承而来。

 

class Person : public RefBase{

public:
    Person() {
        cout <<"Pserson()"<    }

    ~Person()
    {
        cout << "~Person()"<    }
    void printInfo(void)
    {
        cout<<"just a test function"<    }
};

 

上述是我们对于Person类的一个改进,我们还可以进一步进行改进,回顾sp类,sp 类中所定义的私有成员是Person类的实例化对象,那么如果我想要用sp定义任何类型的对象呢,这个时候,就需要使用到模板的概念,下面是改进后的sp类的模板函数的代码:

 

template
class sp
{
private:
    T *p;
    sp() : p(0) {}

    sp(T *other)
    {
        cout<<"sp(T *other)"<        p = other;
        p->incStrong();
    }

    sp(const sp &other)
    {
        cout<<"sp(const sp &other)"<        p = other.p;
        p->incStrong();
    }

    ~sp()
    {
        cout<<"~sp()"<
        if (p)
        {
            p->decStrong();
            if (p->getStrongCount() == 0)
            {
                delete p;
                p = NULL;
            }
        }
    }

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

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

 

实际上也很简单,只是将之前的Person换成了T。更改了sp类,那么也就自然需要更改test_func函数了,更改之后的代码如下所示:

 

template
void test_func(sp &other)
{
    sp s = other;

    cout<<"In test_func: "<getStrongCount()<
    s->printInfo();
}

 

基于上述的改进,我们来编写主函数,代码如下所示:

 

int main(int argc, char** argv)
{
    int i;

    sp other = new Person();

    (*other).printInfo();
    cout<<"Before call test_func: "<getStrongCount()<
    for (i = 0; i < 2; i++)
    {
        test_func(other);
        cout<<"After call test_func: "<getStrongCount()<    }

    return 0;
}

 

至此,就完成了关于智能指针的改进,当然,到目前为止,其还是存在问题的,所存在的问题,将在下一节进行叙述。

 

小结

本节的内容就到这里结束了,所涉及的代码可以通过百度云链接的方式获取到:

 

链接:https://pan.baidu.com/s/1LUL6HqekmwguqYO6V1ETqw 
提取码:vu8p