c++高频总结十一
C++ Coroutine
C++ 协程(Coroutine)
什么是协程?
协程是一种可以在函数执行中间暂停和恢复的特殊函数,它与传统的线程不同,是一种轻量级的并发机制。C++20 引入了协程,提供了原生语言支持,以简化异步编程和生成器模式的实现。
协程的关键特点
- 暂停和恢复:协程可以挂起自身的执行并在稍后恢复,而无需阻塞线程。
- 高效:协程不需要线程的上下文切换,资源消耗更低。
- 灵活性:协程适用于异步 I/O、生成器、任务调度等场景。
C++ 协程的工作机制
C++ 的协程基于以下概念:
- 协程句柄(Coroutine Handle):
- 用于管理协程的生命周期。
- 提供暂停、恢复、销毁等操作。
- 协程状态:
suspend_ready
:协程初始化后是否立即挂起。suspend_suspend
:协程在co_await
时是否挂起。suspend_destroy
:协程销毁前是否挂起。
- 协程的编译器魔法:
- 编译器将协程转化为一个状态机(state machine)。
- 协程的每次挂起和恢复,实质是状态机的状态切换。
C++ 协程的关键组成
以下是实现协程需要的几个关键点:
1. 协程关键字
co_await
:等待异步结果。co_return
:从协程返回值。co_yield
:返回一个中间值,同时保留协程状态以便稍后恢复。
2. 协程的必要组件
协程的实现需要以下类:
- Promise 类型:
- 定义协程的行为(如初始化、挂起、返回值等)。
- 必须实现以下接口:
initial_suspend()
:协程开始时是否挂起。final_suspend()
:协程结束时是否挂起。return_value()
:设置协程返回值。yield_value()
:定义co_yield
的行为。
- 协程句柄:
- 使用
std::coroutine_handle
访问和管理协程。
- 使用
协程的基本示例
异步任务
一个返回 std::future
的协程示例:
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
#include <coroutine>
#include <future>
#include <iostream>
struct Task {
struct promise_type {
std::promise<void> p;
auto get_return_object() {
return Task{p.get_future()};
}
auto initial_suspend() {
return std::suspend_never{};
}
auto final_suspend() noexcept {
return std::suspend_never{};
}
void return_void() {
p.set_value();
}
void unhandled_exception() {
p.set_exception(std::current_exception());
}
};
std::future<void> future;
explicit Task(std::future<void> f) : future(std::move(f)) {}
};
Task example_coroutine() {
std::cout << "Start coroutine" << std::endl;
co_await std::suspend_always{};
std::cout << "Resume coroutine" << std::endl;
}
int main() {
auto task = example_coroutine();
task.future.get(); // 等待协程完成
return 0;
}
协程的典型应用场景
异步 I/O
- 协程非常适合非阻塞 I/O。
- 结合
co_await
,可以简化回调地狱问题。
生成器
使用
co_yield
实现生成器,可以按需生成值。示例:
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
#include <coroutine> #include <iostream> struct Generator { struct promise_type { int current_value; auto get_return_object() { return Generator{*this}; } auto initial_suspend() { return std::suspend_always{}; } auto final_suspend() noexcept { return std::suspend_always{}; } void return_void() {} void unhandled_exception() { std::terminate(); } auto yield_value(int value) { current_value = value; return std::suspend_always{}; } }; using handle_type = std::coroutine_handle<promise_type>; handle_type handle; explicit Generator(promise_type& p) : handle(handle_type::from_promise(p)) {} ~Generator() { handle.destroy(); } int next() { handle.resume(); return handle.promise().current_value; } }; Generator generate_numbers() { for (int i = 1; i <= 5; ++i) co_yield i; } int main() { auto gen = generate_numbers(); for (int i = 0; i < 5; ++i) { std::cout << gen.next() << std::endl; } return 0; }
任务调度
- 协程可以作为调度器的基础,实现轻量级任务并发。
协程的优势
- 代码清晰:协程避免了回调地狱,让代码更加线性化。
- 高效:协程的上下文切换开销远低于线程。
- 灵活:适用于多种并发和生成场景。
注意事项
- 协程的复杂性:
- 实现 Promise 类型需要额外的学习成本。
- 性能问题:
- 协程虽然轻量,但如果频繁创建或销毁,仍可能引入开销。
- 异常处理:
- 协程中需要显式处理异常,否则可能导致意外行为。
C++20 的协程功能为异步编程和生成器带来了革命性的提升,但需要对底层机制(如 Promise、句柄)有较深入的理解才能充分利用其能力。
extern C有什么作用
在 C++ 中,extern "C"
是一种链接指示符,用于告诉编译器按照 C 的方式来处理函数或变量的链接,而不是按照 C++ 的方式。它主要用于 C 和 C++ 混合编程时,解决两种语言的链接兼容性问题。
背景
C 和 C++ 在符号链接(symbol linkage)机制上有不同之处:
- C 使用“简单符号”链接。例如,函数名
foo
的符号就是foo
。 - C++ 使用“名称修饰”(name mangling)机制,以支持函数重载。函数名
foo(int)
在符号表中可能会变成_Z3fooi
(不同的编译器实现可能略有不同)。
使用场景
当 C++ 程序需要调用 C 库函数,或者 C 库需要调用 C++ 函数时,extern "C"
用于消除这些差异。
用法
1. 包裹函数声明
如果在 C++ 中引用一个 C 库的头文件,需要用 extern "C"
包裹:
1
2
3
extern "C" {
#include "some_c_library.h"
}
这会告诉编译器,some_c_library.h
中声明的函数使用 C 的链接方式。
2. 用于函数定义
在 C++ 中定义一个 C 函数时:
1
2
3
extern "C" void my_function(int a) {
// C 风格的函数实现
}
3. 用于头文件
如果一个头文件需要同时支持 C 和 C++,通常会这样处理:
1
2
3
4
5
6
7
8
9
10
#ifdef __cplusplus
extern "C" {
#endif
// C 风格的函数声明
void c_function(int a);
#ifdef __cplusplus
}
#endif
这样可以确保头文件既能被 C 程序直接使用,也能被 C++ 程序以 C 的方式链接。
作用总结
- 解决符号链接兼容性问题:防止 C++ 名称修饰导致的链接错误。
- 便于 C++ 调用 C 库:许多传统的库(如 POSIX 库)是用 C 编写的。
- 支持 C 和 C++ 混合开发:简化两种语言的接口互通。
注意事项
- 只能用于函数和变量声明,不能直接用于类或模板,因为 C 不支持这些 C++ 特性。
- 使用
extern "C"
包裹的代码不能使用 C++ 的功能,例如重载。
示例:
1
2
3
4
5
6
7
8
9
10
// C 库中的函数
extern "C" {
void c_function(int x);
}
// 调用 C 函数
int main() {
c_function(10); // 使用 C 的链接方式
return 0;
}
通过 extern "C"
,你可以确保在 C++ 中调用 C 函数时链接无误,同时保持代码的兼容性和可移植性。
C++ memoryder/elf文件格式/中断对于操作系统的作用
下面是对 C++ Memory Model、ELF 文件格式 和 中断在操作系统中的作用 的详细解析。
1. C++ Memory Model
定义
C++ 的内存模型定义了在多线程环境下,线程之间如何通过共享内存进行交互。它涉及以下内容:
- 顺序一致性(Sequential Consistency):程序按逻辑顺序执行,且所有线程看到的内存操作顺序一致。
- 数据竞争(Data Race):多个线程在未加同步的情况下同时访问同一内存位置,其中至少有一个是写操作。
关键概念
- 可见性(Visibility):一个线程的修改对其他线程是否可见。
- 有序性(Ordering):线程对内存操作的顺序。
- 原子操作(Atomicity):某些操作不会被其他线程打断。
C++11 Memory Model
C++11 引入了新的内存模型和原子操作,通过 std::atomic
和内存序列(memory order)来控制:
内存序列类型
:
memory_order_relaxed
: 不保证顺序,仅保证原子性。memory_order_acquire/release
: 保证加载/存储的可见性和有序性。memory_order_seq_cst
: 保证全局顺序一致性。
锁和屏障
:
- 使用
std::mutex
和std::lock_guard
进行线程同步。 - 使用
std::atomic_thread_fence
进行内存屏障。
- 使用
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> data(0);
void writer() {
data.store(10, std::memory_order_release);
}
void reader() {
int value = data.load(std::memory_order_acquire);
std::cout << "Value: " << value << std::endl;
}
int main() {
std::thread t1(writer);
std::thread t2(reader);
t1.join();
t2.join();
}
以上代码通过 memory_order_release
和 memory_order_acquire
确保写入操作对读取操作可见。
2. ELF 文件格式
定义
ELF(Executable and Linkable Format)是 UNIX 和 Linux 系统上广泛使用的可执行文件格式,也支持动态库和核心转储文件。
ELF 文件结构
ELF 文件分为以下几个主要部分:
- 文件头(ELF Header): 包含文件类型(可执行、动态链接、核心转储)、架构信息(32 位或 64 位)、入口点地址等。
- 程序头表(Program Header Table): 描述程序运行时需要加载的段(segment),如代码段(text)、数据段(data)等。
- 节头表(Section Header Table): 描述文件的各个部分(section),如符号表、字符串表等。常见的 Section:
.text
: 存放代码。.data
: 存放已初始化的全局变量。.bss
: 存放未初始化的全局变量。.symtab
: 符号表。.strtab
: 字符串表。
ELF 的动态链接
- 动态库信息存储在
.dynamic
段。 - 链接器使用
.plt
(Procedure Linkage Table)和.got
(Global Offset Table) 来支持动态符号解析。
示例:用 readelf
工具查看 ELF 文件
1
readelf -h my_program
可以显示 ELF 文件头信息,如入口点地址和段偏移。
3. 中断在操作系统中的作用
定义
中断是一种硬件或软件触发的信号,用于通知 CPU 需要立即处理的事件。
分类
- 硬件中断:
- 由外部设备(如键盘、硬盘)触发。
- 通过中断控制器(如 APIC)处理。
- 软件中断:
- 由程序通过指令(如
int
)触发。 - 常用于系统调用。
- 由程序通过指令(如
- 异常(Exception):
- 由 CPU 检测到的错误(如除零、页错误)触发。
中断的作用
- 提高系统响应能力:
- 中断允许操作系统在事件发生时立即响应,而无需持续轮询硬件。
- 支持多任务操作:
- 中断使得操作系统可以通过定时中断实现任务切换,从而支持多任务。
- 设备驱动程序的基础:
- 中断用于通知操作系统设备状态的变化(如 I/O 完成)。
- 异常处理:
- 当程序出现非法操作(如非法内存访问)时,中断用于转移到异常处理程序。
中断处理流程
- 中断发生:
- 中断信号通过中断控制器发送到 CPU。
- 保存上下文:
- CPU 保存当前寄存器状态。
- 执行中断服务程序(ISR):
- 跳转到预先定义的中断向量表中的处理程序。
- 恢复上下文:
- ISR 执行完毕后,恢复原始寄存器状态。
示例:x86 的中断指令
在 x86 架构中,int
指令可以触发中断:
int 0x80 ; 触发系统调用
中断与系统调用
系统调用通过软件中断实现。例如,在 Linux 中,int 0x80
触发内核进入系统调用处理程序。
总结
- C++ Memory Model:提供多线程内存访问的规则,确保线程间操作的可见性和有序性。
- ELF 文件格式:标准化的文件格式,用于描述可执行文件、动态库及核心转储文件的布局。
- 中断:是现代操作系统实现事件驱动和多任务处理的核心机制,用于提高系统响应效率。
这些概念结合在一起,为现代计算机系统的高效运行提供了基础。
C++的符号表
在 C++ 中,符号表 是编译器和链接器使用的一种数据结构,用于存储源代码中的标识符(变量、函数、类等)及其相关信息,如类型、作用域、地址等。
符号表的作用
符号表在编译和链接过程中扮演关键角色:
- 名称解析:
- 编译器通过符号表确定标识符的定义和作用域。
- 类型检查:
- 符号表存储了标识符的类型信息,编译器利用它检查类型是否匹配。
- 代码生成:
- 符号表提供了标识符的地址信息,供编译器生成目标代码。
- 链接解析:
- 链接器使用符号表解析外部符号(例如跨文件或跨库的函数调用)。
符号表的构成
符号表的内容主要包括:
- 标识符名称:如变量名、函数名、类名等。
- 类型信息:包括变量类型、函数返回类型及参数类型等。
- 作用域信息:标识符的可见范围。
- 内存地址:标识符在内存或寄存器中的具体地址(通常在代码生成阶段)。
- 其他信息:
- 链接属性(如是否是外部链接)。
- 存储类别(如静态变量、全局变量)。
C++ 中符号表的构建
1. 编译时符号表
在编译时,编译器会扫描源代码并构建符号表,用于:
- 局部变量和函数的作用域管理。
- 模板实例化时跟踪特定类型的模板。
例如:
1
2
3
4
5
int global_var = 10; // 全局变量
void my_function() {
int local_var = 5; // 局部变量
}
编译器构建的符号表可能如下:
名称 | 类型 | 作用域 | 地址 |
---|---|---|---|
global_var | int | 全局 | 静态存储区地址 |
my_function | void() | 全局 | 函数入口地址 |
local_var | int | my_function | 栈地址 |
2. 链接时符号表
链接器会根据编译器生成的符号表,处理外部符号和未解析的符号。例如:
1
2
3
4
5
6
7
8
9
10
11
// file1.cpp
extern int global_var;
void foo();
// file2.cpp
int global_var = 42;
void foo() {
// Some code
}
- 编译器:为
global_var
和foo
生成未解析符号。 - 链接器:将
file2.cpp
中的定义解析为file1.cpp
的引用。
符号表与作用域
C++ 的作用域对符号表有重要影响:
- 局部作用域:
- 每个函数都有一个符号表,仅存储局部变量和参数。
- 全局作用域:
- 全局符号表存储全局变量和函数。
- 类作用域:
- 类的符号表存储成员变量和成员函数。
- 命名空间作用域:
- 命名空间中的符号表用于组织相关符号,避免名称冲突。
示例:
1
2
3
4
5
6
7
8
namespace MyNamespace {
int var = 10;
class MyClass {
public:
int member_var;
};
}
符号表可能包括:
MyNamespace::var
MyNamespace::MyClass::member_var
符号表与名称修饰(Name Mangling)
C++ 支持函数重载和模板,这会导致符号表中出现多个具有相同名字的标识符。为了区分这些标识符,编译器使用 名称修饰。
名称修饰示例:
1
2
void foo(int);
void foo(double);
在符号表中,可能会存储以下修饰后的符号(基于具体的编译器):
_Z3fooi
(foo(int)
)_Z3food
(foo(double)
)
调试信息中的符号表
在生成调试信息时,符号表会包含更多信息,供调试器(如 GDB)使用:
- 源代码中的行号。
- 局部变量的名称和作用域。
- 数据类型和结构。
可以通过工具查看符号表,例如:
1
nm my_program
输出示例:
符号类型 | 含义 |
---|---|
T | 全局函数符号 |
D | 已初始化的全局变量 |
B | 未初始化的全局变量 |
U | 未定义的符号(外部符号) |
总结
C++ 的符号表是编译器和链接器在编译和链接过程中不可或缺的工具,主要负责:
- 管理标识符信息(如类型和作用域)。
- 支持名称解析(特别是在多文件或复杂项目中)。
- 生成目标代码。
通过理解符号表的构造和用途,可以更好地理解 C++ 编译过程及其底层实现原理。
C++的单元测试
C++ 单元测试
单元测试是对软件中最小可测试单元(如函数、类或模块)进行验证的过程。通过单元测试,可以确保代码的功能符合预期,同时便于代码重构和维护。
C++ 单元测试框架
C++ 提供了多个成熟的单元测试框架,常用的包括:
- Google Test (GTest):
- Google 提供的开源框架,功能强大,支持断言、多线程测试等。
- 官方文档:Google Test
- Catch2:
- 轻量级框架,语法简洁,易于上手。
- 官方文档:Catch2
- CppUnit:
- 经典的 xUnit 风格单元测试框架。
- 官方文档:CppUnit
- Boost.Test:
- Boost 库的一部分,支持强大的断言和日志功能。
- 官方文档:Boost.Test
单元测试的基本概念
1. 测试用例 (Test Case)
- 对一个功能或模块的具体测试。
- 例如,测试一个函数是否返回正确的结果。
2. 测试集 (Test Suite)
- 一组相关测试用例的集合。
3. 断言 (Assertion)
- 用于验证测试结果是否符合预期。
- 常见断言:
ASSERT_EQ(expected, actual)
(相等)ASSERT_TRUE(condition)
(条件为真)ASSERT_THROW(expression, exception)
(抛出指定异常)
使用 Google Test 示例
1. 安装 Google Test
下载并编译 Google Test:
1 2 3 4
git clone https://github.com/google/googletest.git mkdir build && cd build cmake .. && make sudo make install
2. 基本用例
以下是一个简单的 Google Test 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <gtest/gtest.h>
// 被测试的函数
int add(int a, int b) {
return a + b;
}
// 测试用例
TEST(AdditionTest, PositiveNumbers) {
ASSERT_EQ(add(2, 3), 5); // 2 + 3 = 5
}
TEST(AdditionTest, NegativeNumbers) {
ASSERT_EQ(add(-2, -3), -5); // -2 + -3 = -5
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
3. 运行测试
编译和运行测试代码:
1
2
g++ -o test_program test.cpp -lgtest -lgtest_main -pthread
./test_program
输出示例:
1
2
3
4
5
6
7
8
9
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[ RUN ] AdditionTest.PositiveNumbers
[ OK ] AdditionTest.PositiveNumbers (0 ms)
[ RUN ] AdditionTest.NegativeNumbers
[ OK ] AdditionTest.NegativeNumbers (0 ms)
[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (1 ms total)
[ PASSED ] 2 tests.
使用 Catch2 示例
1. 安装 Catch2
下载单个头文件:
1
wget https://github.com/catchorg/Catch2/releases/download/v3.0.0/catch.hpp
2. 基本用例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define CATCH_CONFIG_MAIN // 提供主函数
#include "catch.hpp"
// 被测试的函数
int multiply(int a, int b) {
return a * b;
}
// 测试用例
TEST_CASE("Multiplication works correctly") {
REQUIRE(multiply(2, 3) == 6);
REQUIRE(multiply(-2, -3) == 6);
REQUIRE(multiply(0, 5) == 0);
}
3. 运行测试
编译和运行代码:
1
2
g++ -o test_program test.cpp
./test_program
输出示例:
1
2
===============================================================================
All tests passed (3 assertions in 1 test case)
单元测试的好处
- 提高代码质量:
- 发现并修复功能缺陷。
- 通过断言明确函数的输入输出行为。
- 方便重构:
- 测试可以确保重构不会破坏现有功能。
- 回归测试:
- 新增功能时验证旧代码仍然正常工作。
- 文档作用:
- 测试代码能说明函数如何使用及其预期行为。
单元测试的实践建议
- 测试粒度适当:
- 单元测试应专注于单一功能或模块。
- 测试边界情况:
- 包括零值、负值、极大值、极小值等边界条件。
- 保持测试独立性:
- 测试之间应相互独立,不依赖外部状态。
- 覆盖率:
- 测试应尽量覆盖所有代码分支。
- 持续集成:
- 将单元测试集成到 CI/CD 流程中,保证代码质量。
总结
C++ 单元测试通过工具(如 Google Test 或 Catch2)实现,能够提升代码质量并减少错误。在实际开发中,应结合项目需求选择合适的框架,同时注重测试设计和覆盖率,以获得更好的代码可靠性和可维护性。