0%

C++线程知识简介

目的

假设我们使用的是多核 CPU,且无法避免使用多线程。站在产品的稳定性和性能优化的角度,对线程知识进行简介。

范围

为了在多线程的编程环境中,更好发挥多核CPU的性能,并对多线程相关的缺陷加以了解并进行规避。着重于讲并行而非并发的情况。

并行(Parallel)与并发(Concurrent):

  • 并行: 单线程。
  • 并发: 多线程。

并发:

任务1 任务2
执行语句1
执行语句2
执行语句3
执行语句4

并行:

任务1 任务2
执行语句1 执行语句2

热身

  1. 一个数据如果一个时刻是只读的,那么在这个时刻该数据是线程安全的。
  2. 一个数据被多个线程同时读写,那么该数据是线程不安全的。
  3. 在同一个线程中,对一个普通互斥量加锁两次,会发生死锁。
  4. intunsignedchardouble 等基本类型,均为线程不安全的。
  5. 互斥量的创建、加锁和解锁操作本身,并不耗时。

场景

线程间共享数据

场景举例:

  1. 通信线程之间同步消息序列号。
  2. ATS线程写入列车信息,其他线程读取。
  • C++ STL 互斥量版本
1
2
3
4
std::mutex mtx;
mtx.lock();
....
mtx.unlock();
  • C++ STL 读写锁版本
1
2
3
4
5
6
7
8
9
10
11
12
13
std::shared_mutex mutex;
int sharedData = 0;

void readerThread()
{
std::shared_lock<std::shared_mutex> lock(mutex);
}

void writerThread()
{
std::unique_lock<std::shared_mutex> lock(mutex);
sharedData += 1;
}
写入线程 读取线程1 读取线程2
readerThread() readerThread()
writerThread()
  • C++ STL 原子变量版本
1
2
3
4
5
6
7
8
9
10
11
12
13
std::atomic<int> i;

/// 线程安全
void set()
{
i++;
}

/// 线程安全
int get()
{
return i;
}
  • Windows API 版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
HANDLE hMutex;

// 创建互斥量
hMutex = CreateMutex(NULL, FALSE, NULL);
if (hMutex == NULL)
{
return 0;
}

// 加锁
WaitForSingleObject(hMutex, INFINITE);

...

// 解锁
ReleaseMutex(hMutex);

// 关闭互斥量
CloseHandle(hMutex);
  • Windows 临界区版本
1
2
3
4
5
6
7
8
9
10
CRITICAL_SECTION cs;
// 初始化临界区
InitializeCriticalSection(&cs);
// 加锁
EnterCriticalSection(&cs);
...
// 解锁
LeaveCriticalSection(&cs);
// 删除临界区
DeleteCriticalSection(&cs);

注: EnterCriticalSectionWaitForSingleObject 的区别:

  1. WaitForSingleObject 因为涉及到 用户态和内核态的切换,更慢。
  2. WaitForSingleObject 可以用于进程间的同步,而 EnterCriticalSection 不能。
  3. WaitForSingleObject 可以达到超时等待的效果,而 EnterCriticalSection 会一直等待。
  4. 在同一线程中多次调用 WaitForSingleObjectEnterCriticalSection 都不会产生死锁。
  • pthread 版本
1
2
3
4
5
6
7
8
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);

...

pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
  • 注:pthread_mutex_t 默认是不可被同一线程加锁两次的,即不可重入。如果想要可以重入则需要设置属性。
1
2
3
4
5
6
pthread_mutexattr_t mutex_attr;
pthread_mutexattr_init(&mutex_attr);
pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_RECURSIVE); // 设置为可重入

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &mutex_attr); // 使用属性,初始化互斥量
  • gos 库版本
1
2
3
4
5
6
7
HANDLE mutex_ = gos_mutex_init();
gos_mutex(mutex_);

...

gos_unmutex(mutex_);
gos_mutex_free(mutex_);

注:

  1. gos 库版本的互斥量,在 window 下是可重入的, 在 linux 下是不可重入的。

后台运行周期性任务

1
2
3
4
5
6
7
8
class NTPClient
{
public:
/// 进行NTP同步, 后台线程周期运行
void ntp_sync();
/// 获取NTP是否成功, 业务线程调用
bool get_ntp_valid();
};
  • C++ STL 标准库版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
NTPClient client;

void ntp_sync_thread()
{
while (true)
{
client.ntp_sync();
std::this_thread::sleep_for(std::chrono::seconds(10));
}
}

std::thread t(ntp_sync_thread);
/// 分离线程
t.detach();
  • pthread 版本:
1
2
3
4
5
6
7
8
9
pthread_attr_t      stAttr;

/// 设置 pthread 属性
pthread_attr_init(&stAttr);
pthread_attr_setdetachstate(&stAttr, PTHREAD_CREATE_DETACHED);

/// 初始化线程
pthread_t thread_id;
pthread_create(&thread_id, NULL, threadFunction, &client);
  • windows 版本:
1
2
3
4
5
6
7
/// 1. Windows API:
HANDLE hThread = CreateThread(NULL, 0, threadFunction, &client, 0, NULL);
// 分离线程
CloseHandle(hThread);

/// 2. Vistual C++ CRT 版本
uintptr_t hThread = _beginthreadex(NULL, 0, threadFunction, &client, 0, NULL);

为什么选择 _beginthreadex 而不是 CreateThread?

_beginthreadex 为每个线程分配自己的tiddata内存结构, 其中保存了 C 语言中的全局变量, 如 errno

参考资料: windows 多线程: CreateThread、_beginthread、_beginthreadex、AfxBeginThread 的区别

  • gos 库版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class NTPClientThread: public GThread
{
public :
NTPClientThread()
{
Start();
}

virtual GOS_THREAD_RET ThreadEntry(void* pPara)
{
while (true)
{
client.ntp_sync();
GThread::Sleep(10000);
}
return 0;
}

private:
NTPClient client;
};

后台运行耗时任务(一次性任务)

场景举例:

  1. 耗时函数放到后台运行,结果想要获取时再主动获取。
  2. 两个执行时间非常长的函数,并行执行可节省时间。
  3. 等待打印机打印的同时,继续执行其他任务。

实现思路:

  1. 启动一个线程,运行一个函数,函数运行结束,线程退出。
  2. 业务线程: 创建后台线程。
  3. 后台线程: 运行函数,函数结束后退出。
  4. 业务线程: 等待后台线程结束后,取得函数结果。
  • C++ STL 标准库版本
1
2
3
4
5
6
7
8
9
10
11
int i = 0;

std::thread t([&i](){
i = 1;
});

// 等待后台线程结束
t.join();

// 获取线程函数结果
std::cout << i << std::endl;
  • C++ STL 异步版本
1
2
3
4
5
6
7
8
9
10
11
12
13
int i = 0;

// 使用 std::async 在后台执行函数并获取结果
auto future = std::async(std::launch::async, [&i]() {
i = 1;
return i;
});

// 等待后台线程结束并获取结果
i = future.get();

// 输出线程函数结果
std::cout << i << std::endl;
  • Windows API 版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int i = 0;

DWORD WINAPI f(LPVOID lpParam)
{
i = 1;
return 0;
}

HANDLE hThread = CreateThread(NULL, 0, f, NULL, 0, NULL);

// 等待后台线程结束
WaitForSingleObject(hThread, INFINITE);

// 关闭线程句柄
CloseHandle(hThread);

// 获取线程函数结果
std::cout << i << std::endl;
  • pthread 版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int i = 0;

void f()
{
i = 1;
}

pthread_t t;
pthread_create(&t, NULL, f, NULL);

// 等待后台线程结束
pthread_join(t, NULL);

// 获取线程函数结果
std::cout << i << std::endl;

线程主动停止与资源释放

线程正在运行时,对线程进行销毁(free, delete),可能会访问到已经被释放的内存,导致程序崩溃。

因此,线程停止时需要等待线程结束后再释放资源。

对于 joinable 的线程,需要调用 join 函数等待线程结束后再释放资源。

但对于 detach 的线程,如何知晓线程函数执行完毕。

场景举例: 视频播放线程的主动停止。

1
2
3
4
5
6
7
8
9
class ThreadPlayAndRec : public GThread
{};

ThreadPlayAndRec* p = new ThreadPlayAndRec();
p->Start();
...
p->Stop();
/// 通知线程结束后需要等待多久,线程函数才会结束
delete p;

解决办法: 设置结束标识位来判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
VOID ThreadPlayAndRec::Free()
{
m_ulThreadState = THREAD_STATE_CLOSING;
Stop();
while(1)
{
if (m_ulThreadState == THREAD_STATE_FREE)
{
break;
}

gos_sleep_1ms(1);
}

}

ThreadPlayAndRec* p = new ThreadPlayAndRec();
p->Start(); ///< 通知线程开始
...
p->Stop(); ///< 通知线程结束
p->Free(); ///< 阻塞等待线程结束
delete p;

温州S2项目对 GThread 的改动:

1
2
3
ThreadPlayAndRec* p = new ThreadPlayAndRec();
p->Start(); ///< 通知线程开始
delete p; ///< 等待线程结束后, 释放资源

线程唤醒(线程池)

场景举例: 线程池中的线程,在任务队列出现任务时,唤醒一个线程进行处理。

  • Linux 信号量举例
1
2
3
4
5
6
7
8
9
10
11
/// 初始化
sem_init()

/// 等待信号量唤醒
sem_wait()

/// 信号量唤醒
sem_post()

/// 回收资源
sem_destroy()
生产者线程 业务线程1 业务线程2
初始化 sem_init()
等待唤醒 sem_wait()
等待唤醒 sem_wait()
消息入列 sem_post()
被唤醒后, 取出消息并处理
回收资源 sem_destroy()
  • pthread 条件变量版本
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
26
27
28
29
30
31
32
33
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int flag = 0;

void* threadFunction(void* arg) {
pthread_mutex_lock(&mutex);
while (!flag) {
pthread_cond_wait(&cond, &mutex);
}
printf("Thread received signal\n");
pthread_mutex_unlock(&mutex);

...

return NULL;
}

int main() {
pthread_t thread;
pthread_create(&thread, NULL, threadFunction, NULL);

/// 主线程等待一段时间后发送信号
sleep(2);
pthread_mutex_lock(&mutex);
flag = 1;
/// 唤醒线程
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);

pthread_join(thread, NULL);

return 0;
}
  • C++ STL 风格代码举例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void producers()
{
std::unique_lock<std::mutex> lock(queue_mutex);
/// 这个封装好的函数放入任务列表中
tasks.emplace(&f);

/// 通知一个阻塞中的线程,任务队列中有任务了
condition.notify_one();
}

void consumer()
{
std::unique_lock<std::mutex> lock(this->queue_mutex);
/// 阻塞等待直到函数返回 true
this->condition.wait(lock, [this] { return !this->tasks.empty(); });
/// 从任务队列中拿出来一个任务
task = this->tasks.front();
this->tasks.pop();
}
生产者线程 业务线程1 业务线程2
条件变量阻塞等待(wait())
条件变量阻塞等待(wait())
消息入列
唤醒线程(notify_one())
被唤醒(条件变量停止阻塞)
获取消息并执行业务

线程安全中所涉及的问题

死锁

  • 情况1:
线程1 线程2
获取互斥量1 获取互斥量2
获取互斥量2 获取互斥量1

解决办法:

  1. 一次只获取一个互斥量。
1
2
3
4
5
6
7
8
9
10
11

{
std::lock_guard<std::mutex> lock1(mutex1);
...
}

{
std::lock_guard<std::mutex> lock2(mutex2);
...
}

  1. 使用 STL 的语法
1
2
3
4
{
std::scoped_lock lock(mutex1, mutex2);
std::cout << "Main thread acquired locks" << std::endl;
}
  • 情况2:
线程1
获取互斥量1
获取互斥量1

解决办法:

  1. 使用带有可重入属性的互斥量。
1
2
3
4
5
6
7
8
9
10
11
/// STL
std::recursive_mutex mutex;

/// pthread
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

/// window API
EnterCriticalSection(&cs);
...
LeaveCriticalSection(&cs);
  • 情况3:
线程1 线程2
等待线程2 join 等待线程1 join

解决办法:

  1. 在同一线程创建其他线程,也在同一线程进行 join。

ABA问题

线程1 线程2
查询余额, 并存储进变量 i 查询余额, 并存储进变量 i
if(i >= 50) if(i >= 50)
i = i - 50; i = i - 50;
更新余额为 50
更新余额为 50

解决办法:

  1. 串行执行
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
26
27
|          线程1           |          线程2           |
| :----------------------: | :----------------------: |
| 获取互斥量 mtx1 | 获取互斥量 mtx1 |
| 获取互斥量成功 | |
| 查询余额, 并存储进变量 i | |
| if(i >= 50) | |
| i = i - 50; | |
| 更新余额为 50 | |
| 释放互斥量 | |
| | 获取互斥量成功 |
| | 查询余额, 并存储进变量 i |
| | if(i >= 100) |
| | i = i - 50; |
| | 更新余额为 50 |

2. 使用 CAS(Compare And Swap) 的方法

```c++
| 线程1 | 线程2 |
| :--------------------------------------------------: | :--------------------------------------------------: |
| 查询余额, 并存储进变量 i | 查询余额, 并存储进变量 i |
| if(i == 100) | if(i == 100) |
| i = i - 50; | i = i - 50; |
| 如果当前值为 100 则更新约为 50(compare_and_exchange) | |
| 更新成功,事务结束 | |
| | 如果当前值为 100 则更新约为 50(compare_and_exchange) |
| | 更新失败,事务结束 |

初始化单例

C++11之前以下全局变量,线程不安全。

1
ThreadPlayAndRec g_ThreadPlayAndRec; ///< 全局变量,在程序启动时线程不安全
  • 错误的做法1:
1
2
3
4
5
6
ThreadPlayAndRec* p = NULL;

if(!p)
{
p = new ThreadPlayAndRec();
}
线程1 线程2
if(!p) if(!p)
new ThreadPlayAndRec(); new ThreadPlayAndRec();
p 被赋值 p 被赋值
线程1 线程2
if(!p)
new ThreadPlayAndRec(); if(!p)
p 被赋值 new ThreadPlayAndRec();
p 被赋值
  • 错误的做法2:
1
2
3
4
5
6
7
8
9
10
11
ThreadPlayAndRec* p = NULL;
std::mutex mtx;

if(!p) // 第一次检查
{
std::lock_guard<std::mutex> guard(mtx);
if(!p) // 第二次检查
{
p = new ThreadPlayAndRec();
}
}

正常流程:

线程1 线程2
if(!p) 第一次检查 if(!p) 第一次检查
尝试获取互斥量 尝试获取互斥量
获取互斥量成功
if(!p) 再次判断是否为空
new ThreadPlayAndRec();
p 被赋值
释放互斥量
获取互斥量成功
if(!p) 再次判断是否为空
p 不为空,流程结束

错误流程:

线程1 线程2
if(!p) 第一次检查
尝试获取互斥量
获取互斥量成功
if(!p) 再次判断是否为空
new ThreadPlayAndRec();
p 被赋值 if(!p) 第一次检查

最后一步产生了读写竞争。

  • 解决办法:
  1. 在 C++11 标准下使用 全局/静态变量。
  2. 使用 C++11 中提供的 std::call_once 保证初始化函数只被调用一次。
1
2
3
std::once_flag flag;
ThreadPlayAndRec* p = NULL;
std::call_once(flag, []() { p = new ThreadPlayAndRec(); });
  1. 使用互斥量
1
2
3
4
5
6
7
8
ThreadPlayAndRec* p = NULL;
std::mutex mtx;

std::lock_guard<std::mutex> guard(mtx);
if(!p)
{
p = new ThreadPlayAndRec();
}
  1. 在 C++11 之前,使用 Linux API 或者 Windows API 函数。

linux:

1
2
3
4
5
6
7
8
9
pthread_once_t once_control = PTHREAD_ONCE_INIT;

void init_function()
{
std::cout << "Initialization function executed" << std::endl;
}

/// 只会执行一次
pthread_once(&once_control, init_function);

windows:

1
2
3
4
5
6
7
EnterCriticalSection(&cs);
if (!initialized)
{
init_function();
initialized = true;
}
LeaveCriticalSection(&cs);
  1. 不管, 成本最低也是最适合我们的方法。

  2. 使用 gos::singleton, 其实现思路是 C++11 之前使用 double-check lock 的方法, C++11 之后使用 std::call_once

多个线程同时开始

测试线程需要被测试函数同时并行执行。

  • C++ STL 条件变量实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
std::mutex mutex_;
std::condition_variable cv_;
bool ready_ = false;

std::jthread j([this]() mutable
{
std::unique_lock<std::mutex> mutex(mutex_);
/// 等待同步开始
cv_.wait(mutex, [this] { return ready_; });
/// 执行函数
f();
}));

/// 锁定互斥量,以修改 ready_
mutex_.lock();
ready_ = true;
/// 发送给所有的线程,可以开始运行
cv_.notify_all();
mutex_.unlock();

隐藏的多线程问题

  • 场景1: 在通信线程,直接使用修改界面的语句。
    注: 界面有自己的线程调度,如果在其他线程中直接修改界面,可能会导致界面崩溃。

  • 场景2: 在回调函数中使用共享变量(全局、静态或类成员变量)。
    注: 回调函数一定在其他线程中执行,如果在回调函数中使用共享变量,可能会导致线程不安全。

Issue 规范化建议

目的(Objective)

使用 Issue 对软件工程的过程进行记录,对软件工程各种议题进行流程记录,方便复盘和分析统计。

本次不对以下议题进行讨论:

  1. 缺陷管理流程
  2. 缺陷描述技巧
  3. 缺陷解决方法

要求(Requirements)

内容(Content)

必备元素:

  1. 问题描述
  2. 相关日志

最好有复现的图片、动图或视频

以下是温州二号线 Bug Issue 模版:

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
## 概要

(简明扼要的描述你遇到的BUG)

## 相关方

(抄送给谁)

## 运行环境描述

(如服务器运行在哪台机器,数据库用的是哪个,分别都是什么操作系统,ip 都是什么)

## 复现步骤

复现概率: 100%

(怎么样让这个问题复现以及概率? 非常重要!)

## 期待的正确现象是什么?

(该功能正常运行的现象是什么)

## 相关日志、截图

(把相关日志粘贴在这里, 请使用代码块格式(```)来显示命令行输出或日志)

以下是温州二号线 Feature Issue 模版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
## 描述理想的解决方案或功能请求

(对功能进行清晰简洁的描述)

### 难度、影响评估

(实现难度与对目前功能或代码的影响)

### 使用频率评估

(该功能使用频率)

### 需求追踪

(添加更多关于谁提出这个需求,即公司,个人(以及其身份),他们付给我们多少钱,他们的等级是什么。)

标签(Label)

对项目总结, Issue 大致分为:

  1. 缺陷(Bug)
  2. 需求(Feature)
  3. 其他: 运维(Ops), 问题(Question)

大部分为缺陷。

对于缺陷,我们关注以下几点:

  1. 是否是业主上报
  2. 是否导致现场功能不可用
  3. 是否是实验室人员上报
  4. 导致缺陷的原因是什么
  5. 导致缺陷的人是谁

对于需求,我们关注以下几点:

  1. 是否是必须实现的
  2. 是否是业主提出的
  3. 是否是内部提出的优化项

所以提议对 Issue 标签(Label)的具体结构进行设计:

  • 缺陷

  • 来源: 现场

  • 来源: 内部

  • 缺陷关闭: 逻辑错误

  • 缺陷关闭: 环境/操作问题

  • 缺陷关闭: 误报

  • 缺陷关闭: 无法复现

  • 缺陷关闭: 重复

  • 缺陷关闭: 其他

  • 失误: 研发人员1

  • 失误: 研发人员2

  • 需求: 必须

  • 需求: 优化

  1. 来源: 现场: 包括来源为现场实施人员和外部输入(标书、业主人员、总包人员、对接厂商人员)。
  2. 标签为需求: xxx缺陷 需要标识对应的来源: xxx
  3. 标签为来源: 现场在关闭时,需要标识对应缺陷关闭: xxx
  4. 标签为缺陷关闭: 逻辑错误的,需要标识对应的失误: xxx

Label 应拥有统一的配色。

  • 缺陷(#ff0000)

  • 来源: 现场(#ff0000)

  • 来源: 内部(#AD4363)

  • 缺陷关闭: 逻辑错误(#ff0000)

  • 缺陷关闭: 环境/操作问题(#5843AD)

  • 缺陷关闭: 误报(#5843AD)

  • 缺陷关闭: 无法复现(#5843AD)

  • 缺陷关闭: 重复(#5843AD)

  • 缺陷关闭: 其他(#5843AD)

  • 失误: 研发人员1(#AD8D43)

  • 失误: 研发人员2(#AD8D43)

  • 需求: 必须(#007FFF)

  • 需求: 优化(#44AD8E)

配色效果如下:

问: 结构体重载 operator== 能否直接使用 memcmp 实现?

1
2
3
4
5
6
7
8
9
10
struct obj
{
/// 这样实现是否正确
bool operator==(const obj& other) const
{
return memcmp(this, &other, sizeof(other)) == 0;
}

...
};

先说结论:不可以。

理由为:

  1. POD 类型的结构体,拥有容器类型的成员中可能存在动态内存,其地址肯定不一样,但内容可能一样,因此不能直接使用 memcmp 来比较。
  2. POD 类型,数组中没有被赋值的垃圾值,会影响内存比较结果。如果保存了指针又想要比较指针指向的内容是否一致,则也不能直接使用 memcmp 来比较。

见如下代码:

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
26
27
28
29
30
31
32
#include <iostream>
#include <cassert>

struct obj
{
bool operator==(const obj& other) const
{
return memcmp(this, &other, sizeof(other)) == 0;
}

unsigned ulUserID;
char acName[32];
unsigned ulDCType;
};

int main()
{
obj st;
obj st1;

assert(st == st1); ///< 无法通过

st.ulUserID = 1;
st.ulDCType = 1;
st.acName[0] = 'a';
st1.ulUserID = 1;
st1.ulDCType = 1;
st1.acName[0] = 'a';
assert(st == st1); ///< 无法通过

return 0;
}

解决办法:

C++11 以前:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct obj
{
bool operator==(const obj& other) const
{
return ulUserID == other.ulUserID
&& ulDCType == other.ulDCType
&& strcmp(acName, other.acName, sizeof(acName)) == 0;
}

unsigned ulUserID;
char acName[32];
unsigned ulDCType;
};

C++11 以后 C++20 以前:

1
2
3
4
5
6
7
8
struct obj
{
bool operator==(const obj& other) const = default;

unsigned ulUserID;
char acName[32];
unsigned ulDCType;
};

C++20 以后:

struct obj
{
    auto operator<=>(const obj&) const = default;

    unsigned ulUserID;
    char acName[32];
    unsigned ulDCType;
};

通常在 C++ 语言中,我们无法直接对结构体打印,但是可以通过某种方式来打印结构体。

使用友元函数打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class my_class
{
public:
int i;
std::string s;

friend std::ostream& operator<<(std::ostream& os, const my_class& obj)
{
return os << "i: " << obj.i << " s: " << obj.s;
}
};

int main()
{
my_class obj;
obj.i = 1;
obj.s = "hello";
std::cout << obj << std::endl;
return 0;
}

使用宏定义简化定义流程

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
26
27
28
29
30
31
32
33
34
35
#define FRIEND_STREAM_1(type, x1)                                           \
friend std::ostream& operator<<(std::ostream& os, const type& type_obj) \
{ \
os << #type << ": this(" << &type_obj << "), "; \
os << #x1 << "(" << type_obj.x1 << ")"; \
return os; \
}

#define FRIEND_STREAM_2(type, x1, x2) \
friend std::ostream& operator<<(std::ostream& os, const type& type_obj) \
{ \
os << #type << ": this(" << &type_obj << "), "; \
os << #x1 << "(" << type_obj.x1 << "), "; \
os << #x2 << "(" << type_obj.x2 << ") "; \
return os; \
}

#define FRIEND_STREAM_3(type, x1, x2, x3) \
friend std::ostream& operator<<(std::ostream& os, const type& type_obj) \
{ \
os << #type << ": this(" << &type_obj << "), "; \
os << #x1 << "(" << type_obj.x1 << "), "; \
os << #x2 << "(" << type_obj.x2 << "), "; \
os << #x3 << "(" << type_obj.x3 << ") "; \
return os; \
}

class my_class
{
public:
int i;
std::string s;

FRIEND_STREAM_2(my_class, i, s);
};

MsgPack 是一个序列化库,可以将对象序列化为二进制数据,也可以将二进制数据反序列化为对象。

与其他数据序列化格式(如JSON和XML)相比,MsgPack 具有更高的性能和更小的序列化大小。它的序列化和反序列化过程非常快速,并且生成的二进制数据体积小,节省带宽和存储空间。

MsgPack 提供了多种语言的实现,包括但不限于 C/C++、Java、Python、JavaScript、Ruby、Go、C#、PHP 等,使得不同语言的应用程序可以方便地进行数据交换和通信。

MsgPack 的官方网站:https://msgpack.org/

MsgPack C++ 版本开源代码库

官网简单示例

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
26
27
#include <msgpack.hpp>
#include <vector>
#include <string>
#include <iostream>

int main(void) {
// serializes this object.
std::vector<std::string> vec;
vec.push_back("Hello");
vec.push_back("MessagePack");

// serialize it into simple buffer.
msgpack::sbuffer sbuf;
msgpack::pack(sbuf, vec);

// deserialize it.
msgpack::object_handle oh =
msgpack::unpack(sbuf.data(), sbuf.size());

// print the deserialized object.
msgpack::object obj = oh.get();
std::cout << obj << std::endl; //=> ["Hello", "MessagePack"]

// convert it into statically typed object.
std::vector<std::string> rvec;
obj.convert(rvec);
}

编译并输出:

1
2
3
$ g++ -Ipath_to_msgpack/include hello.cc -o hello
$ ./hello
["Hello", "MessagePack"]

流式序列化

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
26
27
28
29
30
31
32
33
34
#include <msgpack.hpp>
#include <iostream>
#include <string>

int main() {
// serializes multiple objects using msgpack::packer.
msgpack::sbuffer buffer;

msgpack::packer<msgpack::sbuffer> pk(&buffer);
pk.pack(std::string("Log message ... 1"));
pk.pack(std::string("Log message ... 2"));
pk.pack(std::string("Log message ... 3"));

// deserializes these objects using msgpack::unpacker.
msgpack::unpacker pac;

// feeds the buffer.
pac.reserve_buffer(buffer.size());
memcpy(pac.buffer(), buffer.data(), buffer.size());
pac.buffer_consumed(buffer.size());

// now starts streaming deserialization.
msgpack::object_handle oh;
while(pac.next(oh)) {
std::cout << oh.get() << std::endl;
}

// results:
// $ g++ -Ipath_to_msgpack/include stream.cc -o stream
// $ ./stream
// "Log message ... 1"
// "Log message ... 2"
// "Log message ... 3"
}

序列化 arraymap

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 <msgpack.hpp>
#include <iostream>
#include <string>

int main() {
// serializes multiple objects into one message containing an array using msgpack::packer.
msgpack::sbuffer buffer;

msgpack::packer<msgpack::sbuffer> pk(&buffer);
pk.pack_array(3);
pk.pack(std::string("Log message ... 1"));
pk.pack(std::string("Log message ... 2"));
pk.pack(std::string("Log message ... 3"));

// serializes multiple objects into one message containing a map using msgpack::packer.
msgpack::sbuffer buffer2;

msgpack::packer<msgpack::sbuffer> pk2(&buffer2);
pk2.pack_map(2);
pk2.pack(std::string("x"));
pk2.pack(3);
pk2.pack(std::string("y"));
pk2.pack(3.4321);

}

序列化自定义类型

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
26
27
28
29
#include <msgpack.hpp>
#include <vector>
#include <string>

class myclass {
private:
std::string m_str;
std::vector<int> m_vec;
public:
MSGPACK_DEFINE(m_str, m_vec);
};

int main() {
std::vector<myclass> vec;
// add some elements into vec...

// you can serialize myclass directly
msgpack::sbuffer sbuf;
msgpack::pack(sbuf, vec);

msgpack::object_handle oh =
msgpack::unpack(sbuf.data(), sbuf.size());

msgpack::object obj = oh.get();

// you can convert object to myclass directly
std::vector<myclass> rvec;
obj.convert(rvec);
}

封装序列化函数

继承的方式

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#include <msgpack.hpp>
#include <vector>
#include <iostream>
#include <string>
#include <random>

/// 获取随机整数
int GetRandomInt(int min = 0, int max = 1000000)
{
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_int_distribution<> dis(min, max);
return dis(gen);
}

/// 获取随机字符串
std::string GetRandomString(size_t len = 10)
{
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_int_distribution<> dis(0, 25);

std::string str;
for (int i = 0; i < len; ++i)
{
str.push_back('a' + dis(gen));
}

return str;
}

/// 序列化函数基类
template <typename T>
class MsgPackSerialization
{
public:
virtual ~MsgPackSerialization() = default;
bool operator==(const MsgPackSerialization&) const = default;

/// 序列化
std::vector<char> Serialization()
{
msgpack::sbuffer sbuf;
msgpack::pack(sbuf, *(static_cast<T*>(this)));
std::vector<char> vec(sbuf.data(), sbuf.data() + sbuf.size());
return vec;
}

/// 反序列化
void Deserialization(const std::vector<char>& vec)
{
msgpack::object_handle oh = msgpack::unpack(vec.data(), vec.size());
msgpack::object obj = oh.get();
obj.convert(*(static_cast<T*>(this)));
}
};

/// 集成序列化类
class myclass : public MsgPackSerialization<myclass>
{
public:
bool operator==(const myclass&) const = default;
myclass& operator=(const myclass& rhs)
{
if (this != &rhs)
{
m_str = rhs.m_str;
m_vec = rhs.m_vec;
}
return *this;
}

/// 获取随机对象
static myclass GetRandomObj()
{
myclass obj;
obj.m_str = GetRandomString(10);
auto num = GetRandomInt(0, 10);
for (int i = 0; i < num; ++i)
{
obj.m_vec.push_back(GetRandomInt(0, 1000));
}

return obj;
}

public:
/// 为 msgpack 定义序列化和反序列化的字段
MSGPACK_DEFINE(m_str, m_vec);

private:
std::string m_str;
std::vector<int> m_vec;
};

int main()
{
/// 获取随机对象
auto obj = myclass::GetRandomObj();
decltype(obj) obj2;
/// 序列化
auto vec = obj.Serialization();
/// 反序列化
obj2.Deserialization(vec);

if (obj2 == obj)
{
std::cout << "obj2 == obj" << std::endl;
}
else
{
std::cout << "obj2 != obj" << std::endl;
}

return 0;
}

使用更加复杂的类进行序列化

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class myclass2 : public MsgPackSerialization<myclass2>
{
public:
bool operator==(const myclass2&) const
{
return m_str == m_str && m_vec == m_vec
&& m_class == m_class && m_vec_class == m_vec_class;
}

/// 获取本类的随机对象
static myclass2 GetRandomObj()
{
myclass2 obj;
obj.m_str = GetRandomString(10);
auto num = GetRandomInt(0, 10);

for (int i = 0; i < num; ++i)
{
obj.m_vec.push_back(GetRandomInt(0, 1000));
obj.m_vec_class.push_back(myclass::GetRandomObj());
}

obj.m_class = myclass::GetRandomObj();

return obj;
}

public:
/// 为 msgpack 定义序列化和反序列化的字段
MSGPACK_DEFINE(m_str, m_vec, m_class, m_vec_class);

private:
std::string m_str;
std::vector<int> m_vec;
myclass m_class;
std::vector<myclass> m_vec_class;
};

int main()
{
/// 获取随机对象
auto obj = myclass2::GetRandomObj();
decltype(obj) obj2;
/// 序列化
auto vec = obj.Serialization();
/// 反序列化
obj2.Deserialization(vec);

if (obj2 == obj)
{
std::cout << "obj2 == obj" << std::endl;
}
else
{
std::cout << "obj2 != obj" << std::endl;
}

return 0;
}

组合的方式

MSGPACK_DEFINE 宏定义的下面添加如下两个函数,即可拥有序列化和反序列化的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
#define MSGPACK_DEFINE(...) ...\
std::shared_ptr<std::vector<char>> Serialization() \
{ \
msgpack::sbuffer sbuf; \
msgpack::pack(sbuf, *this); \
return std::make_shared<std::vector<char>>(sbuf.data(), sbuf.data() + sbuf.size()); \
} \
void Deserialization(const std::shared_ptr<std::vector<char>>& vec) \
{ \
msgpack::object_handle oh = msgpack::unpack(vec->data(), vec->size()); \
msgpack::object obj = oh.get(); \
obj.convert(*this); \
}

使用 C++ 来实现定时器事件,可以使用 std::thread 来实现,也可以使用 std::async 来实现。

std::thread

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

int main()
{
/// 创建线程
std::thread t([]() {
while (true)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Hello World!" << std::endl;
}
});

/// 线程从主线程分离
t.detach();

/// 阻止主线程退出
while (true)
{
}
return 0;
}

运行结果:

image

std::async

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <future>
#include <thread>

int main()
{
/// 函数对象递归调用自己
std::function<void(int)> f = [&](int i) {
/// 休息1秒
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Hello World!" << i << std::endl;
f(i);
};

std::async(std::launch::async, f, 43);

/// 阻止主线程退出
while (true)
{
}
return 0;
}

运行结果:

image

日志文件通常包含大量打印,如何快速定位问题,是每个程序员所需要掌握的技能。

本篇文章教你使用 VSCode 及其插件来查看日志。

日志文件查看

多日志文件查看

通常一个问题包含多个文件,所以多文件搜索变得很重要。

我通常会使用 issue 号的名称来创建专门的日志文件夹,如下图:

image

在文件夹内使用 VSCode 打开即可在文件夹内查看日志, 搜索也是可以默认搜索文件夹内所有文件

image

错误日志太多, 难以搜索真正错误

我们经常遇到一个 error 日志打印出来其实并不意味着错误(首先我们应该先把错误日志改为警告等级,或者直接删除该条打印),但该条日志疯狂打印,导致掩盖了原有的信息。

如果我们把刷屏的日志删除掉就可以直接搜索 error,来查看真正的错误信息了。

那我们有办法删除它么?当然有,使用正则表达式来选中这些日志

正则表达式

1
2
3
4
5
6
^.*你想要匹配的字符串.*\r?\n?

例:
^.*retry send app msg failed.*\r?\n?
^.*Find no position.*\r?\n?
^.*SetRegisterStatus: strLongNum.*\r?\n?

在 VsCode 中勾选正则表达式查找替换所有找到的日志为空字符串,即可达到删除重复日志的目的。

image

想要突出显示特定的日志

可以使用日志高亮插件 “Log File Highlighter”, 在 VSCode 插件商店搜索安装即可。

该插件可以使用配置来制定高亮的日志:

image

image

使用如下配置:

可以让 ERROR, error 等文本,在日志文件中被显示为红色

1
2
3
4
5
6
7
"logFileHighlighter.customPatterns": [
{
/// 错误,必须处理的信息
"pattern": "ERROR|error|FATAL|fatal|critical|FAILED",
"foreground": "#FF0000"
},
],

使用效果:

image

根据我自己的使用习惯,我把日志的高亮进行分类:

  1. 错误,必须处理的信息
  2. 警告,必须注意的信息
  3. 信息,值得注意的问题
  4. 调试,用于调试程序的信息
  5. 详细信息,可以忽略的信息
  6. 当前关注的信息(需要每次更新)

以下是我的配置:

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
26
27
28
29
30
31
32
33
"logFileHighlighter.customPatterns": [
{
/// 错误,必须处理的问题
"pattern": "ERROR|error|FATAL|fatal|critical|FAILED",
"foreground": "#FF0000"
},
{
/// 警告,必须注意的信息
"pattern": "WARN|warn|warning|percent: 99.|percent: 100|\\b(?:9\\d(?:\\.\\d{1,2})?|100(?:\\.0{1,2})?)%|\\(s\\)",
"foreground": "#FFFF00"
},
{
/// 信息,值得注意的信息
"pattern": "(\"DCCapacityPos\":|get APP request call|INFO |info |RUN|OK|PASSED|Send DC handle req call which have ulDCCapaCity|spPID->usInstID|spPID->usTaskID|link auth successful, client|RequestCallReq|Dialling is handle as link call.|Function Avg Time:|frequency count|get SA msg:|server_close_connect|OnServerDisconnectClient usClientID|tcp_send: send failed, Socket|SetButtonStatus|get request call|is not need save link group call|handle auto pick up group|Dialling is handle as group call.|eSDK_SendSMS|Skip not auto pick up group|is not auto pick up!|get app request call|Group Call Speaker Change:|OnRequestCallReq|\\(31\\)|\\(34\\)|\\(36\\))",
"foreground": "#42B883"
},
{
/// 调试,用于调试程序的信息
"pattern": "debug|Debug|DEBUG|Spend Time|Memory Change|Total Time|interval_time_factory",
"foreground": "#007FFF"
},
{
/// 详细信息,可以忽略的信息
"pattern": "DETAIL|detail",
"foreground": "#2F90B9"
},
{
/// 当前关注的信息(需要每次更新)
"pattern": "6021102",
"foreground": "#000000",
"background": "#19F334"
}
],

SIL2(Safety Integrity Level 2) 是一种安全完整性级别,用于评估和确定安全相关系统的可靠性和性能。SIL是根据国际标准IEC 61508(功能安全)和相关行业标准(如IEC 61511、ISO 26262等)定义的。

SIL2认证是指通过对特定系统或设备进行评估和验证,确认其满足SIL2级别的要求。SIL2级别通常适用于具有中等安全风险的系统,要求系统在设计、实施和操作过程中采取特定的安全措施和功能,以确保其能够达到预期的安全性能。

本次我参与的 SIL2 认证是只做软件部分的认证,其中设计的标准为 EN50128-2011

目前 SIL2 认证工作已经基本完成,现总结此次认证工作的经验。

EN50128 整体脉络

EN50128 更多的是在强调软件质量保障。

软件质量保障是以计划为指导,计划指导实践。

EN50128 推荐/要求的整体流程为:

  1. 计划阶段
  2. 需求阶段
  3. 架构与设计阶段
  4. 组件设计阶段
  5. 组件实现与测试阶段
  6. 集成阶段
  7. 总体测试与最后确认阶段
  8. 软件部署
  9. 软件维护

其中 2 ~ 7 作为软件研发的主要流程,1和8 ~ 9 作为软件工程管理过程。

计划阶段

所有计划的制定,独立于研发过程。

需求阶段

研发的第一个过程,准确定义需求和需求对应的测试。

架构与设计阶段

对整个系统进行定义,分为哪几个组件,网络拓扑怎么搭建,使用什么硬件,使用什么数据库。

定义组件之间的接口。

组件实现与测试阶段

编码与调试阶段。

集成阶段

对软件组件进行集成,对真实的硬件进行集成。

总体测试与最后确认

对已经集成好的整体系统,对照软件需求测试用例进行测试并记录。发行说明,总体测试报告与软件确认报告。

软件部署

软件发布计划隶属于配置管理计划,规定了每个基线版本包含什么功能,什么时候发布。

而跟随着软件发布的就是软件部署。

软件部署分为:部署计划、部署操作手册、部署记录、部署记录报告。

软件维护

运维阶段(升级)。

软件维护计划、软件变更记录、软件维护记录。

此次认证过程得到的收获

西方主导思想:计划指导实践。

但是在没有计划时,没有构思后面工作流程的能力。

在计划不成熟或不合理时,经常会出现无视规定,灵活应对。

那有的人有心,在实施后会更新计划。但更多的人没有能力将自己的经验推广给每个部门内的人,也没有管理者来主导更新流程。

甚至有人没有责任心,回来不更新计划,完全按照后面自己的意思去实施。

那计划本身已经被弱化,后续逐渐被丢弃。

所以我认为在一个没有经验的项目上,一上来的计划没有必要急着写,而是先写个框架,后续慢慢补充细节。

就比如如上的生命周期,各个阶段完全可以顺序打乱去处理。

需求规范的制定,保证了测试用例的诞生,有依据,

确定了软件架构的设计的。

判断一个人的工作是否完成,应该以计划中的规定来判定,比如测试通过了,那是如何通过的。

版本发布了,那版本中有哪些功能,那些缺陷,那些更改。

软件部署了,部署的什么版本,哪些升级,哪些没有升级。

思路

只讲 SIL2 的要求,给出生命周期,以及要干的事情。

压缩 SIL2 的要求到我们的生命周期中。

为每个周期设立检查点。

对部分代码的查看总结

Wireshark 是一款开源的网络协议分析工具,用于捕获和分析网络数据包。它可以在各种操作系统上运行,包括Windows、Mac和Linux。

简介

Wireshark 的主要功能包括:

  1. 数据包捕获:Wireshark 可以捕获计算机网络上的数据包,包括传输层和网络层的数据。它可以监听网络接口,捕获经过该接口的数据包,并显示它们的详细信息。

  2. 数据包分析:Wireshark 可以解析捕获的数据包,并以易于理解的方式显示各个协议层的字段和值。它支持多种协议,包括以太网、IP、TCP、UDP、HTTP、DNS等。通过分析数据包,可以检查网络通信中的问题、识别潜在的安全漏洞,并进行性能优化。

  3. 过滤和搜索:Wireshark 提供了强大的过滤和搜索功能,以帮助用户快速定位感兴趣的数据包。用户可以使用过滤器来过滤特定协议、源/目标IP地址、端口号等。此外,Wireshark 还提供了高级搜索功能,可以根据特定的字段值或表达式搜索数据包。

  4. 统计和报告:Wireshark 可以生成各种统计信息和报告,以帮助用户分析网络流量和性能。它可以提供关于数据包数量、协议分布、流量图表、响应时间等方面的统计数据,并支持导出报告到不同的格式。

使用技巧

报文着色

我们可以自定义报文的颜色高亮,操作如下:

点击 视图 -> 着色规则

image

见到如下界面, 点击新建着色规则

image

输入对应的过滤规则,则可以看到对应的报文着色

过滤规则与过滤报文规则相同,例如:

  1. tcp.port == 62001, 查看使用 62001 端口的 tcp 协议的报文
  2. frame contain "MsgID: 112": 查看包含字符串 MsgID: 112 的报文

image

自定义协议

我们可以使用 lua 插件来实现 wireshark 自动解析二进制报文的功能,见如下操作:

其中需要编写 PROJECT_TEST.lua 文件放置到wiresharkplugin 目录下(例如: C:\Program Files\Wireshark\plugins)

image

可以对照文件修改对应的私有协议,方便现场抓包分析

std::filesystem 是 C++17 中引入的标准库,用于处理文件系统操作。它提供了一组类和函数,用于执行文件和目录的创建、删除、遍历、重命名、复制等操作,以及获取文件和目录的属性信息。

std::filesystem::path 核心概念解释

std::filesystem::path 类用于表示文件系统路径。它提供了一组函数,用于获取路径的各个部分,以及将路径转换为字符串。

该对象把 linuxwindows 中的路径进行统一的封装,规避掉了不同系统下的路径分隔符不同的问题。

该对象可以方便的通过 std::string 进行构造,可以方便的转换为 std::string

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <filesystem>

int main()
{
std::filesystem::path p1("C:\\Windows\\System32\\drivers\\etc\\hosts");
std::filesystem::path p2("C:/Windows/System32/drivers/etc/hosts");
std::filesystem::path p3("C:", std::filesystem::path::format::generic_format);
std::filesystem::path p4("Windows/System32/drivers/etc/hosts", std::filesystem::path::format::generic_format);
std::filesystem::path p5("hosts", std::filesystem::path::format::generic_format);
std::filesystem::path p6("C:\\Windows\\System32\\drivers\\etc\\hosts", std::filesystem::path::format::native_format);
std::filesystem::path p7("C:/Windows/System32/drivers/etc/hosts", std::filesystem::path::format::native_format);
std::filesystem::path p8("C:", std::filesystem::path::format::native_format);
std::filesystem::path p9("Windows/System32/drivers/etc/hosts", std::filesystem::path::format::native_format);
std::filesystem::path p10("hosts", std::filesystem::path::format::native_format);

std::cout << p1 << std::endl;
std::cout << p2 << std::endl;
std::cout << p3 << std::endl;
std::cout << p4 << std::endl;
std::cout << p5 << std::endl;
std::cout << p6 << std::endl;
std::cout << p7 << std::endl;
std::cout << p8 << std::endl;
std::cout << p9 << std::endl;
std::cout << p10 << std::endl;

std::cout << p1.string() << std::endl;
std::cout << p2.string() << std::endl;
std::cout << p3.string() << std::endl;
std::cout << p4.string() << std::endl;
std::cout << p5.string() << std::endl;
std::cout << p6.string() << std::endl;
std::cout << p7.string() << std::endl;
std::cout << p8.string() << std::endl;
std::cout << p9.string() << std::endl;
std::cout << p10.string() << std::endl;

return 0;
}

使用场景

获取当前路径

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <filesystem>

int main()
{
std::filesystem::path p = std::filesystem::current_path();
std::cout << p << std::endl;

auto str = p.str();
std::cout << str << std::endl;
return 0;
}

获取绝对路径

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <filesystem>

int main()
{
std::filesystem::path p = std::filesystem::absolute("hosts");
std::cout << p << std::endl;

auto str = p.str();
std::cout << str << std::endl;
return 0;
}

判断文件或文件夹是否存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <filesystem>

int main()
{
std::filesystem::path p("hosts");
if (std::filesystem::exists(p))
{
std::cout << "exists" << std::endl;
}
else
{
std::cout << "not exists" << std::endl;
}
return 0;
}

创建文件夹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <filesystem>

int main()
{
std::filesystem::path p("test");
/// 递归创建文件夹
if (std::filesystem::create_directories(p))
{
std::cout << "create directory success" << std::endl;
}
else
{
std::cout << "create directory failed" << std::endl;
}
return 0;
}

std::filesystem::create_directorystd::filesystem::create_directories 的区别
std::filesystem::create_directory 用于创建单个目录,如果目录已经存在,则函数会抛出 std::filesystem::filesystem_error 异常。
std::filesystem::create_directories 用于创建多级目录,如果目录已经存在,则函数不会抛出异常。
因此,如果你需要创建多级目录,可以使用 std::filesystem::create_directories,如果你只需要创建单个目录,则可以使用 std::filesystem::create_directory

创建文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <filesystem>

int main()
{
std::filesystem::path p("test.txt");
if (std::filesystem::create_directory(p))
{
std::cout << "create file success" << std::endl;
}
else
{
std::cout << "create file failed" << std::endl;
}
return 0;
}

获取当前磁盘容量

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <filesystem>

int main()
{
std::filesystem::path p("C:");
std::filesystem::space_info info = std::filesystem::space(p);
std::cout << "capacity: " << info.capacity << std::endl;
std::cout << "free: " << info.free << std::endl;
std::cout << "available: " << info.available << std::endl;
return 0;
}

获取文件大小

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <filesystem>

int main()
{
std::filesystem::path p("hosts");
std::cout << "file size: " << std::filesystem::file_size(p) << std::endl;
return 0;
}

获取文件夹下所有文件

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>
#include <vector>
#include <regex>
#include <string>
#include <filesystem>
#include <ranges>
#include <algorithm>

std::vector<std::string> get_directory_files(const std::string& path, const std::regex& reg = std::regex("(.*)"), bool is_recursive = false)
{
auto p = std::filesystem::path(path);
if (!std::filesystem::exists(p.string()))
{
return {};
}

std::vector<std::string> vec;
auto f = [&](const auto& dir_entry)
{
auto fp = dir_entry.path();
/// 过滤文件夹
if (!std::filesystem::is_directory(std::filesystem::status(fp)))
{
auto file_name = fp.filename().string();
/// 正则匹配
if (std::regex_match(file_name, reg))
{
vec.push_back(fp.string());
}
}
};

if (is_recursive)
{
/// 递归查找
std::ranges::for_each(std::filesystem::recursive_directory_iterator{p}, f);
}
else
{
/// 非递归查找
std::ranges::for_each(std::filesystem::directory_iterator{p}, f);
}

std::ranges::sort(vec);
return vec;
}

int main()
{

/// 默认情况下,查找所有文件, 非递归
auto c0 = get_directory_files(dir1.string());
/// 非递归,查找所有文件
auto c1 = get_directory_files(dir1.string(), std::regex("(.*)"), false);
/// 非递归,查找特定文件 .log
auto c2 = get_directory_files(dir1.string(), std::regex("(.*\\.log)"), false);
/// 递归,查找所有文件
auto c3 = get_directory_files(dir1.string(), std::regex("(.*)"), true);
/// 递归,查找特定文件 .log
auto c4 = get_directory_files(dir1.string(), std::regex("(.*\\.log)"), true);

return 0;

}

文件重命名

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <filesystem>

int main()
{
std::filesystem::path p("hosts");
std::filesystem::path new_p("hosts_new");
std::filesystem::rename(p, new_p);
return 0;
}

移动文件

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <filesystem>

int main()
{
std::filesystem::path p("hosts_new_copy");
std::filesystem::path new_p("hosts_new_copy_move");
std::filesystem::rename(p, new_p);
return 0;
}

拷贝文件

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <filesystem>

int main()
{
std::filesystem::path p("hosts_new");
std::filesystem::path new_p("hosts_new_copy");
std::filesystem::copy(p, new_p);
return 0;
}

拷贝文件夹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool copy_directory(const std::string& from, const std::string& to) noexcept
{
if (!std::filesystem::is_directory(from))
{
return false;
}

if (!std::filesystem::exists(from))
{
return false;
}

if (std::filesystem::exists(to))
{
return false;
}
/// 文件存在的情况下则更新,文件递归拷贝
const auto copy_options = std::filesystem::copy_options::update_existing | std::filesystem::copy_options::recursive;

std::error_code code;
std::filesystem::copy(from, to, copy_options, code);
return !code;
}