当前位置: 首页 > news >正文

一文详解 C++ 继承体系

继承是 C++ 面向对象的核心机制之一,用来复用代码、表达“is-a”关系并支持运行时多态(virtual functions)。但 C++ 的继承体系很强大也很复杂:有访问控制(public/protected/private)、单继承/多继承、虚继承、虚函数表(vptr/vtable)、对象切片(slicing)、构造/析构顺序、name hiding、以及模板技巧(CRTP)等。弄清这些细节能避免常见的 BUG 和运行时错误。

优惠:https://secret-thanks.com/bi3/VX0.PC3Bp_vyb/mSVLJjZBDI0z2mMWDbQlwINQDLAd3LL/TTYdwhN/DbA-0IM_DJgi

基本语法与访问控制

struct Base {
public:int x;
protected:int p;
private:int q;virtual void vf() {}
};struct Derived : public Base {// public 继承:Base 的 public -> public, protected -> protected
};struct Derived2 : protected Base {// protected 继承:Base 的 public/protected -> protected 在 Derived2 中
};struct Derived3 : private Base {// private 继承:Base 的 public/protected -> private 在 Derived3 中
};
  • public 继承:表示 Derived is-a Base,允许外部把 Derived* 隐式转换为 Base*,常用的“接口继承”方式。
  • protected/private 继承:更像是“实现继承 / 用于实现”的手段(“implemented-in-terms-of”),外部不能隐式把 Derived 转为 Base(或受限制)。极少用于 API 设计,常用于库内部实现细节。
  • struct 默认继承/成员访问是 publicclass 默认是 private

成员继承与名字隐藏(name hiding)

  • 派生类继承基类的非 private 成员(数据、函数、类型别名等),但访问控制仍然受继承类型影响。
  • 名字隐藏:如果派生类定义了与基类同名函数(即使签名不同),基类中同名的所有重载都会被隐藏。要引入基类的重载,使用 using
struct Base { void f(int); void f(double); };
struct Derived : Base {using Base::f; // 引入基类的 f 重载集合void f(char);  // 新增一个重载
};

构造 / 析构 顺序(重要)

构造顺序(most-derived -> bases? careful!):

  1. 虚基类(virtual bases)先被构造(只构造一次),顺序按照最派生类继承声明的某个规则(通常是从左到右和声明顺序,细节由标准定义)。
  2. 然后按派生类 base-specifier-list 中从左到右顺序构造非虚基类
  3. 再构造派生类的成员(按在类中声明的顺序)。
  4. 最后进入派生类构造函数体(body)。

析构顺序与之相反(派生析构体先运行,然后成员,然后 base,最后虚 base)。

注意:对于虚继承,最派生类负责调用虚基类的构造函数并传参(即虚基由 most-derived 初始化)。

对象布局、vptr/vtable、对象切片

  • 为了实现动态绑定(virtual functions),编译器通常在每个多态(含虚函数)的对象中放一个指向该类型 vtable 的指针(称为 vptr)。vtable 存放函数指针用于动态调用。
  • 多重/虚继承时,可能存在多个 vptr 或额外的指针(如 vbptr)用于基类偏移调整,具体实现依编译器而异。
  • 对象切片(slicing):把派生类对象按值赋给基类对象会丢失派生部分:
struct Base { virtual ~Base() = default; int b; };
struct Derived : Base { int d; };Derived dd; Base b = dd; // slicing:b 不包含 d

因此要传递多态对象应使用指针或引用(Base*/Base& 或 智能指针)。

虚函数与运行时多态

struct Base {virtual void foo() { std::cout << "Base\n"; }virtual ~Base() = default; // 若要通过 Base* 删除派生对象,必须 virtual
};struct Derived : Base {void foo() override { std::cout << "Derived\n"; }
};
  • virtual 声明使得调用 ptr->foo() 在运行时按对象的动态类型选择实现(动态绑定)。
  • override(C++11)用于标注:编译器会检查该函数确实覆盖了基类的虚函数(避免写错签名后非覆盖导致的问题)。
  • final 可以阻止进一步重写:void foo() final;
  • 虚析构:如基类打算作为多态基类,必须有虚析构函数,否则通过 Base* 删除 Derived* 会导致未定义行为(派生析构未被调用)。

纯虚函数与抽象类(interface)

struct Interface {virtual void f() = 0; // 纯虚函数virtual ~Interface() = default;
};
  • 有纯虚函数的类是抽象类,不能实例化。
  • 抽象类常用作接口(纯虚 + 无数据成员)。
  • 纯虚函数也可以有实现(少见),但仍使类抽象:virtual void f() = 0; 在类外定义 void Interface::f(){...}

协变返回(Covariant return types)

派生类重写虚函数时允许返回协变类型(返回指针或引用时):

struct Base { virtual Base* clone() const; };
struct Derived : Base { Derived* clone() const override; } // 合法:Derived* 是 Base* 的协变类型

多重继承(multiple inheritance)

C++ 允许从多个基类派生:

struct A { int a; virtual ~A()=default; };
struct B : A {};
struct C : A {};
struct D : B, C {}; // D 中存在两个 A 子对象(如果不是虚继承)
  • 多重继承带来二义性D d; d.a; 会编译错误(不确定访问哪个 A::a),必须显式 d.B::ad.C::a
  • 如果基类都是多态(含 virtual),并想要共享同一个基类子对象(避免重复),则用 虚继承(virtual base)。

虚继承(diamond / 菱形问题)

经典菱形:

   A/ \B   C\ /D

如果 B、C 都继承自 A,而 D 又从 B、C 继承,默认会有两个 A 子对象。若想在 D 中只保留一个 A 子对象,B 和 C 应该虚继承 A:

struct A { int a; };
struct B : virtual A {};
struct C : virtual A {};
struct D : B, C {}; // 只有一个 A 子对象,由 D 初始化
  • 初始化:最派生类 D 负责初始化虚基 A(传递构造参数)。B、C 不再单独构造 A。
  • 代价:虚继承会引入额外的实现复杂度(编译器可能增加偏移表或指针来在运行时找到虚基),导致对象体积或访问成本上升。具体代价依实现而异(但通常是存在的)。

超低价esim流量卡:https://www.wanmoon.mom/redteago-esim/

指针/引用转换、dynamic_cast 与 RTTI

  • static_cast<T*>:在继承链上做编译时转换(不做运行时检查)。
  • dynamic_cast<T*>:在多态基类上做安全向下转换(需要包含虚函数以启用 RTTI)。如果失败,返回 nullptr(指针情况);引用失败抛 std::bad_cast
Base* b = ...;
Derived* d = dynamic_cast<Derived*>(b);
if (d) { /* 成功 */ }
  • typeid:检查对象的动态类型(多态类型时返回动态类型),需注意 typeid(*ptr) 要保证 ptr 非空。

名称查找 / overload resolution 的细微点

  • 名称查找先在派生类进行,若找到对应名称则基类中同名会被隐藏(无论签名),接着在该名字对应的可用集合中做重载解析。
  • 解决隐藏常用方法:using Base::f; 或者显式 Base::f() 调用。

空基优化(EBO:Empty Base Optimization)

  • 如果基类是空类(没有非静态数据成员),编译器通常会把它作为空子对象并不增加派生对象的大小(EBO),这在如 std::tuple 等模板库里很常见,用以节省空间。
  • 但如果派生类有多个同类型的空基,标准允许但具体细节和符号链接(type identity)有关;一般情况下 EBO 会被使用。

模板技巧:CRTP(静态多态)

CRTP(Curiously Recurring Template Pattern)是一种静态多态替代运行时虚函数的技巧:

template<typename Derived>
struct BaseCRTP {void interface() { static_cast<Derived*>(this)->implementation(); }
};struct Impl : BaseCRTP<Impl> {void implementation() { /* ... */ }
};

优点:无虚拟表开销(编译期分派),适合性能敏感场景;缺点:不能在运行时基类指针上统一操作(不是运行时多态)。

常见坑与问答(FAQ)

  • Q:覆写没有被调用?
    A:可能没有把基类函数声明为 virtual,或发生了切片(by-value),或签名不同(const/ref/参数/返回类型不一致导致并非覆盖)。用 override 可快速发现签名问题。
  • Q:为什么 delete base_ptr 导致内存泄漏/未定义行为?
    A:基类析构函数不是 virtual,导致派生析构未被调用。多态基类应有虚析构。
  • Q:什么时候用虚继承?
    A:只有在确实需要解决菱形重复基类子对象时使用;它有运行时成本,通常尽量避免复杂的多重继承设计。
  • Q:继承还是组合?
    A:如果是“is-a” 用继承;如果只是“has-a” 或复用实现首选组合(成员持有)—组合更灵活、耦合更低。
  • Q:如何安全下转(downcast)?
    A:使用 dynamic_cast(带多态基类)或设计更清晰的 API(避免运行时检查),谨慎使用 static_cast

实战示例(菱形 + 构造参数)

#include <iostream>struct A {A(int v) : x(v) { std::cout<<"A("<<x<<")\n"; }int x;
};struct B : virtual A {B(int v) : A(v) {} // 无效?B 不能单独决定虚基的初始化(取决于 most-derived)
};struct C : virtual A {C(int v) : A(v) {}
};struct D : B, C {D(int v) : A(v), B(v), C(v) { // most-derived D 必须显式初始化虚基 Astd::cout<<"D\n";}
};int main(){D d(42);std::cout<<d.x<<"\n";
}

输出会显示 A(42) 只被构造一次(由 D 初始化),然后 D 的构造继续。

http://www.dtcms.com/a/323400.html

相关文章:

  • [C/C++线程安全]_[中级]_[多线程如何使用共享锁提升性能]
  • EP06:【DL 第二弹】动态计算图与梯度下降入门
  • 【C/C++】具有C风格的强制类型转换:显式类型转换、隐式类型转换,C语言强制类型转换
  • 第六章:【springboot】框架springboot原理、springboot父子工程与Swagger
  • 算法题(183):质量检测
  • 《答客难》东方朔
  • 网络原理-初识
  • FreeRTOS入门知识(初识RTOS任务调度)(三)
  • AVL树的四种旋转
  • 【Python 语法糖小火锅 · 第 4 涮】
  • 资深全栈工程师面试题总结
  • 【牛客刷题】小红的区间删除
  • 第16届蓝桥杯Scratch选拔赛初级及中级(STEMA)2024年11月24日真题
  • Linux之shell脚本篇(四)
  • SQL 172 未完成试卷数大于1的有效用户
  • 9. 堆和栈有什么区别
  • 01数据结构-图的邻接矩阵和遍历
  • 从零开始理解编译原理:设计一个简单的编程语言
  • svg 图片怎么设置 :hover 时变色
  • 交 换
  • sigaction 中 sa_handler = SIG_IGN 的深度解析与应用实践
  • day14 - html5
  • 2025年TOP5服装类跟单软件推荐榜单
  • 复杂正则语句(表格数据)解析
  • CentOS7运行AppImage
  • 历史数据分析——首旅酒店
  • 电子电气架构 --- 48V车载供电架构
  • ubuntu修改密码
  • 基于dynamic的Druid 与 HikariCP 连接池集成配置区别
  • 论文阅读 2025-8-3 [FaceXformer, RadGPT , Uni-CoT]