适合具备 C 语言基础的 C++ 入门教程(十三)

前言

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

智能指针的引入

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

class Person {
    
public:

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

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

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

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: "<<s->getStrongCount()<<endl;

	s->printInfo();	
}

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

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

	sp other = new Person();

	cout<<"Before call test_func: "<<other->getStrongCount()<<endl;

	for (i = 0; i < 2; i++)
	{
		test_func(other);
		cout<<"After call test_func: "<<other->getStrongCount()<<endl;
	}
	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()"<<endl;
	}

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

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

template<typename T>
class sp
{
private:
    T *p;
    sp() : p(0) {}
	
	sp(T *other)
	{
		cout<<"sp(T *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;
			}
		}
	}

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

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

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

template<typename T>
void test_func(sp<T> &other)
{
	sp<T> s = other;

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

	s->printInfo();
}

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

int main(int argc, char** argv)
{
    int i;
    
    sp<Person> other = new Person();
    
	(*other).printInfo();
	cout<<"Before call test_func: "<<other->getStrongCount()<<endl;

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

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

小结

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

链接:https://pan.baidu.com/s/1LUL6HqekmwguqYO6V1ETqw 
提取码:vu8p 
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值