编译时多态与运行时多态

一、根本设计目标

  1. 零开销抽象(Zero-overhead Abstraction)
    C++的设计哲学强调“不为未使用的特性付出代价”。两种多态提供了不同场景下的最优选择:

    • 编译时多态:无运行时开销,适合性能敏感场景
    • 运行时多态:牺牲少量性能换取动态灵活性
  2. 多重编程范式支持
    C++同时支持:

    • _面向对象编程_(运行时多态)
    • _泛型编程_(编译时多态)

二、运行时多态(动态绑定)

实现方式:虚函数(virtual)+ 继承
设计理论

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal {
public:
virtual void speak() const = 0; // 动态绑定接口
};
class Dog : public Animal {
void speak() const override { std::cout << "Woof!"; }
};
class Cat : public Animal {
void speak() const override { std::cout << "Meow!"; }
};

// 运行时根据实际对象类型决定调用
void makeSound(const Animal& a) {
a.speak(); // 动态决议(通过虚函数表vtable)
}

存在意义

  1. 运行时类型决策:程序运行时根据对象实际类型调用函数
  2. 二进制接口稳定:基类接口不变时,派生类可独立扩展
  3. 面向对象核心特性:实现“开闭原则”(对扩展开放,对修改关闭)

代价

  • 虚表(vtable)和虚指针(vptr)的内存开销
  • 间接跳转的性能损耗(通常约5-10个CPU周期)
阅读更多...

八股文底层 Day1

C++对象模型

12e443jd.vjg.png|300

组件类型 存储位置 是否占用对象空间
非静态数据成员 对象内部 ✔️ 是
虚函数表指针(vptr) 对象头部(通常) ✔️ 是
普通成员函数 代码段(.text) ❌ 否
虚函数 代码段(.text) ❌ 否
虚函数表(vtable) 只读数据段(.rodata) ❌ 否
静态数据成员 全局/静态数据段 ❌ 否
静态成员函数 代码段(.text) ❌ 否

没有虚函数(平凡类型)

  • 指针类型是为了在解引用的时候确认读取多少个字节
  • 结构体和类在C++的内存实现上一样的,只是在编译器层面添加类权限访问控制
  • 结构体或者类,本质上类似与数组相同,是一块连续的内存,可以进行强制类型转换。访问结构体或者类的成员的时候,相当于根据成员的类型来计算偏移量
  • 在继承的时候,根据子类继承声明的顺序,将多个父类的成员添加到子类成员之前
  • 成员函数只是在一般函数的基础上由编译器隐式的在参数列表添加this指针
阅读更多...

虚函数机制详解

只有通过指针/引用间接访问时,才能保持动态类型。

1. 虚表指针(vptr)的本质

  • 每个包含虚函数的类有自己的虚函数表(vtable)
  • 每个对象实例在内存开头有一个隐藏的vptr指针,指向一个虚函数表,在对象创建时由构造函数设置

2. 虚函数表的本质

  • 编译期创建:虚函数表是编译器在编译阶段为每个包含虚函数的类生成的静态数据
  • 全局唯一:每个类只有一个虚函数表,存放在程序的只读数据段
    1
    &Base::vtable # 获取虚函数表地址

RTTI 指针

  • 指向 type_info 结构
  • 包含类型名、继承关系等信息
  • 用于 typeiddynamic_cast

虚函数指针

  • 按声明顺序排列
  • 覆盖函数替换基类槽位
  • 纯虚函数通常指向pure_virtual_called(终止程序)

偏移量条目(多重继承)

  • 用于 this 指针调整,
  • 格式:{offset, 0}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Derived的Base1虚表:
┌───────────────┐
│ RTTI (Derived)│
├───────────────┤
│ &Derived::f1 │ // 覆盖Base1::f1
├───────────────┤
│ &Derived::fd │ // 新增函数放入第一个基类虚表
└───────────────┘

Derived的Base2虚表:
┌───────────────┐
│ 偏移量 │ → 到完整对象的偏移(通常-8或-12)
├───────────────┤
│ RTTI (Derived)│
├───────────────┤
│ &Derived::f2 │ // 覆盖Base2::f2
└───────────────┘
1
2
3
4
5
6
7
8
9
10
11
内存地址: 0x402000
┌──────────────┬─────────────────────────┬──────────────────────────────┐
│ 偏移量(字节) │ 内容 (8字节) │ 说明 │
├──────────────┼─────────────────────────┼──────────────────────────────┤
│ 0x00 │ 0x403100 (RTTI) │ 指向Derived的type_info │
│ 0x08 │ 0x402100 (&Derived::func1)│ 覆盖Base::func1的地址 │
│ 0x10 │ 0x401200 (&Base::func2) │ 继承Base::func2的地址 │
│ 0x18 │ 0x402300 (~Derived完整析构)│ 完整析构函数地址 │
│ 0x20 │ 0x402350 (~Derived基析构)│ 基本析构函数地址 │
│ 0x28 │ 0x402400 (&Derived::func3)│ 新增虚函数func3的地址 │
└──────────────┴─────────────────────────┴──────────────────────────────┘
阅读更多...

类继承时的内存布局

1. 单继承(无虚函数)

1
2
3
4
5
6
7
8
9
10
11
class Base {
public:
int a;
int b;
};

class Derived : public Base {
public:
int c;
int d;
};

内存布局:

1
2
3
4
5
6
Base对象 (8字节):
┌───────────┐
│ a (4) │
├───────────┤
│ b (4) │
└───────────┘
1
2
3
4
5
6
7
8
9
10
Derived对象 (16字节):
┌───────────┐ ← 起始地址
│ Base::a │
├───────────┤
│ Base::b │
├───────────┤
│ Derived::c│
├───────────┤
│ Derived::d│
└───────────┘

特点:

  • 派生类对象包含完整的基类子对象
  • 派生类新增成员追加在基类成员之后
  • 内存连续,无额外开销
阅读更多...

操作系统篇

进程和线程之间有什么区别

进程是资源分配和调度的基本单位。****线程是程序执行的最小单位,线程是进程的子任务,是进程内的执行单元。 一个进程至少有一个线程,一个进程可以运行多个线程,这些线程共享同一块内存。

资源开销:

  • 进程:由于每个进程都有独立的内存空间,创建和销毁进程的开销较大。进程间切换需要保存和恢复整个进程的状态,因此上下文切换的开销较高。
  • 线程:线程共享相同的内存空间,创建和销毁线程的开销较小。线程间切换只需要保存和恢复少量的线程上下文,因此上下文切换的开销较小。

通信与同步:

  • 进程:由于进程间相互隔离,进程之间的通信需要使用一些特殊机制,如管道、消息队列、共享内存等。
  • 线程:由于线程共享相同的内存空间,它们之间可以直接访问共享数据,线程间通信更加方便。

安全性:

  • 进程:由于进程间相互隔离,一个进程的崩溃不会直接影响其他进程的稳定性。
  • 线程:由于线程共享相同的内存空间,一个线程的错误可能会影响整个进程的稳定性。
阅读更多...

计算机网络篇

介绍一下TCP/IP模型和OSI模型的区别

OSI模型, 是国际标准化组织(ISO)制定的一个用于计算机或通信系统间互联的标准体系,将计算机网络通信划分为七个不同的层级,分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

  • 物理层(Physical Layer):负责传输原始的比特流,定义了物理介质的电气、机械、过程和功能特性。
  • 数据链路层(Data Link Layer):负责将物理层的比特流封装成帧,提供错误检测和纠正功能。
  • 网络层(Network Layer):负责路由选择和数据包的转发,主要协议是IP(Internet Protocol)。
  • 传输层(Transport Layer):负责端到端的可靠传输,主要协议是TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)。
  • 会话层(Session Layer):负责建立、管理和终止会话。
  • 表示层(Presentation Layer):负责数据的表示、加密和压缩。
  • 应用层(Application Layer):负责提供应用程序之间的通信,如HTTP、FTP、SMTP等。

虽然OSI模型在理论上更全面,但在实际网络通信中,TCP/IP模型更为实用。 TCP/IP模型分为四个层级,每个层级负责特定的网络功能。

  1. 应用层:该层与OSI模型的应用层和表示层以及会话层类似,提供直接与用户应用程序交互的接口。它为网络上的各种应用程序提供服务,如电子邮件(SMTP)、网页浏览(HTTP)、文件传输(FTP)等。
  2. 传输层:该层对应OSI模型的传输层。它负责端到端的数据传输,提供可靠的、无连接的数据传输服务。主要的传输层协议有TCP和UDP。TCP提供可靠的数据传输,确保数据的正确性和完整性;而UDP则是无连接的,适用于不要求可靠性的传输,如实时音频和视频流。
  3. 网络层:该层对应OSI模型的网络层。主要协议是IP,它负责数据包的路由和转发,选择最佳路径将数据从源主机传输到目标主机。IP协议使用IP地址来标识主机和网络,并进行逻辑地址寻址。
  4. 网络接口层:该层对应OSI模型的数据链路层和物理层。它包含硬件地址(MAC地址)的管理,负责物理传输媒介的传输,并提供错误检测和纠正的功能。
阅读更多...

Redis篇

Redis有什么优缺点?为什么用Redis查询会比较快

参考答案

(1) Redis有什么优缺点?

Redis 是一个基于内存的数据库,读写速度非常快,通常被用作缓存、消息队列、分布式锁和键值存储数据库。它支持多种数据结构,如字符串、哈希表、列表、集合、有序集合等, Redis 还提供了分布式特性,可以将数据分布在多个节点上,以提高可扩展性和可用性。但是Redis 受限于物理内存的大小,不适合存储超大量数据,并且需要大量内存,相比磁盘存储成本更高。

(2)为什么Redis查询快

  • 基于内存操作: 传统的磁盘文件操作相比减少了IO,提高了操作的速度。
  • 高效的数据结构:Redis专门设计了STRING、LIST、HASH等高效的数据结构,依赖各种数据结构提升了读写的效率。
  • 单线程:单线程操作省去了上下文切换带来的开销和CPU的消耗,同时不存在资源竞争,避免了死锁现象的发生。
  • I/O多路复用:采用I/O多路复用机制同时监听多个Socket,根据Socket上的事件来选择对应的事件处理器进行处理。
阅读更多...

C++篇

静态变量和全局变量、局部变量的区别、在内存上是怎么分布的

  1. 静态局部变量
  • 特点:
    • 作用域:仅限于声明它们的函数或代码块内部。
    • 生命周期:静态局部变量在程序的整个运行期间都存在,只初始化一次(在第一次使用前)。
    • 初始化:在首次进入函数时初始化,并保持值直到程序结束。
  • 使用场景:
    • 当你需要一个仅在函数内部使用,但希望其值在函数调用之间保持不变的变量时。
    • 适用于需要缓存数据以提高性能的情况。
  • 内存分布:静态局部变量存储在全局/静态存储区。
  1. 局部变量
  • 特点:
    • 作用域:局部变量仅在声明它们的函数或代码块内部可见。
    • 生命周期:局部变量在函数调用时创建,函数调用结束后销毁。
    • 初始化:必须在使用前显式初始化。
  • 使用场景:
    • 需要临时存储数据,且这些数据只在当前作用域内使用时。
    • 作为循环计数器或中间计算结果。
  • 内存分布:局部变量存储在栈上,与它们所在的作用域(如函数)相关联。
  1. 全局变量
  • 特点:
    • 作用域:全局变量在整个程序中都是可见的,可以在任何函数或代码块中访问。
    • 生命周期:全局变量同样具有静态存储期,它们在程序的整个运行期间都存在。
    • 初始化:通常在程序启动时初始化。
  • 使用场景:
    • 当你需要在程序的多个部分共享数据时。
    • 适用于存储配置信息或程序的状态信息。
    • 需要注意全局变量可能导致代码难以测试和维护。
  • 内存分布:全局变量也存储在全局/静态存储区。
阅读更多...

MySQL篇

一条 SQL 查询语句是如何执行的

  1. 连接器:连接器负责跟客户端建立连接、获取权限、维持和管理连接。
  2. 查询缓存: MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以key-value 对的形式,被直接缓存在内存中。
  3. 分析器:你输入的是由多个字符串和空格组成的一条SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。
  4. 优化器:优化器是在表里面有多个索引的时候,决定使用哪个索引; 或者在一个语句有多表关联(join )的时候,决定各个表的连接顺序。
  5. 执行器: MySQL通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。
阅读更多...
  • Copyrights © 2023-2025 John Doe
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信