目录引言一、多态的概念二、多态的定义及实现一构成条件二虚函数的深入理解三虚函数的重写覆盖三、抽象类一概念二接口继承和实现继承四、多态的原理一虚函数表二动态绑定与静态绑定五、单继承和多继承关系中的虚函数表一单继承中的虚函数表二多继承中的虚函数表六、C11 override 和 final一final二override七、重载、覆盖重写、隐藏重定义的对比八、结语引言在 C 编程世界里多态是一个强大且迷人的特性。它允许我们以统一的接口来处理不同类型的对象实现 “一种接口多种形态”。这不仅提升了代码的灵活性与可维护性还极大地增强了程序的扩展性。今天就让我们深入探究 C 多态的奥秘。一、多态的概念多态通俗来讲就是同一行为在不同对象上表现出不同的状态。就像现实生活中买票这个行为普通人买票是全价学生买票是半价军人买票可能有优先特权。在 C 中多态使得我们可以用相同的函数调用根据对象类型的不同执行不同的操作。二、多态的定义及实现一构成条件1. 虚函数被 virtual 关键字修饰的类成员函数。--子类可以不加virtual推荐都写class Person { public: virtual void BuyTicket() { std::cout 买票 - 全价 std::endl; } };2. 虚函数覆盖派生类中定义与基类虚函数返回值类型、函数名字、参数列表完全相同的虚函数。class Student : public Person { public: virtual void BuyTicket() { std::cout 买票 - 半价 std::endl; } };3. 通过基类指针或引用调用虚函数不可用子类指针或引用调用虚函数- 使用父类指针或引用可以指向不同的子类对象通过相同的父类接口来调用不同子类实现的方法从而体现多态性。如果仅使用子类指针或引用只能调用子类自身定义或重写的方法无法通过统一的父类接口来实现多种不同的行为也就无法充分体现多态的特性。不可用父类对象父类对象调用的方法是在父类中定义的方法不会调用子类重写后的方法。多态的核心是根据对象的实际类型来决定调用哪个类中重写的方法而父类对象的类型是固定的不能根据不同的情况表现出不同的行为无法体现多态性。class Person { public: virtual void BuyTicket() { std::cout 买票 - 全价 std::endl; } }; class Student : public Person { public: virtual void BuyTicket() { std::cout 买票 - 半价 std::endl; } }; void Func(Person people) { people.BuyTicket(); } int main() { Person Mike; Student Johnson; Func(Mike); Func(Johnson); return 0; }在上述代码中 Func 函数通过基类 Person 的引用调用 BuyTicket 虚函数根据传入对象是 Person 还是 Student 会分别执行对应的 BuyTicket 函数版本实现多态行为。二虚函数的深入理解虚函数是实现多态的关键。当类中包含虚函数时编译器会为该类生成一个虚函数表虚表不同于继承每个对象中会包含一个指向虚函数表的指针 vptr 。在调用虚函数时程序通过对象的 vptr 找到虚函数表再从表中获取对应函数的地址进行调用。class Base { public: virtual void Func1() { std::cout Base::Func1 std::endl; } virtual void Func2() { std::cout Base::Func2 std::endl; } private: int _b 1; };对于包含虚函数的 Base 类对象其大小除了成员变量占用的空间外还会包含一个 vptr 的大小通常在 32 位系统下为 4 字节64 位系统下为 8 字节。三虚函数的重写覆盖派生类重写基类虚函数时需满足函数签名返回值类型、函数名、参数列表完全相同。不过存在两个例外1.协变基类与派生类虚函数返回值类型不同即基类虚函数返回基类对象的指针或引用派生类虚函数返回派生类对象的指针或引用。例如class A {}; class B : public A {}; class Person { public: virtual A* f() { return new A; } }; class Student : public Person { public: virtual B* f() { return new B; } };1.析构函数的重写如果基类的析构函数为虚函数派生类析构函数只要定义无论是否加 virtual 关键字都与基类的析构函数构成重写。尽管函数名不同基类 ~Person 派生类 ~Student 但编译器会对析构函数名称做特殊处理统一成 destructor。class Person { public: virtual ~Person() { std::cout ~Person() std::endl; } }; class Student : public Person { public: virtual ~Student() { std::cout ~Student() std::endl; } };三、抽象类一概念包含纯虚函数在虚函数声明后面加上 0 的类称为抽象类也叫接口类。抽象类不能实例化对象派生类继承抽象类后只有重写纯虚函数才能实例化对象。纯虚函数规范了派生类必须实现的接口。class Car { public: virtual void Drive() 0; }; class Benz : public Car { public: virtual void Drive() { std::cout Benz - 舒适 std::endl; } }; class BMW : public Car { public: virtual void Drive() { std::cout BMW - 操控 std::endl; } }; void Test() { Car* pBenz new Benz; pBenz-Drive(); Car* pBMW new BMW; pBMW-Drive(); }二接口继承和实现继承普通函数的继承是实现继承派生类继承基类函数的实现可以直接使用函数。虚函数的继承是接口继承派生类继承基类虚函数的接口目的是为了重写以达成多态。所以如果不实现多态就不要把函数定义成虚函数。四、多态的原理一虚函数表当类中有虚函数时编译器会为类创建虚函数表。以简单的单继承为例class Base { public: virtual void Func1() { std::cout Base::Func1 std::endl; } private: int _b 1; }; class Derive : public Base { public: virtual void Func1() { std::cout Derive::Func1 std::endl; } private: int _d 2; };在上述代码中 Base 类对象包含 vptr 和成员变量 _b Derive 类对象包含从 Base 继承的部分 vptr 和 _b 以及自身的成员变量 _d 。 Derive 重写了 Func1 其虚函数表中 Func1 的地址是 Derive::Func1 的地址而从 Base 继承的虚函数如还有其他未重写虚函数会将基类虚函数表中的对应函数地址拷贝过来。1. 派生类对象d中也有一个虚表指针d对象由两部分构成一部分是父类继承下来的成员虚表指针也就是存在部分的另一部分是自己的成员。2. 基类b对象和派生类d对象虚表是不一样的这里我们发现Func1完成了重写所以d的虚表中存的是重写的Derive::Func1所以虚函数的重写也叫作覆盖覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法覆盖是原理层的叫法。3. 另外Func2继承下来后是虚函数所以放进了虚表Func3也继承下来了但是不是虚函数所以不会放进虚表。4. 虚函数表本质是一个存虚函数指针的指针数组一般情况这个数组最后面放了一个nullptr。5. 总结一下派生类的虚表生成a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。6. 这里还有一个我们很容易混淆的问题虚函数存在哪的虚表存在哪的 答虚函数存在虚表虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针不是虚函数虚函数和普通函数一样的都是存在代码段的只是他的指针又存到了虚表中。另外对象中存的不是虚表存的是虚表指针。那么虚表存在哪里答案是常量区二动态绑定与静态绑定1. 静态绑定又称前期绑定在程序编译期间确定程序的行为例如函数重载编译时就能确定调用哪个函数版本。2. 动态绑定又称后期绑定在程序运行期间根据具体对象的类型确定调用的具体函数多态就是通过动态绑定实现的。五、单继承和多继承关系中的虚函数表一单继承中的虚函数表class Base { public: virtual void func1() {std::cout Base::func1 std::endl;} virtual void func2() {std::cout Base::func2 std::endl;} private: int _b; }; class Derive : public Base { public: virtual void func1() {std::cout Derive::func1 std::endl;} virtual void func3() {std::cout Derive::func3 std::endl;} virtual void func4() {std::cout Derive::func4 std::endl;} private: int _d; };在单继承中派生类会拷贝基类虚函数表内容并重写被覆盖的虚函数地址新增的虚函数按声明顺序添加到虚函数表后面。观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数也可以认为是他的一个小bug。那么我们如何查看d的虚表呢下面我们使用代码打印出虚表中的函数class Base { public: virtual void func1() { std::cout Base::func1 std::endl; } virtual void func2() { std::cout Base::func2 std::endl; } private: int _b; }; class Derive : public Base { public: virtual void func1() { std::cout Derive::func1 std::endl; } virtual void func3() { std::cout Derive::func3 std::endl; } virtual void func4() { std::cout Derive::func4 std::endl; } private: int _d; }; typedef void(*VFPTR) (); void PrintVTable(VFPTR vTable[]) { // 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数 cout 虚表地址 vTable endl; for (int i 0; vTable[i] ! nullptr; i) { printf( 第%d个虚函数地址 :0X%x,-, i, vTable[i]); VFPTR f vTable[i]; f(); } cout endl; } int main() { Base b; Derive d; VFPTR * vTableb (VFPTR*)(*(int*)b); PrintVTable(vTableb); VFPTR* vTabled (VFPTR*)(*(int*)d); PrintVTable(vTabled); return 0; }二多继承中的虚函数表class Base1 { public: virtual void func1() { std::cout Base1::func1 std::endl; } virtual void func2() { std::cout Base1::func2 std::endl; } private: int b1; }; class Base2 { public: virtual void func1() { std::cout Base2::func1 std::endl; } virtual void func2() { std::cout Base2::func2 std::endl; } private: int b2; }; class Derive : public Base1, public Base2 { public: virtual void func1() { std::cout Derive::func1 std::endl; } virtual void func3() { std::cout Derive::func3 std::endl; } private: int d1; };多继承时派生类会有多个虚函数表分别对应各个基类未重写的虚函数放在第一个继承基类部分的虚函数表中重写的虚函数会更新对应虚函数表中的函数地址。但是我们观察到六、C11 override 和 final一finalC98禁止继承的方式私有构造函数禁止外部创建对象核心逻辑构造函数私有化 → 外部无法通过new/ 栈方式直接创建对象类内提供静态方法作为唯一创建入口控制对象创建规则。示例#include iostream using namespace std; class SingleObj { private: // 1. 私有构造函数外部不能直接创建 SingleObj() { cout 对象创建仅类内可调用 endl; } public: // 2. 静态方法对外提供唯一创建入口 static SingleObj* createObj() { return new SingleObj(); // 类内可访问私有构造 } // 测试方法 void show() { cout 对象可用 endl; } }; int main() { // 错误构造函数私有外部无法直接创建 // SingleObj obj; // SingleObj* p new SingleObj(); // 正确通过静态方法创建 SingleObj* obj SingleObj::createObj(); obj-show(); return 0; }私有析构函数禁止外部销毁对象核心逻辑析构函数私有化 → 外部无法直接delete对象避免内存泄漏 / 非法销毁类内提供成员方法作为唯一销毁入口控制对象释放规则。示例#include iostream using namespace std; class SafeObj { private: // 1. 私有构造仅类内创建 SafeObj() { cout 对象创建 endl; } // 2. 私有析构仅类内销毁 ~SafeObj() { cout 对象销毁 endl; } public: // 创建入口 static SafeObj* create() { return new SafeObj(); } // 销毁入口类内可访问私有析构 void destroy() { delete this; // 仅通过该方法释放对象 } }; int main() { SafeObj* obj SafeObj::create(); // 错误析构私有外部不能delete // delete obj; // 正确通过类内方法销毁 obj-destroy(); return 0; }C11禁止继承的方式final修饰虚函数表示该虚函数不能再被重写同时也修饰类表示类不能被继承。修饰虚函数表示该虚函数不能再被重写class Car { public: virtual void Drive() final {} }; class Benz : public Car { public: // 编译报错Drive不能被重写 virtual void Drive() { std::cout Benz - 舒适 std::endl; } };二overrideoverride 用于检查派生类虚函数是否重写了基类某个虚函数如果没有重写则编译报错。例如class Car { public: virtual void Drive() {} }; class Benz : public Car { public: virtual void Drive() override { std::cout Benz - 舒适 std::endl; } };七、重载、覆盖重写、隐藏重定义的对比类别作用域条件重载两个函数在同一作用域。函数名相同参数不同个数、类型、顺序。覆盖重写两个函数分别在基类和派生类的作用域。函数名、参数、返回值都必须相同协变除外且函数必须是虚函数。隐藏重定义两个函数分别在基类和派生类的作用域。函数名相同基类和派生类的同名函数不构成重写就是重定义。八、结语C 多态是一个复杂而精妙的特性从概念到实现从原理到应用都有着丰富的内涵。掌握多态能让我们编写出更加灵活、可维护和高效的代码。在实际编程中合理运用多态结合虚函数、抽象类等知识可以更好地应对各种复杂的业务需求。希望通过这篇博客大家能对 C 多态有更深入的理解和掌握。