关键词
virtual
-
作用
- 定义虚函数
- 仅定义未实现的函数为纯虚函数
- 有纯虚函数的类,叫抽象类,无法直接实例化
-
原理
- 虚函数是通过在类的虚函数表(vtable)中存储函数指针来实现的
- 编译器会为包含虚函数的类创建一个虚函数表和指向虚函数表的虚函数指针(vptr),虚函数表是一个指向虚函数的指针数组
- 每个定义虚函数的类都有自己的虚函数表,包含该类所定义的虚函数地址
- 当派生类重写(Override)基类的虚函数时,会在其自己的虚函数表中存储该虚函数的新地址,覆盖了基类虚函数表中相应位置的函数指针
- 每个派生类都有自己独立的虚函数表,其中存储着派生类覆盖(Override)或新增的虚函数的地址
- 当通过指向基类的指针或引用调用虚函数时,会通过虚函数指针查找对应的虚函数表完成调用,这个过程是在运行时动态决定的,被称为动态绑定
- 虚函数是通过在类的虚函数表(vtable)中存储函数指针来实现的
-
注意
- 基类不可以是虚函数,析构函数在有资源释放时必须是虚函数
- 虚函数通过查找虚函数表调用的,而虚函数是内存中的一片区域,需要先构造对象并创建内存空间,才能完成虚函数的调用,所以构造函数不能为虚函数
- 析构函数,在存在释放内存空间的代码时,必须设置为虚函数,负责会存在因虚构函数无法调用导致的内存泄露问题
- 基类不可以是虚函数,析构函数在有资源释放时必须是虚函数
volatile
提醒编译器使用 volatile 声明的变量随时可能改变,在编译期间不进行指令重排优化。C++ 标准保证 volatile 变量之间不会重排,不保证 volatile 和非 volatile 变量的重排优化。
此外,还有硬件级别的指令重排,volatile 无法保证。
内存
内存模型
内存管理
- 代码段
- 数据段
- 堆
- 栈
语言
四种强制转换
- static_cast
- 编译时期的静态类型检查
- 相当于C语⾔中的强制转换
- 不能实现普通指针数据(空指针除外)的强制转换
- ⼀般⽤于⽗类和⼦类指针、引⽤间的相互转换
- 没有运⾏时类型检查来保证转换的安全性
- dynamic_cast
- 运⾏时的检查
- 动态转换的类型和操作数必须是完整类类型或空指针、空引⽤
- const_cast
- 强制去掉不能被修改的常数特性
- reinterpret_cast
- 将指针或引⽤转换为⼀个⾜够⻓度的整型、将整型转换为指针或引⽤类型
1 | const_cast<std::string &>(service); // const std::string &service; |
GLIBC 每次内存分配都会进行系统调用吗
glibc 维护一个内存池,分配内存时优先从内存池获取,分配失败再向操作系统分配内存。
概念
- Chunk,glibc 对内存进行分配和管理的基本单元,chunk 是 glibc 维护的一块连续内存
- Arena,一组相关的内存 Chunk 的集合,glibc 使用多个 Arena 来管理内存分配,其中至少有一个主 Arena(main arena),还可以包括线程特定的 Arena(thread arena)。每个 Arena 都有自己的 Chunk 空闲链表和其他数据结构,用于记录已分配和空闲的内存块。
- Heap,是指进程可用的虚拟地址空间中用于动态内存分配的部分,Heap 由一系列的内存区域(region)组成,每个内存区域都是通过调用 brk 或 mmap 系统调用来获取
Heap 是进程可用的虚拟地址空间中的动态内存区域,Arena 是 Heap 内部的内存池,Chunk 是 Arena 中的内存分配单元。
获取内存的系统调用
- brk,通过调整堆的结束地址来分配和释放内存,对于小型内存分配比较高效
- mmap,通过请求操作系统分配新的虚拟内存区域,适合大块内存的分配
分配流程
- 线程首先需要获取一个 Arena。查找环形链表获取未加锁的 Arena,如果都已加锁则创建 Arena(总数有限制)
- 搜索 Chunk。尝试 fastbin 等机制,查找合适的 Chunk
- 创建 Chunk。如果未找到,则会调用 brk(主 Arena) 或 mmap(非主 Arena)向操作系统申请内存,扩充 Heap
- 分割 Chunk。对获取的 Chunk 按照用户需要的大小进行内存切分,并返回起始内存指针
- 内存对齐。glibc 确保返回的内存块满足特定的对齐要求
特殊函数
包含完整特殊函数的类
1 | class SampleClass { |
初始化列表
- 不能使用初始化列表来初始化 vector 的情况
- 当元素类型没有默认构造函数: 如果
std::vector
的元素类型没有默认构造函数或者是不可复制的类型 - 当元素类型是有状态的: 如果
std::vector
的元素类型是具有状态的类(即具有非平凡的构造函数和析构函数) - 当元素类型是引用类型:
std::vector
不能持有引用类型的元素
- 当元素类型没有默认构造函数: 如果
示例
1 | void run { |
Trivially Copyable
trivially copyable 类型包括如下:
- 未加 const 或 volatile 修饰符(cv-unqualified)的标量类型(scalar type)
- 基本整型
- 浮点型
- 字符型
- 布尔型
- trivially copyable 对象类型(trivially copyable class),同时满足以下条件
- 没有非平凡(non-trivial)的拷贝构造函数
- 没有非平凡(non-trivial)的移动构造函数
- 没有非平凡(non-trivial)的复制赋值操作符
- 没有非平凡(non-trivial)的移动赋值操作符
- 有一个平凡的析构函数
- 上述类型的 arrays
- 上述类型的 cv-unqualified 类型
Lock Free
Lock Free 是一种并发编程的概念,用于描述一种编写多线程代码的技术或算法,该技术或算法在没有使用传统的互斥锁(mutex)机制的情况下实现了线程安全。
Memory Order
Memory Order(内存序)是用于指定原子操作和多线程间内存可见性的概念。
memory_order_relaxed
: 最轻量级的内存序,没有任何同步或顺序要求,允许重排和乱序执行memory_order_acquire
: 获取语意memory_order_release
: 释放语意memory_order_acq_rel
: 结合了memory_order_acquire
和memory_order_release
,同时有获取和释放的语义。适用于具有获取和释放语义的原子操作。memory_order_seq_cst
: 最严格的内存序,提供全局顺序一致性。保证所有线程对原子操作的执行都具有相同的全局顺序
memory_order_consume 实现成本高,使用复杂,通常被
memory_order_acquire
替代
Value Initialization
1 | // 变量定义, 但未初始化 |
以标量为例,未初始化的变量,其值是未定义的,具体行为取决于编译器。
STL
常见数据结构及底层实现
数据结构 | 底层实现 | 是否连续内存 | 备注 |
---|---|---|---|
vector | 数组 | 是 | |
list | 双向链表 | 否 | |
map / set | 红黑树 | 否 | 平衡树,更新后进行平衡 |
unordered_map | 哈希表 | 否 |
容器
vector
1 | v.at(index); |
queue
1 | q.front(); |
stack
1 | s.top(); |
算法
sort
1 | std::vector<int> v1 = {1, 3, 2, 4, 6, 5}; |
thread
c++ thread
1 |
|
detach
1 | void payload() { |
注意
- 当进程退出时,操作系统会停掉所有由该进程创建的线程(detach 后也会被 kill)
c thread
1 | struct Arg { |
信号量
mutex & shared_mutex
1 | include "mutex" |
unique_lock & lock_guard
std::lock_guard
是一个轻量级的互斥锁封装,它提供了一种方便的方式来管理互斥锁的锁定和释放。std::unique_lock
是一个更加灵活和功能强大的互斥锁封装,提供了与 std::lock_guard
类似的锁定和释放互斥锁的功能,但还可以进行更多的操作。std::unique_lock
允许在构造函数中选择锁定模式,包括延迟锁定、递归锁定和尝试锁定等。
Tips
- 为什么 condition_variable 的 wait 方法使用 unique_lock
- lock_guard、scoped_lock 无法获取 mutex,unique_lock 可以
condition_variable
1 | // wait |
说明
- Predicate > mutex,即便获取信号量,不满足 Predicate,仍不退出 wait
- duration/time_point > Predicate,即时不满足 Predicate,到达指定时间限制,仍会退出 wait
示例
1 | using namespace std::literals; |
并发控制
- 互斥锁
- 读写锁
- atomic
- thread local
有哪些锁机制
锁 | 说明 | 备注 |
---|---|---|
std::mutex | 互斥锁 | |
std::shared_mutex | 读写锁(共享互斥锁) | |
std::recursive_mutex | 可重入锁(递归锁) | 需要程序确保每次上锁都会释放 |
std::timed_mutex | 计时互斥锁 | |
std::recursive_timed_mutex | 计时递归锁 | 需要程序确保每次上锁都会释放 |
如何实现自旋锁
可以通过原子操作和循环来实现自旋锁。
1 | include <atomic> |
atomic
std::atomic
提供了一种机制,使得多线程环境下对特定类型的变量进行原子操作成为可能。通过使用 std::atomic
创建的原子类型,我们可以确保在多线程环境中读写这些变量时,不会出现数据竞争的问题。这为并发编程提供了更高的可靠性和可预测性。
对于 atomic 模板类型,要求必须是 trivially-copyable,可以通过 std::is_trivially_copyable<T>
判断。
atomic<T>::is_lock_free
is_lock_free
用来检查特定类型的原子操作是否为无锁。
1 | // CASE 1 |
初始化
1 | std::atomic<int> a; |
赋值
1 | std::atomic<int> a; |
使用方式
- 对临界资源做竞争保护
- 创建对应类型的 atomic 变量
- 设置值
store
- 赋值
exchange
compare_exchange_weak
compare_exchange_strong
- 使用
load
或()
操作符来获取值
协程(coroutines)
C++ 20 引入了协程,详见 cppreference - coroutines。在此之前的标准,可以通过 makecontext()/swapcontext()
来手动管理线程的 Context 切换,实现协程。或者使用其他库的实现,比如 boost::corountines
、brpc 等。
实现
- C++ 20
boost::corountines
- bloomberg quantum 是一个可扩展的 C++ 协程框架
- 底层实现包含两个 thread pool
- Fiber Pool,运行协程任务的主线程池
- IO Thread Pool,运行 IO 任务的线程池
- 底层实现包含两个 thread pool
- libco ,腾讯开源的协程库,已许久未更新
- libaco
字符串
查找
1 | char *strchr(const char *string, int c); // NULL / 第一个位置 |