
1. 继承定义继承是面向对象程序设计中最核心的特性之一它允许一个类派生类基于另一个类基类来定义从而自动获得基类的成员变量和成员函数。继承体现了类之间的层次关系是代码复用的重要手段。通过继承派生类可以复用基类中已有的功能同时可以增加新的成员或修改基类成员的行为从而构建出更复杂、更具体的类型体系。2. 原类型与新类型在继承关系中被继承的类称为基类Base Class也叫做父类或原有类型通过继承产生的新类称为派生类Derived Class也叫做子类或新类型。派生类是一种特殊的基类它既拥有基类的全部特性又具备自身新增的特性。这种“是一种”is-a的关系是继承的核心语义。3. 语法格式C 中定义派生类的基本语法格式如下class 派生类名 : 继承方式 基类名 { // 派生类新增的成员 };其中继承方式可以是 public、protected 或 private默认情况下为 private 继承。例如class Animal { public: void eat() { cout 动物在吃东西 endl; } }; class Dog : public Animal { public: void bark() { cout 狗在叫 endl; } };4. 继承过程继承的过程可以理解为编译器在创建派生类对象时会先调用基类的构造函数来初始化基类部分然后再执行派生类自身的构造函数来初始化派生类新增的成员。派生类对象在内存中的布局是基类子对象位于前部派生类新增成员位于其后。从代码复用的角度看继承的过程就是把基类的成员“吸收”进派生类并根据继承方式调整这些成员的访问权限最终形成一个完整的派生类类型。5. 继承方式C 支持三种继承方式public公有继承、protected保护继承和 private私有继承。不同的继承方式决定了基类成员在派生类中的访问权限。5.1 公有继承公有继承是最常用的继承方式它保持了基类成员的原有访问权限基类的 public 成员在派生类中仍为 publicprotected 成员仍为 protectedprivate 成员不可直接访问。公有继承体现了is-a关系。5.2 保护继承保护继承将基类的 public 成员和 protected 成员都变为派生类中的 protected 成员private 成员仍不可直接访问。保护继承常用于不希望外部直接使用基类接口但允许进一步派生的情况。5.3 私有继承私有继承将基类的 public 成员和 protected 成员都变为派生类中的 private 成员private 成员仍不可直接访问。私有继承体现的是has-a关系即组合关系外部无法通过派生类访问基类接口。不同继承方式下基类和派生类关系继承方式基类 public 成员基类 protected 成员基类 private 成员关系语义public 继承publicprotected不可访问is-aprotected 继承protectedprotected不可访问is-a受限private 继承privateprivate不可访问has-a6. 构造函数构造函数不能被继承但派生类构造函数可以显式调用基类构造函数来初始化基类部分。如果派生类构造函数没有显式调用基类构造函数编译器会默认调用基类的无参构造函数。class Animal { public: Animal(string name) : m_name(name) {} protected: string m_name; }; class Dog : public Animal { public: Dog(string name, int age) : Animal(name), m_age(age) {} private: int m_age; };构造函数的调用顺序是先基类后派生类析构函数的调用顺序则相反先派生类后基类。7. 函数隐藏当派生类中定义了与基类同名无论参数列表是否相同的成员函数时基类的同名函数在派生类中会被隐藏。这意味着通过派生类对象调用该名称的函数时只会调用派生类版本基类版本需要通过作用域限定符来访问。class Base { public: void func() { cout Base::func() endl; } void func(int x) { cout Base::func(int) endl; } }; class Derived : public Base { public: void func() { cout Derived::func() endl; } }; int main() { Derived d; d.func(); // 调用 Derived::func() // d.func(10); // 错误基类 func(int) 被隐藏 d.Base::func(10); // 通过作用域限定符访问基类版本 return 0; }函数隐藏与函数重写不同隐藏发生在非虚函数或参数列表不同的情况下而重写要求函数是虚函数且参数列表完全相同。8. 构造/析构函数继承构造函数和析构函数都不能被继承原因如下构造函数名称必须与类名相同派生类名称与基类不同因此基类构造函数无法作为派生类构造函数被继承。构造函数负责初始化本类特有成员基类构造函数无法初始化派生类新增的成员因此必须由派生类自己定义构造函数。析构函数名称也必须与类名相同同样因为类名不同基类析构函数无法被继承。析构顺序要求析构函数需要按照先派生后基类的顺序执行这要求每个类都有自己的析构函数。9. 赋值兼容原则赋值兼容原则赋值兼容规则是指在公有继承下派生类对象可以赋值给基类对象、基类引用可以绑定派生类对象、基类指针可以指向派生类对象。这是因为派生类对象“是一种”基类对象。class Animal {}; class Dog : public Animal {}; Dog dog; Animal a dog; // 派生类对象赋值给基类对象对象切片 Animal ref dog; // 基类引用绑定派生类对象 Animal* ptr dog; // 基类指针指向派生类对象注意对象赋值会发生“对象切片”即派生类新增的成员被丢弃只保留基类部分。而引用和指针不会发生切片它们仍然指向完整的派生类对象。10. 虚函数虚函数是在基类中使用 virtual 关键字声明的成员函数它允许在派生类中被重写从而实现运行时多态。虚函数的定义格式如下class Base { public: virtual void show() // 虚函数 { cout Base::show() endl; } };虚函数必须是非静态成员函数构造函数不能是虚函数但析构函数可以是虚函数。10.1 实现原理与虚函数表虚函数的实现依赖于虚函数表vtable和虚指针vptr。每个包含虚函数的类都会生成一张虚函数表表中存放该类所有虚函数的地址。每个对象内部会有一个隐藏的虚指针指向所属类的虚函数表。当通过基类指针或引用调用虚函数时编译器会通过虚指针找到虚函数表再从表中取出对应的函数地址进行调用。这个过程在运行时完成因此称为动态绑定。class Base { public: virtual void func1() {} virtual void func2() {} }; class Derived : public Base { public: virtual void func1() override {} // 重写 func1 };在上述例子中Base 的虚函数表包含 func1 和 func2 的地址Derived 的虚函数表包含重写后的 func1 地址和继承的 func2 地址。10.2. 函数原型虚函数的函数原型函数签名包括返回类型、函数名和参数列表。要实现函数重写派生类中的虚函数必须与基类虚函数具有完全相同的函数原型返回类型、函数名、参数列表都一致。class Base { public: virtual void print(int x); // 虚函数原型 }; class Derived : public Base { public: virtual void print(int x); // 与基类原型一致构成重写 };C11 引入了 override 关键字用于显式标记重写函数编译器会检查函数原型是否匹配。10.3. 虚函数作用虚函数的主要作用是实现运行时多态即通过基类指针或引用调用虚函数时实际执行的是派生类中重写的版本。这使得程序可以在运行时根据对象的实际类型选择合适的行为增强了代码的灵活性和可扩展性。虚函数也是实现“开闭原则”的重要手段新增功能时只需增加新的派生类并重写虚函数无需修改已有代码。10.4 函数重写函数重写覆盖是指派生类中定义与基类虚函数具有相同函数原型的函数从而替换基类虚函数的实现。重写要求基类函数必须是虚函数。函数原型返回类型、函数名、参数列表必须完全相同。访问权限可以不同但通常保持相同。class Shape { public: virtual double area() { return 0; } }; class Circle : public Shape { public: virtual double area() override { return 3.14 * r * r; } private: double r; };10.5. 纯虚函数与抽象类纯虚函数是在虚函数声明末尾加上 0的虚函数它没有函数体。包含纯虚函数的类称为抽象类抽象类不能实例化对象只能作为基类被继承。class Shape { public: virtual double area() 0; // 纯虚函数 virtual ~Shape() {} }; class Circle : public Shape { public: virtual double area() override { return 3.14 * r * r; } };派生类必须实现基类的所有纯虚函数否则派生类也是抽象类不能实例化。抽象类用于定义接口规范强制派生类实现特定行为。10.6. 虚析构函数当通过基类指针删除派生类对象时如果基类析构函数不是虚函数则只会调用基类析构函数派生类析构函数不会被调用导致派生类资源无法释放造成内存泄漏。将基类析构函数声明为虚函数可以解决这个问题。class Base { public: virtual ~Base() { cout Base 析构 endl; } }; class Derived : public Base { public: ~Derived() { cout Derived 析构 endl; } }; int main() { Base* p new Derived(); delete p; // 先调用 Derived 析构再调用 Base 析构 return 0; }11. 多继承多继承是指一个派生类同时继承多个基类。C 支持多继承派生类会包含所有基类的子对象。class A { public: void funcA() { cout A::funcA() endl; } }; class B { public: void funcB() { cout B::funcB() endl; } }; class C : public A, public B { public: void funcC() { cout C::funcC() endl; } }; int main() { C c; c.funcA(); // 调用 A 的成员函数 c.funcB(); // 调用 B 的成员函数 c.funcC(); // 调用 C 自己的成员函数 return 0; }多继承可能带来二义性问题如果多个基类中有同名成员派生类访问时需要指定基类作用域。12. 虚继承虚继承用于解决多继承中的菱形继承问题。当派生类通过多条路径继承同一个基类时该基类会在派生类中产生多份拷贝造成数据冗余和二义性。虚继承让共同基类只保留一份拷贝。class A { public: int value; }; class B : virtual public A {}; class C : virtual public A {}; class D : public B, public C {}; // D 中只有一份 A 的拷贝虚继承通过虚基类表vbptr实现编译器会调整成员访问路径确保共同基类只有一份实例。13. 多态多态是指同一操作作用于不同对象时产生不同的行为。C 中的多态分为编译时多态和运行时多态。13.1 编译时多态编译时多态是指在程序编译阶段就能确定调用哪个函数的多态形式也称为静态多态或早期绑定。它通过函数重载、运算符重载和模板实现编译器根据函数名、参数类型和参数数量在编译期完成函数匹配运行时不产生额外查找开销因此执行效率较高。下面通过函数重载演示编译时多态#include iostream using namespace std; class Calculator { public: int add(int a, int b) { return a b; } double add(double a, double b) { return a b; } int add(int a, int b, int c) { return a b c; } }; int main() { Calculator calc; cout calc.add(1, 2) endl; // 调用 int add(int, int) cout calc.add(1.5, 2.5) endl; // 调用 double add(double, double) cout calc.add(1, 2, 3) endl; // 调用 int add(int, int, int) return 0; }编译时多态通过函数重载和运算符重载实现编译器在编译阶段根据参数类型和数量确定调用哪个函数。13.2 运行时多态运行时多态是指在程序运行阶段才能确定调用哪个函数的多态形式也称为动态多态或后期绑定。它通过虚函数和继承实现程序在运行时根据对象的实际类型借助虚函数表和虚指针动态确定调用哪个函数版本从而让同一操作在不同对象上表现出不同行为。下面通过虚函数演示运行时多态#include iostream using namespace std; class Shape { public: virtual void draw() { cout 绘制图形 endl; } }; class Circle : public Shape { public: virtual void draw() override { cout 绘制圆形 endl; } }; class Rectangle : public Shape { public: virtual void draw() override { cout 绘制矩形 endl; } }; void render(Shape s) { s.draw(); // 运行时动态绑定 } int main() { Circle c; Rectangle r; render(c); // 输出绘制圆形 render(r); // 输出绘制矩形 return 0; }运行时多态通过虚函数和继承实现程序在运行阶段根据对象的实际类型动态确定调用哪个函数版本。为什么析构函数建议设置为虚函数析构函数建议设置为虚函数的原因当通过基类指针删除派生类对象时如果基类析构函数不是虚函数则只会调用基类析构函数派生类中动态分配的资源无法释放造成内存泄漏。将基类析构函数声明为虚函数后delete 操作会先调用派生类析构函数再调用基类析构函数确保资源完整释放。只要类可能被继承且可能通过基类指针删除派生类对象就应该将析构函数声明为虚函数。构造函数为什么不能设置为虚函数构造函数不能设置为虚函数原因如下虚函数表尚未建立对象在构造过程中虚指针和虚函数表还未初始化完成无法通过虚函数表进行动态绑定。构造顺序要求构造函数必须按照先基类后派生类的顺序执行而虚函数机制要求先确定对象的实际类型这在构造阶段无法完成。语义矛盾虚函数用于实现运行时多态而构造函数的作用是创建对象并初始化此时对象还不存在多态没有意义。14. 总结本文系统梳理了 C 继承与多态的完整知识体系核心要点如下继承的本质继承允许派生类基于基类复用代码体现is-a关系通过 public、protected、private 三种继承方式控制基类成员在派生类中的访问权限。构造与析构构造函数和析构函数不能被继承构造顺序为先基类后派生类析构顺序相反派生类构造函数需显式调用基类构造函数完成基类部分初始化。函数隐藏与重写同名非虚函数构成隐藏需作用域限定符访问基类版本虚函数且原型完全一致才构成重写实现运行时多态。虚函数机制虚函数通过虚函数表vtable和虚指针vptr实现动态绑定纯虚函数定义抽象类接口强制派生类实现基类析构函数建议声明为虚函数避免通过基类指针删除派生类对象时造成资源泄漏。多继承与虚继承多继承可能带来二义性需指定基类作用域虚继承通过虚基类表解决菱形继承问题确保共同基类只有一份拷贝。多态的实现编译时多态通过函数重载、运算符重载和模板在编译期确定调用运行时多态通过虚函数和继承在运行期动态绑定是面向对象设计的核心能力。掌握继承与多态是深入理解 C 面向对象编程的关键。建议读者结合代码示例动手实践重点体会虚函数表机制、构造析构顺序以及多态在实际项目中的应用场景。