文章

c++高频总结十一

C++ Coroutine

C++ 协程(Coroutine)

什么是协程?

协程是一种可以在函数执行中间暂停和恢复的特殊函数,它与传统的线程不同,是一种轻量级的并发机制。C++20 引入了协程,提供了原生语言支持,以简化异步编程和生成器模式的实现。


协程的关键特点

  1. 暂停和恢复:协程可以挂起自身的执行并在稍后恢复,而无需阻塞线程。
  2. 高效:协程不需要线程的上下文切换,资源消耗更低。
  3. 灵活性:协程适用于异步 I/O、生成器、任务调度等场景。

C++ 协程的工作机制

C++ 的协程基于以下概念:

  1. 协程句柄(Coroutine Handle)
    • 用于管理协程的生命周期。
    • 提供暂停、恢复、销毁等操作。
  2. 协程状态
    • suspend_ready:协程初始化后是否立即挂起。
    • suspend_suspend:协程在 co_await 时是否挂起。
    • suspend_destroy:协程销毁前是否挂起。
  3. 协程的编译器魔法
    • 编译器将协程转化为一个状态机(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;
}

协程的典型应用场景

  1. 异步 I/O

    • 协程非常适合非阻塞 I/O。
    • 结合 co_await,可以简化回调地狱问题。
  2. 生成器

    • 使用 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;
      }
      
  3. 任务调度

    • 协程可以作为调度器的基础,实现轻量级任务并发。

协程的优势

  1. 代码清晰:协程避免了回调地狱,让代码更加线性化。
  2. 高效:协程的上下文切换开销远低于线程。
  3. 灵活:适用于多种并发和生成场景。

注意事项

  1. 协程的复杂性:
    • 实现 Promise 类型需要额外的学习成本。
  2. 性能问题:
    • 协程虽然轻量,但如果频繁创建或销毁,仍可能引入开销。
  3. 异常处理:
    • 协程中需要显式处理异常,否则可能导致意外行为。

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 的方式链接。

作用总结

  1. 解决符号链接兼容性问题:防止 C++ 名称修饰导致的链接错误。
  2. 便于 C++ 调用 C 库:许多传统的库(如 POSIX 库)是用 C 编写的。
  3. 支持 C 和 C++ 混合开发:简化两种语言的接口互通。

注意事项

  1. 只能用于函数和变量声明,不能直接用于类或模板,因为 C 不支持这些 C++ 特性。
  2. 使用 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 ModelELF 文件格式中断在操作系统中的作用 的详细解析。


1. C++ Memory Model

定义

C++ 的内存模型定义了在多线程环境下,线程之间如何通过共享内存进行交互。它涉及以下内容:

  • 顺序一致性(Sequential Consistency):程序按逻辑顺序执行,且所有线程看到的内存操作顺序一致。
  • 数据竞争(Data Race):多个线程在未加同步的情况下同时访问同一内存位置,其中至少有一个是写操作。

关键概念

  • 可见性(Visibility):一个线程的修改对其他线程是否可见。
  • 有序性(Ordering):线程对内存操作的顺序。
  • 原子操作(Atomicity):某些操作不会被其他线程打断。

C++11 Memory Model

C++11 引入了新的内存模型和原子操作,通过 std::atomic 和内存序列(memory order)来控制:

  1. 内存序列类型

    • memory_order_relaxed: 不保证顺序,仅保证原子性。
    • memory_order_acquire/release: 保证加载/存储的可见性和有序性。
    • memory_order_seq_cst: 保证全局顺序一致性。
  2. 锁和屏障

    • 使用 std::mutexstd::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_releasememory_order_acquire 确保写入操作对读取操作可见。


2. ELF 文件格式

定义

ELF(Executable and Linkable Format)是 UNIX 和 Linux 系统上广泛使用的可执行文件格式,也支持动态库和核心转储文件。

ELF 文件结构

ELF 文件分为以下几个主要部分:

  1. 文件头(ELF Header): 包含文件类型(可执行、动态链接、核心转储)、架构信息(32 位或 64 位)、入口点地址等。
  2. 程序头表(Program Header Table): 描述程序运行时需要加载的段(segment),如代码段(text)、数据段(data)等。
  3. 节头表(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 需要立即处理的事件。

分类

  1. 硬件中断
    • 由外部设备(如键盘、硬盘)触发。
    • 通过中断控制器(如 APIC)处理。
  2. 软件中断
    • 由程序通过指令(如 int)触发。
    • 常用于系统调用。
  3. 异常(Exception)
    • 由 CPU 检测到的错误(如除零、页错误)触发。

中断的作用

  1. 提高系统响应能力
    • 中断允许操作系统在事件发生时立即响应,而无需持续轮询硬件。
  2. 支持多任务操作
    • 中断使得操作系统可以通过定时中断实现任务切换,从而支持多任务。
  3. 设备驱动程序的基础
    • 中断用于通知操作系统设备状态的变化(如 I/O 完成)。
  4. 异常处理
    • 当程序出现非法操作(如非法内存访问)时,中断用于转移到异常处理程序。

中断处理流程

  1. 中断发生
    • 中断信号通过中断控制器发送到 CPU。
  2. 保存上下文
    • CPU 保存当前寄存器状态。
  3. 执行中断服务程序(ISR)
    • 跳转到预先定义的中断向量表中的处理程序。
  4. 恢复上下文
    • ISR 执行完毕后,恢复原始寄存器状态。

示例:x86 的中断指令

在 x86 架构中,int 指令可以触发中断:

int 0x80    ; 触发系统调用

中断与系统调用

系统调用通过软件中断实现。例如,在 Linux 中,int 0x80 触发内核进入系统调用处理程序。


总结

  1. C++ Memory Model:提供多线程内存访问的规则,确保线程间操作的可见性和有序性。
  2. ELF 文件格式:标准化的文件格式,用于描述可执行文件、动态库及核心转储文件的布局。
  3. 中断:是现代操作系统实现事件驱动和多任务处理的核心机制,用于提高系统响应效率。

这些概念结合在一起,为现代计算机系统的高效运行提供了基础。

C++的符号表

在 C++ 中,符号表 是编译器和链接器使用的一种数据结构,用于存储源代码中的标识符(变量、函数、类等)及其相关信息,如类型、作用域、地址等。

符号表的作用

符号表在编译和链接过程中扮演关键角色:

  1. 名称解析:
    • 编译器通过符号表确定标识符的定义和作用域。
  2. 类型检查:
    • 符号表存储了标识符的类型信息,编译器利用它检查类型是否匹配。
  3. 代码生成:
    • 符号表提供了标识符的地址信息,供编译器生成目标代码。
  4. 链接解析:
    • 链接器使用符号表解析外部符号(例如跨文件或跨库的函数调用)。

符号表的构成

符号表的内容主要包括:

  • 标识符名称:如变量名、函数名、类名等。
  • 类型信息:包括变量类型、函数返回类型及参数类型等。
  • 作用域信息:标识符的可见范围。
  • 内存地址:标识符在内存或寄存器中的具体地址(通常在代码生成阶段)。
  • 其他信息:
    • 链接属性(如是否是外部链接)。
    • 存储类别(如静态变量、全局变量)。

C++ 中符号表的构建

1. 编译时符号表

在编译时,编译器会扫描源代码并构建符号表,用于:

  • 局部变量和函数的作用域管理。
  • 模板实例化时跟踪特定类型的模板。

例如:

1
2
3
4
5
int global_var = 10; // 全局变量

void my_function() {
    int local_var = 5; // 局部变量
}

编译器构建的符号表可能如下:

名称类型作用域地址
global_varint全局静态存储区地址
my_functionvoid()全局函数入口地址
local_varintmy_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_varfoo 生成未解析符号。
  • 链接器:将 file2.cpp 中的定义解析为 file1.cpp 的引用。

符号表与作用域

C++ 的作用域对符号表有重要影响:

  1. 局部作用域:
    • 每个函数都有一个符号表,仅存储局部变量和参数。
  2. 全局作用域:
    • 全局符号表存储全局变量和函数。
  3. 类作用域:
    • 类的符号表存储成员变量和成员函数。
  4. 命名空间作用域:
    • 命名空间中的符号表用于组织相关符号,避免名称冲突。

示例:

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);

在符号表中,可能会存储以下修饰后的符号(基于具体的编译器):

  • _Z3fooifoo(int)
  • _Z3foodfoo(double)

调试信息中的符号表

在生成调试信息时,符号表会包含更多信息,供调试器(如 GDB)使用:

  • 源代码中的行号。
  • 局部变量的名称和作用域。
  • 数据类型和结构。

可以通过工具查看符号表,例如:

1
nm my_program

输出示例:

符号类型含义
T全局函数符号
D已初始化的全局变量
B未初始化的全局变量
U未定义的符号(外部符号)

总结

C++ 的符号表是编译器和链接器在编译和链接过程中不可或缺的工具,主要负责:

  • 管理标识符信息(如类型和作用域)。
  • 支持名称解析(特别是在多文件或复杂项目中)。
  • 生成目标代码

通过理解符号表的构造和用途,可以更好地理解 C++ 编译过程及其底层实现原理。

C++的单元测试

C++ 单元测试

单元测试是对软件中最小可测试单元(如函数、类或模块)进行验证的过程。通过单元测试,可以确保代码的功能符合预期,同时便于代码重构和维护。


C++ 单元测试框架

C++ 提供了多个成熟的单元测试框架,常用的包括:

  1. Google Test (GTest):
    • Google 提供的开源框架,功能强大,支持断言、多线程测试等。
    • 官方文档:Google Test
  2. Catch2:
    • 轻量级框架,语法简洁,易于上手。
    • 官方文档:Catch2
  3. CppUnit:
    • 经典的 xUnit 风格单元测试框架。
    • 官方文档:CppUnit
  4. 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)

单元测试的好处

  1. 提高代码质量
    • 发现并修复功能缺陷。
    • 通过断言明确函数的输入输出行为。
  2. 方便重构
    • 测试可以确保重构不会破坏现有功能。
  3. 回归测试
    • 新增功能时验证旧代码仍然正常工作。
  4. 文档作用
    • 测试代码能说明函数如何使用及其预期行为。

单元测试的实践建议

  1. 测试粒度适当
    • 单元测试应专注于单一功能或模块。
  2. 测试边界情况
    • 包括零值、负值、极大值、极小值等边界条件。
  3. 保持测试独立性
    • 测试之间应相互独立,不依赖外部状态。
  4. 覆盖率
    • 测试应尽量覆盖所有代码分支。
  5. 持续集成
    • 将单元测试集成到 CI/CD 流程中,保证代码质量。

总结

C++ 单元测试通过工具(如 Google Test 或 Catch2)实现,能够提升代码质量并减少错误。在实际开发中,应结合项目需求选择合适的框架,同时注重测试设计和覆盖率,以获得更好的代码可靠性和可维护性。

本文由作者按照 CC BY 4.0 进行授权