免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

吃透C++继承与多态!从基础语法到菱形继承、切片问题(保姆级教程)

吃透C++继承与多态!从基础语法到菱形继承、切片问题(保姆级教程) 目录前言1.类继承1.1 概述1.2 语法1.3 代码示例1.4 执行结果2. 虚函数实现多态的核心2.1 概述2.2 基础语法2.3 代码示例2.4 执行结果3. 纯虚类与抽象基类3.1 概述3.2 基础语法3.3 代码示例3.4 执行结果4. 继承后的访问控制重点易错4.1 概述4.2 权限规则4.3 protected 权限核心作用面试常问4.4 代码示例5. 继承的类的作用域与名称隐藏5.1 概述5.2 代码示例5.3 执行结果6. 继承的构造与析构顺序面试必考6.1 核心规则6.2 代码示例6.3 执行结果7.菱形继承多继承重难点解决方案7.1概述7.2普通菱形继承的两大致命问题7.3解决方法虚继承 virtual7.4代码示例7.5 虚继承底层原理7.6菱形继承构造顺序面试必考8. 继承与容器解决对象切片问题8.1 切片问题概述8.2 切片问题错误示例8.3 正确方案指针/智能指针存储8.4 智能指针选择建议结语前言在C面向对象编程中继承与多态是核心中的核心也是面试必考、项目必用的重难点很多新手学C时只会写简单的继承代码却搞不懂虚函数、抽象类、访问权限、菱形继承、对象切片这些高频考点写代码频繁报错、面试一问就懵。本文整理了C继承全套核心知识点从零基础概述、语法案例到底层原理、面试坑点、问题解决方案全覆盖搭配可直接运行的完整代码、执行结果、通俗白话讲解零基础也能一次性吃透收藏这篇搞定C继承多态所有难点1.类继承1.1 概述类继承是面向对象编程中的一个核心概念允许一个类派生类从另一个类基类继承属性和行为。通过继承派生类可以重用基类的代码并根据需要添加新的成员或重写现有成员。1.2 语法C继承标准语法class 子类 : 继承方式 父类class Base { public: void baseFunction(); protected: int protectedMember; private: int privateMember; }; class Derived : public Base { // 公有继承 public: void derivedFunction(); };1.3 代码示例#include iostream // 基类 class Animal { public: void eat() const { std::cout Animal eats. std::endl; } protected: int age; }; // 派生类 class Dog : public Animal { public: void bark() const { std::cout Dog barks. std::endl; } void setAge(int a) { age a; // 访问受保护成员 } int getAge() const { return age; } }; int main() { Dog myDog; myDog.eat(); // 继承自Animal myDog.bark(); // Dog特有 myDog.setAge(5); std::cout Dogs age: myDog.getAge() std::endl; return 0; }1.4 执行结果Animal eats. Dog barks. Dogs age: 52. 虚函数实现多态的核心2.1 概述虚函数virtual允许派生类重新定义基类中的函数以实现多态性。在运行时根据对象的实际类型调用相应的函数版本普通函数是静态绑定编译期确定调用函数而虚函数可以实现运行时动态绑定2.2 基础语法class Base { public: virtual void display(); };2.3 代码示例#include iostream class Shape { public: virtual void draw() const { // 虚函数 std::cout Drawing a shape. std::endl; } virtual ~Shape() {} // 虚析构函数 }; class Circle : public Shape { public: void draw() const override { // 重写虚函数 std::cout Drawing a circle. std::endl; } }; class Square : public Shape { public: void draw() const override { // 重写虚函数 std::cout Drawing a square. std::endl; } }; void render(const Shape shape) { shape.draw(); // 动态绑定根据实际对象类型调用对应的draw() } int main() { Circle c; Square s; Shape genericShape; render(c); // 输出: Drawing a circle. render(s); // 输出: Drawing a square. render(genericShape); // 输出: Drawing a shape. return 0; }2.4 执行结果Drawing a circle. Drawing a square. Drawing a shape.3. 纯虚类与抽象基类3.1 概述纯虚函数基类中只声明、不实现的虚函数语法为virtual 函数() 0;。抽象基类包含至少一个纯虚函数的类。核心特性1.无法实例化不能创建对象2.强制子类必须重写所有纯虚函数否则子类也为抽象类无法实例化3.常用于定义统一接口规范子类行为接口设计思想3.2 基础语法class Base { public: virtual void pureVirtualFunction() 0; // 纯虚函数 virtual void printm() 0; };3.3 代码示例#include iostream // 抽象基类 class Vehicle { public: virtual void startEngine() 0; // 纯虚函数 virtual ~Vehicle() {} // 虚析构函数 }; class Car : public Vehicle { public: void startEngine() override { std::cout Car engine started. std::endl; } }; class Motorcycle : public Vehicle { public: void startEngine() override { std::cout Motorcycle engine started. std::endl; } }; int main() { // Vehicle v; // 错误: 不能实例化抽象类 Car car; Motorcycle bike; car.startEngine(); // 输出: Car engine started. bike.startEngine(); // 输出: Motorcycle engine started. Vehicle* v1 car; Vehicle* v2 bike; v1-startEngine(); // 动态绑定输出: Car engine started. v2-startEngine(); // 动态绑定输出: Motorcycle engine started. return 0; }3.4 执行结果Car engine started. Motorcycle engine started. Car engine started. Motorcycle engine started.4. 继承后的访问控制重点易错4.1 概述C继承分为三种方式public、protected、private决定了父类成员在子类中的访问权限是代码封装的核心。默认继承方式为private。4.2 权限规则公有继承public inheritance基类的public成员在派生类中保持public。基类的protected成员在派生类中保持protected。基类的private成员在派生类中不可访问。保护继承protected inheritance基类的public和protected成员在派生类中都变为protected。私有继承private inheritance基类的public和protected成员在派生类中都变为private。下面有个图进行直观了解4.3 protected 权限核心作用面试常问很多新手分不清private和protected核心区别protected子类可以直接访问、修改外部(main、其他类)无法访问用于父类预留接口/属性专供子类拓展使用private只有本类可访问子类、外部均不可访问4.4 代码示例#include iostream class Base { public: int publicMember; protected: int protectedMember; private: int privateMember; }; class PublicDerived : public Base { public: void accessMembers() { publicMember 1; // 可访问 protectedMember 2; // 可访问 // privateMember 3; // 错误privateMember 在派生类中不可访问 } }; class ProtectedDerived : protected Base { public: void accessMembers() { publicMember 1; // 转为 protected protectedMember 2; // 转为 protected // privateMember 3; // 错误 } }; class PrivateDerived : private Base { public: void accessMembers() { publicMember 1; // 转为 private protectedMember 2; // 转为 private // privateMember 3; // 错误 } }; int main() { PublicDerived pubDer; pubDer.publicMember 10; // 可访问 // ProtectedDerived protDer; // protDer.publicMember 10; // 错误publicMember 在 ProtectedDerived 中为 protected // PrivateDerived privDer; // privDer.publicMember 10; // 错误publicMember 在 PrivateDerived 中为 private return 0; }5. 继承的类的作用域与名称隐藏5.1 概述子类作用域会覆盖父类作用域子类定义与父类同名的普通函数会隐藏父类所有同名函数名称隐藏而虚函数不会被隐藏只会被重写。可通过作用域解析符 ::手动调用父类被隐藏的成员5.2 代码示例#include iostream class Base { public: void display() const { std::cout Display from Base std::endl; } virtual void show() const { std::cout Show from Base std::endl; } }; class Derived : public Base { public: void display() const { // 隐藏基类的 display std::cout Display from Derived std::endl; } void callBaseDisplay() const { Base::display(); // 使用作用域解析符调用基类的 display } void show() const { // 重写 show show 是虚函数可实现多态 std::cout Show from Derived std::endl; } }; int main() { Derived d; d.display(); // 调用 Derived::display d.callBaseDisplay(); // 调用 Base::display d.show(); // 调用 Derived::show Base* bPtr d; bPtr-display(); // 调用 Base::display因为 display 不是虚函数 bPtr-show(); // show 是虚函数调用 Derived::show否则调用 Base::show return 0; }5.3 执行结果Display from Derived Display from Base Show from Derived Display from Base Show from Base // show 是虚函数则输出: Show from Derived6. 继承的构造与析构顺序面试必考6.1 核心规则在继承体系中类的构造函数和拷贝控制函数拷贝构造函数、拷贝赋值运算符、析构函数的调用顺序和行为需要注意。构造顺序先父类、后子类父类初始化完成子类才能复用资源析构顺序先子类、后父类与构造完全相反底层内存视角子类对象的内存空间前半部分存储父类非静态成员变量后半部分存储子类新增成员必须先初始化父类内存再初始化子类内存。6.2 代码示例#include iostream #include string class Base { public: Base() { std::cout Base default constructor std::endl; } Base(const std::string name) : name_(name) { std::cout Base parameterized constructor: name_ std::endl; } Base(const Base other) : name_(other.name_) { std::cout Base copy constructor std::endl; } Base operator(const Base other) { std::cout Base copy assignment std::endl; if (this ! other) { name_ other.name_; } return *this; } virtual ~Base() { std::cout Base destructor std::endl; } protected: std::string name_; }; class Derived : public Base { public: Derived() : Base(Default Derived) { std::cout Derived default constructor std::endl; } Derived(const std::string name, int value) : Base(name), value_(value) { std::cout Derived parameterized constructor: value_ std::endl; } Derived(const Derived other) : Base(other), value_(other.value_) { std::cout Derived copy constructor std::endl; } Derived operator(const Derived other) { std::cout Derived copy assignment std::endl; if (this ! other) { Base::operator(other); value_ other.value_; } return *this; } ~Derived() override { std::cout Derived destructor std::endl; } private: int value_; }; int main() { std::cout Creating d1: std::endl; Derived d1; std::cout \nCreating d2: std::endl; Derived d2(Custom Derived, 42); std::cout \nCopy constructing d3 from d2: std::endl; Derived d3 d2; std::cout \nAssigning d1 d2: std::endl; d1 d2; std::cout \nExiting main... std::endl; return 0; }6.3 执行结果Creating d1: Base parameterized constructor: Default Derived Derived default constructor Creating d2: Base parameterized constructor: Custom Derived Derived parameterized constructor: 42 Copy constructing d3 from d2: Base copy constructor Derived copy constructor Assigning d1 d2: Derived copy assignment Base copy assignment Exiting main... Derived destructor Base destructor Derived destructor Base destructor Derived destructor Base destructor用大白话来说就是老爸先诞生、初始化好自己所有物资之后孩子才诞生使用老爸的物资。销毁的时候孩子先走老爸最后销毁。7.菱形继承多继承重难点解决方案7.1概述菱形继承是C多继承的经典问题一个顶层基类被两个中间子类同时继承最终一个子类同时继承这两个中间类形成菱形结构。结构关系顶层父类Animal动物中间子类Sheep羊、Tuo驼最终子类SheepTuo羊驼继承链路Animal → Sheep、Animal → Camel、SheepCamel → SheepCamel这就是典型菱形继承。7.2普通菱形继承的两大致命问题1. 数据冗余重复内存因为 Sheep 继承了一份 AnimalCamel 也继承了一份 Animal。最终 SheepCamel 对象中存在两份 Animal 成员。原本只需要一份 age现在变成两份 age浪费内存。2. 访问二义性编译报错当我们写st.age 10;编译器不知道你要访问Sheep 的 Animal还是 Camel的 Animal直接报错。7.3解决方法虚继承 virtual在中间类继承时添加virtual虚继承让顶层基类在最终子类中只保留一份内存彻底解决冗余和二义性。class Sheep :virtual public Animal{ }; class Camel :virtual public Animal { };7.4代码示例#includeiostream class Animal{ public: int _age; }; //利用虚继承 解决菱形继承的问题 //继承前加virtual 变为虚继承 //Animal类称为 虚基类 class Sheep :virtual public Animal{ }; class Camel :virtual public Animal { }; class Sheepcamel :public Sheep, public Camel{ }; void test1() { Sheepcamel sc; sc.Sheep::_age18; sc.Camel::_age200; //当菱形继承两个父类拥有相同数据需要加以作用域区分 std::coutsc.Sheep::_agestd::endl; std::coutsc.Camel::_agestd::endl; //虚继承后 都只有一份数据为最后的 std::coutsc._agestd::endl; } int main() { test1(); return 0; }7.5 虚继承底层原理普通继承中间类直接复制父类所有成员最终子类存在多份父类数据冗余严重虚继承中间类不再复制父类数据仅存储vbptr虚基类指针指向vbtable虚基类表通过指针偏移量所有中间类共享唯一一份顶层父类成员vbptr 和 vbtable 作用vbptr虚基类指针vbtable虚基类表存放偏移量访问 age 时对象首地址 偏移量 唯一父类成员地址实现多个子类共享同一份父类成员7.6菱形继承构造顺序面试必考固定顺序虚基类(顶层父类) → 中间子类 → 最终子类原因必须优先初始化共享的唯一父类资源再初始化派生类保证数据统一。8. 继承与容器解决对象切片问题8.1 切片问题概述C 容器如std::vector、std::list等通常存储对象的副本而非指向对象的指针。因此当与继承结合使用时可能导致切片Object Slicing问题即仅存储基类部分丢失派生类特有的信息。为了实现多态性推荐使用指针或智能指针存储对象。8.2 切片问题错误示例#include iostream #include vector class Base { public: virtual void show() const { std::cout Base show std::endl; } virtual ~Base() {} }; class Derived : public Base { public: void show() const override { std::cout Derived show std::endl; } }; int main() { std::vectorBase vec; Derived d; vec.push_back(d); // 切片发生派生类特有部分被丢弃 vec[0].show(); // 输出: Base show return 0; }想要保留子类完整数据、实现多态容器必须存储父类指针/智能指针避免对象拷贝切片。8.3 正确方案指针/智能指针存储#include iostream #include vector #include memory class Base { public: virtual void show() const { std::cout Base show std::endl; } virtual ~Base() {} }; class Derived : public Base { public: void show() const override { std::cout Derived show std::endl; } }; int main() { std::vectorstd::unique_ptrBase vec; vec.emplace_back(std::make_uniqueDerived()); vec[0]-show(); // 输出: Derived show return 0; }8.4 智能指针选择建议std::unique_ptr独占所有权不可复制只能移动适用于明确的单一所有权场景。std::shared_ptr共享所有权可以被多个指针共享和引用计数适用于需要多个所有者的场景。结语本篇文章全覆盖了C继承与多态的所有核心考点从基础继承语法、虚函数多态、抽象类到访问权限、作用域、构造析构顺序再到面试高频的菱形继承、对象切片问题搭配可直接运行的代码、底层原理、避坑方案零基础可学、面试可背、项目可用后续会持续更新C核心进阶教程关注我告别碎片化学习
返回列表