C++---段错误(SIGSEGV)

一、段错误的底层原理

段错误(Segmentation Fault),POSIX标准中对应SIGSEGV信号(编号11),本质是:

进程访问了CPU虚拟地址空间中,操作系统未给该进程授权的虚拟内存区域,或访问方式违反了该内存区域的权限规则,由CPU的MMU(内存管理单元)触发页异常,操作系统内核捕获后向违规进程发送SIGSEGV信号,默认行为是终止进程并生成core dump。

1.1 段错误的硬件-内核触发流程

现代Linux系统采用段页式内存管理 ,x86_64/ARM架构已弱化分段机制,段错误本质是非法页异常,完整触发流程:

  1. 进程执行指令,访问一个虚拟地址(VA);

  2. CPU的MMU接收VA,先查TLB快表(Translation Lookaside Buffer),未命中则查询内存中的页表;

  3. 页表查询会出现3种情况,仅前2种会触发段错误:

    异常类型 触发场景 内核处理
    无效页异常(触发段错误) 虚拟地址不在进程的VMA(虚拟内存区域)链表中(地址不存在);访问权限与VMA权限不匹配 发送SIGSEGV,终止进程
    无效页异常(触发段错误) 虚拟地址在VMA中,但物理页已被回收/未映射(如已释放的堆内存) 发送SIGSEGV,终止进程
    有效缺页异常(正常流程) 虚拟地址在VMA中,物理页未分配(如COW写时复制、文件页未加载) 内核分配物理页,修复页表,进程继续执行
  4. 若进程未注册SIGSEGV自定义处理函数,默认直接终止进程;若注册了处理函数,仅能执行异步安全操作,否则会触发二次崩溃。

1.2 关键概念

  1. 为什么叫"段错误"?
    早期UNIX采用纯分段内存管理,访问超出段边界/权限会触发段错误,名称沿用至今。现代系统的段错误本质是页异常,但核心逻辑依然是「违反内存区域的访问规则」
  2. SIGSEGV vs SIGBUS(总线错误,编号7)
    • SIGSEGV虚拟地址层面的违规(地址不存在、权限违规、越界);
    • SIGBUS物理地址层面的违规(ARM架构对齐错误、mmap文件被截断、不存在的硬件物理地址)。

二、段错误的根因

所有段错误的本质,都可归为「地址非法」或「权限违规」两大类

2.1 非法指针访问(占比最高)

核心是指针指向的地址不在进程的合法VMA中,解引用直接触发段错误。

2.1.1 空指针解引用
  • 本质 :Linux系统默认将0地址起始的低地址空间设为不可访问,对nullptr(0地址)的任何解引用都会触发段错误。

  • 典型代码

    cpp 复制代码
    // 基础场景
    int* p = nullptr;
    *p = 100; // 写空指针,直接段错误
    
    // ROS2机器人高频场景:节点初始化失败后无校验
    rclcpp::Node::SharedPtr node = nullptr;
    // 节点创建失败后未做空检查,直接解引用
    node->create_publisher<std_msgs::msg::String>("topic", 10);
  • 机器人高频场景:传感器驱动open/ioctl失败后无校验,直接访问缓冲区指针;ROS2发布者/订阅者/服务端创建失败后直接调用成员函数。

2.1.2 未初始化野指针
  • 本质 :指针定义后未初始化,存储栈上的随机垃圾值,指向随机虚拟地址,大概率不在进程VMA中。

  • 典型代码

    cpp 复制代码
    void process_scan() {
        pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_ptr; // 未初始化
        cloud_ptr->width = 1000; // 解引用未初始化智能指针,段错误
    }
2.1.3 悬空指针(Dangling Pointer)
  • 本质指针指向的内存已被释放/销毁,但指针未置空,依然指向已被内核回收的地址,访问触发段错误

  • 平时简单单节点、一直 spin 不销毁:
    抓 [this] 能用,一辈子不崩都正常。
    工程规范、工业级、可复用节点:
    不能直接抓 [this],属于隐性内存安全漏洞

  • 机器人开发最高频场景(ROS2回调重灾区):

    cpp 复制代码
    class RobotNode : public rclcpp::Node {
    public:
        RobotNode() : Node("robot_node") {
            // 错误:lambda捕获this指针,节点销毁后this悬空
            sub_ = this->create_subscription<std_msgs::msg::String>(
                "topic", 10, [this](const std_msgs::msg::String::SharedPtr msg) {
                    this->process_msg(msg); // 节点销毁后,this为悬空指针,段错误
                });
        }
    private:
        rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_;
        void process_msg(const std_msgs::msg::String::SharedPtr msg) {}
    };

    正确示例

cpp 复制代码
 #include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

// 必须继承 std::enable_shared_from_this
class RobotNode : public rclcpp::Node, public std::enable_shared_from_this<RobotNode> {
public:
    RobotNode() : Node("robot_node") {
        // 用 weak_ptr 捕获,而不是 this
        auto weak_ptr = weak_from_this();

        sub_ = this->create_subscription<std_msgs::msg::String>(
            "topic", 10, [weak_ptr](const std_msgs::msg::String::SharedPtr msg) {
                // 尝试锁定,节点还活着才执行
                auto node = weak_ptr.lock();
                if (!node) return;

                // 安全调用成员函数
                node->process_msg(msg);
            });
    }

private:
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_;

    void process_msg(const std_msgs::msg::String::SharedPtr msg) {
        RCLCPP_INFO(this->get_logger(), "Received: %s", msg->data.c_str());
    }
};
  • 其他高频场景:detach线程引用捕获局部变量,函数退出后变量栈内存被回收,线程访问悬空引用;多线程中一个线程释放内存,另一个线程仍在访问
2.1.4 非法类型转换/函数指针错误
  • 本质 :滥用reinterpret_cast/static_cast,将不兼容类型的指针强制转换,导致访问越界;函数指针指向非代码段地址,调用时触发不可执行内存访问。
  • 典型场景:ROS2消息类型强制转换不兼容;传感器驱动中断处理函数指针未初始化。

2.2 内存越界访问

核心是访问的地址在VMA中,但超出了内存区域的合法边界,严重时踩中页边界触发段错误,也是机器人点云/图像处理的重灾区。

2.2.1 数组/缓冲区/容器越界
  • 典型代码:

    cpp 复制代码
    // 点云数据越界(机器人高频)
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
    cloud->width = 1000;
    cloud->height = 1;
    cloud->resize(cloud->width * cloud->height);
    cloud->points[1000] = pcl::PointXYZ(1,2,3); // 下标越界,段错误
    
    // OpenCV图像越界(机器人高频)
    cv::Mat img(480, 640, CV_8UC3);
    img.at<cv::Vec3b>(480, 0) = cv::Vec3b(0,0,0); // 行号越界,段错误
    
    // STL容器越界
    std::vector<float> lidar_ranges(1080);
    lidar_ranges[1080] = 0; // []无边界检查,越界触发段错误
  • 关键避坑:STL容器优先用at()代替[]at()会做边界检查并抛出异常,不会直接段错误。

2.2.2 栈溢出(Stack Overflow)
  • 本质:Linux默认栈大小为8MB,嵌入式实时系统通常仅1-2MB,栈使用超出边界触发段错误。
  • 机器人高频场景:
    1. 无限递归(如路径规划递归A*算法无终止条件);
    2. 栈上分配超大数组(如int arr[1024*1024]);
    3. 实时线程栈大小设置过小,点云处理时栈溢出。
2.2.3 共享内存/MMAP越界
  • 机器人高频场景:传感器驱动mmap映射DMA缓冲区/硬件寄存器,访问超出映射长度;ROS2零拷贝共享内存越界。

  • 典型代码:

    cpp 复制代码
    int fd = open("/dev/lidar", O_RDWR);
    // 映射4KB设备内存
    void* map_ptr = mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    // 错误:无mmap失败校验,直接访问MAP_FAILED地址
    *(volatile int*)((char*)map_ptr + 4096) = 0; // 超出映射边界,段错误

2.3 内存权限违规访问

核心是访问的地址合法,但操作违反了该内存区域的权限规则 (Linux VMA权限:PROT_READ/PROT_WRITE/PROT_EXEC)。

2.3.1 向只读段写入数据
  • 本质 :进程的.rodata(只读数据段)、.text(代码段)权限为读+执行,无写权限,写入直接触发段错误。

  • 典型代码:

    cpp 复制代码
    const char* str = "hello world"; // 字符串常量存储在.rodata只读段
    str[0] = 'H'; // 向只读段写入,段错误
    
    const int a = 10;
    int* p = const_cast<int*>(&a); // 滥用const_cast去掉const属性
    *p = 20; // 向只读段写入,段错误
2.3.2 执行不可执行内存(NX保护)
  • 本质 :现代CPU默认开启NX(No-eXecute)位,数据段、堆、栈默认不可执行,跳转到这些地址执行指令触发段错误
  • 典型场景:函数指针指向栈/堆上的数据,调用时触发段错误。

2.4 已释放内存的非法操作(多线程并发重灾区)

核心是内存已被free/delete释放,虚拟地址已被内核回收,后续操作触发段错误。

2.4.1 释放后使用(Use-After-Free, UAF)
  • 机器人多线程高频场景:无锁共享数据,一个线程释放内存,另一个线程仍在访问。

  • 典型代码:

    cpp 复制代码
    // 线程1:释放点云数据
    std::thread t1([&]() {
        delete cloud_ptr;
        cloud_ptr = nullptr;
    });
    // 线程2:无锁访问,存在TOCTOU竞争
    std::thread t2([&]() {
        if(cloud_ptr != nullptr) {
            // 此时线程1已释放内存,cloud_ptr变为悬空指针  ,检查和使用之间存在时间差(TOCTOU)
            cloud_ptr->points[0] = pcl::PointXYZ(1,2,3); // UAF,段错误
        }
    });

TOCTOU (Time Of Check, Time Of Use)

检查 (Check) 和 使用 (Use) 之间存在时间差 → 数据被篡改 → 安全检查失效

检查(Check):程序判断 /tmp/tempfile 是普通文件、无风险

窗口期:攻击者把文件替换成软链接,指向 /etc/shadow(敏感文件)

使用(Use):程序按原计划打开文件 → 直接读到了敏感数据

2.4.2 重复释放/无效释放/内存管理函数混用
  • 典型场景:
    1. 同一块内存执行两次delete/free(Double Free);
    2. 释放栈上的指针、野指针(Invalid Free);
    3. new[]delete混用、mallocdelete混用,破坏堆元数据触发段错误;
    4. 两个shared_ptr管理同一块裸指针,析构时重复释放。

2.5 多线程/并发场景特有段错误

机器人ROS2节点、多传感器回调、算法并行处理均为多线程场景,90%的偶发段错误都源于此。

  1. 线程间共享数据无同步:多线程同时读写指针/数据结构,无锁保护导致指针被篡改、内存破坏;
  2. 线程生命周期管理错误detach线程引用捕获局部变量,函数退出后变量被回收,触发悬空引用;
  3. 信号处理函数违规 :在SIGINT/SIGSEGV处理函数中调用非异步安全函数(printf/malloc/锁),导致重入问题、二次崩溃;
  4. 虚函数表被破坏:多线程同时修改多态对象,导致虚函数表指针(vptr)被覆盖,调用虚函数时访问非法地址。

2.6 C++面向对象特性相关段错误

  1. 已销毁对象的虚函数调用 :对象已被delete,虚函数表已释放,调用虚函数触发段错误;
  2. 浅拷贝问题:类包含指针成员,默认拷贝构造函数为浅拷贝,两个对象指向同一块内存,析构时重复释放;
  3. 半初始化对象:构造函数初始化失败,未抛出异常,返回半初始化对象,成员指针未初始化,访问触发段错误。

2.7 系统/编译/机器人特有段错误场景

  1. ABI不兼容:不同gcc版本、不同编译选项编译的库与可执行文件链接,虚函数表/调用约定不匹配,触发段错误(机器人交叉开发);
  2. 地址对齐错误:ARM架构下,多字节数据访问未对齐,触发总线错误/段错误(传感器驱动);
  3. 64位地址截断:将64位指针强制转为32位int,高32位丢失,指针变为非法地址;
  4. 实时系统特有 :实时线程中调用malloc/new,内存分配失败返回nullptr,解引用触发段错误;
  5. 工控机硬件相关:直接访问未映射的内核空间地址、硬件寄存器地址超出映射范围。

三、段错误的全流程排查方案

3.1 排查前置条件:生成带调试信息的程序

所有排查的前提是编译时保留调试信息,否则无法将地址映射到代码行号。

  • 编译选项 :添加-ggdb(gdb优化的调试信息),release模式下也可添加,不影响运行性能,仅增大可执行文件体积;

  • 示例:

    bash 复制代码
    # 直接编译
    g++ -O2 -ggdb -Wall -Wextra main.cpp -o main
    # ROS2 CMakeLists.txt添加
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ggdb -Wall -Wextra -fno-omit-frame-pointer")
  • 必做优化:开启编译警告-Wall -Wextra -Werror,将警告转为错误,编译时即可消除80%的隐患。

3.2 第一层级:必现段错误的快速定位

3.2.1 直接用gdb运行程序,一步定位行号

适用于本地x86环境、必现的段错误,是最直接的排查手段。

bash 复制代码
# 1. 普通程序启动
gdb ./your_program
# 2. gdb内运行程序
(gdb) run
# 3. 触发段错误后,查看完整栈回溯(含局部变量)
(gdb) bt full   #打印崩溃时的完整函数调用链 + 所有局部变量的值
# 4. 查看触发位置的代码上下文
(gdb) list
# 5. 查看关键变量/指针的值
(gdb) print cloud_ptr
  • ROS2节点专用用法:

    bash 复制代码
    ros2 run --prefix "gdb -ex run --args" your_package your_node
    # 触发段错误后,直接输入bt full查看栈回溯
3.2.2 启用core dump,事后离线分析

Core Dump(核心转储):程序异常崩溃瞬间,操作系统把该进程完整的内存镜像、寄存器、函数调用栈、线程状态、全局变量等现场数据,保存成一个 core 文件的行为。

适用于工控机部署的程序、无法实时用gdb运行的场景,段错误触发后保留完整现场。

步骤1:永久配置core dump(工控机必做)
bash 复制代码
# 1. 临时启用(当前终端有效)
ulimit -c unlimited

# 2. 永久启用:修改/etc/security/limits.conf,添加以下2行
* soft core unlimited
* hard core unlimited

# 3. 设置core文件存储路径与命名规则:修改/etc/sysctl.conf,添加
kernel.core_pattern = /var/core/core_%e_%p_%t
kernel.core_uses_pid = 0

# 4. 生效配置
sysctl -p
mkdir -p /var/core && chmod 777 /var/core
步骤2:分析core文件
bash 复制代码
# 格式:gdb 可执行文件路径 core文件路径
gdb ./your_program /var/core/core_your_program_1234_1234567890
# 进入gdb后,直接查看栈回溯
(gdb) bt full
3.2.3 用addr2line快速定位指令地址

适用于仅能获取系统日志的场景,通过dmesg中的段错误地址定位代码行。

bash 复制代码
# 1. 查看dmesg中的段错误信息
dmesg | grep segfault
# 输出示例:[12345.678901] your_program[1234]: segfault at 0 ip 0000555555555123 sp 00007fffffffd120 error 6 in your_program[555555554000+1000]

# 2. 用addr2line将指令地址ip转为代码行
addr2line -f -e ./your_program 0000555555555123
# 输出:函数名 + 代码文件与行号

3.3 第二层级:根因定位专业工具

3.3.1 AddressSanitizer(ASAN):内存错误检测神器

Google开发的内存检测工具,运行时实时检测内存错误,比valgrind快10倍以上,,是C++开发的必备工具。

  • 使用方法:

    bash 复制代码
    # 编译时添加编译与链接选项
    g++ -fsanitize=address -ggdb -fno-omit-frame-pointer main.cpp -o main
    # 运行程序,触发错误时会直接打印:错误类型、代码行号、调用栈、内存分配/释放位置
    ./main
  • ROS2 CMakeLists.txt配置:

    cmake 复制代码
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -ggdb -fno-omit-frame-pointer")
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=address")
  • 可检测的段错误相关问题:空指针解引用、堆/栈/全局数组越界、Use-After-Free、Double Free、栈溢出、内存泄漏。

3.3.2 ThreadSanitizer(TSAN):多线程偶发段错误排查利器

专门用于检测多线程场景下的数据竞争,绝大多数偶发段错误都源于数据竞争,TSAN能精准定位竞争的位置、两个线程的调用栈。

  • 使用方法:

    bash 复制代码
    g++ -fsanitize=thread -ggdb -fno-omit-frame-pointer main.cpp -o main
    ./main
  • 注意:TSAN内存开销较大,不适合实时生产环境,仅用于调试。

3.3.3 其他辅助工具
工具 适用场景
Valgrind 非实时程序的内存错误检测,ASAN无法使用的场景
UBSan 检测C++未定义行为(类型转换错误、对齐错误等)
strace/ltrace 跟踪系统调用/库函数调用,排查ioctl/mmap/open失败导致的段错误
watch(gdb) 设置内存观察点,监控被篡改的指针/内存,定位内存踩踏问题

3.4 第三层级:偶发段错误的排查方案

偶发段错误大多源于多线程时序、边界条件、数据竞争,排查核心是稳定复现+现场保留

  1. 稳定复现:用rosbag循环回放传感器数据、压力测试提高负载、固定复现的时序条件,提高复现概率;
  2. 捕获现场:永久开启core dump,确保每次段错误都能生成core文件,事后分析栈回溯;
  3. 数据竞争检测:用TSAN运行程序,复现问题,直接定位数据竞争的位置;
  4. 防御性日志:添加线程安全的日志,打印线程ID、时间戳、函数名、关键指针的值、数据大小,定位触发段错误的模块与时序;
  5. 缩小范围:逐步注释代码、关闭模块,缩小问题范围,定位触发段错误的最小代码集。

3.5 第四层级:嵌入式ARM板卡远程排查

适用于Jetson/RK3588等机器人嵌入式板卡,无法本地运行gdb的场景。

  1. gdbserver远程调试

    bash 复制代码
    # 1. 嵌入式板卡启动gdbserver
    gdbserver 0.0.0.0:1234 ./your_program
    # 2. 本地开发环境用交叉gdb连接
    aarch64-linux-gnu-gdb ./your_program
    (gdb) target remote 板卡IP:1234
    (gdb) run
  2. 离线core分析:板卡上生成core文件后,拷贝到本地,用交叉gdb分析栈回溯。


四、段错误修复与工程化预防

4.1 针对根因的直接修复方案

错误类型 核心修复方案
空指针/野指针/悬空指针 1. 所有指针定义时必须初始化(nullptr/合法地址);2. 解引用前必须做空指针检查;3. 内存释放后立即置空指针;4. 用new(std::nothrow)避免分配失败抛出异常
内存越界 1. 数组/容器访问前必须做边界检查;2. 优先用at()代替[];3. 栈上禁止分配超大数组,大内存用vector/堆分配;4. 递归函数必须有明确的终止条件
权限违规 1. 禁止修改const变量/字符串常量;2. 禁止滥用const_cast;3. 禁止执行数据段/栈/堆上的代码
已释放内存操作 1. 禁止Use-After-Free,释放后指针置空;2. 禁止内存管理函数混用;3. 类指针成员必须实现深拷贝,或禁用拷贝构造函数
多线程并发错误 1. 共享数据必须用mutex/shared_mutex保护;2. 禁止detach线程引用捕获局部变量;3. ROS2回调捕获节点shared_ptr而非this指针;4. 信号处理函数仅调用异步安全函数
C++面向对象错误 1. 构造函数初始化失败必须抛出异常,禁止返回半初始化对象;2. 向下转型用dynamic_cast而非static_cast;3. 禁用默认浅拷贝

4.2 工程化预防

4.2.1 优先使用智能指针,杜绝裸指针

C++11+智能指针(unique_ptr/shared_ptr/weak_ptr)通过RAII机制自动管理内存生命周期,是预防内存错误的最有效手段,机器人开发中所有点云、图像、传感器数据必须用智能指针管理。

  • 最佳实践:

    cpp 复制代码
    // 推荐:unique_ptr独占所有权,无额外开销,自动释放
    std::unique_ptr<pcl::PointCloud<pcl::PointXYZ>> cloud = std::make_unique<pcl::PointCloud<pcl::PointXYZ>>();
    // 推荐:shared_ptr共享所有权,ROS2消息/回调场景使用
    auto cloud = pcl::make_shared<pcl::PointCloud<pcl::PointXYZ>>();
    
    // ROS2回调生命周期安全写法:捕获shared_ptr而非this
    auto self = shared_from_this();
    sub_ = create_subscription<std_msgs::msg::String>("topic", 10, [self](const auto& msg) {
        self->process_msg(msg); // 节点生命周期被shared_ptr延长,避免this悬空
    });
4.2.2 严格遵循RAII设计模式

资源获取即初始化,将资源(设备句柄、内存、锁)的生命周期与对象绑定,构造时获取资源,析构时释放资源,完全避免手动管理资源导致的错误。

  • 机器人场景:传感器设备打开/关闭、文件操作、锁的加锁/解锁,全部用RAII封装。
4.2.3 实时系统内存管理规范

机器人实时线程必须遵循以下规则,避免段错误与实时性问题:

  1. 实时线程中禁止调用malloc/free/new/delete,这些函数非实时安全,且可能分配失败返回nullptr;
  2. 提前预分配所有需要的内存,用内存池/对象池管理,避免运行时动态分配;
  3. mlock锁定实时线程的内存,避免被交换到磁盘导致缺页异常。
4.2.4 防御性编程规范
  1. 所有函数输入参数必须做合法性校验(指针非空、长度合法、数据范围有效);
  2. 所有系统调用必须检查返回值(open/ioctl/mmap/socket),失败后必须做错误处理,禁止继续执行;
  3. 所有数组/容器访问必须做边界检查;
  4. 传感器数据必须做长度/范围校验,避免无效数据导致越界。
4.2.5 工具链常态化检测
  1. 代码提交前,用ASAN/TSAN/UBSan运行单元测试,确保无内存错误;
  2. CI/CD流水线集成静态检查工具(clang-tidy/cppcheck),自动发现代码隐患;
  3. 每次版本迭代,用rosbag做全场景回归测试,暴露偶发段错误。

五、常见误区与避坑指南

  1. 误区1:Debug模式正常,Release模式段错误是编译器问题
    真相:99%的情况是代码存在未定义行为(未初始化指针、野指针),Debug模式下内存被初始化为0,Release模式下内存为随机值,触发段错误。
  2. 误区2:用了智能指针就不会有段错误
    真相:智能指针仅管理生命周期,循环引用、悬空weak_ptr、裸指针滥用依然会触发段错误。
  3. 误区3:数组越界一定会触发段错误
    真相:小范围越界若在同一页内,不会触发段错误,但会破坏相邻内存,导致后续代码出现不可预测的内存踩踏,比段错误更难排查。
  4. 误区4:SIGSEGV可以捕获后让程序继续运行
    真相:SIGSEGV处理函数仅能执行打印日志、生成core dump等最小化操作,不能让程序继续运行,此时内存已被破坏,继续运行会导致更严重的问题。
相关推荐
乐观勇敢坚强的老彭1 小时前
day515C++信奥循环嵌套强化03
开发语言·c++
Irene19911 小时前
(表格+词源+前端类比的方式)记忆常用 Linux 命令
linux
杜子不疼.1 小时前
【C++ AI 大模型接入 SDK】 - 环境搭建
开发语言·数据库·c++
怀旧,1 小时前
【C++项目】负载均衡式在线OJ
开发语言·c++·负载均衡
算个文科生吧1 小时前
奥比 Gemini 305 局域网 MJPEG 推流
机器人
nj01281 小时前
Linux 根分区占满排查与 SSH 暴力破解日志清理记录
linux·运维·ssh
MaikieMaiky2 小时前
C++ STL 系列(一):string 容器详解与示例
开发语言·c++
xingfujie2 小时前
第2章:服务器规划与基础环境配置
linux·运维·微服务·云原生·容器·kubernetes·负载均衡
努力努力再努力wz2 小时前
【Qt入门系列】深入理解信号与槽:从事件响应到自定义信号机制
c语言·开发语言·数据结构·数据库·c++·qt·mysql