当前位置: 首页 > news >正文

C++如何使用调试器(如GDB、LLDB)进行程序调试保姆级教程(2万字长文)

C++作为一门高性能、接近底层的编程语言,其复杂性和灵活性为开发者提供了强大的能力,同时也带来了更高的调试难度。与一些高级语言不同,C++程序往往直接操作内存,涉及指针、引用、多线程等特性,这些都可能成为错误的温床。例如,一个未初始化的指针可能导致程序崩溃,而一个细微的越界访问可能在运行时悄无声息,却在后续引发不可预知的后果。手动排查这些问题不仅耗时费力,还往往难以准确定位问题根源。调试器的出现,恰恰是为了解决这一困境。它不仅仅是一个工具,更像是开发者的“第三只眼”,能够深入程序的运行过程,揭示隐藏在代码背后的真相。

调试器的价值在于它提供了一种结构化的方式来分析程序行为。通过设置断点、监视变量、查看调用栈等功能,开发者可以暂停程序的执行,检查特定时刻的内存状态和变量值,从而快速锁定问题的来源。想象一下,如果没有调试器,开发者可能需要通过大量日志输出或者反复修改代码来猜测错误位置,这种方式不仅效率低下,还可能引入新的问题。而借助调试器,开发者能够直接“窥探”程序的内部运行机制,极大地提升了排查效率。例如,在处理一个复杂的多线程程序时,调试器可以帮助你捕捉线程死锁的瞬间,查看每个线程的状态和资源占用情况,这种能力是单纯的代码阅读或日志分析无法比拟的。

更重要的是,调试器不仅仅是解决问题的工具,它还是学习和提升编程技能的重要途径。对于初学者来说,调试器提供了一个直观的窗口,帮助他们理解代码的执行流程和变量的变化过程。比如,当你学习C++中的指针和引用时,调试器可以让你清晰地看到指针指向的内存地址以及引用的绑定对象,这种直观性有助于加深对语言特性的理解。对于中级开发者而言,调试器则是一个优化和改进代码的利器。通过分析程序的性能瓶颈或资源使用情况,他们可以发现隐藏的低效代码段,从而进一步提升程序质量。

在实际开发中,C++程序员常用的调试工具有GDB(GNU Debugger)和LLDB(LLVM Debugger)等。这些工具各有特色,但核心目标一致:帮助开发者快速定位和修复代码中的问题。GDB作为开源社区的经典之作,功能强大且跨平台,支持多种编程语言,尤其在Linux环境下广受欢迎。LLDB则是LLVM项目的一部分,设计现代化,界面友好,与macOS和Xcode集成紧密。无论选择哪一种工具,掌握调试器的基本操作和常用命令,都是每位C++开发者必备的技能。毕竟,工具本身只是手段,如何运用这些工具去发现问题、分析问题并最终解决问题,才是调试的真正精髓。

值得一提的是,调试并不仅仅是修复错误的过程,它还是一种思维方式的体现。优秀的开发者往往能够在调试中培养出对代码的敏感度和对问题的洞察力。他们不会满足于表面上的“程序能跑”,而是会深入探究每一个异常现象背后的原因。例如,一个看似简单的段错误(Segmentation Fault)可能指向内存管理的问题,而通过调试器一步步追溯,你可能会发现是数组越界、指针悬挂或者堆栈溢出等问题。这种从现象到本质的探索,不仅解决了当前问题,还为未来的开发积累了宝贵的经验。

为了帮助读者更好地理解和掌握C++调试的技巧,本文将围绕调试器的使用展开深入探讨。内容将从调试器的基本概念入手,详细介绍GDB和LLDB这两大主流工具的安装、配置和核心功能。随后,将聚焦于调试过程中的常用命令,例如如何设置断点、如何查看变量值、如何分析调用栈等,并通过具体的代码示例展示这些命令的实际应用。此外,还会探讨一些高级调试技巧,例如内存泄漏检测、多线程调试以及性能分析,帮助读者应对更复杂的开发场景。无论是初学者希望快速上手,还是中级开发者想要提升技能,本文的最终目标都是为你提供一套系统化、实用性强的调试方法论。

需要强调的是,调试并非一蹴而就的技能,它需要大量的实践和耐心。初学者可能会在第一次使用调试器时感到困惑,面对一堆陌生的命令和输出信息无从下手。但随着经验的积累,你会逐渐发现调试器的强大之处,并形成自己的调试习惯。例如,在调试一个复杂的C++程序时,你可以先从大范围的断点开始,逐步缩小问题范围,再结合变量监视和日志分析,最终锁定问题代码。这种分层递进的调试策略,正是无数开发者在实践中总结出的宝贵经验。

为了让理论与实践相结合,本文会通过具体的案例和代码片段,展示调试器在实际开发中的应用场景。以下是一个简单的C++代码示例,展示了如何通过调试器快速定位一个常见的数组越界问题:
 

int main() {int arr[5] = {1, 2, 3, 4, 5};for (int i = 0; i <= 5; i++) { // 注意这里的越界访问std::cout << arr[i] << " ";}return 0;
}



在上面的代码中,循环条件i <= 5导致了对数组arr的越界访问。如果直接运行,程序可能会崩溃或者输出垃圾值,但具体原因并不直观。通过调试器,你可以设置一个断点在循环处,逐行执行代码,并观察i的值和arr[i]的内容,从而迅速发现问题所在。这样的实例将在后续内容中反复出现,帮助读者从实际操作中体会调试器的魅力。

此外,为了让读者对调试命令有一个直观的认识,以下是一个简要的表格,列出了一些常用的GDB和LLDB命令及其功能。这些命令将在后续内容中详细讲解,但在这里先做一个预览:

命令GDB 示例LLDB 示例功能描述
设置断点break mainbreakpoint set --name main在指定函数或行号处暂停程序执行
运行程序runrun启动程序执行,直到断点或异常发生
查看变量值print varprint var显示指定变量的当前值
继续执行continuecontinue从当前断点继续运行程序
查看调用栈backtracethread backtrace显示函数调用栈,追踪代码执行路径

这些命令只是调试器功能的冰山一角,但已经足以展示它们在开发中的重要性。通过学习和掌握这些工具和技巧,你将能够在C++开发中游刃有余,面对各种复杂问题时不再手足无措。

 

第一章:C++调试的基础知识

在软件开发的世界中,编写代码只是旅程的起点,而确保代码能够按照预期运行则是更为关键的一环。特别是在C++这种高性能且底层的编程语言中,开发者常常会遇到各种复杂问题,从简单的拼写错误到难以捉摸的内存泄漏。这就引出了调试这一核心技能,它不仅是排查和修复问题的手段,更是提升代码质量和开发者自身能力的重要途径。本章节将从调试的基本概念入手,探讨其目的和意义,区分调试与测试的不同之处,并深入分析C++程序中常见的错误类型,最后自然过渡到调试器作为解决这些问题的重要工具。

调试,简单来说,是指在程序开发过程中查找、定位并修复代码中错误的过程。它的核心目标是确保程序能够按照设计意图正确运行,同时避免潜在的隐患。无论是初学者还是经验丰富的开发者,调试都是不可或缺的一部分。想象一下,当你编写了一段复杂的C++代码,编译通过后运行却没有得到预期的输出,或者程序在运行中突然崩溃,调试就是帮助你找到问题根源并加以解决的关键步骤。调试的目的不仅仅是修复错误,更是通过分析问题来加深对代码逻辑和语言特性的理解,从而避免类似问题的再次发生。

值得注意的是,调试与测试虽然都与程序质量息息相关,但它们的关注点和方法却有显著区别。测试通常是在代码完成后,通过设计一系列输入和预期输出来验证程序是否符合需求,更多关注的是功能的正确性和完整性。调试则是在发现问题后,深入代码内部,分析具体的行为和逻辑,找出错误的根源。换句话说,测试是发现问题,而调试是解决问题。举个例子,假设你开发了一个计算器程序,测试时发现输入“2 + 3”得到的结果是“6”,这表明存在问题;而调试则是进一步检查代码,发现可能是由于运算符逻辑判断错误导致的加法被误判为乘法。通过这样的对比,可以更清晰地理解两者的分工与联系。

在C++程序开发中,调试的重要性尤为突出,因为C++语言的特性决定了其错误类型多样且复杂。常见的错误可以大致分为三类:语法错误、逻辑错误和运行时错误。语法错误是最容易发现和修复的一类,通常在编译阶段就能被捕获,例如缺少分号、拼写错误或不正确的语法结构。这类问题虽然简单,但对于初学者来说,可能会因为不熟悉语言规则而耗费时间。逻辑错误则更为隐蔽,它不会导致编译失败,但程序运行结果与预期不符。例如,在一个排序算法中,开发者可能错误地设置了循环条件,导致数组只排序了一部分元素。这种问题的排查往往需要仔细分析代码逻辑,甚至逐行检查。

运行时错误是C++开发者面临的更大挑战,尤其是在涉及内存管理和指针操作时。内存泄漏是一个经典问题,指的是程序在动态分配内存后未能及时释放,导致内存资源被持续占用,最终可能引发性能下降甚至程序崩溃。以下是一个简单的内存泄漏示例代码:
 

int main() {int* ptr = new int(10); // 动态分配内存std::cout << *ptr << std::endl;// 忘记调用 delete ptr 释放内存return 0;
}



在上面的代码中,new分配的内存没有通过delete释放,导致内存泄漏。虽然这个例子很简单,但在大型项目中,类似的错误可能隐藏在复杂的逻辑中,难以手动发现。此外,野指针(指向已释放或无效内存的指针)和数组越界等问题也常常困扰开发者。这些错误不仅难以定位,还可能导致程序行为不可预测,甚至在某些情况下长时间潜伏,直到特定条件触发才暴露出来。

除了上述错误类型,C++程序还可能面临多线程环境下的问题,例如死锁和数据竞争。由于C++提供了对底层的直接控制,开发者需要手动管理线程同步和资源共享,这增加了调试难度。试想一个场景:两个线程同时访问共享资源,由于缺乏适当的锁机制,其中一个线程修改了数据,导致另一个线程读取到错误值。这种问题往往需要深入分析线程执行顺序和资源访问模式,单凭肉眼检查代码几乎无从下手。

面对如此多样的错误类型,开发者显然需要系统化的方法和工具来应对。这就引出了调试器的作用。调试器是一种专门设计用于帮助开发者定位和修复程序错误的工具,它通过提供断点设置、变量监视、调用栈分析等功能,让开发者能够以更直观的方式观察程序的运行状态。例如,当程序在运行时崩溃时,调试器可以帮助你回溯到崩溃发生前的最后几步代码,查看变量值和函数调用顺序,从而快速锁定问题根源。相比于传统的日志打印方式(即在代码中插入std::cout语句输出中间结果),调试器无疑更加高效和灵活。

为了更直观地说明调试器的价值,不妨考虑一个实际场景。假设你正在开发一个C++程序,用于处理大规模数据文件,程序运行一段时间后总是无故退出。单纯依靠日志打印,你可能需要在代码中插入大量输出语句,逐一检查每个函数的执行情况,这不仅耗时,还可能因为日志过多而难以分析。而如果使用调试器,你可以直接在怀疑有问题的代码段设置断点,程序运行到该点时自动暂停,然后检查变量值和内存状态,快速判断是否发生了数组越界或指针错误。这样的方式显然能够大幅提升效率。

当然,调试器的功能远不止于此。它还可以帮助开发者深入理解代码的执行流程。例如,通过单步执行功能,你可以逐行观察程序如何处理输入数据,哪些条件分支被触发,哪些循环被执行了多少次。这种细致的观察不仅有助于解决问题,还能让开发者对自己的代码有更深的认识,甚至发现潜在的优化空间。特别是在学习C++的过程中,调试器就像一个导师,带领你探索语言的底层机制,比如指针如何操作内存,对象如何在栈和堆上分配等。

为了更清晰地总结C++中常见的错误类型及其特点,以下通过一个表格进行展示:

错误类型特点典型示例排查难度
语法错误编译时即可发现,通常是拼写或格式问题缺少分号、括号不匹配
逻辑错误程序运行结果与预期不符,需检查逻辑循环条件错误、算法实现错误
内存泄漏动态分配内存未释放,导致资源占用忘记调用delete释放new分配的内存
野指针/越界访问访问无效或超出范围的内存,行为不可预测数组越界、指针指向已释放内存
多线程问题线程间资源竞争或同步错误死锁、数据竞争极高

通过这个表格可以看出,C++程序的错误类型从简单到复杂,排查难度也逐步提升。对于初学者来说,掌握基本的语法规则可以避免大部分编译错误;而对于进阶开发者,内存管理和多线程问题则需要更深入的知识和工具支持。这正是调试器大显身手的地方,它不仅能够应对复杂的运行时问题,还能通过直观的界面和强大的功能,降低调试的门槛。
 

第二章:常用调试器简介——GDB和LLDB

常用调试器简介——GDB和LLDB



在C++程序开发中,调试器是开发者不可或缺的工具,它能够帮助我们深入代码的执行过程,定位问题的根源,并修复潜在的错误。在众多调试工具中,GDB(GNU Debugger)和LLDB(LLVM Debugger)无疑是两个最为广泛使用的选项。它们不仅功能强大,而且在不同的开发环境中有着各自的独特优势。接下来,将深入探讨这两种调试器的历史背景、适用平台、安装方法以及各自的优缺点,以便开发者能够根据自己的需求做出明智的选择。
 

GDB:经典的GNU调试器



GDB,作为GNU项目的一部分,诞生于1986年,由Richard Stallman主导开发。作为一个开源的调试工具,GDB的历史几乎与现代软件开发同步。它最初是为GNU操作系统设计的,但随着时间的推移,它被广泛应用于各种Unix-like系统,包括Linux和macOS,甚至通过MinGW等工具支持Windows平台。GDB的主要目标是提供一个通用的调试环境,支持多种编程语言,包括C、C++、Fortran和Ada等,其中C++是其最核心的应用领域之一。

在适用平台方面,GDB几乎是Linux开发者的首选工具。它与GCC(GNU Compiler Collection)紧密集成,能够无缝处理由GCC编译的C++程序。此外,GDB支持远程调试,这对于嵌入式系统开发尤为重要。开发者可以通过GDB连接到远程目标设备,实时监控和调试代码。即便是在资源受限的环境中,GDB的轻量级设计也能保证其运行效率。

安装GDB的过程通常非常简单。在大多数Linux发行版(如Ubuntu、CentOS)中,可以通过包管理器快速完成安装。例如,在Ubuntu上,只需运行以下命令:
 

sudo apt update
sudo apt install gdb



安装完成后,可以通过gdb --version检查版本信息,确保工具正常运行。对于Windows用户,可以通过MinGW或Cygwin安装GDB,尽管其体验可能不如Linux平台流畅。macOS用户则可以通过Homebrew安装,命令如下:
 

brew install gdb



GDB的优势在于其成熟度和广泛的社区支持。几十年的发展使得GDB积累了丰富的文档和教程,开发者遇到问题时往往能快速找到解决方案。此外,GDB支持多种调试模式,包括命令行界面和与IDE(如Eclipse、CLion)集成的图形化界面,适应不同开发者的习惯。然而,GDB也并非完美无缺。其命令行界面的学习曲线较陡,对于初学者来说可能显得复杂。此外,GDB在处理大规模C++项目时,特别是在符号解析和模板调试方面,效率可能不如一些现代工具。
 

LLDB:现代化的LLVM调试器



相比GDB的历史悠久,LLDB是一个相对年轻的调试器,诞生于2011年,作为LLVM项目的一部分。LLVM是一个现代化的编译器基础设施项目,而LLDB则是专为其设计的调试工具,旨在替代GDB在某些场景下的角色。LLDB最初由Apple主导开发,目的是为macOS和iOS开发提供更好的调试体验,但它同样支持Linux和Windows平台,逐渐成为跨平台的调试选择。

LLDB的设计理念是现代化和高效。它与Clang编译器深度集成,能够更好地处理C++11及后续标准中的复杂特性,如模板和智能指针。此外,LLDB提供了更友好的用户界面和更快的符号解析速度,尤其是在调试大型C++项目时表现尤为出色。LLDB还内置了对多线程和并行程序调试的优化支持,这在现代多核处理器环境中显得尤为重要。

从适用平台来看,LLDB在macOS上几乎是默认选择。Xcode开发环境内置了LLDB,开发者无需额外安装即可使用。对于Linux用户,LLDB也可以通过包管理器安装,例如在Ubuntu上:
 

sudo apt update
sudo apt install lldb



Windows用户则需要通过LLVM的官方发布版本进行安装,或者借助WSL(Windows Subsystem for Linux)运行LLDB。需要注意的是,LLDB在Windows上的支持相对较新,可能存在一些兼容性问题。

LLDB的安装过程与GDB类似,但在使用体验上,LLDB提供了更现代化的命令语法和更智能的自动补全功能。例如,LLDB的命令设计更加直观,开发者可以通过help命令快速了解用法。此外,LLDB支持脚本化调试,允许开发者使用Python编写自定义调试逻辑,这对于自动化调试任务非常有用。

尽管LLDB在性能和用户友好性上有着显著优势,但它也有一些局限性。相比GDB,LLDB的社区规模较小,文档和教程资源相对有限。此外,LLDB对某些嵌入式系统和老旧平台的支持不如GDB全面,开发者在选择时需要考虑自己的目标环境。
 

GDB与LLDB的对比分析



为了更直观地展示GDB和LLDB之间的差异,以下通过一个表格对两者的核心特性进行对比:

特性GDBLLDB
开发历史始于1986年,历史悠久始于2011年,较为现代化
主要平台Linux、Unix-like,部分支持WindowsmacOS、Linux,部分支持Windows
编译器集成与GCC深度集成与Clang深度集成
用户界面命令行为主,学习曲线较陡命令行友好,支持智能补全
性能适用于小型到中型项目,模板调试稍弱适用于大型项目,模板调试更高效
脚本支持支持Python脚本,但功能有限内置Python脚本支持,功能强大
社区与文档社区庞大,文档丰富社区较小,文档相对有限
嵌入式调试支持广泛,适用于资源受限环境支持有限,偏向现代硬件环境

从表格中可以看出,GDB和LLDB各有千秋。GDB凭借其悠久的历史和广泛的平台支持,特别适合Linux环境下的传统开发以及嵌入式系统调试。而LLDB则更适合现代C++开发,尤其是在使用Clang编译器或macOS平台时,其性能和用户体验往往更胜一筹。
 

如何选择合适的调试器



在实际开发中,选择调试器需要结合具体的项目需求和开发环境。如果你的项目主要在Linux平台上运行,并且使用GCC编译器,GDB可能是最自然的选择。它的稳定性和丰富的社区支持能够帮助你快速解决问题。此外,如果你的项目涉及嵌入式系统或需要在资源受限的环境中调试,GDB的轻量级设计和远程调试功能将发挥重要作用。

相反,如果你的开发环境以macOS为中心,或者项目大量使用C++11及以上标准的新特性,LLDB可能是更好的选择。特别是在使用Clang编译器时,LLDB能够提供更高效的符号解析和更精确的调试信息。此外,LLDB的现代化设计和脚本支持也为自动化调试提供了更多可能性。

值得一提的是,GDB和LLDB并非完全互斥的选择。许多开发者会在不同项目中切换使用两者,甚至在同一个项目中结合使用。例如,在初期开发阶段,可以借助LLDB的友好界面快速定位问题,而在后期优化或跨平台测试时,切换到GDB以利用其广泛的平台支持。
 

实际案例:调试环境的选择



为了进一步说明选择调试器的重要性,假设你正在开发一个跨平台的C++应用程序,目标是同时支持Linux和macOS。在Linux上,你选择了GCC作为编译器,并使用GDB进行调试,成功定位了一个多线程死锁问题。而在macOS上,由于Xcode默认集成了LLDB,你发现LLDB在处理C++模板相关的错误时效率更高。通过这种方式,你充分利用了两种调试器的优势,确保了项目在不同平台上的稳定性。

此外,现代IDE如CLion和Visual Studio Code也支持同时配置GDB和LLDB,开发者可以在不改变代码的情况下切换调试器。这种灵活性进一步降低了选择调试器的风险,让你能够专注于代码本身的质量。
 

第三章:调试器的基本使用流程

在C++开发中,调试器是排查代码问题、理解程序运行逻辑的得力工具。无论是GDB还是LLDB,它们的基本使用流程都围绕着几个核心步骤展开:编译时启用调试信息、启动调试器、加载目标程序以及初步调试操作。掌握这些步骤,不仅能帮助开发者快速定位问题,还能为后续更复杂的调试奠定基础。本章节将以GDB和LLDB为例,详细讲解调试器的基本使用流程,并通过一个简单的C++程序示例,展示如何进入调试模式并进行初步操作。
 

编译时启用调试信息



调试的第一步始于代码编译阶段。为了让调试器能够访问源代码的行号、变量名等信息,必须在编译时启用调试选项。通常,这通过在编译命令中添加-g标志来实现。无论是使用GCC还是Clang,这一选项都会在生成的可执行文件中嵌入调试符号,而不会影响程序的运行逻辑。

以GCC为例,假设我们有一个简单的C++程序main.cpp,编译命令如下:
 

g++ -g -o main main.cpp



这里的-g标志告诉编译器生成调试信息。如果不加这一选项,调试器将无法映射可执行文件中的指令到源代码行,也无法显示变量的值或函数调用栈。需要注意的是,调试符号会增加可执行文件的大小,但在开发阶段,这点空间开销完全值得。

对于Clang(常用于LLDB调试),命令几乎相同:
 

clang++ -g -o main main.cpp



此外,若程序需要优化,可以结合-O选项,但建议在调试时避免高等级优化(如-O2或-O3),因为优化可能会改变代码执行顺序或内联函数,导致调试信息不准确。通常,-O0(默认无优化)是调试时的最佳选择。

编译完成后,可执行文件main就包含了调试符号,可以被GDB或LLDB加载并解析。值得一提的是,如果项目使用构建工具如CMake,可以在CMakeLists.txt中设置CMAKE_BUILD_TYPE为Debug,以自动启用-g选项。
 

启动调试器与加载程序



编译好程序后,下一步是启动调试器并加载可执行文件。以GDB为例,启动方式非常直观,只需在终端输入以下命令:
 

gdb ./main



执行后,GDB会进入交互式命令行界面,并显示一些版本信息和提示符(gdb)。此时,调试器已经加载了程序的符号表,但程序尚未开始运行。开发者可以通过命令与GDB交互,设置断点、查看代码等。

对于LLDB,启动过程类似,但命令略有不同:
 

lldb ./main



LLDB同样会进入一个交互式界面,提示符为(lldb)。与GDB相比,LLDB的界面设计更现代化,命令语法也更接近自然语言,但核心功能并无本质区别。

值得注意的是,如果程序已经在运行中(比如在服务器环境中),也可以通过调试器附加到进程上。以GDB为例,使用gdb attach 命令即可连接到指定进程ID的程序,而LLDB则使用process attach --pid 。这种方式适用于无法直接重启程序的场景,但需要确保程序编译时已启用调试信息。
 

初步调试操作与常用命令



加载程序后,调试器进入待命状态,等待开发者输入指令。以下以一个简单的C++程序为例,展示如何进入调试模式并执行初步操作。假设我们有以下代码main.cpp:
 


int calculateSum(int a, int b) {int sum = a + b;return sum;
}int main() {int x = 5;int y = 3;int result = calculateSum(x, y);std::cout << "Sum: " << result << std::endl;return 0;
}



这是一个简单的程序,定义了一个计算两数之和的函数,并在main函数中调用它。接下来,我们将使用GDB和LLDB分别调试这段代码。
 

使用GDB进行初步调试



启动GDB后,可以通过以下步骤开始调试:

1. 设置断点:在程序运行前,设置断点是常见操作。假设我们想在main函数开始处暂停,可以输入:
 

   break main



GDB会确认断点已设置,并显示断点编号和位置。如果想在特定行设置断点,可以使用break main.cpp:9(假设第9行是int result = calculateSum(x, y);)。

2. 运行程序:输入run命令(或简写为r),GDB会启动程序并在断点处暂停。此时,程序尚未执行到断点后的代码。

3. 查看当前状态:当程序暂停时,可以使用list命令(或l)查看当前代码位置,GDB会显示源代码的几行内容,帮助确认当前位置。

4. 查看变量值:在断点处,可以用print命令(或p)检查变量值。例如,输入p x会显示变量x的值为5。

5. 单步执行:若想逐行执行代码,可以使用next命令(或n),它会执行当前行并停在下一行。如果需要进入函数内部,则使用step命令(或s)。

通过这些操作,可以初步了解程序的运行状态。例如,在main函数暂停后,单步执行到calculateSum调用时,选择step进入函数,观察参数a和b的值是否符合预期。
 

使用LLDB进行初步调试



LLDB的操作流程与GDB类似,但命令语法略有差异。以下是对应步骤:

1. 设置断点:在LLDB中,设置断点使用breakpoint set命令。例如,设置main函数断点:
 

   breakpoint set --name main



或者在特定行设置断点:breakpoint set --file main.cpp --line 9。LLDB会返回断点编号和详细信息。

2. 运行程序:输入run(或r)启动程序,LLDB会在断点处暂停。

3. 查看当前状态:使用list命令查看源代码,或者用frame info获取当前栈帧信息。

4. 查看变量值:LLDB中查看变量值使用print或p,例如p x会显示变量x的值。

5. 单步执行:与GDB类似,LLDB使用next(或n)跳到下一行,使用step(或s)进入函数内部。

LLDB的一个优势是其命令补全功能和更友好的错误提示,尤其适合新手开发者。例如,若输入错误命令,LLDB会提供可能的修正建议。
 

调试流程中的注意事项



在实际调试中,有几点需要特别关注,以确保流程顺畅。首先,始终确认编译时已启用-g选项,否则调试器无法提供有用的信息。其次,断点的设置应尽量精确,避免在无关位置浪费时间。如果程序较大,可以结合条件断点(如GDB的break ... if或LLDB的breakpoint set ... --condition)来提高效率。

另外,调试时建议保持代码的简洁性,尤其是初学者应避免在复杂逻辑中设置过多断点,以免迷失方向。对于大型项目,调试器可能会因为符号表过大而响应缓慢,此时可以考虑分模块调试,或者使用strip工具临时移除不必要的调试符号。
 

调试命令速查表



为了方便对比GDB和LLDB的常用命令,以下提供一个简洁的表格,涵盖调试初期最常用的操作:

功能GDB 命令LLDB 命令
启动程序run 或 rrun 或 r
设置断点(函数)break breakpoint set --name 
设置断点(行号)break :breakpoint set --file --line 
查看代码list 或 llist
查看变量值print  或 pprint  或 p
单步执行(下一行)next 或 nnext 或 n
进入函数step 或 sstep 或 s
继续执行continue 或 ccontinue 或 c

总结与实践建议



调试器的基本使用流程并不复杂,但熟练掌握需要一定的实践积累。从编译时启用调试信息,到启动调试器、设置断点、查看变量和单步执行,这些步骤构成了调试的基础框架。无论是GDB还是LLDB,核心思想都是通过控制程序执行流程,逐步验证代码逻辑,找出潜在问题。

对于初学者,建议从简单的程序入手,熟悉命令的使用,并逐步过渡到复杂项目。调试不仅是排查问题的工具,也是深入理解代码运行机制的途径。例如,通过观察变量值的变化,可以更直观地理解指针、引用等概念;通过调用栈的分析,可以掌握函数调用的层次关系。

在后续的调试实践中,不妨尝试将调试器与IDE(如VS Code、CLion)集成,许多现代IDE都内置了对GDB和LLDB的支持,提供图形化界面,进一步降低学习曲线。但无论使用何种工具,理解调试器的基本流程始终是不可或缺的技能。希望通过本章节的内容,开发者能够快速上手调试工具,为更高效的C++开发打下坚实基础。

第四章:常用调试命令详解(一):断点与程序控制

在C++程序的调试过程中,调试器如GDB和LLDB提供了强大的工具集,帮助开发者精准定位问题并理解代码的执行逻辑。其中,断点设置和程序执行控制是调试的核心功能。通过合理使用这些命令,开发者可以暂停程序运行,检查变量状态,逐步执行代码,甚至跳过某些代码块。本部分将深入探讨GDB和LLDB中用于断点管理和程序控制的核心命令,剖析它们的用途、用法以及适用场景,并结合实际代码示例展示如何运用这些工具高效调试。
 

断点的设置与管理



断点是调试器中最为基础且重要的功能之一,它允许开发者在代码的特定位置暂停程序执行,以便检查当前的运行状态。在GDB中,设置断点主要依赖break命令,而在LLDB中则使用breakpoint set或简写b。这两个命令支持多种方式指定断点位置,包括源代码行号、函数名以及特定条件。

在GDB中,假设我们有一个简单的C++程序,包含一个循环计算阶乘的函数。我们希望在循环开始时暂停执行以观察中间结果。可以这样设置断点:
 

(gdb) break factorial.cpp:10



这里的factorial.cpp:10表示在文件factorial.cpp的第10行设置断点。如果直接指定函数名,如break factorial,断点会设置在该函数的入口处。此外,GDB还支持条件断点,比如只在某个变量达到特定值时暂停:
 

(gdb) break factorial.cpp:10 if i == 5



这意味着程序只有在循环变量i等于5时才会暂停,非常适合排查特定场景下的问题。

在LLDB中,设置断点的语法略有不同,但功能类似。例如,同样在文件行号处设置断点:
 

(lldb) breakpoint set --file factorial.cpp --line 10



或者简化为:
 

(lldb) b factorial.cpp:10



如果需要基于函数名设置断点,LLDB提供了直观的方式:
 

(lldb) breakpoint set --name factorial



值得注意的是,LLDB对断点的管理更加现代化,支持正则表达式匹配函数名,这在调试大型项目时尤其有用。

设置断点后,程序会在断点处暂停,此时开发者可以查看变量值、调用栈等信息。GDB和LLDB都提供了查看和管理断点的命令,如GDB的info breakpoints和LLDB的breakpoint list,它们会列出所有断点的编号、位置和状态。如果某个断点不再需要,可以通过delete(GDB)或breakpoint delete(LLDB)移除。
 

程序执行的控制



一旦程序在断点处暂停,接下来的关键是如何控制其执行流程。GDB和LLDB提供了多种命令,让开发者可以逐步执行代码、跳过某些部分或直接继续运行。

在GDB中,continue命令(简写为c)是最常用的执行控制命令之一。它会让程序从当前暂停位置继续运行,直到遇到下一个断点或程序结束。这在验证代码是否能正常运行到预期位置时非常有用。例如,假设我们在一个多函数调用的程序中设置了多个断点,使用continue可以快速跳转到下一个断点,而无需手动逐行执行。

然而,有时我们需要更细粒度的控制,这时next(简写为n)和step(简写为s)就派上用场了。next命令会执行当前行代码,并停在下一行,但如果当前行包含函数调用,它不会进入函数内部,而是直接执行完函数并返回结果。这适合用于快速跳过已知无问题的函数调用。而step则更深入,如果当前行有函数调用,它会进入函数内部,停在函数的第一行代码。这对于追踪函数内部逻辑或排查调用链中的问题尤为重要。

为了直观展示这些命令的区别,假设我们有以下C++代码:
 

int add(int a, int b) {return a + b;
}int main() {int x = 5;int y = 10;int result = add(x, y); // 断点设置在此行std::cout << "Result: " << result << std::endl;return 0;
}



在GDB中,如果断点设置在int result = add(x, y)这一行,使用next命令会直接执行完add函数并停在下一行输出语句,而使用step命令则会进入add函数内部,停在return a + b这一行。这样的差异让开发者可以根据需求选择合适的调试深度。

在LLDB中,类似的命令是next(或n)和step(或s),功能与GDB完全一致。此外,LLDB还提供了thread step-over和thread step-in作为别名,强调这些命令作用于当前线程,这在大规模多线程程序调试中显得尤为贴心。
 

实际调试场景中的应用



为了更清晰地展示这些命令在实际调试中的作用,让我们以一个稍微复杂的C++程序为例,模拟一个常见的逻辑错误。假设我们有一个计算数组中最大值的函数,但结果总是偏小:
 

int findMax(const std::vector& arr) {if (arr.empty()) return -1;int max = arr[0];for (size_t i = 0; i < arr.size(); i++) { // 错误:应从1开始遍历if (arr[i] > max) {max = arr[i];}}return max;
}int main() {std::vector numbers = {3, 7, 2, 9, 1, 5};int result = findMax(numbers);std::cout << "Max: " << result << std::endl; // 预期输出9,实际输出7return 0;
}



在调试这个程序时,我们首先在findMax函数的循环开始处设置断点:
 

(gdb) break findMax.cpp:7



程序运行到断点后,我们使用next命令逐行执行循环,观察max变量的变化(通过print max查看)。很快可以发现,循环从i = 0开始时,max已经赋值为arr[0],导致后续比较逻辑有误。此时,我们不需要进入更深层次的函数调用,next就足以帮助定位问题。

如果问题隐藏在更深的函数调用中,比如findMax内部调用了其他辅助函数,step命令就能派上用场,让我们深入每一层调用,检查参数传递和返回值是否符合预期。
 

断点与执行控制的结合策略



在实际开发中,断点和执行控制命令往往需要结合使用,形成高效的调试策略。例如,在处理大型项目时,可以先在关键函数入口设置断点,使用continue快速定位到感兴趣的模块,然后通过step或next逐步分析具体逻辑。此外,条件断点可以在数据量巨大的循环中节省时间,只在满足特定条件时暂停,避免手动逐行检查。

另一个实用技巧是利用断点的启用与禁用功能。GDB的disable和enable命令,以及LLDB的breakpoint disable和breakpoint enable,可以临时关闭某些断点,避免程序频繁暂停。这种方式在调试复杂流程时尤为有效,尤其是在前期已定位大致问题区域,后期只关注特定代码段的情况下。
 

命令对比与工具选择



虽然GDB和LLDB在断点设置和程序控制上的核心功能类似,但它们在细节和用户体验上有所差异。GDB作为更传统的调试器,命令简洁且广泛支持,适合脚本化调试或在资源受限的环境中使用。LLDB则更注重现代开发体验,提供了更友好的输出格式和对复杂项目(如Swift和Objective-C混合项目)的额外支持。开发者可以根据项目需求和个人习惯选择合适的工具,甚至在某些情况下结合使用两者,比如在macOS上默认使用LLDB,而在Linux服务器上依赖GDB。

 

第五章:常用调试命令详解(二):变量与内存查看

在调试C++程序时,仅仅设置断点和控制程序执行流程是远远不够的。开发者往往需要深入了解程序运行时的内部状态,尤其是变量的值、内存的分布以及数据的变化情况。通过调试器提供的变量与内存查看工具,可以快速定位逻辑错误、内存泄漏或未初始化的数据问题。GDB和LLDB作为两大主流调试器,提供了丰富的命令来帮助开发者检查变量值、监视变量变化以及查看内存内容。本部分将深入探讨这些命令的使用方法,并结合实际示例展示如何利用它们高效调试。
 

变量查看:从基础到深入



在调试过程中,查看变量的当前值是最常见的需求。GDB提供了print命令(简写为p),而LLDB则使用print或p命令,两者的功能相当接近。假设程序在某个断点暂停,我们可以通过这些命令打印变量的值。例如,考虑以下简单的C++代码片段:
 


int main() {int x = 42;double y = 3.14;std::cout << "Hello, Debugging!" << std::endl;return 0;
}



在GDB中,如果程序暂停在std::cout语句之前,可以输入以下命令查看变量x和y的值:
 

(gdb) p x
$1 = 42
(gdb) p y
$2 = 3.14



在LLDB中,操作类似:
 

(lldb) p x
(int) $0 = 42
(lldb) p y
(double) $1 = 3.14



值得注意的是,GDB和LLDB都支持格式化输出。例如,在GDB中,可以使用p/x以十六进制格式查看变量值,这对于调试指针或内存地址特别有用:
 

(gdb) p/x x
$3 = 0x2a



此外,调试器还支持查看复杂数据结构的内容,比如数组或结构体。假设有一个数组int arr[3] = {1, 2, 3};,在GDB中可以这样查看:
 

(gdb) p arr
$4 = {1, 2, 3}



如果数组较大,可以通过指定范围来查看部分元素,例如p arr[0]@2显示从索引0开始的两个元素。LLDB中类似的操作可以通过p arr直接打印整个数组,或者结合索引访问特定元素。

对于指针和引用,调试器同样提供了便利。假设有一个指针int* ptr = &x;,可以通过p *ptr查看指针所指向的内容。这种解引用的方式在调试动态分配内存或链表等数据结构时尤为重要。
 

持续监视:动态追踪变量变化



仅仅在某个断点查看变量值有时不足以发现问题,尤其是在循环或复杂逻辑中,变量值可能在多次迭代中发生变化。为此,GDB提供了display命令,用于在每次程序暂停时自动显示指定变量的值。假设我们希望持续监视变量x,可以在GDB中输入:
 

(gdb) display x
1: x = 42



每次程序暂停(例如遇到断点或单步执行),GDB都会自动打印x的当前值。如果不再需要监视,可以通过undisplay 1取消,数字1是display命令分配的标识符。

在LLDB中,虽然没有直接的display命令,但可以通过watchpoint或结合p命令实现类似效果。更为常见的是利用LLDB的图形界面或集成开发环境(如Xcode)提供的变量监视窗口来持续追踪。

更进一步,GDB和LLDB都支持设置监视点(watchpoint),即当变量值发生变化时自动暂停程序执行。这在调试难以预测的变量修改问题时非常有用。在GDB中,设置监视点可以使用watch命令:
 

(gdb) watch x
Hardware watchpoint 2: x



此时,若x的值被修改,程序会立即暂停,并显示修改前后的值。LLDB中对应的命令是watchpoint set variable x,效果类似。需要注意的是,监视点可能会影响程序性能,尤其是在监视频繁变化的变量时,因此应谨慎使用。
 

内存查看:深入程序底层



除了变量值,开发者有时需要直接检查内存的内容,尤其是在处理指针、动态分配内存或调试内存泄漏时。GDB提供了x命令(examine memory)来查看指定内存地址的内容,而LLDB则使用memory read或简写x命令。

假设有一个指针int* ptr = new int(100);,我们可以通过以下方式在GDB中查看其指向的内存内容:
 

(gdb) x ptr
0x7ffff7dd0b70: 100



x命令支持多种格式参数,例如x/x以十六进制显示,x/d以十进制显示,x/s以字符串形式显示(适用于字符数组)。如果需要查看连续的内存块,可以指定数量和单位,例如x/4xw ptr表示从ptr开始查看4个字(word,通常是4字节)的内容,以十六进制格式显示。

在LLDB中,类似的操作可以通过以下命令完成:
 

(lldb) memory read ptr
0x7ffff7dd0b70: 64 00 00 00  // 100 in little-endian



内存查看在调试低级问题时尤为重要。例如,当程序出现野指针或内存越界时,直接检查内存内容可以帮助确定数据是否被意外覆盖。以下是一个简单的表格,总结了GDB和LLDB中常用的内存查看格式:

格式参数GDB 示例LLDB 示例描述
十进制x/d ptrmemory read -f d ptr以十进制显示内存内容
十六进制x/x ptrmemory read -f x ptr以十六进制显示内存内容
字符串x/s ptrmemory read -f s ptr以字符串形式显示内存内容
字节单位x/4xb ptrmemory read -s 1 -c 4 ptr查看4个字节的内容

实际案例:追踪逻辑错误



为了将理论与实践结合,来看一个具体的调试案例。假设我们编写了一个计算数组平均值的函数,但结果始终不正确:
 

double calculateAverage(const std::vector& vec) {double sum = 0.0;for (size_t i = 0; i <= vec.size(); i++) {  // 注意这里的错误:i <= vec.size()sum += vec[i];}return sum / vec.size();
}



在调试时,我们可以使用GDB或LLDB设置断点,并在循环中监视变量sum和i的变化。以GDB为例:

1. 首先在循环内部设置断点:

   (gdb) break calculateAverage.cpp:6



2. 运行程序并在断点处暂停后,设置监视i和sum:

   (gdb) display i(gdb) display sum



3. 使用next命令逐行执行,观察i的值是否超出预期范围。

很快会发现,i的值达到了vec.size(),导致访问了越界的元素。通过这种方式,迅速定位到循环条件中的错误,并修改为i < vec.size()。
 

调试技巧与注意事项



在实际使用变量和内存查看命令时,有几点经验值得分享。针对复杂数据结构,如std::vector或std::map,直接打印可能不够直观,GDB支持通过Python脚本扩展显示格式(称为pretty printers),可以更清晰地展示容器内容。LLDB同样内置了对STL容器的友好显示支持,确保在调试时启用这些功能。

此外,内存查看时需注意字节序问题(大端或小端),不同架构可能导致数据显示差异,必要时应结合十六进制和十进制格式交叉验证。针对大型程序,频繁使用监视点或内存查看可能会显著降低性能,建议在问题范围缩小后再启用这些功能。

另一个常见问题是如何处理未初始化的变量。通过调试器查看变量值时,若发现值为随机数或异常值,往往提示未初始化问题。此时,可以回溯代码,检查变量是否在所有路径上都被赋值。
 



通过变量与内存查看命令,开发者能够深入程序运行时的状态,追踪数据变化,定位逻辑错误或内存问题。GDB和LLDB提供的工具虽然语法略有不同,但核心思想一致:从变量值到内存内容,提供多层次的观察视角。熟练掌握这些命令,不仅能提高调试效率,还能加深对C++程序运行机制的理解。接下来,将进一步探讨如何利用调试器分析调用栈和线程状态,帮助解决更复杂的多线程问题或递归错误。

第六章:常用调试命令详解(三):堆栈与线程调试

在C++程序调试的过程中,调用堆栈和多线程问题是两个常见的复杂领域。调用堆栈记录了程序执行的函数调用路径,是定位逻辑错误或崩溃原因的关键线索;而多线程程序则因其并发特性,容易引发死锁、竞争条件等难以复现的问题。借助调试器如GDB和LLDB提供的强大命令,我们可以深入分析堆栈信息和线程行为,从而高效解决问题。这一章节将聚焦于堆栈查看与线程调试的核心命令,结合具体实例,探讨如何利用这些工具快速定位问题根源。
 

1. 调用堆栈分析:从函数调用路径入手



调用堆栈是程序执行的“历史记录”,它以栈的形式保存了当前线程中所有活动函数的调用关系。当程序发生崩溃或逻辑错误时,查看堆栈信息往往是第一步。通过堆栈,我们可以追溯到问题发生的函数调用路径,了解代码执行的上下文。

在GDB中,查看调用堆栈的核心命令是backtrace(简写为bt)。这个命令会列出当前线程的函数调用栈,从最顶层的当前函数开始,一直追溯到程序入口点main函数。例如,假设我们有一个简单的C++程序,其中包含嵌套函数调用:
 

void func3() {int* ptr = nullptr;*ptr = 42; // 故意制造崩溃
}void func2() {func3();
}void func1() {func2();
}int main() {func1();return 0;
}



编译并运行上述代码时,程序会因空指针解引用而崩溃。使用GDB加载程序并设置断点后,当崩溃发生时,输入backtrace命令,输出可能如下:
 

#0  func3 () at main.cpp:5
#1  func2 () at main.cpp:9
#2  func1 () at main.cpp:13
#3  main () at main.cpp:17



从输出中可以清晰看到,崩溃发生在func3函数的第5行,而调用路径依次经过了func2、func1和main。这种堆栈信息为我们提供了问题的上下文,帮助快速定位错误代码。

在LLDB中,类似的命令是thread backtrace(简写为bt)。其输出格式与GDB略有不同,但核心信息一致,同样会列出调用栈的每一帧(frame),包括函数名、源代码文件和行号。

堆栈信息不仅用于崩溃分析,也可以帮助理解程序执行流程。如果调用栈过深或包含意外的函数调用,可能提示逻辑设计问题。为此,GDB提供了frame命令(简写为f),用于切换到堆栈中的特定帧并查看其上下文。例如,输入frame 2可以切换到调用栈中的第2帧(即func1),并使用list命令查看该帧的源代码片段,或用print查看局部变量值。LLDB中对应的命令是frame select,功能一致。

值得注意的是,堆栈信息在优化编译模式下可能不完整,因为编译器可能会内联函数或移除调试符号。为确保堆栈信息的准确性,建议在调试时使用-g标志编译代码,并避免过高的优化级别(如-O2或-O3)。
 

2. 深入堆栈帧:变量与上下文检查



切换到特定堆栈帧后,调试器允许我们检查该帧中的局部变量、参数和寄存器状态。这对于定位函数调用中的参数传递错误或变量状态异常尤为重要。在GDB中,info locals命令可以列出当前帧中的所有局部变量及其值,而info args则显示函数参数。LLDB中,frame variable命令可以同时显示局部变量和参数,输出更加直观。

假设在上述示例中,我们切换到func2帧并检查变量状态:
 

(gdb) frame 1
(gdb) info locals
No locals.
(gdb) info args
No arguments.



由于func2函数没有局部变量和参数,输出为空。但如果函数内部定义了变量或传递了参数,这些命令将显示对应的值,帮助我们判断调用是否符合预期。

此外,堆栈分析还可以结合条件断点或监视点使用。例如,在程序崩溃前设置断点,并在关键函数帧中检查变量变化,可以更精准地捕捉问题根源。这种方法在调试递归函数或复杂调用链时尤为有效。
 

3. 多线程调试:切换与状态查看



现代C++程序广泛使用多线程来提升性能,但多线程也带来了死锁、竞争条件等复杂问题。调试器在处理多线程程序时,提供了强大的线程管理功能,帮助开发者分析线程状态和行为。

在GDB中,info threads命令可以列出程序中所有活跃线程的信息,包括线程ID、当前状态和所在函数。例如,在一个多线程程序中,输入该命令可能得到如下输出:
 

  Id   Target Id         Frame 
* 1    Thread 0x7ffff7fc5740 (LWP 12345) main () at main.cpp:102    Thread 0x7ffff6fc4640 (LWP 12346) worker () at worker.cpp:5



其中,带有*的线程是当前活动的线程。使用thread 命令可以切换到指定线程,例如thread 2会切换到ID为2的线程,并显示其调用栈和上下文。切换后,可以像单线程程序一样使用backtrace、print等命令分析线程状态。

LLDB中,线程管理的命令是thread list,用于列出所有线程,而thread select 则用于切换线程。两者的使用逻辑一致,只是语法略有不同。

线程切换在调试多线程程序时至关重要。例如,当一个线程因死锁而卡住时,我们可以通过切换到该线程,查看其调用栈和变量状态,判断是否在等待某个资源。以下是一个简单的多线程死锁示例:
 

std::mutex mtx1, mtx2;void thread1() {mtx1.lock();mtx2.lock();mtx2.unlock();mtx1.unlock();
}void thread2() {mtx2.lock();mtx1.lock();mtx1.unlock();mtx2.unlock();
}int main() {std::thread t1(thread1);std::thread t2(thread2);t1.join();t2.join();return 0;
}



在上述代码中,thread1和thread2可能因锁的获取顺序不同而发生死锁。使用GDB调试时,可以通过info threads查看两个线程的状态,并切换到每个线程,检查其调用栈,确认它们是否卡在lock操作上。
 

4. 线程问题定位:竞争条件与断点



除了死锁,竞争条件是多线程程序的另一大难题。由于线程调度不可预测,竞争条件往往难以复现。调试器虽然无法完全解决这个问题,但可以通过设置线程特定的断点或监视点来辅助分析。

在GDB中,断点可以结合线程ID使用。例如,break worker.cpp:5 thread 2会在指定线程执行到worker.cpp的第5行时暂停程序,而其他线程不受影响。LLDB中,类似的设置可以通过breakpoint set --file worker.cpp --line 5 --thread-id 2实现。

此外,监视点(watchpoint)在调试竞争条件时也非常有用。假设某个共享变量被多个线程访问,我们可以在变量上设置监视点,捕捉其被修改的时刻,并结合线程信息判断是否存在非法访问。这种方法虽然会显著降低程序运行速度,但对于难以复现的问题,往往是唯一的有效手段。
 

5. 调试技巧与注意事项



在实际调试中,堆栈和线程分析往往需要结合其他工具和技巧。例如,使用core dump文件可以事后分析程序崩溃时的堆栈状态,而启用日志记录则有助于追踪多线程程序的执行顺序。此外,现代IDE如Visual Studio或CLion集成了调试器功能,通过图形化界面展示堆栈和线程信息,进一步降低了调试难度。

需要注意的是,多线程调试对性能影响较大,尤其是在设置大量断点或监视点时。建议在调试前精简测试用例,缩小问题范围。同时,C++标准库提供的线程调试工具,如std::mutex的超时机制,也可以辅助定位问题。

相关文章:

  • day51—二分法—x 的平方根(LeetCode-69)
  • 计算机基本理论与 ARM 相关概念深度解析
  • 一、I/O的相关概念
  • JavaScript之Webpack的模块加载机制
  • es数据导出
  • Unity Post Processing 小记 【使用泛光实现灯光亮度效果】
  • 第2讲、Tensor高级操作与自动求导详解
  • gradle eclipse [.project .classpath .settings]
  • 【有啥问啥】深入理解 Layer Normalization (LayerNorm):深度学习的稳定基石
  • 【物理学】电磁学——电动势
  • 说一下Drop与delete区别
  • Kafka批量消费部分处理成功时的手动提交方案
  • 页面需要重加载才能显示的问题修改
  • openstack热迁移、冷迁移、疏散
  • SQL注入原理及防护方案
  • 基于BenchmarkSQL的OceanBase数据库tpcc性能测试
  • Java异常处理全面指南:从基础到高级实践
  • [MCU]SRAM
  • 路由协议基础
  • 【JS-Leetcode】2621睡眠函数|2629复合函数|2665计数器||
  • 十大券商看后市|A股风险偏好有望边际改善,市场仍处黄金坑
  • 视觉周刊|2025上海车展的科技范
  • 加总理:目前没有针对加拿大人的“活跃威胁”
  • 旧衣服旧纸箱不舍得扔?可能是因为“囤物障碍”
  • 孟泽:我们简化了历史,因此也简化了人性
  • 从地下金库到地上IP,看海昏汉文化“最美变装”