C++继承机制详解:从语法到多态与设计原则 📅 2026/7/19 5:16:47 1. 项目概述为什么继承是C面向对象的基石如果你已经写过一些C的类封装了数据和函数体会到了面向对象编程带来的数据管理便利那么接下来你一定会遇到一个现实问题代码的重复。比如你写了一个Animal类里面有name、age属性和eat()、sleep()方法。现在你需要写一个Dog类它显然也有名字、年龄也会吃和睡但除此之外它还会bark()。难道要把Animal里的代码再抄一遍吗继承Inheritance就是为了解决这个问题而生的。简单说继承允许我们基于一个已有的类称为基类或父类来定义一个新的类称为派生类或子类。派生类会自动获得父类的所有成员数据和方法同时可以添加自己特有的成员或者修改继承来的方法的行为。这不仅仅是代码复用的利器更是构建复杂、层次化软件系统的核心思想。从桌面软件的UI控件按钮、文本框都继承自一个基本的Widget类到游戏引擎中的实体系统Player、Enemy都继承自Entity再到业务系统中的各种模型AdminUser继承自User继承无处不在。理解继承是真正从“写C语法”迈向“用C思想设计程序”的关键一步。它直接关系到你对多态、抽象类、接口等更高级概念的理解。很多初学者在这里会感到困惑比如搞不清public、protected、private继承的区别或者滥用继承导致代码僵化。别担心我会结合我踩过的坑带你从最实用的角度把C继承的里里外外讲清楚。2. 继承的核心概念与三种访问权限2.1 继承的基本语法与“是一个”关系继承的语法很简单在定义派生类时使用冒号:指明继承关系。class BaseClass { // 基类成员 }; class DerivedClass : access-specifier BaseClass { // 派生类新增成员 };这里的access-specifier就是访问说明符决定了派生类如何“看待”从基类继承来的成员我们稍后详解。首先必须建立一个核心认知继承应该表示“是一个is-a”的关系。也就是说DerivedClass的对象在逻辑上应该也是BaseClass的一种。Dog是一个AnimalCar是一个VehicleButton是一个Widget。如果这种关系不成立比如Circle继承Rectangle虽然它们都是形状但圆“不是一个”矩形这种设计就很糟糕会导致逻辑混乱。在决定使用继承前务必先问自己子类对象是否在任何地方都可以被当作父类对象来使用如果是那继承很合适如果不是或许组合has-a是更好的选择。2.2 public, protected, private继承深度解析这是继承中最容易混淆也最重要的部分。它控制着两件事派生类内部对基类成员的访问权限。派生类对象以及通过派生类对基类成员的访问权限。我们先回顾一下类本身的三个访问级别public: 任何地方都可以访问。protected: 只有类自身、友元和派生类可以访问。private: 只有类自身和友元可以访问。现在结合继承方式我们通过一个表格来彻底理清假设基类Base定义如下class Base { public: int public_member; protected: int protected_member; private: int private_member; };继承方式在派生类内部对基类成员的访问权限派生类对象/外部代码通过派生类访问基类成员的权限实际含义与用途public 继承public_member-publicprotected_member-protectedprivate_member-不可见只能访问从基类继承来的public成员即现在的public_member。最常用。严格表示“是一个”关系。派生类对象对外表现出的基类接口public部分保持不变。这是接口继承。protected 继承public_member-protectedprotected_member-protectedprivate_member-不可见所有从基类继承来的成员在外部都无法通过派生类对象直接访问。不常用。表示“在实现上基于...”但对外不暴露基类的接口。基类的public接口变成了派生类protected的实现细节。这是一种实现继承且关系不对外公开。private 继承public_member-privateprotected_member-privateprivate_member-不可见所有从基类继承来的成员在外部都无法通过派生类对象直接访问。可以看作一种**“用...来实现”的强组合关系。基类的所有公开和保护接口都成了派生类的私有实现。在大多数情况下使用对象组合将基类对象作为派生类的私有成员比private继承更清晰、耦合度更低**。核心记忆点与避坑指南基类的private成员在任何继承方式下对派生类都是不可见的。这是封装性的体现。如果派生类需要访问应将其设为protected。public继承是唯一能维持“是一个”关系的继承方式。如果你希望Derived对象能被用在任何需要Base对象的地方必须用public继承。protected和private继承会改变基类public成员在派生类中的访问级别这破坏了基类的接口契约通常意味着设计上可能存在更好的替代方案如组合。实战中95%以上的情况你应该使用public继承。除非你有非常特殊的理由并且清楚知道自己在做什么否则不要使用另外两种。2.3 构造与析构派生类对象的生命历程创建一个派生类对象时并不是直接从“无”到“有”。它的内部包含了一个完整的基类子对象。因此构造和析构的顺序至关重要。构造顺序由内而外首先构造基类部分。调用基类的构造函数。然后按照声明顺序初始化派生类自己的成员变量。最后执行派生类构造函数的函数体。析构顺序由外而内与构造完全相反首先执行派生类析构函数的函数体。然后按照声明顺序的逆序销毁派生类自己的成员变量。最后销毁基类部分。调用基类的析构函数。这个顺序是自动的、强制性的。你的主要职责在于如何将派生类构造函数的信息传递给基类构造函数答案是使用成员初始化列表。派生类不能直接初始化从基类继承来的成员必须通过调用基类的构造函数来完成。class Base { public: int value; Base(int v) : value(v) { // 基类构造函数 std::cout Base constructed with value std::endl; } }; class Derived : public Base { public: int extra; // 在派生类构造函数的初始化列表中调用基类构造函数 Derived(int v, int e) : Base(v), extra(e) { std::cout Derived constructed with extra extra std::endl; } }; int main() { Derived d(10, 20); // 输出 // Base constructed with 10 // Derived constructed with extra 20 }重要提示如果基类没有默认构造函数即无参构造函数或者你希望用特定参数初始化基类必须在派生类构造函数的初始化列表中显式调用基类构造函数否则编译会报错。3. 函数重写、多态与虚函数机制3.1 虚函数与动态绑定多态的实现原理继承不仅仅是为了复用代码更重要的是实现“多态”Polymorphism。多态允许我们使用基类的指针或引用来调用派生类中重写的函数这是面向对象设计最强大的特性之一。没有多态的例子class Animal { public: void speak() { std::cout Animal sound! std::endl; } }; class Dog : public Animal { public: void speak() { std::cout Woof! std::endl; } // 隐藏了基类的speak }; int main() { Dog dog; Animal* animalPtr dog; // 基类指针指向派生类对象 animalPtr-speak(); // 输出什么 - 输出 “Animal sound!” }这里animalPtr-speak()调用的是Animal::speak()而不是Dog::speak()。因为编译器在编译时根据指针的类型Animal*确定了要调用的函数地址这叫做静态绑定。为了实现我们期望的行为通过基类指针调用派生类的函数我们需要虚函数Virtual Function。class Animal { public: virtual void speak() { std::cout Animal sound! std::endl; } // 声明为虚函数 }; class Dog : public Animal { public: void speak() override { std::cout Woof! std::endl; } // 重写虚函数 }; int main() { Dog dog; Animal* animalPtr dog; animalPtr-speak(); // 输出 “Woof!” —— 正确 }在基类中将函数声明为virtual在派生类中使用override关键字C11引入强烈建议使用进行重写。此时animalPtr-speak()调用的是Dog::speak()。这是因为编译器使用了动态绑定或晚期绑定。动态绑定的原理简化版 当一个类包含虚函数时编译器会为该类的每个对象添加一个隐藏的指针通常称为vptr虚函数表指针。这个vptr指向一个属于该类的vtable虚函数表。vtable是一个函数指针数组按顺序存放了该类所有虚函数的地址。派生类在继承时会复制基类的vtable并用自己重写的函数地址替换对应的条目。当通过基类指针调用虚函数时程序会通过对象的vptr找到对应的vtable。在vtable中找到该虚函数的索引位置。调用该位置存储的函数地址。因为dog对象的vptr指向的是Dog类的vtable而Dog类的vtable中speak项指向的是Dog::speak所以最终调用了正确的函数。这就是多态的核心机制。3.2 override与final关键字让意图更清晰override明确告知编译器和你自己这个函数意在重写基类的虚函数。如果拼写错误、参数列表不匹配或者基类没有对应的虚函数编译器会报错。这能有效防止因疏忽导致的错误。class Derived : public Base { public: void someFunction() override; // 好清晰且有编译器检查 // void someFuntion() override; // 编译错误拼写错误基类无此函数 };final可以用于类或虚函数。用于类表示这个类不能被继承。class SuperFinalClass final { ... };用于虚函数表示这个虚函数在派生类中不能再被重写。class Base { public: virtual void cannotOverride() final { ... } }; class Derived : public Base { public: // void cannotOverride() override; // 编译错误函数是final的 };3.3 纯虚函数与抽象类定义接口有时基类仅仅代表一个抽象概念它无法也不应该为某个函数提供有意义的实现。例如Shape形状类有一个draw()函数但Shape本身怎么画呢它是抽象的。这时可以使用纯虚函数。class Shape { public: virtual void draw() const 0; // 纯虚函数用 0 标识 virtual double area() const 0; // 可以包含非虚函数和成员变量 void printInfo() const { std::cout Area: area() std::endl; // 可以调用纯虚函数 } };包含至少一个纯虚函数的类称为抽象类。抽象类不能实例化对象。它的作用是为所有派生类定义一个统一的接口协议。派生类必须重写所有纯虚函数否则派生类也会成为抽象类。class Circle : public Shape { public: Circle(double r) : radius(r) {} void draw() const override { std::cout Drawing a circle. std::endl; } double area() const override { return 3.14159 * radius * radius; } private: double radius; }; // Shape s; // 错误不能创建抽象类的对象 Circle c(5.0); // 正确 c.printInfo(); // 正确输出 Area: 78.5397抽象类是C中实现接口的关键机制它强制派生类遵守特定的行为规范是实现多态和设计模式如策略模式、工厂模式的基础。4. 多重继承、菱形继承与虚继承4.1 多重继承的语法与潜在问题C允许一个派生类同时从多个基类继承这就是多重继承。class InputDevice { /* ... */ }; class OutputDevice { /* ... */ }; class TouchScreen : public InputDevice, public OutputDevice { // 继承了InputDevice和OutputDevice的所有成员 /* ... */ };这看起来很强大一个TouchScreen既是输入设备又是输出设备。但多重继承引入了复杂性最著名的就是菱形继承问题Diamond Problem。4.2 菱形继承问题与虚继承解决方案考虑这个经典场景class Animal { public: int age; }; class Mammal : public Animal {}; class Bird : public Animal {}; class Bat : public Mammal, public Bird {}; // 蝙蝠既是哺乳动物又是鸟Bat对象在内存中是什么样它包含了两份Animal的子对象一份来自Mammal一份来自Bird。这导致数据冗余Bat对象里有两个age成员。二义性当访问bat.age时编译器不知道你指的是从Mammal继承来的age还是从Bird继承来的age必须使用作用域解析符bat.Mammal::age来指明这非常反直觉。我们希望的模型是Bat只包含一份Animal的公共部分。这就需要虚继承Virtual Inheritance。class Animal { public: int age; }; class Mammal : virtual public Animal {}; // 虚继承 class Bird : virtual public Animal {}; // 虚继承 class Bat : public Mammal, public Bird {};通过虚继承Mammal和Bird共享同一个Animal基类子对象。现在Bat对象中只有一份age访问bat.age也没有二义性了。4.3 虚继承的代价与使用建议虚继承解决了菱形继承问题但它是有代价的性能开销虚继承的实现通常比普通继承更复杂涉及额外的间接层虚基类指针可能影响内存访问效率。初始化责任转移在虚继承体系中最底层的派生类如Bat负责直接初始化虚基类如Animal。即使Mammal或Bird的构造函数也初始化了Animal它们对虚基类的初始化也会被Bat的初始化所忽略。class Animal { public: Animal(int a) : age(a) {} int age; }; class Mammal : virtual public Animal { public: Mammal() : Animal(1) {} // 对Bat对象无效 }; class Bird : virtual public Animal { public: Bird() : Animal(2) {} // 对Bat对象无效 }; class Bat : public Mammal, public Bird { public: Bat() : Animal(3) {} // 必须且只有这个初始化有效 };实战建议慎用多重继承。优先考虑单继承组合的方式来设计类层次。大多数情况下多重继承带来的麻烦大于便利。如果必须使用多重继承并且可能出现菱形结构务必使用虚继承。明确虚基类的初始化责任。记住是最底层的派生类负责初始化虚基类。接口类只有纯虚函数的抽象类的多重继承通常比较安全因为它们没有数据成员不会导致数据冗余问题。这也是很多现代C设计如COM、Java/C#的接口所采用的思路。5. 继承中的隐藏、切片与常见陷阱5.1 名字隐藏Name Hiding这不是多态而是一个容易踩坑的查找规则。如果派生类定义了一个与基类同名的成员函数无论参数是否相同那么基类的所有同名函数在派生类的作用域中都会被隐藏。class Base { public: void func(int x) { std::cout Base::func(int) std::endl; } void func(double x) { std::cout Base::func(double) std::endl; } }; class Derived : public Base { public: void func(const char* s) { std::cout Derived::func(const char*) std::endl; } // 隐藏了基类的所有func }; int main() { Derived d; d.func(hello); // 正确调用Derived::func // d.func(10); // 错误Base::func(int)被隐藏了 d.Base::func(10); // 正确使用作用域解析符强制调用 }即使Derived::func的参数类型是const char*它也把Base中两个参数类型不同的func都隐藏了。这是因为名字查找发生在重载决议之前。编译器先在Derived的作用域里找func找到了就不再向外层基类作用域查找了。解决方法在派生类中使用using声明将基类的同名函数引入派生类作用域使其参与重载。class Derived : public Base { public: using Base::func; // 引入Base中的所有func void func(const char* s) { std::cout Derived::func(const char*) std::endl; } }; int main() { Derived d; d.func(10); // 正确现在可以调用Base::func(int) d.func(3.14); // 正确调用Base::func(double) d.func(hello); // 正确调用Derived::func }5.2 对象切片Object Slicing这是值语义语言如C在继承中一个特有的问题。当你用一个派生类对象去初始化或赋值给一个基类对象时会发生对象切片只有派生类对象中属于基类的那部分被复制过去派生类特有的部分被“切”掉了。class Base { public: int base_data; Base(int x) : base_data(x) {} }; class Derived : public Base { public: int derived_data; Derived(int x, int y) : Base(x), derived_data(y) {} }; void processBase(Base b) { std::cout b.base_data std::endl; } int main() { Derived d(1, 999); Base b d; // 对象切片发生在这里 // b现在是一个纯粹的Base对象只包含base_data1derived_data(999)丢失了。 processBase(d); // 传值调用同样会发生切片 }更隐蔽的是如果基类有虚函数切片后的对象其vptr也会被重置为基类的vtable多态性完全丧失。如何避免切片使用指针或引用这是最根本的解决方法。通过基类指针或引用来操作派生类对象不会发生切片并且能保持多态。void processBaseRef(Base b) { /* ... */ } void processBasePtr(Base* b) { /* ... */ } processBaseRef(d); // 安全无切片 processBasePtr(d); // 安全无切片避免在容器中直接存储基类对象std::vectorBase存储Derived对象会导致切片。应该使用std::vectorBase*或智能指针std::vectorstd::unique_ptrBase。考虑将基类设为抽象类抽象类不能实例化从根本上防止了值传递导致的切片虽然指针/引用切片仍然可能但抽象类通常意味着你需要多态所以用指针/引用是合理的。5.3 构造函数与析构函数不被继承这是一个基本原则但新手容易误解。派生类不会继承基类的构造函数、析构函数、赋值运算符和友元关系。派生类需要定义自己的构造函数并在初始化列表中调用基类构造函数。派生类可以定义自己的析构函数它会在对象销毁时自动调用基类的析构函数。如果派生类需要特殊的拷贝/移动语义需要自己定义并正确调用基类的对应操作。5.4 赋值运算符的注意事项如果派生类使用了编译器生成的默认赋值运算符operator它会自动调用基类的赋值运算符来处理基类部分。但如果你在派生类中自己定义了赋值运算符你必须显式调用基类的赋值运算符否则基类部分不会被正确赋值。class Base { public: Base operator(const Base other) { if (this ! other) { base_data other.base_data; } return *this; } int base_data; }; class Derived : public Base { public: Derived operator(const Derived other) { if (this ! other) { Base::operator(other); // 必须显式调用 derived_data other.derived_data; } return *this; } int derived_data; };忘记调用Base::operator是一个常见错误会导致“部分赋值”即派生类新增成员被赋值了但基类部分还保持原样造成数据不一致。6. 实战设计一个简单的图形系统让我们用一个综合例子来串联所学知识。我们将设计一个简单的图形系统包含Shape抽象基类以及Circle、Rectangle等派生类并演示多态和对象管理的常见模式。6.1 基类与派生类定义#include iostream #include string #include vector #include memory #include cmath // 抽象基类形状 class Shape { public: Shape(const std::string name) : name_(name) {} virtual ~Shape() default; // 虚析构函数确保通过基类指针删除派生类对象时正确调用派生类析构函数 // 纯虚函数定义接口 virtual double area() const 0; virtual double perimeter() const 0; virtual void draw() const 0; // 非虚函数提供通用功能 const std::string name() const { return name_; } void printInfo() const { std::cout Shape: name_ std::endl; std::cout Area: area() std::endl; std::cout Perimeter: perimeter() std::endl; draw(); } private: std::string name_; }; // 派生类圆形 class Circle : public Shape { public: Circle(double radius) : Shape(Circle), radius_(radius) { if (radius_ 0) { throw std::invalid_argument(Radius must be positive.); } } double area() const override { return M_PI * radius_ * radius_; } double perimeter() const override { return 2 * M_PI * radius_; } void draw() const override { std::cout Drawing a circle with radius radius_ std::endl; } double radius() const { return radius_; } private: double radius_; }; // 派生类矩形 class Rectangle : public Shape { public: Rectangle(double width, double height) : Shape(Rectangle), width_(width), height_(height) { if (width_ 0 || height_ 0) { throw std::invalid_argument(Width and height must be positive.); } } double area() const override { return width_ * height_; } double perimeter() const override { return 2 * (width_ height_); } void draw() const override { std::cout Drawing a rectangle width_ x height_ std::endl; } private: double width_; double height_; };6.2 使用多态与智能指针管理对象直接存储对象会导致切片存储原始指针需要手动管理内存。现代C的最佳实践是使用智能指针。int main() { // 使用unique_ptr管理单个对象明确所有权 std::unique_ptrShape circle std::make_uniqueCircle(5.0); std::unique_ptrShape rect std::make_uniqueRectangle(4.0, 6.0); circle-printInfo(); std::cout --- std::endl; rect-printInfo(); std::cout \n Using polymorphism with a container \n; // 使用容器存储基类智能指针实现多态集合 std::vectorstd::unique_ptrShape shapes; shapes.push_back(std::make_uniqueCircle(3.0)); shapes.push_back(std::make_uniqueRectangle(2.0, 5.0)); shapes.push_back(std::make_uniqueCircle(1.5)); double totalArea 0.0; for (const auto shape : shapes) { shape-printInfo(); totalArea shape-area(); std::cout std::endl; } std::cout Total area of all shapes: totalArea std::endl; // 尝试添加一个“正方形”类作为练习。 // class Square : public Rectangle { ... }; return 0; }6.3 扩展思考与设计挑战添加新形状尝试添加一个Triangle类。你需要决定如何存储三角形的数据如三条边或底和高。注意在构造函数中进行合法性检查如三角形两边之和大于第三边。实现Square类正方形是一种特殊的矩形。你会选择让Square继承Rectangle吗这似乎符合“是一个”关系。但这里有个陷阱如果Rectangle有setWidth和setHeight方法Square继承它们就会破坏正方形的约束宽高相等。这引出了面向对象设计中的一个著名问题“正方形-矩形问题”。它提醒我们继承关系不仅要看“是一个”还要考虑行为是否可替代Liskov Substitution Principle, LSP。一个更好的设计可能是让Square和Rectangle都继承自一个更通用的Quadrilateral四边形类或者使用组合。添加变换操作为Shape添加一个virtual void scale(double factor)纯虚函数并在派生类中实现。思考缩放因子为负或零时该如何处理。工厂模式创建一个ShapeFactory类根据字符串如circle和参数动态创建对应的Shape对象。这可以将对象的创建逻辑与使用逻辑解耦。7. 继承的设计原则与替代方案7.1 里氏替换原则LSP这是面向对象设计的五大原则SOLID之一与继承紧密相关。其核心思想是子类对象必须能够替换掉其父类对象并且程序的行为不会发生改变。这意味着子类不应该比父类有更严格的前置条件对输入的要求。子类不应该比父类有更宽松的后置条件对输出的承诺。子类不应该改变父类方法声明的行为例如把非虚函数重定义成做完全不同的事。前面提到的“正方形-矩形”问题就是违反LSP的典型例子。如果代码期望一个Rectangle并调用setWidth和setHeight那么传入一个Square对象就会导致意外行为因为修改宽会同时修改高。在这种情况下用继承来建模“正方形是一个矩形”就不合适。7.2 组合优于继承Composition over Inheritance这是现代软件工程中一个非常重要的经验法则。它鼓励我们在代码复用时优先考虑使用组合将一个类的对象作为另一个类的成员而不是继承。组合的优势更低的耦合派生类与基类紧密耦合基类的任何改动都可能影响所有派生类。组合则让类之间的关系更松散。更大的灵活性你可以在运行时动态更换组合的对象。而继承关系在编译时就固定了。避免复杂的继承层次过深的继承树难以理解和维护。更清晰地表达“有一个”关系比如Car有一个Engine这用组合比让Car继承Engine要合理得多。何时使用继承当关系确实是严格的“是一个”关系并且符合里氏替换原则时。当你需要实现多态并且有一系列对象需要被统一接口处理时。当基类是一个稳定的抽象接口并且派生类主要是为了提供不同的实现时。何时使用组合当关系是“有一个”或“用...来实现”时。当你只是想复用另一个类的代码但并不想暴露其全部接口时。当你需要动态改变行为时。7.3 使用final防止过度继承如果你设计了一个类并且不希望它被进一步继承可能是出于安全、性能或设计稳定性的考虑可以在类声明后加上final关键字。class UtilityClass final { // 这个类不能被继承 // ... };这可以防止其他人创建你的类的派生类从而确保类的行为不会被意外修改也使得编译器可以进行一些优化比如去虚化。8. 继承相关的编译、调试与性能考量8.1 常见编译错误与排查“不能将‘Derived’转换为‘Base’”**检查继承方式。如果你使用了private或protected继承那么派生类对象不能隐式转换为基类指针/引用。通常你需要的是public继承。“对非虚函数的重写隐藏了重载函数”这就是前面提到的名字隐藏问题。使用using Base::functionName;引入基类函数或者使用不同的函数名。“纯虚函数未被重写”/“抽象类无法实例化”检查派生类是否重写了基类的所有纯虚函数。注意函数签名返回类型、参数列表、const限定必须完全一致。使用override关键字可以让编译器帮你检查。“没有合适的默认构造函数可用”如果基类没有默认构造函数你必须在派生类构造函数的初始化列表中显式调用基类的有参构造函数。“虚函数调用不明确”在多重继承中如果两个基类有同名同参数的虚函数派生类重写时会产生二义性。需要使用作用域解析符指定重写哪一个或者重新设计类层次。8.2 调试技巧查看对象内存布局与vtable在复杂的继承尤其是多重继承和虚继承中理解对象在内存中的布局对调试很有帮助。虽然标准没有规定但大多数编译器有相似的模式。你可以使用sizeof()运算符查看对象大小这能间接反映是否有额外的vptr或虚基类指针。一些编译器如GCC提供了-fdump-class-hierarchy选项可以在编译时输出类的内存布局和vtable信息。在调试器中如GDB、VS Debugger你可以查看对象的成员变量观察基类子对象和派生类成员的位置。8.3 性能影响虚函数调用开销虚函数调用比普通函数调用多一次间接寻址通过vptr找vtable再找函数地址。在绝大多数应用中这个开销微乎其微不应成为你拒绝使用多态的理由。设计清晰、可维护的代码远比微小的性能优化更重要。只有在性能极其关键的代码路径如内层循环每秒调用上亿次中虚函数开销才可能需要考虑。此时可以考虑使用CRTP奇异递归模板模式实现静态多态通过模板在编译期确定调用消除运行时开销。但这增加了代码复杂性和编译时间。将虚函数调用移出循环在循环外通过基类接口确定具体行为在循环内使用非虚函数或内联函数。使用final类或函数在某些情况下编译器如果能在编译期确定对象的实际类型例如对象被声明为final类的具体类型而非指针可能会进行去虚化优化将虚函数调用转换为直接调用。记住不要过早优化。首先用清晰、正确的面向对象设计包括合理的虚函数使用实现功能然后在性能分析工具的指导下对真正的热点进行优化。掌握继承你就掌握了C面向对象编程的半壁江山。它让你能构建出层次清晰、易于扩展的代码结构。但也要时刻牢记它的复杂性尤其是多重继承和对象切片带来的陷阱。在实践中坚持“public继承表示is-a关系”、“优先使用组合”、“为多态基类声明虚析构函数”这些基本原则能帮你避开大多数坑。从设计一个像图形系统这样的小项目开始亲手写一写、调一调遇到问题再回头看看原理你会对继承有更深刻的理解。