在上一则教程中,叙述了当处于多线程环境下时,智能指针所指向的引用计数可能会因为此导致引用计数出问题,因此,引入了原子操作的相关概念,换句话说,这种操作也被称之为是轻量级指针,那对于这种轻量型指针又会存在什么问题呢?本节内容将着重叙述这个问题。另外需要注意的是,关于最近几次的内容互相之间都是息息相关的,需要结合上下文进行理解,同时,因为涉及到的代码比较多,如果哪里没有说明白的地方,需要下载对应的源代码进行对照分析。好了,接下来,进入本次内容的分享。
在说明强指针这个概念之前,我们先从代码的角度慢慢分析,首先,假设,我们现在有如下两个智能指针:
image-20210313102009432
如上图所示,A 指针指向了 B,B 指针指向了 A,这样会导致什么后果呢,我们看如下所示的代码,在上一节轻量级指针的基础上,我们构建这样的 Person
类代码:
class Person : public LightRefBase<Person>
{
private:
sp<Person> father;
sp<Person> son;
public:
Person()
{
cout << "Person()" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
void setFather(sp<Person> &father)
{
this->father = father;
}
void setSon(sp<Person> &son)
{
this->son = son;
}
void printInfo(void)
{
cout << "just a test function" << endl;
}
};
基于此,我们编写一个测试函数,代码如下所示:
void test_func(void)
{
sp<Person> father = new Person();
sp<Person> son = new Person();
father->setSon(son);
son->setFather(father);
}
然后,我们是主函数,代码如下所示:
int main(int argc, char **argv)
{
test_func();
return 0;
}
在分析,强指针之前,我们先来分析以下上述过程中构造函数和析构函数的一个过程,对于类里面的各个对象成员以及类本身而言,它的构造顺序基于这样一个原理:如果对象里面含有其他对象成员,那么在构造的时候:先构造其他对象成员,再构造对象本身;而析构时:顺序则刚刚相反。基于这样一个原理,我们来分析如下所示的代码:
sp<Person> father = new Person();
对于 new Person()
来说,Person
对象里面的father
先被构造,然后紧接着是Person
对象里面的son
被构造,最后是Person
对象本身被构造。对于这句代码,Person
对象的指针传给了sp<Person> father
,这也就导致了sp(T *other)
被调用,而这步操作也就增加了这个Person
对象的引用计数(此时引用计数值等于 1)
紧接着,我们来看下面这句代码:
sp<Person> son = new Person();
对于这句代码它的原理和上一句是相同的,首先是Person
对象里的father
先被构造,然后是son
被构造,紧接着是Person
对象本身被构造。Person
对象的指针传给sp<Person> son
,导致sp(T *other)
被调用,它增加了Person
对象的引用计数,现在这个值等于1。
到这里,我们知道,两个 Person
对象的引用计数都等于1,我们接着看如下所示的代码:
father->setSon(son);
这个函数里面有一个等号赋值操作,其实这个等号是经过重载的,我们来看等号重载的代码:
image-20210313110017132
可以看到,在这个函数里,有对于引用计数自增的操作,也就是说通过father->setSon(son)
操作会使得son
所指向的引用计数值自增,所以到这里,son
所指向的对象的引用计数的值就为2
。
同样的,我们再来看如下所示的代码:
son->setFather(father);
根据上述分析, father
所指向的对象的引用计数值变为2,这么一来,当函数test_func
调用完成然后退出的时候,会释放相应的局部变量,但是我们之前在叙述智能指针的时候,提到过,要释放智能指针所指向的对象的内存,需要当所指向的对象的引用计数为 0 的时候,才能将其释放掉,所以上述代码就算test_func
运行结束,也不会去销毁对象的内存。下图是上述代码的一个示意图:
image-20210313112741305
向上述这种情况,就称之为是强指针,也就是 A 指向 B,A 决定 B 的生死,最后,我们来看,我们最初给出的代码的运行结果:
image-20210313113733367
可以看到虽然test_func
执行完毕,但是并没有执行析构函数,要如何解决这个问题呢,就需要引入弱指针的相关概念。
引入弱指针的原因在上述已经说明了,那么具体应该如何做呢?对比于上述的强指针,我们可以引入一个弱指针的计数,同时增加一些弱指针计数的相关操作,下面是简化的代码:
class RefBase
{
private:
int mStrong;
int mWeak;
public:
void incStrong(void);
void decStrong(void);
void incWeak(void);
void decWeak(void);
};
写到这里我们其实又可以将弱指针再抽象为一个对象,在这里,(incStrong
不放入是因为为了兼容之前轻量级指针对引用计数的操作),如下面的代码所示:
class StrongWeakRef_type
{
private:
int mstrong;
int mweak;
public:
void incWeak();
void decWeak();
};
我们在接触面向对象的编程中,可以知道一个原则,就是说,在头文件中是不想看到私有数据成员的,只想看到接口,那么我们在实现这部分的时候,就可以将其进一步优化,也就是拆分成两部分:
抽象得到的接口类的代码如下所示:
class weakref_type
{
public:
void incWeak(void);
void decWeak(void);
};
将这部分代码放入头文件中,接下来是变化的实现的代码,这部分代码是放在 cpp
文件中的:
class weakref_impl:public weakref_type
{
private:
int mstrong;
int mWeak;
};
基于上述的代码,再来写出我们的 RefBase
类的代码:
class RefBase
{
weakref_impl * mRefs;
};
这样,我们就完成了一个强弱指针统一实现的一个基本思路,下图是关于上述的一个分析过程的一个示意图:
image-20210313145313229
上述仅仅是一个思路,具体的代码比这要复杂,就不进行剖析了,我们来看基于刚刚所述的这个弱指针,我们继续来实现我们的 Person
类。
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <utils/RefBase.h>
using namespace std;
using namespace android;
class Person : public RefBase {
private:
wp<Person> father;
wp<Person> son;
public:
Person() {
cout <<"Pserson()"<<endl;
}
~Person()
{
cout << "~Person()"<<endl;
}
void setFather(sp<Person> &father)
{
this->father = father;
}
void setSon(sp<Person> &son)
{
this->son = son;
}
void printInfo(void)
{
cout<<"just a test function"<<endl;
}
};
上述代码中引入的是Android
源代码的相关头文件,在这基础上实现的 Person
类,我们可以看到相对于前文所述的 Person
类,sp
变成了wp
,其他代码不变,同样是造成了对 father
和son
所指向的对象的两次引用,但是在程序执行的时候,结果如下所示:
image-20210313150255127
这也正是使用弱指针所带来的效果。
这就是本次所要分享的内容,涉及到强指针和弱指针的相关介绍,所涉及的代码和Android
源代码相关,如果想要查看源代码的朋友,可以通过下方的百度云链接进行下载。
链接:https://pan.baidu.com/s/1NlHqvrdvKI9IBLj_EKnk1g
提取码:w7nj