面试-基础知识

记录面试的基础知识(要点速记),用于面试前的速览(临阵磨枪)

一、操作系统

Linux 进程

进程和线程:进程(资源分配单位)、线程(最小调度单位)

  • 线程:创建开销小、上下文切换快

线程和协程:协程是用户级轻量级线程,没有内核态切换开销

  • C++ 20 协程:一种特殊的可重入函数(挂起或者恢复运行)

进程和程序:进程(程序的一次执行过程、动态)、程序(静态,代表算法实体)

进程状态:创建、就绪、运行、阻塞、停止

  • 运行->就绪:时间片用完 or 被高优先级进程抢占
  • 运行->阻塞:IO 请求 or 等待资源 or 等待事件

孤儿进程、僵尸进程

  • 孤儿进程:父进程挂,子进程被 init 收养
  • 僵尸进程:子进程挂,父进程没回收子进程
    • 解决:杀死父进程;建立子进程时就脱离父子关系(fork 两次,运行孙子,关闭父亲:手动制作孤儿)

进程间协作关系:通信(交换数据)、互斥(处理资源竞争)、同步(协调点)

  • 通信:共享内存、消息队列、Socket、管道、信号
    • 共享内存同步:信号量
  • 互斥:互斥锁(自旋锁、读写锁)、信号量
    • 互斥锁:用于保护共享资源,确保同一时刻只有一个线程可以访问资源
    • 信号量:控制对有限数量资源的访问,允许同时访问资源
  • 同步:信号量、条件变量、事件、屏障

线程安全

  • 本质:存在资源共享 & 数据竞争,原子性问题(操作不能原子性的完成)
  • 解决:同步机制,原子操作

进程切换过程:保存上下文、更新 PCB、新进程 + 更新 PCB、更新内存数据结构

进程管理

  • 进程描述:PCB == task_struct
  • 进程管理:进程描述节点构建成进程队列,根据队列在 CPU 上调度
  • 进程调度:调度的对象就是 task_struct,使用调度器(Deadline 调度器 > RT 调度器 > CFS 调度器)调度

进程绑定:将进程绑定到指定的 CPU 核心上运行

  • 作用:减少调度开销
  • 亲和性:软亲和性、硬亲和性
  • 操作:命令(taskset);API 接口(sched_setaffinity、sched_getaffinity)

锁类型:互斥锁、自旋锁、读写锁、自适应锁(尝试获取一定次数,获取不到就休眠;互斥和自旋的折中选择)

锁的实现原理

  • 互斥锁:使用 CAS,如果 swap 不成功,则线程进入队列同等优先级队列末尾(休眠)
  • 自旋锁:通过 TAS,设置 flag

原子操作实现

  • 硬件指令:TAS(Test-and-Set)、CAS(Compare-and-Swap)、Fetch-and-Add、Exchange
  • 自旋锁:底层是 TAS
  • 互斥锁:底层是 CAS
  • 信号量:底层是 Fetch-and-Add 和 Fetch-and-Sub

CAS:原子的更新值,等于预期值才更新

  • 问题:ABA 问题(无法识别这种情况)

死锁四个必要条件

  • 互斥条件:资源一次只能被一个进程使用
  • 不可抢占:资源不能被强制剥夺
  • 占有等待条件:占有资源的同时等待其它资源
  • 循环等待条件:等待进程形成循环链

死锁问题

  • 死锁避免:按照顺序加锁(避免死锁);银行家算法
  • 死锁处理:设置锁的获取时间上限(主动放弃策略);监测并中断线程——看门狗(被动放弃策略,丢失上下文,处理结构做反馈处理)

场景-三个线程轮流打印:使用锁 + 条件变量,条件变量控制轮到那个线程打印数据(打印后更新要打印的线程 id),notify 其它线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
int current = 0; // 当前应该打印的线程编号

void printNumber(int id, int total) {
for (int i = 0; i < total; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [id] { return current == id; });

std::cout << "Thread " << id + 1 << ": " << (id + 1) << std::endl;

// 更新当前应该打印的线程编号
current = (current + 1) % 3;

// 通知下一个线程
cv.notify_all();
}
}

std::thread t1(printNumber, 0, total);
t1.join();

多核一致性

  • 缓存与内存一致性:写直达(同时写内存和缓存)、写回(写缓存)
  • 缓存一致性:总线嗅探 + MESI 协议(降低总线压力)

Linux 内存

局部性原理:(缓存、内存、文件系统、数据库)

  • 时间局部性:较短的时间间隔,程序会访问同一个位置
  • 空间局部性:较短的时间间隔,程序会访问相邻的位置

虚拟内存结构:内核空间、栈、文件映射与匿名映射(内存映射区、动态链接库)、堆、BSS(未初始化的)、数据段(初始化的)、代码段。Linux 物理内存管理

  • 变量存储区:BSS 段存储了未初始化的全局变量(或者初始化为0的);数据段存储了初始化的全局变量(初始化不为0)
  • 分段:权限管理

15288240a7bb805995e4ce7224d68c18

虚拟内存

  • 段页式:(段选择再分页:逻辑地址、线性地址、物理地址)
  • TLB 缓存,与 MMU 模块交互

虚拟地址转物理地址

  • 步骤:虚拟地址分解(页号+页内偏移)、查找页表(根据页号获取物理页帧号,可能是多级的页表)、生成物理地址(物理页帧号 X 页大小 + 页内偏移)
  • 多级页表:减少页表占用内存空间(按需加载部分的页表)
  • TLB:(Translation Lookaside Buffer)加速地址转换的缓存(MMU中的),缓存了最近使用的映射关系
  • 物理页不存在:进行页面置换,将页从磁盘加载到物理内存

虚拟内存比物理内存大

  • 机制:分页机制(不是所有的页都在物理内存);内存交换(换出不适用的页)

页面置换

  • 算法:FIFO(先进先出)、LRU(最近最少使用:实现略复杂)、LFU(最不经常使用:可能导致新页面会置换)

虚拟内存作用:隔离保护、共享内存、简化程序设计(无需关心实际分布)、支持大内存、提高内存利用率

线程内存

  • 共享:文件等公用资源、堆、数据段(全局变量和静态变量)、代码段
  • 独享:栈

进程 fork:拷贝 PCB(父进程的一个副本),子进程独占代码段、数据段和堆栈内存(一开始共享,写操作才为子进程分配内存空间(COW))

  • fork 后父进程返回子进程 ID(用于追踪子进程);子进程返回 0(区分父子进程)
  • fork 配合 exec:exec 将替换现有的程序,相互配合用来创建新进程
  • COW:推迟或者避免数据拷贝(redis 用来创建内容映像)
  • clone:可以指定在创建新进程时指定哪些资源共享或者哪些资源复制(适用于创建线程,或者细粒度控制资源共享的场景)

栈帧

  • 作用:支持函数调用和返回
  • 内容:局部变量、参、返回地址

Linux IO

IO 模型:阻塞、非阻塞、IO 多路复用、信号驱动、异步 IO(aio、io_uring)

异步 IO:发起 IO 操作之后可以去干别的事

  • 作用:允许应用同时处理多个请求;不需要多余的操作(不断观察状态之类的,即非阻塞的情形)

多路复用:提高 I/O 操作效率的机制(目标端:一个进程能同时监听多个文件描述符;源端:非阻塞 IO 大量发起系统调用)

  • select:数量有限(1024-32、2028-64)、轮询查找就绪 fd,效率低、fds 集合拷贝,高并发不适用
  • poll:解决了数量有限问题、无需每次重建 fds 集合
  • epoll:红黑树跟踪 fds、事件驱动机制,返回就绪 fd,无需再轮询

零拷贝技术

  • 通用场景
    • DMA:负责将数据从磁盘搬到内存(减轻 CPU 负担),CPU 告诉 DMA 搬运细节
  • 文件传输:零拷贝技术
    • 流程:磁盘 -> 内核 -> 用户 -> socket -> 网卡
    • mmap + write:mmap 后用户空间和内核空间共享文件缓冲区,write 时从文件缓冲区搬运到 socket 缓冲区(CPU);mmap 映射和 socket 搬到网卡都是 DMA
    • sendfile:磁盘到内核缓冲区(DMA),缓冲区描述传递给 socket 缓冲区,SG-DMA 将内核缓冲区拷贝到网卡缓冲区

Linux 内核

Linux 启动:BIOS/UEFI,BIOS 读取 MBR,读取主引导记录找到激活的主分区,MBR 将控制权交给 PBR (Grub),Grub 一阶段(准备硬件)、Grub 二阶段(准备内核)、内核引导(使用 initramfs(或者 initrd.img) 中的驱动程序和工具来挂载真正的根文件系统),系统初始化

内核态、用户态:内核态运行系统与硬件交互;用户态运行用户程序

  • 目的:防止用户进程误操作或者恶意破坏操作系统
  • 切换:系统调用、异常、外部设备中断
  • 减少系统调用(减少切换过程,提高性能):无锁编程、协程(单线程内部调度)

内核态、用户态切换过程:保护用户态上下文,切换到内核态,执行系统调用,恢复用户态上下文,返回用户态

存储系统:虚拟文件系统、设备映射器、通用块层、IO 调度程序、块设备驱动

Linux 编程

链接库

  • 动态链接库:运行时加载
    • 作用:减少可执行文件大小;提高可重用性,减少内存空间占用,即多个进程共享动态链接库;动态更新依赖库文件
    • 链接:指向库文件的符号引用和重定位信息
    • 运行:.dynamic 查找和加载依赖的库文件,解析可执行文件中的符号引用
  • 静态链接库:编译器加入到可执行文件中

调优:追踪系统调用、性能优化命令

嵌入式

中断:事件触发,暂停当前程序,转而处理事件

  • 中断类型:软件中断、硬件中断、定时器中断、异常中断
  • 中断流程:中断请求、中断响应、中断服务、中断返回
  • 中断优先级:用于决定多个中断请求同时发生的情况,高优先级优先处理
  • 避免中断嵌套:禁用中断、设置中断优先级
  • 中断向量表:中断服务地址,发生中断时,根据中断号查找向量表,跳转到对应的程序入口执行

嵌入式存储介质

  • Flash Memory:例如 eMMC(系统存储)
  • SD 卡:系统存储区
  • RAM:DRAM 和 SRAM

voatitle 关键字:应用场景(防止编译器对变量的优化,即编译器假设该值不会发生改变)

  • 硬件寄存器访问:值可能被外部修改,随时读取最新的值
  • 中断服务程序:中断中修改值,循环中需要及时获取到改值的改动
  • 多线程环境:与中断差不多

二、计算机网络

基础

七层模型:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层

四层模型:网络接口层、网际层、运输层、应用层

常见协议

  • 网络层:IP、ICMP、ARP、RARP
  • 传输层:TCP、UDP
  • 应用层:HTTP、DNS、FTP、SMTP

大小端:多字节数据的字节顺序。右侧为小,左侧为大(用指针法判别;或者预定义的宏)

网络协议分层:简化网络通信复杂性,提高系统可维护性和可扩展性。网络设计和实现更加模块化、标准化和易于管理

  • 物理层:物理介质传输
  • 链路层:比特流封装成帧
  • 网络层:包的路由和转发
  • 传输层:端到端的数据传输
  • 会话层:管理不同设备之间的会话(建立、维护和终止)
  • 表示层:数据格式转换、加密和压缩
  • 应用层:提供网络服务和应用程序之间的接口,处理用户请求和响应

中间人攻击

  • 防止:加密通信、VPN、多因素认证

TCP/UDP

区别

  • TCP:可靠、面向连接、字节流传输、全双工、流量控制、拥塞控制
    • 可靠性:序列号+确认号;超时重传+快速重传;校验和
  • UDP:不可靠、无连接、报文传输、多对多(包含点到点)
    • 最大长度:2^32=65535-20;超过最大长度 sendto 会报错(IP层会分片,由 IP 层负责分片和聚合)

三次握手

  • 握手流程:客户端发送初始序列号,服务端确认序列号 & 发送自己的序列号,客户端确认服务端序列号
  • 为什么三次(不是两次):防止重复历史连接(主要,旧SYN连接),同步双方序列号
  • 第三次丢失:服务端重传 SYN-ACK(ACK 是不会重传的)
  • SYN 攻击:把半连接队列打满
    • 解决:增大半连接队列;减少 SYN-ACK 重传次数;记录 IP,丢弃重复请求
  • 每次序号不一样:防止重放攻击和序列号预测;多个连接时避免混淆

四次挥手

  • 挥手流程:一端主动关闭,发送请求(FIN_WAIT_1);对端收到发送确认(对端切换到 CLOSE_WAIT,另一端收到确认切换到 FIN_WAIT_2);对端发送关闭请求(自己切换到 LAST_ACK);一端回复确认,切换到 TIME_WAIT(等待 2*MSL 关闭连接)
  • 为什么等待 2*MSL:连接可靠性,防止对端没收到重发关闭请求
  • 为什么需要四次:优雅关闭连接
  • 为什么需要 TIME_WAIT:可靠的实现全双工连接终止;允许旧连接数据在网络中消逝
  • 合并成三次(② 和 ③ 合并):服务端没有数据要发送 + 开启了 TCP 延迟确认机制

流量控制:(作用于接收者)通过滑动窗口控制发送方的流量;接收方反馈自己的状态让发送方调整滑动窗口大小

拥塞控制: (作用于网络)通过拥塞窗口控制发送速率,窗口通过网络拥塞程度调整

  • 慢启动、拥塞避免;快速重传,快速恢复

监听同一个端口

  • 主进程 + work 子进程(惊群问题,Nginx 使用 accept_mutex 锁解决):主进程执行 bind()、listen() 初始化套接字,然后 fork 新的子进程。在这些子进程中,通过 accept/epoll_wait 同一个套接字来进行请求处理
  • 多进程 + SO_REUSEPORT:
    • 目的:内核级的负载均衡;用于滚动升级(发送信号给旧进程不再处理请求)

HTTP/HTTPS

HTTP 连接过程:URL 处理、DNS 查询(浏览器缓存、本地、DNS 请求)、建立连接发送请求、接收响应、呈现页面

请求报文:请求行、请求头、空行、请求体

响应报文:状态行、响应头、空行、响应体

HTTP 请求方法:GET、POST、OPTION、HEAD、PUT、DELETE

  • GET:用于请求数据,URL传递参数(安全性低,长度限制),幂等的
  • POST:用于提交数据,请求体传递参数,非幂等(可能)

HTTPS 加密过程:获取公钥证书、验证证书、发送会话密钥(公钥加密)、使用会话密钥建立加密连接

HTTPS 连接过程:先三次握手建立连接,再走 TLS 握手(加密连接过程),使用会话密钥建立加密连接

Session、Cookie、Token

  • Session:服务器保存的结构,跟踪用户状态
  • Cookie:客户端保存用户信息机制,是 Session 的一种实现
  • Token:令牌,服务端用来判断是哪个用户

HTTP 版本

  • HTTP 1.0:短连接(每次请求建立一次TCP连接),不支持流水线(一次只能发送一个请求)
  • HTTP 1.1:持久连接(一个TCP连接上可以发送多个请求和响应),支持流水线(一次发多个请求)
  • HTTP 2:二进制传输,多路复用、头部压缩
  • HTTP 3:基于 QUIC(基于UDP的可靠传输)
    • QUIC:可靠 UDP 方案(数据包确认+重传机制),确认方法是乱序确认(通过offset 拼接数据)

DNS

DNS 查询过程:(递归 + 迭代),与本地域名服务器是递归查询的过程;本地域名服务器到根域名服务器或者顶级域名服务器是迭代查询的过程(减轻顶级域名服务器或者根服务器的压力)

三、数据库

缓存

Redis 持久化策略:AOF、RDB

  • AOF:文件大;恢复慢;实时性高
  • RDB:文件小;恢复快;实时性低

数据库概念

BTree 索引和 B+Tree 索引

  • B+Tree 非叶子节点不存储数据

聚簇索引和非聚簇索引

  • 聚簇索引:索引和数据放到一起存储,叶子节点保留了数据行
    • 优缺点:减少磁盘 IO;插入速度依赖插入顺序
  • 非聚簇索引:索引和数据分开存储,叶子节点存储了指向数据的指针

索引覆盖:从非主键索引树上就能查到数据,无需进行二次主键索引树查找

联合索引:遵循最左原则

四大范式

  • 1NF:无重复的列
  • 2NF:属性完全依赖主键
  • 3NF:属性不依赖于其它非主属性
  • 4NF:禁止主键列和非主键列一对多关系不受约束

数据库优化

数据库结构优化

  • 范式优化:表设计合理化(3NF),消除冗余(节省空间)
  • 反范式优化:适当增加冗余(减少 JOIN)
  • 拆分表:减少全表扫描,缩短查询时间(按月分表)

SQL 语句优化

  • where 语句:
    • 避免 != 和 <> 操作(将全表扫描)
    • 避免堆字段 null 判断(将全表扫描)
  • 创建索引(经常判别的字段)
  • 慢日志记录:mysqldumpslow

分表、分区

  • 场景:超大表,SQL 语句已经无法继续优化
  • 操作:
    • 分表:手动将数据分割,创建子表
    • 分区:自动将数据散列,没有子表

读写分离

  • 场景:小幅提升写性能,大幅提高读性能
  • 操作:依赖于数据库的主从复制,主数据库处理写,从数据库处理读(将SQL通过逻辑判断或者中间件选择读写数据库执行语句)

存储过程

  • 定义:SQL 语句和控制语句的预编译集合,保存在数据库中,可以调用执行
  • 场景:模块化设计;执行速度快

pg_repack

  • 目标:在线清理表空间,解决大量更新引起表膨胀问题

四、软件设计

面向对象设计

面向对象六大原则

  • 单一职责原则:一个类只干一件事
  • 开闭原则:类对扩展开放、对修改封闭
  • 里氏替换原则:父类可以替换成子类(就是子类不能覆盖父类方法)
  • 依赖倒置原则:依赖抽象编程(具体类之间不发生依赖关系)
  • 接口隔离原则:类的依赖应该建立在最小范围接口上
  • 迪米特原则:一个对象应该对其它对象有最少的了解(低耦合、高内聚)

设计模式

23 种设计模式

  • 工厂方法:深信服组项目,日志分析系统有不同的数据来源,使用工厂方法来设计这个模块,方便管理和新增新的数据源
  • 单例:深信服组项目,日志控制组件的设计(只有一个实例,减少资源消耗;不适用于变化的对象)
    • 懒汉:静态局部变量
    • 懒汉:双层互斥锁(返回智能指针)
    • 懒汉:call_once
    • 饿汉:代码一运行就创建实例(全局静态变量)
  • 状态:文件系统项目,不同的磁盘设备状态执行同一操作时会有不同的效果(磁盘已挂载状态和磁盘未挂载状态执行磁盘挂载操作,已挂载状态就直接返回已挂载了)

应用设计模式

  • 插件化编程:通过动态加载功能模块(插件)来增强主程序功能的软件设计策略。通过制定标准化接口,确保插件和主程序之间的兼容性与独立性。提高软件灵活性和可扩展性。
  • 动态观测:在应用中植入数据观测接口和数据观测控制(通过宏来选择是否植入数据观测代码,保证正式发布的应用的安全性),使用 Socket 套接字与其通信获取应用实时数据。

模块设计

  • 看门狗:
    • 目的:监控持有锁的线程是否活跃
    • 设计:注册接口(线程注册信息和看门狗处理接口),喂狗接口(线程通过该接口喂自己的狗),看门狗监控线程运行状态(如果线程很久没动就可以通过注册的处理接口来做线程销毁),反注册接口;看门狗处理接口(安全释放资源)+ 终止线程
  • 内存池:
    • 目的:提前申请内存,减少内存的频繁申请和释放
    • 设计:内存对齐、线程安全性
  • 线程池:
    • 目的:提前创建线程,减少线程的创建和销毁
    • 考虑:线程个数(IO密集型/CPU密集型、系统负载、队列任务个数)。IO 密集型设置线程个数为 2N;CPU 密集型设置为 N+1
    • 设计:任务队列(存储任务)、工作线程(从任务队列拿任务并执行);接口设计(初始化,提交任务——任务需要的资源,任务完成后的回调接口)
  • LRU 缓存:
    • 目的:优先资源下的缓存淘汰策略(缓存达到上限时,淘汰最近最少使用的数据)
    • 设计:put 接口 和 get 接口
  • 负载均衡:
    • 文件系统:
      • 大文件分割成小文件,分布在多个磁盘上(通过文件分片实现负载均衡);或者使用 RAID 0 来实现多个磁盘条带化存储,提高读写性能
      • 算法:加权轮询算法
  • 限流器:
    • 作用:限流
    • 设计:令牌桶、漏桶、滑动窗口计数
  • 幂等性:
    • 作用:无论调用多少次接口,都得到相同的结果
    • 设计:标识
  • 一致性哈希
  • Reactor、Proactor
  • ID 生成器
  • 无锁队列
  • 秒杀系统

方案设计

多线程、分治

断点续传

  • 客户端请求携带需要的数据范围,服务端根据需要的数据范围发送部分数据

提高吞吐量

  • 并行化:并行传输(多个连接)、异步传输(无需返回确认)
  • 优化协议:使用 HTTP/2 多路复用、使用 UDP 传输
  • 优化 TCP :增大 TCP 流量窗口
  • 服务端负载均衡:多个服务端处理,分散负载

设计知识

模块:复杂的系统进行分解:

  • 划分标准:功能独立性(相似的功能)、信息隐藏(无需对外部暴露内部信息)、性能考虑(两个部分合成一个)、依赖分析

耦合:模块之间的依赖程度(内容直接调用、共享全局数据区域、通过调用通信)

  • 降低耦合性:模块化设计,信息隐藏

设计流程:需求分析(明确需求和产出)、系统设计(整体架构和结构)、详细设计(模块细节)、编码实现(分模块开发、主流程拓展开发)、测试(单元、集成、系统)、验收、部署和维护

系统设计

  • 外部边界:提出正确的问题,做出合理的假设(场景,系统需要与外部交换的地方),收集建立系统的必要信息
  • 内部设计:基本场景(系统架构设计),发展场景(系统可扩展性)

软件测试

黑盒 & 白盒

  • 白盒:测试人员可以了解和分析被测软件的内部结构,代码和逻辑
    • 场景:单元测试(分支覆盖率)、集成测试
  • 黑盒:测试人员不了解软件内部结构,只关注软件功能
    • 场景:功能测试、系统测试、验收测试

五、语言知识

语言版本

C++ 11 新特性:智能指针、Lambda、Function、左值和右值、关键字(final & override、default & delete、explicit、constexpr、enum class)

语言基础

编译过程:预处理(#开头的内容、删除注释、添加调试信息)、编译(词法分析、语法分析、语义分析、中间代码生成、目标代码生成)、汇编(将汇编代码转换成机器码)、链接(将目标文件链接到一起形成可执行文件,包括地址和空间分配、符号决议和重定位)

static 关键字:限制变量或者函数的作用域

extern 关键字:引用别处定义的变量

  • static 变量不能被 extern 引用(限制作用域了)

引用和指针

  • 初始:引用需要绑定变量(指针可以设置为 NULL)
  • 修改:引用初始化不能修改(指针可以修改)
  • 使用:引用可以直接使用,无需解引用(指针需要解引用)

malloc 和 new

  • 语法:C++ 语法、C 语法
  • 方式:malloc 仅分配内存,new 还包括了构造函数初始化过程;malloc 需要传入大小,new 自动计算大小
  • 返回:malloc 返回 void;new 返回对象指针
  • 失败:malloc 返回 null;new 抛出异常 std::bad_alloc

智能指针:自动管理内存的指针,无需手动释放内存,确保对象正确的销毁

  • std::unique_ptr:独占,内存只能由一个 unique_ptr 拥有,超出作用域自动释放
  • std::shared_ptr:共享,允许多个 shared_ptr 指向同一个对象,最后一个引用超出作用域自动释放
  • std::weak_ptr:监视 shared_ptr 的资源是否存在。用来返回 this 指针和循环引用问题

自动释放逻辑:shared_ptr 维护一个引用计数,当创建一个新的实例时引用计数增加,当销毁一个实例时引用计数减小,当引用计数减小到 0 时,此时会自动释放内存(RAII 思想)

auto:自动推导变量类型,编译时根据变量的初始化表达式来推导变量的类型

  • 编译时类型推导、推导时忽略引用和 const

c++ volatile 关键字:防止编译器优化,每次访问变量都从内存中读取最新的值

内联函数

  • 优点:减少函数调用开销,类似于宏(都是编译期替换的),但是比宏更安全
  • 缺点:代码膨胀(可执行文件变大),编译时间增加

RAII 思想:Resource Acquisition Is Initialization,资源获取即初始化

  • 思想:利用栈上局部变量的自动析构抱枕资源一定会被释放(例如锁的自动释放、打开的文件自动释放)
  • 异常:如果抛出了异常,局部对象会被析构

错误处理:errno(全局错误代码变量)、perror()(打印当前错误码的文本形式,可以提供前缀)、strerror()(返回当前错误码文本指针)

Python GIL 锁:全局解释器锁,属于互斥锁,只允许一个线程保持解释器的控制权

  • WHY:确保内存对象引用计数的
  • 其它语言:Ruby

垃圾回收

  • Python 垃圾回收:依赖引用计数
  • Java 垃圾回收:依赖具体的回收策略

JVM

  • 优缺点:平台无关性、自动垃圾回收;启动时间长、内存占用高

语言类型

  • 编译型:通过编译器将源代码转换成机器码(C、C++、Go、Rust)
  • 解释型:代码在运行时由解释器逐行解释并运行(Python、Ruby)
  • 混合型:转换成中间语言,再由 JVM 解释执行(Java)

面向对象

面向过程 & 面向对象

  • 过程:拆分解决问题步骤,实现函数步骤
  • 对象:将问题事物拆分成对象,对象描述事物在解决问题中的行为

C 实现面向对象 & 为什么

  • 为什么:C 效率高,在资源不是很多的 MCU 场景中使用面向对象的方法来设计程序

面向对象三大特性:封装、继承、多态(实现模块化,可重用)

  • 封装:数据(属性)和数据操作(方法)组合在一个类中
  • 继承:一个类从另一个类获得属性和方法的过程。提高代码复用性和可维护性
  • 多态:不同类的对象使用相同的接口名字,但具有不同的实现特性

C++ 多态:基类的指针在运行时指向派生类对象,并调用派生类的成员函数

  • 实现机制:虚函数(派生类重写基类方法),抽象基类(纯虚函数,实现接口)
    • 条件:通过基类调用虚函数 & 被调用的函数是虚函数且完成对基类虚函数的重写
  • 实现方法:动态多态(虚函数、纯虚函数),静态多态(模板函数、函数重载)

虚函数和纯虚函数:纯虚函数是只定义了一个接口

  • 包含纯虚函数的类叫抽象类,不能被初始化

构造函数 & 析构函数(虚函数)

  • 构造函数不能是虚函数:对象虚函数表未初始化
  • 析构函数是虚函数:确保通过基类销毁对象时能够正确地调用派生类地析构函数,否则只会调用基类析构函数导致资源泄露

重载、重写和隐藏

  • 重载:相同作用域有相同的方法名,但是参数变量不同
  • 重写:派生类中重定义基类的方法(扩展基类的功能)
  • 隐藏:派生类隐藏基类中的同名函数(不管参数是不是相同)

菱形继承

  • 问题:内存重复、命名冲突
  • 解决:虚继承(共同的类在虚函数表中是共享的)

左值和右值

  • 左值:占据内存的一个可识别的位置
  • 右值:不是左值的就是右值

构造、复制、移动:构造、复制、移动:

  • 构造:构造函数,生成本类
  • 复制:内容的拷贝
  • 移动:内容的转移
  • 复制构造:MyClass(const MyClass& other)
  • 移动构造:MyClass(MyClass&& other) noexcept
  • 复制赋值:MyClass& operator=(const MyClass& other)
  • 移动赋值:MyClass& operator=(MyClass&& other) noexcept

类型擦除:一种隐藏具体信息的编程技术

  • 方法:基类+虚函数、模板+包装类、std::any(C++ 17)

内存管理

内存分配

  • 堆上分配:brk & sbrk(改变堆大小),malloc & free、calloc 和 realloc、posix_memalign(分配对齐)内存分配-堆
    • 跟踪调试:mtrace 和 muntrace、Valgrind、使用智能指针
  • 栈上分配:alloca 内存分配-栈

内存对齐

  • 作用:提高 CPU 访问速度
  • 方法:posix_memalign

内存泄漏:申请的内存没有释放

  • valgrind–memcheck:监听分配和释放内存的函数调用(感知内存泄漏、访问越界、使用未初始化内存、使用已释放内存)
  • 重载 new/delete:跟踪调用(在全局父类中进行选择性的重载,要注意多线程的问题)
  • perf:添加 malloc 探针,跟踪分配器事件
  • 自定义设计:上面的都无法跟踪内存块的使用过程。如果参考智能指针,对指针做一层封装,就可以跟踪所有的申请和释放过程了(通过封装宏来增加文件、行号等信息)。不足之处是侵入式编程,重新定义了指针,通过宏来控制是否进行跟踪

Python 内存回收机制

  • 主动回收:

STL

STL 类型

  • 序列容器:array、vector、deque、list、forward_list
  • 关联容器:set & multiset、map & multimap(红黑树)
  • 无序容器:unordered_*(哈希表)
  • 其它容器:string、pair、tuple
  • 容器适配器:stack、queue、priority_queue

迭代器失效

  • 序列式容器:删除元素会使后面的迭代器失效,但是 erase 会返回下一个有效的 iterator
  • 链表式容器:删除元素不会导致其它迭代器失效,erase 也会返回下一个有效的 iterator
  • 关联式容器:删除元素不会导致其它迭代器失效(erase 返回值为 NULL)

模板

  • 目的:同样的代码适用于不同类型下的使用,实现代码复用(泛型编程)
  • 原理:编译阶段根据模板类型确定应该产生什么类
  • 推导:根据模板参数的实际类型推导出模板参数类型

push_back & emplace_back

  • push_back:接收对象,创建临时对象,移动或者复制
  • emplace_back:接收构建参数,原地构造对象

vector 扩容机制

  • size(实际已经使用的);capacity(已使用+未使用)
  • 扩容方案:两倍扩容(插入空间比原空间小);插入空间扩容(插入空间比原空间大)
  • 最佳方案:两倍?最佳应该是斐波那契数列,能够复用原来的内存空间
    • 因子越大,分配越多,内存利用率低;因子越小,分配越少,再分配可能性越大

cast 方法

  • static_cast:编译时类型转换,基本类型、指针类型
  • dynamic_cast:运行时类型转换,基类转派生类(确实是派生类,否则失败)
  • const_cast:添加或移除变量的const或volatile限定符
  • reinterpret_cast:低级别类型转换,不考虑指针之间的关系

move & forward:(C++ 11)

  • move:将对象转换成右值引用
  • forward:完美转发,用于模板编程,保证参数的类型在转发过程中保持不变

std::variant:类型安全的联合体,存储多种已知类型的情况(C++ 17)

  • std::get:通过类型 T 访问 std::variant 中的值。如果当前存储的类型不是 T,会抛出 std::bad_variant_access 异常
  • std::get_if:返回一个指向 std::variant 中类型为 T 的值的指针。如果当前存储的类型不是 T,返回 nullptr
  • std::visit:使用访问者模式(Visitor Pattern)来处理 std::variant 中的值(std::visit)

std::any:存储任意类型的值(C++ 17)

  • 通过 std::any_cast 可以访问当前存储的值,如果当前存储的类型不是 T,会抛出 std::bad_any_cast 异常

工程

Make & Makefile

  • 配置文件:.config 文件,写入配置选项

make & cmake

  • gcc:编译指令(make 的调用对象)
  • make:执行者,负责按照给定的规则执行构建操作
  • cmake:协调者,负责生成构建规则,使得 make 和其它构建工具可以完成工作

调试问题

GDB 调试原理:通过系统调用 ptrace 实现的

  • 设置断点:断点处指令重写为 INT 3(中断 SIGTRAP),到达之后指令被修改回来
  • 主进程跟踪子进程的所有信息(ptrace实现)

调试方法

  • 定位问题 & 复现问题
  • 解决问题(查找原因:Google + 官方文档)
  • 记录归档

细节问题

最大值的宏:#define MAX(a, b) ((a) > (b) ? (a) : (b))

  • 问题:展开副作用(自增运算)
  • 解决:使用 inline 内联函数

六、框架

Docker

blog.csdn.net/FateZRK/article/details/125750830

虚拟化

  • 定义:一台计算机虚拟为多态逻辑计算机
  • 作用:应用程序和系统内核资源解耦,以操作系统级别隔离,提高资源利用率
  • 瓶颈:每个任务的处理效率会打折扣

Docker 原理

  • 容器:独立的软件包,包含运行引用程序所需的一切。容器共享操作系统内核
  • 镜像:包含创建容器所有文件和配置(只读的模板)
  • 引擎:负责管理容器的生命周期,包括创建、启动、停止和删除。包括:
    • Docker Daemon:运行主机上的后台服务,管理镜像、容器、网络和存储卷
    • Docker CLI:命令行工具,与 Docker Daemon 交互
    • REST API:外部工具与 Docker Daemon 交互接口
  • 运行时:负责运行容器的底层组件。docker 使用 runc 作为容器运行时(标准容器运行时),负责创建和运行容器,并与主机操作系统内核交互
  • 命名空间:Linux 内核提供的一种隔离机制,用于隔离进程的资源。docker 使用命名空间来隔离容器的进程、网络、文件系统、用户和主机名资源,实现容器之间的隔离
  • 控制组:控制组(cgroups)是 Linux 内核提供的一种资源管理机制,用于限制和隔离进程的资源使用(CPU、内存、磁盘 IO)

RPC

www.xiaolincoding.com/network/2_http/http_rpc.html#%E4%BD%BF%E7%…

**RPC **:计算机通信协议,就像调用本地函数一样。隐藏网络通信复杂性,使得分布式系统开发更加简单和直观

RPC 原理:客户端调用、参数序列化、网络传输、服务器接收和执行、结果序列化、网络传输、客户端接收

  • 传输前序列化

RPC 框架:(注册中心、客户端、服务端)

  • gRPC:Google 做的
  • dubbo:阿里做的,国内最早的 rpc 框架

RPC 与 HTTP

  • 性能:RPC 更高效(二进制协议),低延迟(通信开销小);但是 RPC 设计复杂

Kafka

kafka 为什么不丢数据

  • 数据持久化存储 + 副本机制
  • 生产则会确认机制(保证数据写入)
  • 消费者偏移量(确保可以从上次消费位置继续消费)