unique_ptr与shared_ptr设计疑问:卡牌游戏智能指针选型问题
嘿,这个场景我做卡牌游戏原型的时候刚好踩过坑!核心问题其实是你混淆了所有权和访问权——你一开始用unique_ptr是完全正确的,因为卡牌从Deck到Player的流转都是明确的所有权转移,但现在需要Board和GameLogic同时接触同一张卡,不代表要共享所有权呀!下面给你几个符合智能指针设计理念的解决方案:
方案1:明确单一所有权,用引用/原始指针提供访问权
这是最推荐的方案,完全贴合unique_ptr的设计初衷——让一个模块持有卡牌的唯一所有权,其他模块只通过引用或const原始指针来访问,不持有所有权。
具体来说,卡牌的所有权流转应该是这样的:
- Deck → Player:用
unique_ptr转移所有权,Player持有手牌的vector<unique_ptr<Card>> - Player打出卡牌 → Board:Player把
unique_ptr转移给Board,Board持有场上卡牌的vector<unique_ptr<Card>> - GameLogic不需要持有卡牌,而是通过Board提供的接口来访问场上的卡牌
代码示例:
class Board { private: vector<unique_ptr<Card>> played_cards; public: // 接收玩家打出的卡牌,接管所有权 void place_card(unique_ptr<Card> card) { played_cards.push_back(move(card)); } // 给GameLogic提供只读访问(如果需要修改可以提供非const版本) const Card& get_played_card(size_t index) const { // 注意要先做边界检查,避免越界 if (index >= played_cards.size()) { throw out_of_range("Invalid card index"); } return *played_cards[index]; } // 当卡牌离场时,销毁所有权 void remove_card(size_t index) { if (index < played_cards.size()) { played_cards.erase(played_cards.begin() + index); } } }; class GameLogic { private: Board& game_board; // GameLogic持有Board的引用,确保生命周期一致 public: GameLogic(Board& board) : game_board(board) {} void resolve_card_effect() { // 访问场上的第一张卡牌,处理效果 const Card& current_card = game_board.get_played_card(0); if (current_card.get_type() == CardType::ATTACK) { // 执行攻击逻辑 } } };
这个方案的优势:
- 所有权完全清晰,每个卡牌始终只有一个所有者,避免内存泄漏或悬垂指针
- GameLogic只负责逻辑处理,不持有任何实体资源,符合单一职责原则
- 完全不需要
shared_ptr,保持代码简洁优雅
方案2:仅在必要时使用shared_ptr(不推荐除非真的需要共享所有权)
如果你的场景中,卡牌确实需要被多个模块同时拥有(比如某些特殊机制:一张卡同时存在于玩家手牌和场上?这种情况很少见),可以用shared_ptr,但一定要明确共享的边界。
比如,当卡牌被打出时,Player保留一个weak_ptr,Board持有shared_ptr,GameLogic通过Board的shared_ptr访问。这样Player可以检查卡牌是否还存在,但不影响所有权的管理:
class Player { private: vector<weak_ptr<Card>> pending_cards; // 仅用于追踪已打出但未结算的卡牌 public: void play_card(shared_ptr<Card> card, Board& board) { board.place_shared_card(card); pending_cards.push_back(card); } }; class Board { private: vector<shared_ptr<Card>> played_cards; public: void place_shared_card(shared_ptr<Card> card) { played_cards.push_back(card); } };
但再次强调:这个方案只适合真正需要共享所有权的特殊场景,大部分卡牌游戏的流转逻辑都不需要,用方案1足够。
方案3:分离卡牌的属性与状态(进阶优化)
如果你的卡牌有大量固定属性(比如名称、攻击力、效果描述),而只有少量可变状态(比如是否被激活、剩余耐久),可以把这两部分分离:
- 用
shared_ptr<const CardData>共享不可变的属性(所有同类型卡牌复用同一个CardData) - 可变状态存在
CardInstance中,由Board持有unique_ptr<CardInstance>
这样既节省内存,又能保持所有权清晰,适合卡牌种类多的大型游戏。
总结
回到你的问题,你觉得用shared_ptr不优雅是对的——因为你的场景根本不需要共享所有权!优先选择方案1,明确Board是打出后卡牌的所有者,GameLogic通过Board访问卡牌,这样既符合智能指针的设计原则,又能让代码结构更清晰。
内容的提问来源于stack exchange,提问作者user2146414

