背景
上世纪九十年代末期,随着java和c#的兴起,垃圾自动回收机制开始成为一种潮流,这个时候,也就是c++98定义了一种新标准:auto_ptr,提供了垃圾自动回收的机制。
1.auto_ptr
使用方式比较简单
//需要引用std的命名空间
class Player {
public:
int id;
string name;
Player(int id,string name) {
this->id = id;
this->name = name;
}
};
int main()
{
auto_ptr<Player> ptr(new Player(2, "mms"));
cout << ptr->id << endl;
}
不过这玩意过于古老,c++11已经抛弃了它,拥抱unique_ptr和shared_ptr
2. unique_ptr
测试代码:当指针被回收时顺带的带走堆里面的数据
void test() {
unique_ptr<Player> pp(new Player(2, "mms"));
cout << pp->name << endl;
}
int main()
{
test();
}
测试结果
测试代码:当指针置空的时候带走堆里面的数据
void test2() {
unique_ptr<Player> pp(new Player(2, "mms"));
cout << pp->name << endl;
pp = nullptr;
}
int main()
{
test2();
}
测试结果
测试代码:当指针指向其他堆对象时[见异思迁],带走此时的对象
void test3() {
unique_ptr<Player> pp(new Player(2, "mms"));
cout << pp->name << endl;
pp = make_unique<Player>(3, "mme");
}
测试代码:当该堆对象有人接盘时,它的前任去接盘其他堆对象时,无法带走这人
这个测试案例比较复杂
设:
ptr a = T1Heap
ptr b = a
ptr a = T2Heap
此时,a不会顺带的干掉T1Heap
void test3() {
unique_ptr<Player> pp(new Player(2, "mms"));
cout << pp->name << endl;
unique_ptr<Player> pp2;
pp2 = move(pp);
pp = make_unique<Player>(3, "mme");
}
这个时候其实完全可以把unique_ptr当成java或者c#里面的引用来用,只是有些区别就是,堆对象唯一指向一个unique_ptr,当你希望其他unique_ptr去持有这个堆对象时,你只能通过move,也就是将其他指针的对象抢过来,其它指针就没了,典型的一夫一妻制度。
我们可以手动让一个程序崩溃掉
3.shared_ptr
前面讲了unique_ptr,以一夫一妻去约束你的代码,其实对程序员来讲是不自由的,比如对于游戏来讲,我们设计了这样一个框架,存在装备系统,背包系统,技能系统,为了解耦,然我我们每个系统都会定义一个单独的Player指针用于访问某个player,如果我们使用unique_ptr,是不是同一时间只有一个系统去持有,如果我们用单例模式将这个player封装起来就当我没说,因为这个时候代码耦合性会变大,因为我们在写文本代码的时候都会使用xxx.instance.do
这样的句式,当我们尝试剥离装备系统给其他游戏使用时 ,这个时候我们就必须要定义一个Player的Instance,这样会导致你的代码复用性变得很差,为了解决这个问题,我们会使用一种叫代理的东西,
相当于对LocalPlayer进行了一层隔离,这个时候,如果我们单独剥离背包系统给项目组B使用,他们不需要去管LocalPlayer的逻辑(他们有自己的LocalPlayer逻辑),但是背包系统,技能系统,装备系统的代码却不需要去修改,或者只是做微调,这是一种很棒的设计,当然实现的时候unique_ptr其实就不太适合
#pragma once
class Lab1
{
public:
Lab1();
~Lab1();
};
class LocalAgent {
public:
LocalAgent(void* player) {
}
virtual void onEquip() {}
virtual void onBag() {}
virtual void onSkill() {}
};
class EquipSys {
public:
LocalAgent* agent;
};
class BagSys {
public:
LocalAgent* agent;
};
class SkillSys {
public:
LocalAgent* agent;
};
用指针当然非常方便,但是如果你忘记回收,就会…boom!内存泄露,用unique就会产生限制,毕竟是一夫一妻制,这相当于给Agent手动**
我们可以使用shared_ptr
void test4() {
shared_ptr<Player> pp;
pp = make_shared<Player>(123, "mms");
cout << pp->name << endl;
shared_ptr<Player> p2p;
p2p = pp;//让p2p指向mms
cout << p2p->name << " " << pp->name << endl;
pp = make_shared<Player>(456, "mme");
}
显然是支持我们的这种用法的
4. unque_ptr的实现机制
其实讨论它的实现机制没有太多意义,要达成类似的效果其实很简单,前面我们对unique_ptr的各种情况作了测试,可以发现,它和栈对象的机制非常像,只要离开了自己的[作用域],就会析构掉,然后顺便带走堆里面的数据,所以,unique_ptr本质上就是用一个栈对象去包裹这个堆对象,用法跟栈对象其实差不多,但为要用呢?我们讨论一个基本的情况
假如你使用
vector<Player> list
你能否将函数栈里面的Player pp注入到全局list?看似你已经完成了list.push_back(pp)的操作,其实已经狸猫换太子了,这个时候的list[0]已经不是你创建出来的那个pp,而是pp的copy…
有时候,我们不希望这个pp是个copy…
其实我一直觉得这个机制很奇葩,很鸡贼,因为之前一直用的是c#,java
现在,让我们找回c#那种感觉
现在感觉是不是回来啦~~~
一次定义,不需要拷贝,不用狸猫换太子,哎,这种感觉太棒了
所以unique_ptr的机制就是借助栈对象回收的那个瞬间,把它的灵魂伴侣一同拉到地狱,如果不想一起陪葬,就move,跟着另外一个unique_ptr跑就对了…
5.shared_ptr的机制
前面我们对shared_ptr有了较为深刻的理解,其实shared_ptr已经和c#或者java的引用类型很像了,支持多个引用变量访问同一个堆对象,这个时候我们要将这个堆对象放到vector就非常之简单了
回过头来,我们可以思考shared_ptr的机制,运行多个指针去访问堆,当这些指针都置空时,这个堆自动释放,显然,这是种依赖于计数的垃圾回收机制,也就是引用计数法
6. 引用计数法
引用计数法理解起来其实是非常简单的,其实我们一般考虑两种情况就可以了
6.1 计数法的递归回收
class Player {
public:
int id;
string name;
Player(int id,string name) {
this->id = id;
this->name = name;
}
~Player() {
cout << "im release "<<name<<endl;
}
};
比如上述这种代码,它不持有子引用,当我们直接回收
但是我们如果持有子引用:
class TB {
public:
int id;
~TB() {
cout << "tb release " << endl;
}
};
class Player {
public:
int id;
string name;
TB* tb;
Player(int id,string name) {
this->id = id;
this->name = name;
}
~Player() {
cout << "im release "<<name<<endl;
}
};
int main()
{
shared_ptr<Player> pp = make_shared<Player>(123, "mms");
pp->tb = new TB();
pp->tb->id = 444;
pp = nullptr;
}
此时的tb没有被回收
其实我想表达的是什么呢?两个问题,
第一个:计数回收应该学会递归的将tb回收掉
第二个:当我们定义了一个非shared_ptr的TB* tb变量时,是不会触发计数回收的递归特性
修改代码之后
class Player {
public:
int id;
string name;
shared_ptr<TB> tb;
Player(int id,string name) {
this->id = id;
this->name = name;
}
~Player() {
cout << "im release "<<name<<endl;
}
};
再来看
tb被回收掉了
显然触发了递归回收
6.2 循环引用
一个邪恶的例子
class TB {
public:
int id;
TB(int id){this->id = id;}
~TB() {
cout << "tb release " << endl;
}
shared_ptr<Player> player;
};
class Player {
public:
int id;
string name;
shared_ptr<TB> tb;
Player(int id,string name) {
this->id = id;
this->name = name;
}
~Player() {
cout << "im release "<<name<<endl;
}
};
当我们定义
循环的基本定义是让pp->tb = tb;tb->player = pp
就会构成一个环
此时就没有回收,怎么解决这个问题呢?使用weak_ptr
7. weak_ptr
class Player;
class TB {
public:
int id;
TB(int id) {
this->id = id;
}
~TB() {
cout << "tb release " << endl;
}
weak_ptr<Player> player;
};
class Player {
public:
int id;
string name;
weak_ptr<TB> tb;
Player(int id,string name) {
this->id = id;
this->name = name;
}
~Player() {
cout << "im release "<<name<<endl;
}
};
此时就能被回收了,but…
如果结束作用域的话,当pp触发析构时,会自动回收掉自己的weak_ptr
所以weak_ptr也是安全的