目录
在 C++ 编程中,容器(如vector
、list
、map
等)和继承是两个强大的语言特性。当它们结合使用时,可以构建出高度灵活且可扩展的系统。然而,这种组合也带来了许多复杂性和潜在的陷阱。
一、容器基础回顾
1.1 容器分类
C++ 标准库提供了多种容器,主要分为以下几类:
- 顺序容器:
vector
、deque
、list
、forward_list
- 关联容器:
set
、multiset
、map
、multimap
- 无序容器:
unordered_set
、unordered_multiset
、unordered_map
、unordered_multimap
- 容器适配器:
stack
、queue
、priority_queue
1.2 容器的共性
所有容器都支持一些基本操作,如:
- 插入元素:
push_back()
、insert()
等 - 删除元素:
erase()
、pop_back()
等 - 访问元素:
[]
、at()
、front()
、back()
等 - 迭代器操作:
begin()
、end()
等
1.3 容器的内存管理
不同容器有不同的内存管理策略:
vector
:连续内存,动态扩展list
:双向链表,非连续内存map
:红黑树,动态平衡
二、继承基础回顾
2.1 继承的基本概念
继承是面向对象编程的核心概念之一,它允许一个类(派生类)继承另一个类(基类)的属性和方法。
class Shape {
public:
virtual double area() const = 0;
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override { return 3.14 * radius * radius; }
};
2.2 多态与虚函数
通过虚函数和动态绑定,基类指针可以根据实际对象类型调用相应的函数。
void printArea(const Shape& shape) {
std::cout << "Area: " << shape.area() << std::endl;
}
Circle circle(5.0);
printArea(circle); // 动态调用 Circle::area()
2.3 抽象类与纯虚函数
包含纯虚函数的类称为抽象类,不能实例化,只能作为基类。
class Shape {
public:
virtual double area() const = 0; // 纯虚函数
};
三、容器中存储基类指针
3.1 为什么需要存储基类指针
考虑一个图形库,需要存储多种不同类型的图形:
#include <vector>
#include <memory>
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() {}
};
class Circle : public Shape {
// ...
};
class Rectangle : public Shape {
// ...
};
// 存储基类指针的容器
std::vector<Shape*> shapes;
shapes.push_back(new Circle(5.0));
shapes.push_back(new Rectangle(3.0, 4.0));
3.2 使用智能指针管理动态内存
为避免内存泄漏,推荐使用智能指针:
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));
// 遍历容器并调用虚函数
for (const auto& shape : shapes) {
std::cout << "Area: " << shape->area() << std::endl;
}
3.3 容器中存储基类指针的优缺点
- 优点:
- 实现多态,可存储不同派生类对象
- 统一接口,简化代码
- 缺点:
- 手动内存管理容易出错(使用智能指针可解决)
- 切片问题(见下文)
- 性能开销(虚函数调用)
四、对象切片问题
4.1 什么是对象切片
当将派生类对象直接赋值给基类对象时,派生类的特有部分会被 “切掉”,只保留基类部分。
Circle circle(5.0);
Shape shape = circle; // 对象切片,只复制基类部分
4.2 对象切片的危害
对象切片会导致多态失效:
void printArea(Shape shape) { // 按值传递,发生切片
std::cout << "Area: " << shape.area() << std::endl;
}
Circle circle(5.0);
printArea(circle); // 调用的是 Shape::area(),而非 Circle::area()
4.3 避免对象切片的方法
- 使用基类指针或引用:
void printArea(const Shape& shape) { // 按引用传递,避免切片
std::cout << "Area: " << shape.area() << std::endl;
}
- 使用容器存储指针或智能指针:
std::vector<std::shared_ptr<Shape>> shapes;
shapes.push_back(std::make_shared<Circle>(5.0));
五、容器与继承的性能考虑
5.1 虚函数调用开销
虚函数通过虚函数表实现,会有一定的性能开销:
- 额外的内存访问(虚表指针)
- 运行时查找
5.2 内存布局与缓存
存储指针的容器可能导致内存碎片化,影响缓存命中率:
// 内存碎片化示例
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));
// 每个对象可能位于不同的内存位置
5.3 优化建议
- 对于性能敏感的应用,考虑使用静态多态(模板)代替动态多态
- 使用内存池或对象池减少内存分配开销
- 对容器进行预分配(
reserve()
)减少重新分配次数
六、容器与继承的实际应用案例
6.1 图形库实现
#include <iostream>
#include <vector>
#include <memory>
#include <string>
// 手动实现 make_unique
#if __cplusplus < 201402L
namespace std {
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
}
#endif
class Shape {
public:
virtual double area() const = 0;
virtual std::string name() const = 0;
virtual ~Shape() {}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override { return 3.14 * radius * radius; }
std::string name() const override { return "Circle"; }
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override { return width * height; }
std::string name() const override { return "Rectangle"; }
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));
for (const auto& shape : shapes) {
std::cout << shape->name() << " area: " << shape->area() << std::endl;
}
return 0;
}
6.2 游戏角色系统
#include <iostream>
#include <vector>
#include <memory>
class Character {
public:
virtual void attack() = 0;
virtual void defend() = 0;
virtual ~Character() {}
};
class Warrior : public Character {
public:
void attack() override { std::cout << "Warrior attacks with sword!" << std::endl; }
void defend() override { std::cout << "Warrior defends with shield!" << std::endl; }
};
class Mage : public Character {
public:
void attack() override { std::cout << "Mage casts fireball!" << std::endl; }
void defend() override { std::cout << "Mage uses magic barrier!" << std::endl; }
};
int main() {
std::vector<std::shared_ptr<Character>> party;
party.push_back(std::make_shared<Warrior>());
party.push_back(std::make_shared<Mage>());
for (const auto& character : party) {
character->attack();
character->defend();
}
return 0;
}
6.3 事件系统
#include <iostream>
#include <vector>
#include <memory>
// 手动实现 make_unique
#if __cplusplus < 201402L
namespace std {
template<typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args) {
return unique_ptr<T>(new T(std::forward<Args>(args)...));
}
}
#endif
class Event {
public:
virtual void process() = 0;
virtual ~Event() {}
};
class MouseEvent : public Event {
private:
int x, y;
public:
MouseEvent(int x, int y) : x(x), y(y) {}
void process() override {
std::cout << "Processing mouse event at (" << x << ", " << y << ")" << std::endl;
}
};
class KeyEvent : public Event {
private:
char key;
public:
KeyEvent(char k) : key(k) {}
void process() override {
std::cout << "Processing key event: " << key << std::endl;
}
};
class EventManager {
private:
std::vector<std::unique_ptr<Event>> events;
public:
void addEvent(std::unique_ptr<Event> event) {
events.push_back(std::move(event));
}
void processAllEvents() {
for (auto& event : events) {
event->process();
}
events.clear();
}
};
int main() {
EventManager manager;
manager.addEvent(std::make_unique<MouseEvent>(100, 200));
manager.addEvent(std::make_unique<KeyEvent>('A'));
manager.processAllEvents();
return 0;
}
七、容器与继承的常见问题及解决方案
7.1 动态内存管理问题
- 问题:手动管理指针容易导致内存泄漏
- 解决方案:使用智能指针(
std::unique_ptr
、std::shared_ptr
)
7.2 线程安全问题
- 问题:多线程环境下访问容器和对象可能导致竞态条件
- 解决方案:使用同步机制(如
std::mutex
)或原子操作
7.3 容器选择问题
- 问题:选择不适合的容器类型影响性能
- 解决方案:
- 频繁随机访问:使用
vector
- 频繁插入删除:使用
list
- 键值对存储:使用
map
或unordered_map
- 频繁随机访问:使用
7.4 对象生命周期管理
- 问题:容器中的对象生命周期管理不当导致悬空指针
- 解决方案:
- 确保对象生命周期长于容器
- 使用智能指针自动管理生命周期
八、总结
容器与继承是 C++ 中两个强大的语言特性,它们的结合可以构建出高度灵活且可扩展的系统。但在使用过程中,需要注意以下几点:
- 优先使用智能指针:避免手动内存管理,减少内存泄漏风险
- 警惕对象切片:始终使用指针或引用存储对象,避免按值传递
- 考虑性能开销:虚函数调用和内存碎片化可能影响性能
- 选择合适的容器:根据实际需求选择最适合的容器类型
- 注意线程安全:在多线程环境中使用适当的同步机制
通过合理运用容器与继承的组合,可以设计出更加优雅、高效的 C++ 程序。