如何用智能指针实现C++多态容器?相关实现细节咨询
背景
要实现一个维护多态宠物列表的Vet类,最初的裸指针实现如下:
class Vet{ std::vector<Pet*> pets; public: void addPet(Pet* pet); };
添加宠物的代码:
Vet vet; vet.addPet(new Dog{}); vet.addPet(new Cat{});
这种方式需要手动在Vet的析构函数中释放动态分配的宠物对象,存在内存泄漏风险。改用智能指针可以自动管理内存,以下针对相关问题逐一分析:
1. std::unique_ptr与std::shared_ptr的选择及适用场景
- 优先选用std::unique_ptr:如果
Vet是宠物对象的唯一所有者(符合诊所管理当前治疗宠物的场景:宠物在治疗期间归属诊所,结束后不再由诊所持有),unique_ptr是最优解。它轻量无额外开销,明确表达“独占所有权”的语义,避免不必要的复杂度。 - std::shared_ptr的适用场景:仅当需要多个对象共享宠物所有权时使用(比如宠物同时被
Vet和主人对象持有)。但此场景下完全不需要,过度使用会引入引用计数开销,甚至可能引发循环引用问题。 - 无需退回裸指针:只要基类
Pet拥有虚析构函数(多态场景的必要条件,与是否使用智能指针无关),智能指针就能自动正确销毁派生类对象,完全替代裸指针的多态功能。
2. std::unique_ptr下addPet的实现方案对比
三个方案各有优劣,需根据调用习惯和语义需求选择:
方案1-1:值传递std::unique_ptr
void addPet(std::unique_ptr<Pet> pet){ pets.push_back(std::move(pet)); }
调用示例:
vet.addPet(std::make_unique<Dog>()); // 或转移已有指针的所有权 std::unique_ptr<Pet> dog = std::make_unique<Dog>(); vet.addPet(std::move(dog)); // 调用后dog变为空指针
优势:语义清晰,明确要求调用方转移所有权,避免误操作;支持直接传递临时对象(如make_unique的返回值)。
劣势:调用方若想保留原指针需提前拷贝,但此场景下本就该转移所有权,因此不构成问题。
推荐度:★★★★★
方案1-2:左值引用传递std::unique_ptr
void addPet(std::unique_ptr<Pet>& pet){ pets.push_back(std::move(pet)); }
调用示例:
std::unique_ptr<Pet> dog = std::make_unique<Dog>(); vet.addPet(dog); // 调用后dog变为空指针
优势:调用时无需显式写std::move,语法简洁。
劣势:语义模糊,调用方可能未意识到原指针会被清空;无法直接传递临时对象(如vet.addPet(std::make_unique<Dog>())会编译失败),灵活性差。
推荐度:★★☆☆☆
方案1-3:按类型枚举创建对象
enum class PetType { Dog, Cat }; void addPet(PetType pet){ if(pet == PetType::Dog) pets.push_back(std::make_unique<Dog>()); else if(pet == PetType::Cat) pets.push_back(std::make_unique<Cat>()); // 新增宠物类型需修改此处代码 }
调用示例:
vet.addPet(PetType::Dog); vet.addPet(PetType::Cat);
优势:调用方无需接触指针,封装性强。
劣势:扩展性差,新增宠物类型必须修改addPet代码,违反开闭原则;若宠物需要构造参数,实现会极度繁琐。
推荐度:★★★☆☆(仅适用于固定少量宠物类型的场景)
3. std::shared_ptr下addPet的实现方案对比
若确实需要共享所有权,两个方案的核心区别在于是否转移所有权:
方案2-1:值传递std::shared_ptr
void addPet(std::shared_ptr<Pet> pet){ pets.push_back(std::move(pet)); }
调用示例:
vet.addPet(std::make_shared<Dog>()); // 或转移已有指针的所有权 std::shared_ptr<Pet> dog = std::make_shared<Dog>(); vet.addPet(std::move(dog)); // dog的引用计数减1
优势:转移所有权时可避免一次引用计数的增减操作,性能略优。
劣势:调用方若误将仍需使用的指针move,会导致空指针问题。
推荐度:★★★☆☆
方案2-2:const左值引用传递std::shared_ptr
void addPet(const std::shared_ptr<Pet>& pet){ pets.push_back(pet); }
调用示例:
std::shared_ptr<Pet> dog = std::make_shared<Dog>(); vet.addPet(dog); // 复制指针,引用计数加1 vet.addPet(std::make_shared<Cat>()); // 支持直接传递临时对象
优势:语义安全,调用方明确知道原指针不会被修改;支持临时对象传递,使用灵活。
劣势:每次调用会增加一次引用计数,有微小性能开销,但大多数场景下可忽略。
推荐度:★★★★★
内容的提问来源于stack exchange,提问作者Sangjin Kim

