最近在读某书,发现自己有点儿落伍,c++的标准太老旧,以前各项目的代码基本都是裸指针。裸指针带来的很多的问题,也相当的头疼。
近两年主要在写golang,golang用起来的确很顺手,带GC可以减轻很多思想上的包袱。后来仔细一想,其实包袱主要减轻在了资源的管理上面。在golang中创建一个对象,然后到处引用,完全不用考虑该对象何时释放,释放后其它引用到的地方该怎样进行清理。的确资源管理是旧标准c++上最难处理的事情之一,写过大型c++项目的人应该深有体会。
比如在我的游戏中,每个怪物会定时搜索地图上的攻击目标,搜索到何时的目标后,会将自己的target指向目标。逻辑很简单,但是在这几年中,target死亡后把自己delete了,但是有该target的对象依旧引用着它,导致在处理攻击逻辑的时候直接崩溃了。然后就是各种现在看来很简陋或者麻烦的方法来处理该问题:遍历当前场景下所有object,当object为自己的时候进行清理。虽然算是解决了这个问题,但是实现的很不优雅。在大概学习和实践了shared_ptr和weak_ptr后,发现这两个东西解决上面的问题实在是太简单了。
.shared_ptr
与weak_ptr
属于智能指针,shared_ptr
用于通过引用计数管理对象的生命周期,而weap_ptr
和shared_ptr
配对使用来避免交叉引用。
.shared_ptr
可以复制,当被复制后,则引用计数+1,当引用计数为0的时候释放对象。而weak_ptr
可以被shared_ptr
赋值,但是不会增加引用计数。当weak_ptr
使用的时候,必须通过lock
方法来获得shared_ptr
来操作指向的对象。
可能有朋友会想到个问题,weak_ptr
不增加引用计数,那么当之前的shared_ptr
生命周期完结释放了管理的指针,那么weak_ptr
不就成为了管理野指针的智能指针了么?
所以上面写了,weak_ptr
的使用得通过lock
来获得shared_ptr
,假设对象已经被释放了,那么获得的shared_ptr
就是空,所以不存在上面的问题。
由于是自己的笔记,很多细节都省略了。这儿直接说shared_ptr
的另一个特性,就是可以指定释放函数,也就是在被管理的对象由于引用计数为0后析构前,不会调用相应的析构函数,而是直接调用传入的释放函数。这个有个好处,比如可以实现对象池。
下面是一个简单的代码例子,详细在我的github中。
class WeakItem {
public:
WeakItem() {
value_ = 0;
}
~WeakItem() {
printf("delete item key %s\r\n", key_.c_str());
}
public:
int value_;
std::string key_;
};
class WeakFactory : public std::enable_shared_from_this<WeakFactory>
{
public:
WeakFactory() {
}
~WeakFactory() {
printf("delete factory, left item %d\r\n", items_.size());
}
public:
std::shared_ptr<WeakItem> get(std::string key) {
lock_.lock();
auto& wptr = items_[key];
auto sptr = wptr.lock();
if (nullptr == sptr) {
// Not allocated
sptr = std::shared_ptr<WeakItem>(new WeakItem,
std::bind(&WeakFactory::free_item,
std::weak_ptr<WeakFactory>(shared_from_this()),
std::placeholders::_1));
sptr->value_ = 0;
sptr->key_ = key;
// To weak_ptr
wptr = sptr;
}
lock_.unlock();
return sptr;
}
public:
static void main(int argc, char* argv[]) {
printf("%s start\r\n", __FUNCTION__);
std::shared_ptr<WeakItem> powned_item;
std::weak_ptr<WeakFactory> pwfac;
{
// Factory scope
std::shared_ptr<WeakFactory> pfac(new WeakFactory);
pwfac = pfac;
powned_item = pfac->get("owned");
auto tfunc = [pfac]() {
for (int i = 0; i < 100; i++) {
char buf[8] = {0};
sprintf(buf, "%d", i);
std::string key = buf;
auto item = pfac->get(key);
// We do not use it any more out of this scope,
// So item will be released by shared_ptr immediately
}
};
std::thread t1(tfunc);
std::thread t2(tfunc);
t1.join();
t2.join();
}
auto pfac = pwfac.lock();
if (nullptr == pfac) {
printf("factory already deleted\r\n");
} else {
printf("factory not deleted\r\n");
}
// Factory is deleted, owned_item should be delete but not erase from factory
// ...
printf("%s end\r\n", __FUNCTION__);
}
private:
void delete_from_internal(WeakItem* old) {
lock_.lock();
items_.erase(old->key_);
lock_.unlock();
}
// We declare a static function to release the WeakItem
static void free_item(std::weak_ptr<WeakFactory> ptr, WeakItem* old) {
auto sptr = ptr.lock();
if (nullptr != sptr) {
sptr->delete_from_internal(old);
} else {
printf("factory is deleted ..., item %s not erased from items_\r\n",
old->key_.c_str());
}
delete old;
old = nullptr;
}
private:
// Save as weak_ptr rather than shared_ptr
// If we use the shared_ptr, items_ will always hold the shared_ptr
// and items referenced won't be deleted by shared_ptr
std::map<std::string, std::weak_ptr<WeakItem> > items_;
std::mutex lock_;
};
上述代码实现了一个对象池,线程安全并且没有内存泄漏。下面谈几个细节:
weak_ptr
这个应该比较好理解。我们在get中返回了shared_ptr
,假设我们又在工厂内存了相同类型,则该item永远不会被释放(至少在factory销毁前不会被释放)。改用weak_ptr
可以解决这个问题。
shared_ptr
的释放函数在上面的例子中,我们可以看到创建shared_ptr
的时候是这样的:
sptr = std::shared_ptr<WeakItem>(new WeakItem,
std::bind(&WeakFactory::free_item,
std::weak_ptr<WeakFactory>(shared_from_this()),
std::placeholders::_1));
我们制定了一个释放函数,用处在于在shared_ptr
销毁管理对象的时候,我们可以把该对象从factory中移除。这儿的细节比较多,首先,我们看到了shared_from_this
。
shared_from_this
用于获得this指针的shared_ptr
,而获得这种能力的方式是在类必须继承std::enable_shared_from_this<WeakFactory>
,还有一点是该类必须由shared_ptr
所管理。
当然由于factory可能先于item销毁,所以我们将回调中的shared_ptr
又转为了weak_ptr
,在回调中我们判断是否资源有效,假设无效就不会从工厂里进行销毁。在这里,释放函数的形参虽然是weak_ptr
,但是std::bind是值复制,还是会复制shared_ptr
,然后在调用的时候再进行转换,也就是说factory的shared_ptr
又有一个存在了闭包中。只有该闭包被执行并销毁后才会释放factory,也就是该item的释放函数必须得调用后才行,那么就是所有的item被释放后才会释放factory。为了让factory的生命周期不进行延长,我们做了上述的工作。
首先,shared_ptr
不是线程安全的,指的是多线程操作同一个share_ptr
必须有自己的同步措施。而多个线程拥有同一个shared_ptr
的拷贝则是安全的,在多线程下该拷贝析构也是安全的,所以在实践上来说,每个线程必须得有一个shared_ptr
的拷贝。大概流程如下:
shared_ptr
s1。这样每个线程都有自己的shared_ptr
了,至于每个智能指针的销毁则是安全的。
这儿就比较的简单了。我们可以将怪物的target定义为weak_ptr
,当target被销毁的时候,weak_ptr
的lock为空,则可以得知指向的对象已经销毁了,解决这个就是这么简单。