/*给定的Object类会通过ref_保存对其他任意Object对象的引用。
findRefs函数,收集指定root对象所有直接或间接引用到的对象(包括root本身)并保存到refs中。即找出a所有直接或间接的不重复引用关系元素!
*/
class Object //Object类
{public:
Object(const char* n):
name_(n)
{
}
std::string name_;
std::vector<Object*> ref_; // 对其他对象的引用.
};
Object* init(){ //初始化Object对象并且建立引用关系.
#define NEWOBJ(OBJ) Object* OBJ = new Object(#OBJ);
#define A_REF_B(A, B) A->ref_.push_back(B);
NEWOBJ(a);
NEWOBJ(b);
NEWOBJ(c);
NEWOBJ(d);
NEWOBJ(e);
NEWOBJ(f);
NEWOBJ(g);
A_REF_B(a, b);
A_REF_B(b, a);
A_REF_B(a, c);
A_REF_B(c, c);
A_REF_B(c, d);
A_REF_B(c, d);
A_REF_B(d, a);
A_REF_B(d, e);
A_REF_B(e, f);
A_REF_B(e, g);
/*该块函数可以通过宏定义来理解:首先NEWOBJ()函数通过指针指向创建了一个通过“*OBJ”引用对象的空容器vector,然后再根据引用关系函数A_REF_B()来往里面添加 add不同引用对象的指针,说白了就是往空的容器中通过“名”首指针引用关系建立起来的——“指针元素”。添加add方法是宏定义了ref_.push_back();具体就是:
a.ref_.push_back(b);
b.ref_.push_back(a);
a.ref_.push_back(c);
......
那么在实际的内存空间上又是怎样的呢?我们可以这样理解:vector a里面存放了地址指针元素b、c,接着是vector b中存放了地址指针元素a,依次递序。对于相同的引 用(如:A_REF_B(c, c);/A_REF_B(c, d);),只是覆盖了一次而已,并不影响结果。
a={b,c};
b={a};
c={c,d};
d={a,e};
e={f,g};
*/
return a;
}
void findRootRefs(Object* root, std::set<Object*>& refs){
/*该块函数是此程序的核心部分,重要的语句有两条:
① refs.find(root->ref_[i]) != refs.end()
② findRootRefs(root->ref_[i],refs);
c++ stl容器set成员函数:find()--返回一个指向被查找到元素的迭代器。
返回一个元素的迭代器?这实在是很难理解,特别是对于刚刚接触C++或者对C++了解不深的同学来说,根本理解不了。那么,我们换个角度,从该函数在程序中所表 现出来的性质来看,我们就不难看出find()函数的功能到底是什么了!就我的理解,简单的来说,find()函数的功能就是在当前的set集合中去寻找所要查询的元素否“存 有子节点”,因为set集合本身就是“红黑二叉树”;判断其是否有“迭代”,无非就是判断该元素是否是“父节点”,那么返回的就是该元素的“父节点指针型类型的迭代器”。
我这样说,只是为了大家理解find()函数的功能,并不完全正确!明白是怎么回事就行了!接下来,这句语句就不难理解了:
在当前集合中查找root所指向的该元素的“迭代器”(是否有子节点)是否不为空。(具体的描述,等会对着代码跑一遍就更加清楚了)。
②我们再来看第二条语句:首先,很明显这是一个“函数递归”。这也是整个程序的核心!
理解了上面两句语句,那么整个函数块的功能也就出来了,我们对着代码跑一遍:<1>.函数进入时,接收了“参数root指针型引用对象”,其实就是指针引用对象a(由语句Object* root = init()可知,init()返回的就是a);以及一个“指针型引用对象set集合refs”。
<2>.for循环语句首次循环时,i<root->ref_.size(),其实就是i<a->ref_.size(),由给出的引用关系我们可以知道指针对象a、b、c、d、e、f、g所引用的元素size分别是2、1、3、2、0、0;但是for循环语句的跳出却不仅仅由i的大小比较判断来退出,还需要continue来判定。
<3>.接着是if(refs.size()==0)的判断,此时set集合refs为空,执行语句refs.insert(root),即refs.insert(a);继续:
refs.insert(root->ref_[i]); →refs.insert(a->ref_[0]),即refs.insert(b)
findRootRefs(root->ref_[i],refs); →findRootRefs(b,refs)
<4>.for开始第二次循环,此时refs.size()已经不为0了,执行else:
refs.find(root->ref_[i]) != refs.end(); →refs.find(b->ref_[0]),即refs.find(a);在refs中元素b是有引用关系的,也就是说b是个“父节点指针型类型的迭代器”(因为b又引用了a)
因此continue跳出当前for循环。这里要注意的是,此时通过continue跳出所回到的起点是:refs.insert(root->ref_[i]);,并且这里的i是i++之后的值,即:refs.insert(a->ref_[1])→refs.insert(c),继续:
<5>.for开始第三次循环,此时refs中已经插入了元素a、b、c,执行else,refs.find(c->ref_[0]) != refs.end()/refs.find(c) != refs.end(),由引用关系A_REF_B(c, c)、A_REF_B(c, d)可知,c也是个“父节点指针型类型的迭代器”;因此跳出当前循环,继续:
refs.insert(c->ref_[1]) →refs.insert(d)
findRootRefs(root->ref_[i],refs) →findRootRefs(d,refs)
程序的往后步骤与以上类似,通过递归的方法对各个引用对象元素的vector中的指针对象进行“父节点指针型类型的迭代器”判断,从而直接或间接的得到元素a的引用对象指针元素。
值得注意的是,虽然该程序采用了递归这种方法,运用少量的代码、比较复杂的逻辑完成了冗长的遍历;但是在C++中递归这种方法或者说“一次性递归”是不推荐使用的,原因是C++执行函数时,函数的局部变量大多是在栈上创建的;函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率虽然很高,但是分配的内存容量却十分有限;造成的结果是什么呢?对了,造成的结果就是C++被大多数code monkey所诟病的一个主要缺陷“内存泄露”或者“栈溢出”!作为C++初学者来说,看到“stack overflow”这种运行错误几乎已经“习以为常”了!更让人“欲哭无泪”的是,“内存泄露”或者“栈溢出”这种运行错误,程序debug是找不到错误到底是由哪一处的代码错误引起的,特别是前者!原因很简单,因为问题无非就是“内存指向不存在/错误(内存泄露,往往是指针问题造成的)”或者“内存不够用了(栈溢出)”。所以,递归的深度不宜过大(windows栈默认的内存大小是1M);通常情况下,我们一般采用“while+小递归”的方式来解决问题。
因此,下面给出了该题的另一种解法!程序的核心是“模拟堆栈”,避免了递归!
*/
for (int i=0;i<root->ref_.size();i++)
{
if (refs.size()==0)
{
refs.insert(root);
}
else
{
if (refs.find(root->ref_[i]) != refs.end())
{
continue;
}
}
refs.insert(root->ref_[i]);
findRootRefs(root->ref_[i],refs);
}
/*stack.push_back(root);
while(stack.size())
{
Object* top = stack.back();
if(refs.find(top) == refs.end())
{
refs.insert(top);
for(size_t i = 0; i < top->ref_.size(); i++)
stack.push_back(top->ref_[i]);
}
else
{
stack.pop_back();
}
}*/}
int main(){
Object* root = init();
std::set<Object*> refs;
std::set<Object*>::iterator it;
/*std::vector<Object*> stack;*/
findRootRefs(root, refs);
for(it=refs.begin(); it!=refs.end();it++){
printf(((*it)->name_).c_str());
cout <<endl;
}
}