0. 问题
问题是这样,三个类A,B,C。AC都有指针指向同一个B类对象,C类可以回收了刚刚生成的B类对象的内存,A类应该对这个指针进行如何操作,才能确保使用该指针时不会产生野指针问题发生未定义结果?
这是前两天面试的时候面试官问我的问题,当时忘了询问解决方案,我当时的回答时使用智能指针或者进行内存分配管理避免这种情况的发生,但是面试官要求情况就是这样不用智能指针单纯这样情形下如何如何解决,我在网上也没有找到类似的解决方案,所以来请教一下各位大佬这种情况应当如何解决。
题外话
我一直觉得 std::shared_ptr 得有一个 单线程 特化版本。
本来, std::weak_ptr 和 std::shared_ptr 天然构造 了一对观察者和被观察者,可是,为了支持并发安全,std::shared_ptr 带上了锁,性能代价一下子变大,于是,C++程序员只能闲着就自己搞观察者模式了……
我记得 boost.asio 就有类似的设计:为 io_context (以前的 io_service) 单线程运行特化一个性能版本:
explicit io_context( int concurrency_hint); // 为1时,内部凡需锁操作,都会跳过
估计是因为后面才加入的逻辑,所以没有走类型特化。显然,这里用特化,性能更好,并且会更安全(比如:不会允许将单线程版本下的指针和多线程下的指针互相赋值)。
自己依据业务需要实现观察者模式,当然功能更强大,比如“偷窥/观察”的内容可能五花八门——但是,在C++的指针应用中,真的有大量大量大量情况,我们需要“偷窥/观察”的事情,就一件事:
那个对象它死了没?那个对象它死了没??那个对象它死了没???没死我就继续搞,死了?那就这样吧,我放弃。
这种情景需求太常见太常见,以致C++程序员在这种情景下发生的典型错误 ,叫有个术语:“悬挂指针 / Dangling pointer”。
再说一次:本来 shared_ptr/weak_ptr 就可以完美的帮助C++程序员大量的此类工作,可是因为性能原因,不敢用了。
另,std::expiermental 里倒是有一个 observer_ptr<T>, std::experimental::observer_ptr - cppreference.com。它有它的价值,但它只有观察者,是被观察的对象不知情的情况下的观察,所以它更准确的名字 应该叫 “ 偷窥者 / peeper_ptr<T>”。
1. 观察者模式基本概念 (通用设计)
言归正传。A 喜欢“远程”用 B,但 B 的主人是 C;于是 A 每次都得爬 C 家的墙,偷窥一眼确保 B 还是鲜活的,再回家“远程”用 B。
C 从中发现了商机。对 A 说:“别这么费力了,请注册加入我的观察者列表,这样当 B 一死,我就第一时间通知你。会费只需一年五毛。值当不?”
A :“值当!”
2. 针对题意的设计考虑 (业务定制)
C 为什么只收五毛钱的年费呢?因为 C 知道,全城有 5千万个对象,和A 一样的在偷偷观察 B 呢!一年营业额 两千五百万啊!
但是,C 也有它的烦恼:
- B 是自由的,万一它自行和全城的人结合怎么办?到时就没人来翻我家墙了……
- 同理, new 一个新B 一点不难,万一别人自己去创建B了,我还做什么生意啊?
- 记录五千万个观察者,这需要占用我不少资源啊!!身为奸商,我需要减少记录成本!!
- B一死,老子我还真得一个个通知过去,发短信要钱,发微信也很费手指啊!!!
- 最后,有人一年用了几千次B,有人只用一次,我却收统一的年费,这不符合商业道德。
2.1 夺取惟一生杀大权
前两个问题要一起解决 。
除非有什么特殊情况,否则,通常地,负责生的要负责死,负责死的也要负责生。一句话:私有化 B 的构造和析构,再强行逼 B 声明 C 是友元。从此 只有 C 可以生B和杀B!不怕有人抢生意了!代码如下。注意,如前所述,下面代码只能用在单线程环境下。如是多线程环境,请直接使用 shared_ptr/weak_ptr 。
#include <iostream> #include <memory> #include <list> struct B { void Answer() { std::cout << "老娘我真是个万人迷!" << std::endl; } private: B() {}; // 生:私有化 ~B() {}; // 死:私有化 friend struct C; // 说是朋友,其实…… }; struct C { C() : _b(new B) {} // 造B ~C() { DestroyB(); } void DestroyB() // 毁B { delete _b; _b = nullptr; for (auto o : _observers) { o->Notify(); } } B* GetB() { return _b; } /// 快来注册"偷窥"俱乐部 // // 俱乐部 必须 有一个共同的头衔: BObserver // 从而能收到 B 的死亡通知 struct BObserver { void virtual Notify() = 0; }; // 加入俱乐部 void Regist(BObserver* o) { // 简化,这里不去重了 if (o != nullptr) { _observers.push_back(o); } } // 退出俱乐部 void Unregist(BObserver* o) { _observers.remove(o); } private: B* _b; std::list<BObserver *> _observers; }; struct A : C::BObserver { explicit A(B* b) : _b(b) {} // 自己造不了B,只能外面传入 void AskB() { if (!_b) { std::cout << "啊,亲爱的B,你死了!" << std::endl; return; } std::cout << "亲爱的B,自我介绍一下吧?\n"; _b->Answer(); } void Notify() override { _b = nullptr; // 没别的事,就是B死了 } B* _b = nullptr; }; int main() { C c; // 开张啦!全城独家 A a (c.GetB()); // 来了一个客户,叫小a c.Regist(&a); // 它办VIP卡了! a.AskB(); // 小a用户(第一次)请求消费 c.DestroyB(); // 毁 a.AskB(); //小a用户再次请求消费,但此时B已死 }
输出示例:
亲爱的B,自我介绍一下吧? 老娘我真是个万人迷! 啊,亲爱的B,你死了!
2.2 简化
奸商的后面三个问题,也可以一起解决 。它们是:
3. 记录五千万个观察者,这需要占用我不少资源啊……
4. B一死,老子我还真得一个个通知过去……
5. 有人一年用了几千次B,有人只用一次,我却收统一的年费……
很明显,在本例中,不管A有几个对象加入俱乐部,也不管将来有多少新的观察者类型,它们都只是想实现B活着用B,B死了放弃这样一个需求而已……
因此,我们大可不必为了观察者而观察者,奸商 C 只要控制了 B 的生死,再控制好别人只能通过它来访问到全城惟一的 B ,不就好了吗?哪里需要 std::list 来存储客户数据呢?又哪里需要在B死了以后去一一通知呢?
应该把资源,花在真正的商业逻辑。
上,所以,应该在客户每访问一次B时,就记录一下它该交的钱又增加了……
所以,砍掉 list成员,砍掉 BObserver 接口,砍掉 Regist() / Unregist(),砍掉Notify(),砍掉virtual/ override ,砍掉派生,砍掉 面向对象,砍掉设计模式……
#include <iostream> #include <memory> struct B { void Answer() { std::cout << "老娘我真是个万人迷!" << std::endl; } private: B() {}; ~B() {}; friend struct C; // 说是朋友,其实…… }; struct C { C() : _b(new B) {} // 造B ~C() {DestroyB();} void DestroyB() { delete _b; _b = nullptr; } B* GetB() { return _b; } private: B* _b; }; struct A { void AskB(C & c) { if (auto b = c.GetB(); !b) { std::cout << "啊,亲爱的B,你死了!" << std::endl; } else { std::cout << "亲爱的B,自我介绍一下吧?\n"; b->Answer(); } } }; int main() { C c; // 开张啦!全城独家 A a; // 来一个客户 a.AskB(c); // c收钱啦 c.DestroyB(); // 毁 a.AskB(c); // c又要收钱啦…… }
3. 贤者模式
现在, 代码行数减半,逻辑也相应简单很多,因此我们可以认真看一下C,此刻的C做了什么?来看它的代码:
struct C { C() : _b(new B) {} // (a): 造B ~C() {DestroyB();} void DestroyB() { delete _b; _b = nullptr; // (b): _b 死后,将它置空 } B* GetB() // (c): 对外开放 _b { return _b; } private: B* _b; };
我加了 (a), (b), (c) 三个注释,C 也就做了这三件事。事实上它的用户,在使用B时,每次都主动取指针,然后每次都仍然得自行判断 是否为空。这 std::weak_ptr 对 std::shared_ptr 的观察实现很接近:weak_ptr 并不能直接使用,你得 通过lock()来升级以得到一个shared_ptr,lock() 也不一定成功,一样得判断得到是不是空指针。
- 我们的例子
if (auto b = c.GetB()) { ... // b 不为空时执行
- weak_ptr 的例子
if (auto sp = w.lock()) { ... // sp 不为空时执行
客户写这样的代码,是烦不胜烦的。所以,C 到底提供提供了什么价值呢?我们像贤者一样陷入了新的沉思……