C++的多态是个啥,咋用的?
今天我来讲:多态(Polymorphism)。有人说它是面向对象的灵魂,那么,多态到底是个啥?怎么用?
一、多态是个啥?
"多态"这个词来自希腊语,意思是"多种形态"。放在编程世界里,就是:同一段代码,在不同的对象上,可以表现出不同的行为。
举个例子,假设我们有一个 Animal
类,还有两个子类 Dog
和 Cat
:
cpp
class Animal {
public:
virtual void speak() {
std::cout << "动物叫" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "汪汪" << std::endl;
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "喵喵" << std::endl;
}
};
如果写一段代码:
cpp
Animal* p1 = new Dog();
Animal* p2 = new Cat();
p1->speak(); // 输出:汪汪
p2->speak(); // 输出:喵喵
你会发现,同样是 speak()
,但是不同对象的表现不一样,这就是多态。
二、多态分哪几种?
在 C++ 里,多态主要分两类:
- 编译时多态(静态多态) 例如函数重载、模板。这是在编译期就决定了调用哪个函数。
- 运行时多态(动态多态) 也就是我们常说的虚函数 + 继承。这是运行期根据对象的真实类型决定调用哪个函数。
今天我们重点聊 运行时多态,因为这是面向对象的核心。
三、多态的三个必要条件
想在 C++ 里实现运行时多态,需要满足三个条件:
- 有继承(基类和派生类)
- 基类方法是虚函数 (
virtual
关键字) - 通过基类指针或引用调用方法
比如:
cpp
Animal* p = new Dog(); // 基类指针指向派生类
p->speak(); // 发生多态
如果不用 virtual
,就没有多态,调用的永远是 Animal
里的 speak()
。
四、底层虚函数表(vtable)!
为什么 C++ 能在运行时决定调用哪个函数? 答案是 虚函数表机制(Virtual Table)。
编译器在处理含有虚函数的类时,会做几件事:
- 为每个含虚函数的类生成一张 虚函数表(vtable),里面存放该类的虚函数地址。
- 在对象里存一个 虚表指针(vptr),指向对应类的虚表。
- 当你调用虚函数时,编译器会通过
vptr
查虚表,找到正确的函数地址再执行。
简化后的示意图:
css
Animal 对象:
[vptr] -> [Animal::speak]
Dog 对象:
[vptr] -> [Dog::speak]
所以,p->speak()
运行时调用哪一个函数,取决于 vptr
指向哪张虚表。
性能影响大吗?
虚函数调用需要一次额外的指针间接寻址,比普通函数慢一点,但代价很小。
放心用吧,你电脑撑得住~
五、用多态的正确姿势
1. 析构函数要是虚的
如果基类用指针指向派生类对象,那么基类析构函数必须是虚函数,否则会内存泄漏。
cpp
class Animal {
public:
virtual ~Animal() {} // 一定要 virtual!!!
};
如果不用 virtual
,delete p
时只会调用基类析构函数,派生类资源不会释放。
2. 避免 slicing(对象切片)
如果你用值传递,而不是指针或引用,多态会失效。
cpp
Dog d;
Animal a = d; // 切片:只拷贝 Animal 部分
a.speak(); // 输出"动物叫"
3. 多态和模板结合
C++ 的 模板 + 多态 结合非常强大,可以做到编译时多态 + 运行时多态配合,用在策略模式、插件系统等场景。
六、总结~
C++ 的多态就是运行时根据对象的实际类型,动态决定调用哪个方法 ,它通过 虚函数表 实现,是面向对象编程的核心特性之一。
正确使用多态可以让代码更灵活、更易扩展,但也要注意析构、性能和设计合理性。