C++ 动态多态与静态多态:虚函数和 CRTP 的原理与实战对比

文摘   2024-12-20 17:23   浙江  

多态这个词听起来很高深,但其实就是让一段代码在不同场景下能表现出“多种形态”。多态的核心思想是“写一次代码,用在多种情况下”。C++ 里,多态分成两种: 动态多态 和 静态多态 。动态多态靠的是虚函数,而静态多态一般用 CRTP(Curiously Recurring Template Pattern)这种模板技巧来实现。我们今天就来聊聊这两种多态的原理、用法和各自的优缺点。


1


动态多态:虚函数的魔法




动态多态是 C++ 的经典特性。它通过虚函数实现,让子类可以重写父类的行为,从而实现“运行时的多态”。简单来说,就是程序运行时才决定用哪个函数。


虚函数的基本原理

虚函数的实现靠的是“虚函数表”(vtable)。每个含虚函数的类,背后都有一张虚函数表,记录了这个类的虚函数指针。对象调用虚函数时,会通过这张表找到对应的函数。


下面是个简单的例子:


#include <iostream>

using namespace std;

class Animal {

public:

virtual void speak() { // 虚函数

cout << “Animal speaks” << endl;

}

};

class Dog : public Animal {

public:

void speak() override { // 重写父类的虚函数

cout << “Dog barks” << endl;

}

};

int main() {

Animal* animal = new Dog(); // 父类指针指向子类对象

animal->speak(); // 调用子类的 speak

delete animal;

return 0;

}

运行结果 :


Dog barks

这里 animal->speak() 调用的是 Dog 的 speak(),不是 Animal 的。原因是 speak 是虚函数,调用时通过虚函数表动态绑定到了 Dog 的实现。


动态多态的优缺点

优点 :


  • 灵活,支持运行时动态行为变化。
  • 易于扩展,新增子类无需修改父类代码。


缺点 :


  • 多了一层间接调用,性能比直接调用稍差。
  • 占用额外的内存(虚函数表)。


温馨提示 :如果一个类没有虚函数,它的对象通常是没有虚函数表的。不要随便加 virtual,否则会让对象变胖。


2


静态多态:CRTP 的妙用




静态多态是通过模板实现的,编译时就能确定具体的行为,避免了动态多态的运行时开销。CRTP 是实现静态多态的常用技巧,名字听起来很吓人,其实原理很简单——“子类把自己作为模板参数传给父类”。


CRTP 的基本原理

直接上代码,看了就懂:


#include <iostream>

using namespace std;

// CRTP 基类

template <typename Derived>

class Animal {

public:

void speak() {

static_cast<Derived*>(this)->speak(); // 调用子类的实现

}

};

// 子类

class Dog : public Animal<Dog> {

public:

void speak() { // 自己的实现

cout << “Dog barks” << endl;

}

};

int main() {

Dog dog;

dog.speak(); // 调用 Dog 的 speak

return 0;

}

运行结果 :


Dog barks

这里的 Animal<Dog> 是一个模板类,Dog 继承了它,并通过 static_cast 把自己传递给了基类。speak 的调用在编译时就绑定到了具体实现。


静态多态的优缺点

优点 :


  • 没有虚函数表,性能更高。
  • 编译时确定行为,代码更可控。


缺点 :


  • 写起来比较反直觉(这玩意儿看着就怪)。
  • 子类和父类耦合度高,灵活性不如动态多态。


温馨提示 :CRTP 的模板代码会在编译时展开,可能会导致编译时间变长。你要是发现代码编译慢得像蜗牛,看看是不是模板太多了。



3


实战对比:什么时候用动态多态,什么时候用静态多态?




动态多态的适用场景

  • 对象多样性
     :需要支持一大堆子类,比如动物、车辆、用户接口等。
  • 运行时灵活性
     :编译时不知道具体类型,必须靠运行时决定。


比如游戏开发中,所有的敌人都继承自一个 Enemy 基类。敌人类型多种多样,动态多态可以轻松应对。


class Enemy {

public:

virtual void attack() = 0; // 纯虚函数

};

class Orc : public Enemy {

public:

void attack() override {

cout << “Orc attacks with axe!” << endl;

}

};

class Goblin : public Enemy {

public:

void attack() override {

cout << “Goblin throws dagger!” << endl;

}

};

静态多态的适用场景

  • 性能敏感
     :比如嵌入式系统或游戏引擎的核心部分,时间和空间都很宝贵。
  • 类型固定
     :子类类型在编译时就能确定,不需要运行时灵活性。


比如数学库中,向量、矩阵等类型的运算,使用 CRTP 可以让性能最大化。


template <typename Derived>

class Vector {

public:

void add(const Derived& other) {

static_cast<Derived*>(this)->add(other);

}

};

class Vector2D : public Vector<Vector2D> {

public:

void add(const Vector2D& other) {

cout << “Adding 2D vectors” << endl;

}

};

4


动态多态 vs 静态多态




特性
动态多态
静态多态
绑定时间
运行时
编译时
性能
较低(虚函数表查找开销)
较高(无额外开销)
灵活性
高(运行时决定行为)
低(编译时决定行为)
扩展性
强(新增子类无需改基类)
弱(改基类可能影响子类)
复杂度
代码直观,使用简单
写法稍复杂,阅读费劲

这两种多态各有千秋,没有谁好谁坏。动态多态适合“灵活性优先”的场景,而静态多态适合“性能优先”的场景。一个是“跑得快”,一个是“变得快”。你可以根据实际需求,选择最合适的实现方式——这才是 C++ 的精髓。



椰子树的秘密
优质内容创作者
 最新文章