目录一、一个没有多态的问题二、虚函数的基本语法声明虚函数重写虚函数通过基类指针/引用调用三、override关键字C11它的作用常见错误被拦截正确写法四、静态绑定 vs 动态绑定静态绑定编译时确定动态绑定运行时确定对比表五、完整例子动物园的动物叫声六、虚函数的限制七、常见错误1. 忘记写virtual导致静态绑定2. 重写时签名不匹配但忘了override3. 通过对象而非指针/引用调用虚函数4. 在构造函数或析构函数中调用虚函数八、这一篇的收获一、一个没有多态的问题假设你有一个图形绘制程序需要处理多种形状圆形、矩形、三角形。没有多态的写法可能是这样cppclass Circle { public: void draw() { cout 画一个圆 endl; } }; class Rectangle { public: void draw() { cout 画一个矩形 endl; } }; class Triangle { public: void draw() { cout 画一个三角形 endl; } }; // 需要一个一个处理每种形状单独写代码 Circle c; Rectangle r; Triangle t; c.draw(); r.draw(); t.draw();如果有一个“形状数组”想统一调用draw()就麻烦了——因为编译器不知道每个位置到底是什么类型。多态的解决方案让所有形状继承自一个共同的基类Shape然后通过基类指针来操作。cppclass Shape { public: virtual void draw() { cout 画一个形状 endl; } }; class Circle : public Shape { public: void draw() override { cout 画一个圆 endl; } }; class Rectangle : public Shape { public: void draw() override { cout 画一个矩形 endl; } }; // 现在可以统一处理了 Shape* shapes[] {new Circle(), new Rectangle()}; for (int i 0; i 2; i) { shapes[i]-draw(); // 输出画一个圆 / 画一个矩形 }这就是多态的魅力写代码时不知道具体类型运行时决定调用哪个函数。二、虚函数的基本语法声明虚函数在基类中用virtual关键字声明一个成员函数为虚函数cppclass Base { public: virtual void show() { cout Base::show endl; } };重写虚函数在派生类中定义一个签名完全相同的函数来重写cppclass Derived : public Base { public: void show() override { cout Derived::show endl; } };通过基类指针/引用调用cppBase* ptr new Derived(); ptr-show(); // 输出 Derived::show动态绑定 Base ref *new Derived(); ref.show(); // 也是 Derived::show关键只有通过指针或引用调用虚函数时才会发生动态绑定。直接通过对象调用是静态绑定。cppDerived d; d.show(); // 静态绑定编译时就确定调用Derived::show Base b d; // 对象切片 b.show(); // 静态绑定调用Base::show因为b是Base对象三、override关键字C11override是C11引入的关键字强烈建议使用。它的作用明确表达意图告诉读者这个函数是要重写基类的虚函数让编译器帮你检查如果基类没有对应的虚函数签名不匹配编译报错常见错误被拦截cppclass Base { public: virtual void func(int x) {} }; class Derived : public Base { public: void func(double x) override { // ❌ 编译错误 // 基类没有 func(double)签名不匹配 } };没有override的话这只是一个隐藏不是重写不会报错程序逻辑就错了。有了override编译器会帮你发现。正确写法cppclass Derived : public Base { public: void func(int x) override { // ✅ 正确重写 // ... } };四、静态绑定 vs 动态绑定这是理解虚函数的关键。静态绑定编译时确定cppBase obj; obj.show(); // 编译时就确定了调用Base::show编译器看到obj的类型是Base直接生成调用Base::show的代码。动态绑定运行时确定cppBase* ptr getShape(); // 运行时才知道ptr指向什么 ptr-show(); // 运行时才知道调用哪个show编译器不知道ptr到底指向Base、Circle还是Rectangle所以它生成一段代码通过虚函数表在运行时查找真正的函数地址下一讲详细讲。对比表特性静态绑定动态绑定决定时机编译时运行时函数类型非虚函数虚函数调用方式对象名调用指针/引用调用性能快直接调用稍慢查表开销五、完整例子动物园的动物叫声cpp#include iostream #include vector #include string using namespace std; // 基类动物 class Animal { protected: string name; public: Animal(string n) : name(n) {} // 虚函数每个动物叫法不同 virtual void speak() const { cout name 发出某种声音 endl; } // 虚析构函数重要下篇详细讲 virtual ~Animal() {} }; // 派生类狗 class Dog : public Animal { public: Dog(string n) : Animal(n) {} void speak() const override { cout name 汪汪叫汪汪 endl; } void wagTail() const { cout name 摇尾巴 endl; } }; // 派生类猫 class Cat : public Animal { public: Cat(string n) : Animal(n) {} void speak() const override { cout name 喵喵叫喵~ endl; } void climb() const { cout name 爬树 endl; } }; // 派生类鸟 class Bird : public Animal { public: Bird(string n) : Animal(n) {} void speak() const override { cout name 叽叽喳喳啾啾 endl; } void fly() const { cout name 飞翔 endl; } }; // 让动物们依次叫多态的核心用法 void makeAllSpeak(const vectorAnimal* animals) { cout \n 动物大合唱 endl; for (const auto* animal : animals) { animal-speak(); // 动态绑定调用实际类型的版本 } } int main() { // 创建动物数组全部用基类指针指向 vectorAnimal* zoo; zoo.push_back(new Dog(旺财)); zoo.push_back(new Cat(咪咪)); zoo.push_back(new Bird(小小)); zoo.push_back(new Animal(未知生物)); // 统一调用speak每个动物发出自己的叫声 makeAllSpeak(zoo); // 注意通过基类指针无法访问派生类特有函数 // zoo[0]-wagTail(); // ❌ 编译错误Animal没有wagTail // 如果需要访问派生类特有函数需要用dynamic_cast后续章节 // 释放内存 for (auto* animal : zoo) { delete animal; } return 0; }输出text 动物大合唱 旺财汪汪叫汪汪 咪咪喵喵叫喵~ 小小叽叽喳喳啾啾 未知生物发出某种声音注意到没有makeAllSpeak函数只知道参数是Animal*但实际执行时每个动物都发出了自己特有的叫声。这就是多态的力量。六、虚函数的限制不是所有函数都可以是虚函数函数类型可以是虚函数吗原因普通成员函数✅ 可以最常见的虚函数析构函数✅ 可以非常推荐下篇讲静态函数❌ 不可以静态函数属于类不属于对象没有this构造函数❌ 不可以对象还没构造完虚表未建立内联函数⚠️ 不保证虚函数动态绑定内联是编译时展开通常会被忽略友元函数❌ 不可以友元不是成员函数七、常见错误1. 忘记写virtual导致静态绑定cppclass Base { public: void show() { cout Base endl; } // 忘了virtual }; class Derived : public Base { public: void show() { cout Derived endl; } }; Base* p new Derived(); p-show(); // 输出Base因为静态绑定没有多态2. 重写时签名不匹配但忘了overridecppclass Base { public: virtual void func(int x) {} }; class Derived : public Base { public: void func(double x) {} // 参数不同这是隐藏不是重写 // 没有override编译器不报错但逻辑错了 };3. 通过对象而非指针/引用调用虚函数cppDerived d; Base b d; // 对象切片派生类部分被切掉了 b.show(); // 调用Base::show不是多态4. 在构造函数或析构函数中调用虚函数cppclass Base { public: Base() { show(); } // 调用Base::show不会调用Derived::show virtual void show() { cout Base endl; } }; class Derived : public Base { public: void show() override { cout Derived endl; } };在构造期间派生类部分还没构造完成虚表指向基类所以不会发生多态。八、这一篇的收获你现在应该理解用virtual声明虚函数用override标记重写通过基类指针或引用调用虚函数实现动态绑定多态动态绑定的决策在运行时静态绑定的决策在编译时多态让代码可以对扩展开放加新形状不用改旧代码符合开闭原则 小作业写一个MediaPlayer基类有虚函数play()。派生类MP3Player、VideoPlayer、StreamingPlayer各自重写play()。写一个函数playMedia(MediaPlayer)传入不同播放器时播放各自的内容。下一篇预告第15篇《多态二虚函数表vtable内存布局揭秘》——虚函数到底是怎么实现的为什么会有性能开销虚函数表vtable和虚指针vptr的原理是什么了解这些你才算真正精通C多态。