C++基础温故知新

163 阅读27分钟

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 是一个类型修饰符,用于告诉编译器不要对变量进行优化(如缓存到寄存器、重排指令等),因为该变量的值可能被外部因素(如硬件、中断、多线程等)意外修改
restrictrestrict 修饰的指针是唯一一种访问它所指向的对象的方式。只有 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_ptrlock() :线程安全,但返回的shared_ptr需自行保证线程安全。

    • 总结:

    “智能指针通过RAII机制自动化内存管理,主要类型包括:

    1. unique_ptr:独占所有权,适用于明确资源独占的场景;
    2. shared_ptr:共享所有权,通过引用计数自动释放,但需注意循环引用;
    3. weak_ptr:配合shared_ptr解决循环引用,不增加引用计数。
      实现上,unique_ptr通过禁用拷贝实现独占,shared_ptr依赖原子引用计数和控制块,weak_ptr则通过弱引用计数观察资源。
      实际开发中应优先使用make_sharedmake_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 任何类型转换

运算符

运算符的优先级从高到低依次是:

  1. 算术运算符(如+,-,*,/等)优先级最高
  2. 关系运算符(如>,<,==等)次之
  3. 逻辑运算符(如&&,||等)再次之
  4. 赋值运算符(如=)优先级最低

举个例子:
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++ 进程内存分布

内存分布.png

  1. 代码区(Text Segment)

    • 存储程序的机器指令(编译后的二进制代码)
    • 通常是只读的,防止程序意外修改自身指令
    • 在程序生命周期内保持不变
  2. 全局/静态存储区(Data Segment)

    • 已初始化数据段(.data)

      • 存储已初始化的全局变量和静态变量
      • 如:int globalVar = 10; static int staticVar = 20;
    • 未初始化数据段(.bss)

      • 存储未初始化的全局变量和静态变量
      • 程序启动时会被初始化为 0 或 NULL
      • 如:int globalUninitVar; static int staticUninitVar;
  3. 堆区(Heap)

    • 动态内存分配区域
    • 通过 new/malloc 分配,delete/free 释放
    • 分配方向:从低地址向高地址增长
    • 需要手动管理,否则会导致内存泄漏
    • 示例:int* p = new int[10];
  4. 栈区(Stack)

    • 存储函数调用信息、局部变量、函数参数等
    • 分配方向:从高地址向低地址增长
    • 自动管理(进入作用域分配,离开作用域释放)
    • 示例:void func() { int localVar = 5; }
  5. 内存映射区(Memory Mapping Segment)

    • 用于映射动态链接库、共享内存等
    • 也可以用于文件映射 I/O
  6. 全局/静态存储区(Data Segment)

    已初始化数据段(.data): 存储已初始化的全局变量和静态变量 如:int globalVar = 10; static int staticVar = 20; 未初始化数据段(.bss): 存储未初始化的全局变量和静态变量 程序启动时会被初始化为0或NULL 如:int globalUninitVar; static int staticUninitVar;

  7. 堆区(Heap)

    动态内存分配区域 通过new/malloc分配,delete/free释放 分配方向:从低地址向高地址增长 需要手动管理,否则会导致内存泄漏 示例:int* p = new int[10];

  8. 栈区(Stack)

    存储函数调用信息、局部变量、函数参数等 分配方向:从高地址向低地址增长 自动管理(进入作用域分配,离开作用域释放) 示例:void func() { int localVar = 5; }

  9. 内存映射区(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位:232次方字节 理论上,
64位:264次方 

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 个 boolvector 可能仅占用 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++中,锁是多线程编程中确保共享资源安全访问的核心同步工具,通过协调线程对临界区的访问,防止数据竞争和不一致问题

锁的类型及使用场景

一、基础锁类型

互斥锁(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管理:确保锁在异常发生时也能。

死锁常见现象

  1. 系统停滞(界面卡死、服务停止工作)
  2. CPU高占用
  3. 日志停止更新
  4. 资源占用不释放

死锁发生条件

  1. 互斥条件
  2. 请求与保持条件
  3. 不可剥夺条件
  4. 循环等待条件

死锁预防措施

  1. 避免互斥:使用std::atomic或无锁数据结构
  2. 避免请求与保持:确保资源释放后再进行下一步操作
  3. 允许资源剥夺:使用带超时的锁操作
  4. 避免循环等待:使用std::lock同时锁定多个资源

性能优化建议

  1. 减小锁的粒度

  2. 使用读写锁优化读多写少场景

  3. 使用无锁数据结构(如std::atomic或无锁队列)

  4. 使用尝试加锁或超时锁

  5. 替代同步机制:使用条件变量、信号量、事件驱动模型等

  6. 异步编程:采用消息队列、任务分解等方式减少共享数据需求

2.锁与信号量区别 信号量内部维护一个计数器,当有线程访问资源,计数器+1,当线程访问结束-1.当计数器大于0时,线程可以获得资源并将计数器减1;当计数器为0时,线程将被阻塞,直到有资源被释放。

互斥锁:保证同一时间只有一个线程访问资源,不适用于控制多线程并发数的场景(比如线程池中控制线程数量,限制网络连接数量等)。配合shared_mutex(读写锁)实现多读单写(但依然无法控制并发读线程的数量)
信号量:通过计数器允许指定数量的线程同时访问资源(如限制为N个线程),限制并发访问数量(如线程池任务队列、数据库连接池、网络连接数限制)。任务调度(如生产者-消费者问题)。

```
关键区别总结  
特性          互斥锁(Mutex)          信号量(Semaphore)  
并发控制        仅允许1个线程访问           允许N个线程同时访问(可配置)  
计数器         无                           有(通过计数器管理)  
适用场景        严格互斥                    限制并发数或任务调度  
复杂性         低                       高(需手动管理计数器)  
线程阻塞        竞争失败时阻塞                 计数器为0时阻塞

```