C++基础
placement new(定位 new)
struct SharedMemory {
int head;
char packets[1024];
}
void* mem_addres = malloc(2048);
SharedMemory *mem = statci_cast<SharedMemory*>(mem_addres);
new (mem) SharedMemory();
在mem的地址上构造初始化数据,placment new的作用是在已经分配的内存地址上分配对象。和正常写法
A *a = new A();
底层做的事情: 1.分配内存,从堆内存上分配 sizeof(A)大小
2.调用A的构造函数
3.返回新构造对象A的实例指针
位预算
异或运算
a ^ a = 0(相同的两个数异或结果是 0)a ^ 0 = a(任何数与 0 异或结果是它本身)
在计算机中:
- 负数通常以二进制补码表示
- 正数以原码表示
对于负数,先得到负数的绝对值的原码,取反,+1
应用
uint8_t flags; // 0x1:今日登录 0x2:完成战队任务 0x4:参与战队赛
uint8_t flags 用于存储多个状态信息,通过位域(bit field)方式将状态压缩到一个字节中。每一位(bit)代表不同的状态标志:
0x1对应00000001,表示 "今日登录"0x2对应00000010,表示 "完成战队任务"0x4对应00000100,表示 "参与战队赛"
cpp
#include <stdio.h>
#include <stdint.h>
// 定义状态标志
#define FLAG_TODAY_LOGIN 0x1 // 今日登录
#define FLAG_TEAM_TASK 0x2 // 完成战队任务
#define FLAG_TEAM_MATCH 0x4 // 参与战队赛
int main() {
uint8_t flags = 0; // 初始化flags为0,表示没有任何状态
// 设置"今日登录"状态
flags |= FLAG_TODAY_LOGIN;
printf("Flags after TODAY_LOGIN: 0x%02X\n", flags); // 输出当前flags的状态
// 设置"完成战队任务"状态
flags |= FLAG_TEAM_TASK;
printf("Flags after TEAM_TASK: 0x%02X\n", flags); // 输出当前flags的状态
// 检查"今日登录"是否已经设置
if (flags & FLAG_TODAY_LOGIN) {
printf("User has logged in today.\n");
}
// 清除"完成战队任务"状态
flags &= ~FLAG_TEAM_TASK; // 清除标志
printf("Flags after clearing TEAM_TASK: 0x%02X\n", flags); // 输出当前flags的状态
// 检查"参与战队赛"是否已经设置
if (flags & FLAG_TEAM_MATCH) {
printf("User participated in the team match.\n");
} else {
printf("User has not participated in the team match.\n");
}
return 0;
}
decltype 求表达式的类型 && auto
C++ 中的类型限定符
类型限定符提供了变量的额外信息,用于在定义变量或函数时改变它们的默认行为的关键字。
| 限定符 | 含义 |
|---|---|
const | 定义常量,表示该变量的值不能被修改 |
volatile | 编译器可能会将变量缓存到寄存器中以提高性能,但 volatile 强制每次访问都从内存中读取。 volatile 是一个类型修饰符,用于告诉编译器不要对变量进行优化(如缓存到寄存器、重排指令等),因为该变量的值可能被外部因素(如硬件、中断、多线程等)意外修改 |
restrict | 由 restrict 修饰的指针是唯一一种访问它所指向的对象的方式。只有 C99 增加了新的类型限定符 restrict |
mutable | 用于修饰类的成员变量。被 mutable 修饰的成员变量可以被修改,即使它们所在的对象是 const 的 |
static | 用于定义静态变量,表示该变量的作用域仅限于当前文件或当前函数内,不会被其他文件或函数访问 |
register | 用于定义寄存器变量,表示该变量被频繁使用,可以存储在CPU的寄存器中,以提高程序的运行效率。 在 C++11 中被标记为弃用(deprecated),在 C++17 中被正式移除 |
explicit | 当构造函数接受单一参数时,特别需要使用 explicit 来避免隐式转换导致的潜在问题 |
noexcept | 提高性能;与移动语义一起使用 |
static
影响着变量或函数的生命周期,作用域,以及存储位置。
volatile
volatile用来修饰变量, 告诉编译器不要优化该变量的访问,避免对该变量的读写进行不必要的优化
-
常见的应用场景:
- 硬件寄存器:在嵌入式系统编程中,硬件设备的寄存器可能会随时被硬件更改。为了保证对这些寄存器的访问不会被优化掉,通常会用
volatile修饰寄存器变量。 - 多线程编程:在多线程程序中,如果一个线程在修改某个变量,而其他线程正在读取该变量,使用
volatile修饰该变量可以确保每个线程读取到变量的最新值。
- 硬件寄存器:在嵌入式系统编程中,硬件设备的寄存器可能会随时被硬件更改。为了保证对这些寄存器的访问不会被优化掉,通常会用
一个参数可以既是const又是volatile吗?
可以,硬件状态寄存器通常会由外部硬件或外部设备修改,因此需要使用 volatile。而它的值通常不应由程序直接修改,因此可能需要 const。
线程安全:volatile 并不提供线程同步机制
请解释C++中的虚函数机制和多态实现原理
c++虚函数机制和多态原理是是面向对象编程中重要特性,能够让程序根据对象的实际类型来决定调用哪个函数。
虚函数机制:
- 当基类声明函数为
virtual时,编译器会为该类生成一个虚函数表(vtable) ,存储虚函数的地址。派生类若重写(override)虚函数,其vtable中对应项会被替换为新函数地址。 - 每个对象包含一个虚表指针(vptr) ,指向所属类的vtable。调用虚函数时,程序通过vptr查找vtable,再跳转到实际函数地址,实现动态绑定。
多态实现原理:
多态分为: 静态多态(函数重载和模板实现),在编译时确定调用哪个函数;
动态多态:通过虚函数机制实现:
a.通过基类指针或引用调用虚函数时,程序通过对象的vptr找到函数表,从虚函数表中查找对应函数的实际地址,调用该地址指向的函数
总结:虚函数通过vtable和vptr实现动态分发,多态则依赖这一机制在运行时根据对象实际类型调用正确函数,体现“一个接口,多种形态”的特性
底层原理分析:
1.虚函数调用比普通函数多一次间接寻址\
//普通函数调用的寻址过程
obj.normalFunc();
编译器在编译期就能确定函数地址,生成的机器码大致是:
1. 将`obj`的地址加载到寄存器
1. 直接调用已知地址的函数(可能是`call 0x12345678`这样的直接调用)
//虚函数调用的寻址过程
obj.virtualFunc();
需要经过两次内存访问(即多一次间接寻址):
1. **第一次间接寻址** - 获取虚表指针:
- 通过对象地址找到虚表指针(vptr)
- 每个有虚函数的对象都隐含一个vptr成员(通常位于对象起始位置)
2. **第二次间接寻址** - 获取函数地址:
- 通过vptr找到虚函数表(vtable)
- 在虚表中找到对应偏移位置的函数地址
3. **最终调用**:
- 使用找到的函数地址进行调用
2.虚表内容在编译期就已基本确定,虚表存储在程序的只读数据段,因为其内容在程序运行期间通常不会改变。\
- **编译期确定部分**:
- 虚函数的数量和顺序
- 每个虚函数的地址(对于非纯虚函数)
- 虚表的结构和大小
- **运行期可能变化部分**:
- 动态库加载可能导致虚函数地址改变(但虚表结构不变)
3.虚表和虚表指针内存分布
存储位置
虚表存储位置:每个包含虚函数的类通常会有一个虚表(只有一个),被该类的所有实例共享,在编译期确定,存储在数据段
虚表指针:每个对象包含一个指向虚函数表的指针(vptr)。这个指针通常是对象内存的**第一个成员**,这是因为虚函数需要在运行时查找,因此对象需要快速访问它所属类的虚表。vptr在构造函数期间被初始化。
内存布局
class Base {
public:
virtual void func1() {}
virtual void func2() {}
};
class Derived : public Base {
public:
void func1() override {}
virtual void func3() {}
};
虚表内存布局:
Base的虚表:
+-------------------+
| &Base::func1 |
| &Base::func2 |
+-------------------+
Derived的虚表:
+-------------------+
| &Derived::func1 | // 覆盖了Base版本
| &Base::func2 | // 继承自Base
| &Derived::func3 | // 新增虚函数
+-------------------+
对象实例内存布局
Derived对象实例:
+-------------------+
| vptr | -> 指向Derived的虚表
| Base类成员变量 |
| Derived成员变量 |
+-------------------+
-------------------多重继承的布局----------------------
每个基类都有自己的虚表指针:
class Base1 { virtual void f1(); };
class Base2 { virtual void f2(); };
class Derived : public Base1, public Base2 { /*...*/ };
Derived对象:
+-------------------+
| Base1 vptr | -> Derived的Base1虚表
| Base1成员 |
+-------------------+
| Base2 vptr | -> Derived的Base2虚表
| Base2成员 |
+-------------------+
| Derived成员 |
+-------------------+
5.虚函数表本质上是一个包含函数指针的数组,这些指针通常是指向虚函数的地址。为了统一存储不同签名的虚函数指针,编译器通常会将这些指针表示为 void* 类型。这使得虚函数表可以容纳不同类型的函数指针,同时也为调用虚函数时的类型转换提供了灵活性
智能指针
说说智能指针的使用场景和实现原理
智能指针的核心是通过RAII(Resource Acquisition Is Initialization)机制自动管理动态内存的生命周期,解决传统裸指针(new/delete)的以下问题:
- 内存泄漏:忘记手动释放内存。
- 悬垂指针(Dangling Pointer) :访问已释放的内存。
- 重复释放:多次调用
delete导致未定义行为。
C++标准库中的智能指针类型及使用场景
1. std::unique_ptr(独占所有权指针)
-
定义:独占式管理动态资源,同一时间只能有一个
unique_ptr指向同一对象,不可复制但可移动(转移所有权)。 -
实现原理,实现关键:通过禁用拷贝构造函数和赋值运算符(
delete),仅允许移动语义 -
使用场景:
- 需要明确独占资源所有权的场景(如工厂模式返回的对象)。
- 作为类成员变量时,自动释放资源(避免析构函数中手动
delete)。
2. std::shared_ptr(共享所有权指针)
-
定义:通过引用计数实现多个指针共享同一资源,当计数归零时自动释放内存。
-
实现原理,实现关键:引用计数:通过控制块(Control Block)存储引用计数和弱引用计数,通常与对象内存分配在一起。
-
线程安全:引用计数的增减是原子的(C++11保证)。
-
控制块分配:
make_shared:一次性分配对象和控制块的内存(高效)。- 直接
new:可能分开分配对象和控制块(额外开销)。
-
使用场景:
- 需要共享资源所有权的场景(如多线程共享缓存、观察者模式)。
- 避免循环引用时需配合
std::weak_ptr
3. std::weak_ptr(弱引用指针)
-
定义:不增加引用计数,用于观察
shared_ptr管理的资源,解决循环引用问题。 -
使用场景:
- 打破
shared_ptr的循环引用(如双向链表、缓存系统)。 - 需要临时访问共享资源但不希望延长其生命周期(如缓存键的懒加载
- 打破
#include <memory>
struct Node
{
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 避免循环引用
};
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2; node2->prev = node1; // 不会增加引用计数
4. 问题:shared_ptr的循环引用如何解决?
- 场景:两个
shared_ptr相互引用(如A持有B的shared_ptr,B持有A的shared_ptr),导致引用计数永远不为零。 - 解决方案:将其中一个改为
weak_ptr,打破循环。
5. 问题:make_shared和直接new的区别?
-
性能:
make_shared通过一次内存分配完成对象和控制块的创建,更高效。 -
安全性:避免以下代码的内存泄漏风险:
-
func(std::shared_ptr<int>(new int(42)), some_function_that_may_throw());若
some_function_that_may_throw抛出异常,new int(42)已分配但未关联到shared_ptr,导致泄漏。make_shared是原子操作,无此问题。
6. 问题:智能指针能否管理数组?
-
unique_ptr:可以,需指定自定义删除器(std::default_delete<T[]>): -
std::unique_ptr<int[]> arr(new int[10]); -
shared_ptr:默认使用delete而非delete[],需自定义删除器:auto deleter = [](int* p) { delete[] p; }; std::shared_ptr<int> arr(new int[10], deleter);7. 问题:智能指针在多线程中的安全性?
-
shared_ptr的引用计数:线程安全(原子操作)。 -
shared_ptr的对象访问:非线程安全,需额外同步(如互斥锁)。 -
weak_ptr的lock():线程安全,但返回的shared_ptr需自行保证线程安全。 -
总结:
“智能指针通过RAII机制自动化内存管理,主要类型包括:
unique_ptr:独占所有权,适用于明确资源独占的场景;shared_ptr:共享所有权,通过引用计数自动释放,但需注意循环引用;weak_ptr:配合shared_ptr解决循环引用,不增加引用计数。
实现上,unique_ptr通过禁用拷贝实现独占,shared_ptr依赖原子引用计数和控制块,weak_ptr则通过弱引用计数观察资源。
实际开发中应优先使用make_shared和make_unique(C++14),并避免裸指针操作。”
-
右值引用
int num = 10;
int &b = num; //正确
int &c = 10; //错误
const int &c = 10;//正确
const int&& a = 10;//编译器不会报错
类型转换
const_cast
static_cast 隐士类型转换
dynimic_cast 有虚函数类的转换 基类/派生类
reinterpret_cast 任何类型转换
运算符
运算符的优先级从高到低依次是:
- 算术运算符(如+,-,*,/等)优先级最高
- 关系运算符(如>,<,==等)次之
- 逻辑运算符(如&&,||等)再次之
- 赋值运算符(如=)优先级最低
举个例子:
a = 3 + 4 > 5 && b < 2
友元函数
一种可以访问类的私有成员(private)和保护成员(protected)的函数
- 函数指针数组
返回类型 (*数组名[数组大小])(参数类型);
void (*s[5])(int)
void func1(int x){}
void func2(int x){}
vid (*s[5])(int) = {func1,func2,nullptr,nullptr,nullptr}
重载和重写
const修饰的成员函数和非const成员函数可以构成重载。比如:
void func();
void func() const;
构造函数(Constructor)
- 默认构造函数(Default Constructor)
参数化构造函数(Parameterized Constructor)
拷贝构造函数(Copy Constructor)
移动构造函数(Move Constructor) - 赋值运算符(Assignment Operator)
拷贝赋值运算符(Copy Assignment Operator)
移动赋值运算符(Move Assignment Operator
c++随机数
随机数生成的范围
1) [0, n),即rand() % n;
2) [a, b),即rand() % (b - a) + a;
3) [a, b],即rand() % (b - a + 1) + a;
4) (a, b],即rand() % (b - a) + a + 1。
5) a + rand() % n,[a, a + n);
6) [0 , 1)之间的浮点数,rand()/(RAND_MAX * 1.f)
C++11随机数生成方式有:
随机数分布类型
均匀分布
uniform_int_distribution 整数均匀分布
uniform_real_distribution 浮点数均匀分布
伯努利类型分布
bernoulli_distribution 伯努利分布
binomial_distribution 二项分布
正态分布
normal_distribution 正态分布
lognormal_distribution 对数正态分布
random_device
random_device是标准库提供的一个非确定性随机数生成设备,是所有生成器中唯一一个不需要随机数种子的方式。在Linux中,是需要读取/dev/urandom设备。需要注意的是random_device在某些操作系统中是无法使用的,会在构造函数或者调用operator()函数时抛出异常,因此在进行代码移植时,需要格外注意
C++管理废弃API
- C++ 用__attribute__((deprecated(message)))管理废弃API
func,FUNCTION,__PRETTY_FUNCTION__的区别
前两者用于获取函数名称,而后者除了提供函数名称外还能显示参数类型
C++内存
c++ 进程内存分布
-
代码区(Text Segment)
- 存储程序的机器指令(编译后的二进制代码)
- 通常是只读的,防止程序意外修改自身指令
- 在程序生命周期内保持不变
-
全局/静态存储区(Data Segment)
-
已初始化数据段(.data) :
- 存储已初始化的全局变量和静态变量
- 如:
int globalVar = 10; static int staticVar = 20;
-
未初始化数据段(.bss) :
- 存储未初始化的全局变量和静态变量
- 程序启动时会被初始化为 0 或 NULL
- 如:
int globalUninitVar; static int staticUninitVar;
-
-
堆区(Heap)
- 动态内存分配区域
- 通过
new/malloc分配,delete/free释放 - 分配方向:从低地址向高地址增长
- 需要手动管理,否则会导致内存泄漏
- 示例:
int* p = new int[10];
-
栈区(Stack)
- 存储函数调用信息、局部变量、函数参数等
- 分配方向:从高地址向低地址增长
- 自动管理(进入作用域分配,离开作用域释放)
- 示例:
void func() { int localVar = 5; }
-
内存映射区(Memory Mapping Segment)
- 用于映射动态链接库、共享内存等
- 也可以用于文件映射 I/O
-
全局/静态存储区(Data Segment)
已初始化数据段(.data): 存储已初始化的全局变量和静态变量 如:int globalVar = 10; static int staticVar = 20; 未初始化数据段(.bss): 存储未初始化的全局变量和静态变量 程序启动时会被初始化为0或NULL 如:int globalUninitVar; static int staticUninitVar;
-
堆区(Heap)
动态内存分配区域 通过new/malloc分配,delete/free释放 分配方向:从低地址向高地址增长 需要手动管理,否则会导致内存泄漏 示例:int* p = new int[10];
-
栈区(Stack)
存储函数调用信息、局部变量、函数参数等 分配方向:从高地址向低地址增长 自动管理(进入作用域分配,离开作用域释放) 示例:void func() { int localVar = 5; }
-
内存映射区(Memory Mapping Segment)
用于映射动态链接库、共享内存等 也可以用于文件映射I/O
new 什么时候申请物理内存
new 分配的是虚拟内存地址,而不是物理内存。
此时,操作系统只是在进程的 虚拟地址空间(Virtual Address Space) 中标记这块内存为可用,但 物理内存尚未分配
物理内存的分配发生在 程序首次访问该内存 时,由 缺页异常触发
int *p= new int[100];
p[0]=1;// 第一次写入,触发缺页异常
在 Linux(及大多数现代操作系统)中,进程使用的是 虚拟内存(Virtual Memory),而不是直接访问物理内存。
当程序首次访问 new 或 malloc 分配的内存时,会触发 缺页异常(Page Fault),由操作系统动态分配物理内存。
关键结论:
new 分配的是 虚拟内存,物理内存按需分配。
缺页异常 是操作系统动态管理物理内存的核心机制。可通过 mlock、madvise 或 大页内存 优化内存访问性能
malloc(0)的行为是什么?
(1)malloc(0)这个行为是被C语言标准所允许的。
(2)malloc(0)返回的不一定是空指针,不同环境产生的结果不同。
(3)malloc(0)返回的指针指向的空间,可能不能被访问。(再次强调,是可能!!!)
(4)malloc(0)返回空间,要根据环境来定。
(5)malloc(0)所产生的指针,需要传递给free()进行释放。
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <string.h>
int main()
{
char* p=malloc(0);
printf("p = %p \r\n",p);
strcpy(p,"abc");
printf("size = %ld \r\n",malloc_usable_size(p));
free(p);
p = NULL;
return 0;
}
申请到了一个24字节的空间, 输出: p:0x5592063fa2c0,size:24
**malloc申请的最大数?
`malloc` 函数能够分配的最大内存通常是受限于**进程的地址空间**和**系统的实际内存资源**。
32位:2 的32次方字节 理论上,
64位:2的64次方
C++ STL
vector
-
resize() 和 reserve()
resize:实际容器大小,实际元素的个数。新大小>当前大小,则会新增;新大小< 当前大小,则会删除
reserve:容器的容量,预分配大小(减少重新分配的开销,提升性能) -
vector的push_back和emplace_back有什么区别?
都是用来向 std::vector 添加新元素的函数,
主要区别在于对象的创建方式:- push_back 使用已存在的对象,可能会涉及拷贝或移动操作。
- emplace_back 直接在容器中原地构造对象,避免额外开销。 简单来说:
- 如果已经有一个现成的对象,使用 push_back。
- 如果需要传递构造参数创建对象,使用 emplace_back。
class Person{
public: Person(string name,int age)
{
}
};
std::vector<Person> vec;
Person p("bob",10);
vec.push(p);
vec.emplace_back("bob",10);
-
std::vector 的性能陷阱是什么?
std::vector<bool>是 C++ 标准库中一个特殊的容器,虽然它表面上像其他std::vector类型一样,但实际实现和性能特性存在显著差异,容易导致一些性能陷阱和使用误区。问题:
-
按位压缩存储:为了节省空间,
std::vector<bool>通常将每个bool元素压缩为 1 位(而非通常的 1 字节)。例如,一个包含 8 个bool的vector可能仅占用 1 字节内存。 -
非标准代理迭代器:它的迭代器是代理迭代器(返回
std::vector<bool>::reference对象而非直接的bool&),导致以下问题:- 无法获取
bool&引用(如&vec[0]非法)。
std::vector<bool> vec = {true, false, true}; bool& b = vec[0]; // 错误:无法绑定非 const 引用到代理对象 auto& ref = vec[0]; // ref 的类型是 std::vector<bool>::reference,非 bool&- 与其他算法或泛型代码兼容性差(例如
std::sort可能无法直接使用)。
- 无法获取
性能陷阱
(1) 访问速度慢
- 位操作开销:访问单个
bool需要解引用代理对象,可能涉及位掩码和移位操作(尤其是非对齐访问时)。 - 扩容成本高:按位存储的扩容可能需要重新计算位布局,比普通
vector的字节级扩容更耗时。
(2) 替代方案
-
使用
std::vector<char>或std::vector<uint8_t> -
使用
std::bitset(固定大小)
适用场景:已知大小的位集合(编译时确定)。
限制:大小固定,不支持动态扩容。
-
操作系统
进程与线程区别
核心定义
进程(Process)
- 是操作系统资源分配的基本单位(如内存、文件句柄、网络端口等)。
- 拥有独立的地址空间,一个进程崩溃不会直接影响其他进程(隔离性强)。
- 创建、销毁、切换开销大(需通过系统调用完成)。
线程(Thread)
- 是CPU调度的基本单位,属于进程内的执行单元。
- 共享进程的内存空间(如全局变量、堆),但拥有独立的栈和程序计数器。
- 创建、销毁、切换开销小(用户态线程库或内核支持)
关键区别对比表
| 维度 | 进程 | 线程 |
|---|---|---|
| 资源分配 | 独立资源(内存、文件等) | 共享进程资源 |
| 隔离性 | 强(崩溃互不影响) | 弱(共享数据需同步) |
| 通信方式 | 需通过IPC(管道、消息队列等) | 直接通过共享内存或变量 |
| 开销 | 高(需系统调用) | 低(用户态或轻量级内核支持) |
| 并发性 | 多进程并行(真正并行) | 多线程并行(受限于进程资源) |
| 适用场景 | 计算密集型、需要高隔离的任务 | I/O密集型、需要高并发的任务 |
-
为什么多线程比多进程快?
- 线程共享进程资源,避免了进程间通信(IPC)的开销和上下文切换成本。
-
多线程的同步问题如何解决?
- 互斥锁(Mutex)、信号量(Semaphore)、条件变量(Condition Variable)、读写锁等。
线程同步的方式
线程同步的目的是解决多线程环境下的竞态条件(Race Condition) 和数据不一致问题,确保共享资源的访问安全。常见场景包括:
- 多个线程同时修改共享变量(如计数器)。
- 读写同一文件或数据库连接。
- 生产者-消费者模型中的队列操作。
经典线程同步方式及实现
1. 互斥锁(Mutex)
-
定义:通过加锁/解锁机制保证临界区(Critical Section)的互斥访问,同一时间仅一个线程能持有锁。
-
实现:
std::mutex+std::lock_guard(RAII自动释放)。
2. 读写锁(Read-Write Lock)
-
定义:区分读操作和写操作,允许多个线程同时读或独占写,提高并发性。
-
实现:
std::shared_mutex(C++17) -
适用场景:读多写少的场景(如缓存系统)
3. 信号量(Semaphore)
-
定义:通过计数器控制同时访问资源的线程数量,可用于限流或生产者-消费者模型。
-
实现:
std::counting_semaphore(C++20)。 -
经典应用:数据库连接池、线程池限流。
4. 条件变量(Condition Variable)
-
定义:允许线程在某个条件不满足时主动阻塞,直到其他线程通知条件变化,常与互斥锁配合使用。
-
实现:
std::condition_variable+std::unique_lock。
5. 原子操作(Atomic Operations)
-
定义:通过硬件指令(如CAS)实现无锁的线程安全操作,避免显式加锁。
-
实现:
std::atomic
6. 自旋锁(Spinlock)
-
定义:线程在获取锁失败时循环检查(忙等待),而非阻塞,适用于锁持有时间极短的场景。
-
实现:
- C++ :
std::atomic_flag(需手动实现)。 - Linux内核:
spin_lock()。
- C++ :
锁
锁的作用:在C++中,锁是多线程编程中确保共享资源安全访问的核心同步工具,通过协调线程对临界区的访问,防止数据竞争和不一致问题
锁的类型及使用场景
一、基础锁类型
互斥锁(Mutex)
核心功能:确保同一时间仅一个线程能访问共享资源。
关键操作:
lock():获取锁,若锁已被占用则阻塞线程。
unlock():释放锁,允许其他线程访问。
RAII封装:
std::lock_guard:构造时加锁,析构时自动解锁,避免手动解锁遗漏。
std::unique_lock:更灵活,支持延迟加锁、手动解锁及与条件变量配合。
示例场景:保护全局变量、容器等共享资源的修改。
递归锁(Recursive Mutex)
核心功能:允许同一线程多次加锁,避免递归调用导致的死锁。
关键特性:
每次加锁需对应一次解锁,计数器归零时释放锁。
由std::recursive_mutex实现。
示例场景:递归函数中访问共享资源,如树形结构遍历。
定时锁(Timed Mutex)
核心功能:支持超时机制,避免线程无限阻塞。
关键操作:
try_lock_for():指定时间段内尝试获取锁。
try_lock_until():指定绝对时间点前尝试获取锁。
示例场景:实时系统、高并发服务中需控制等待时间的场景。
二、高性能锁类型
读写锁(Read-Write Lock)
核心功能:允许多线程并发读取,写操作独占访问。
关键特性:
读锁(共享锁):多个线程可同时持有。
写锁(独占锁):阻塞其他所有线程。
由std::shared_mutex(C++17引入)实现。
性能优势:读多写少场景下显著提升并发性能。
示例场景:缓存系统、配置文件读取等。
自旋锁(Spinlock)
核心功能:通过忙等待(自旋)尝试获取锁,避免线程切换开销。
关键特性:
适用于锁持有时间极短的场景(如内核临界区)。
C++标准库未直接提供,但可通过std::atomic_flag实现。
示例代码:
#include <atomic>
class Spinlock {
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock() {
while (flag.test_and_set(std::memory_order_acquire)) {}
}
void unlock() {
flag.clear(std::memory_order_release);
}
};
注意事项:长时间自旋会浪费CPU资源,需谨慎使用。
三、高级同步工具
条件变量(Condition Variable)
核心功能:与互斥锁配合,实现线程间条件等待与通知。
关键操作:
wait():释放锁并阻塞,直到被唤醒。
notify_one()/notify_all():唤醒一个或所有等待线程。
示例场景:生产者-消费者模型、任务队列同步。
信号量(Semaphore)
核心功能:通过计数器控制资源访问权限。
关键特性:
C++20引入std::counting_semaphore,支持多线程协调。
适用于限制并发线程数(如连接池管理)。
四、锁的选择与优化策略
选择原则:
通用场景:优先使用std::mutex或std::lock_guard。
读多写少:选择std::shared_mutex提升读性能。
递归调用:使用std::recursive_mutex避免死锁。
超时控制:采用std::timed_mutex防止线程阻塞。
性能优化:
减小锁粒度:将大临界区拆分为多个小区域,减少锁竞争。
避免长时间持有锁:快速完成临界区操作,释放锁以减少阻塞。
无锁设计:在可能的情况下使用原子操作(如std::atomic)替代锁。
死锁预防:
固定加锁顺序:多锁场景下按统一顺序获取锁(如先锁A再锁B)。
使用std::lock:原子化获取多个锁,避免死锁(如std::lock(m1, m2))。
RAII管理:确保锁在异常发生时也能。
死锁常见现象
- 系统停滞(界面卡死、服务停止工作)
- CPU高占用
- 日志停止更新
- 资源占用不释放
死锁发生条件
- 互斥条件
- 请求与保持条件
- 不可剥夺条件
- 循环等待条件
死锁预防措施
- 避免互斥:使用std::atomic或无锁数据结构
- 避免请求与保持:确保资源释放后再进行下一步操作
- 允许资源剥夺:使用带超时的锁操作
- 避免循环等待:使用std::lock同时锁定多个资源
性能优化建议
-
减小锁的粒度
-
使用读写锁优化读多写少场景
-
使用无锁数据结构(如std::atomic或无锁队列)
-
使用尝试加锁或超时锁
-
替代同步机制:使用条件变量、信号量、事件驱动模型等
-
异步编程:采用消息队列、任务分解等方式减少共享数据需求
2.锁与信号量区别 信号量内部维护一个计数器,当有线程访问资源,计数器+1,当线程访问结束-1.当计数器大于0时,线程可以获得资源并将计数器减1;当计数器为0时,线程将被阻塞,直到有资源被释放。
互斥锁:保证同一时间只有一个线程访问资源,不适用于控制多线程并发数的场景(比如线程池中控制线程数量,限制网络连接数量等)。配合shared_mutex(读写锁)实现多读单写(但依然无法控制并发读线程的数量)
信号量:通过计数器允许指定数量的线程同时访问资源(如限制为N个线程),限制并发访问数量(如线程池任务队列、数据库连接池、网络连接数限制)。任务调度(如生产者-消费者问题)。
```
关键区别总结
特性 互斥锁(Mutex) 信号量(Semaphore)
并发控制 仅允许1个线程访问 允许N个线程同时访问(可配置)
计数器 无 有(通过计数器管理)
适用场景 严格互斥 限制并发数或任务调度
复杂性 低 高(需手动管理计数器)
线程阻塞 竞争失败时阻塞 计数器为0时阻塞
```