深入内存调试:Valgrind工具的终极指南(转)
在软件开发的世界里,代码质量就是生命线,而内存管理又是这条生命线中最脆弱的一环。内存泄漏,哪怕只是微小的一处,日积月累,都可能对整个系统造成灾难性的打击,无论是大型企业级应用、实时性要求极高的嵌入式系统,还是对性能锱铢必较的游戏开发。此时,掌握一款强大的内存检测工具至关重要,Valgrind 便是这样的利器。它以其精准、全面的检测能力,成为众多开发者捍卫代码质量的 “秘密武器”。今天,我们就深入探索 Valgrind,看看它如何帮助我们规避内存泄漏,打造坚如磐石的代码。
一、Valgrind 是什么?
在编程的世界里,代码就像是一座宏伟的建筑,而内存管理则是这座建筑的基石。一个小小的内存错误,可能就会引发程序崩溃、数据丢失等灾难性后果。这时候,Valgrind 就像是一位专业的建筑质检员,能够帮助我们找出代码中的内存问题,确保程序的稳定性和可靠性。
Valgrind 是一款用于内存调试、内存泄漏检测和性能分析的软件开发工具,堪称程序员的得力助手。它最初由 Julian Seward 设计,2006 年因其在 Linux x86 平台上的免费内存调试工具上的卓越贡献,荣获第二届 Google-O'Reilly 开放源码奖,并且遵循 GNU 通用公共许可证,是自由软件中的明星产品。
对于 C/C++ 程序员来说,Valgrind 更是不可或缺。在 C/C++ 编程中,内存的分配与释放需要程序员手动管理,稍有不慎就会出现各种问题,比如使用未初始化的内存、内存泄漏、越界访问等。这些问题往往难以察觉,可能在程序运行一段时间后才突然爆发,给调试带来极大的困难。而 Valgrind 就如同一个敏锐的侦探,能够精准地发现这些隐藏的内存 “陷阱”,让我们及时修复问题,避免程序在关键时刻 “掉链子”。
Valgrind的体系结构以下图所示:
二、Valgrind 的强大功能
Valgrind 之所以如此强大,是因为它包含了一系列各有所长的工具,就像一个多功能的瑞士军刀,能够从不同角度剖析我们的程序。接下来,让我们深入了解一下这些工具的独特魅力。
⑴Memcheck:内存问题的 “放大镜”
Memcheck 是 Valgrind 中当之无愧的明星工具,也是使用最为广泛的一个。它就像一个高倍放大镜,能够精准地检测出程序中各种各样的内存问题。无论是使用未初始化的内存、读写已释放的内存块,还是数组下标越界、内存泄漏等,统统都逃不过它的 “法眼”。在开发过程中,这些内存问题往往隐藏得很深,可能在特定的条件下才会暴露出来,给调试带来极大的困扰。而 Memcheck 能够在程序运行时实时监测内存的使用情况,一旦发现问题,立即给出详细的错误报告,包括错误发生的位置、涉及的内存地址等信息,让我们能够迅速定位并修复问题。
因此,它能检测如下问题:
- 未初始化内存的使用;
- 读/写释放后的内存块;
- 读/写超出malloc分配的内存块;
- 读/写不适当的栈中内存块;
- 内存泄漏,指向一块内存的指针永远丢失;
- 不正确的malloc/free或new/delete匹配;
- memcpy()相关函数中的dst和src指针重叠。
⑵Cachegrind:优化缓存的 “指南针”
Cachegrind 则专注于程序的缓存使用情况,为我们优化程序性能提供了有力的支持。它就像是一个精准的指南针,能够帮助我们找到代码中与缓存相关的问题,指引我们优化的方向。在现代计算机体系结构中,CPU 的速度远远快于内存的速度,缓存的作用就显得尤为重要。如果程序不能有效地利用缓存,频繁地从内存中读取数据,就会导致 CPU 等待,从而大大降低程序的性能。Cachegrind 通过模拟 CPU 的缓存行为,详细地统计缓存的命中和未命中情况,为我们提供诸如指令计数、缓存未命中次数、内存引用次数等关键信息。这些信息就像是宝藏地图上的标记,让我们能够清楚地看到程序中哪些部分的缓存利用率不高,进而针对性地进行优化,比如调整数据结构、优化算法,以提高缓存命中率,提升程序的运行速度。
⑶Callgrind:函数调用的 “透视镜”
Callgrind 主要用于分析程序中函数的调用过程,如同一个透视镜,让函数调用的细节一览无余。它能够收集函数调用的相关数据,建立起函数调用关系图,清晰地展示出各个函数之间的调用层次和频率。这对于理解程序的执行流程、发现潜在的性能瓶颈非常有帮助。在大型项目中,函数之间的调用关系错综复杂,很难直观地看出哪些函数的调用开销较大,哪些函数被频繁调用但实际上可以进行优化。Callgrind 不仅可以告诉我们这些信息,还能提供每个函数执行的指令数、缓存使用情况等详细数据。通过分析这些数据,我们可以找出那些占用大量 CPU 资源的 “热点” 函数,对它们进行优化,比如减少不必要的函数调用、优化函数内部的算法,从而提升整个程序的性能。
⑷Helgrind:多线程程序的 “守护者”
在多线程编程的世界里,线程之间的同步与竞争问题就像是隐藏在暗处的 “幽灵”,随时可能导致程序出现难以捉摸的错误。Helgrind 就是专门用来驱赶这些 “幽灵” 的 “守护者”。它致力于检查多线程程序中出现的竞争问题,通过先进的算法,仔细监测内存中被多个线程访问的区域,一旦发现没有正确加锁或同步的情况,就会及时发出警报。这些竞争问题往往会导致程序出现死锁、数据不一致等严重错误,而且由于它们的出现具有不确定性,很难通过常规的调试手段发现。Helgrind 的出现,为多线程程序的调试带来了极大的便利,让我们能够提前发现并解决这些潜在的问题,确保多线程程序的正确性和稳定性。
⑸Massif:内存使用的 “分析师”
Massif 是一位专业的 “分析师”,专注于程序的堆栈内存使用情况。它能够精确地测量程序在运行过程中堆栈内存的使用量,详细地告诉我们堆块、堆管理块和栈的大小,以及内存的分配和释放情况。对于那些需要严格控制内存使用的程序,比如嵌入式系统开发、服务器端程序等,Massif 的作用尤为突出。通过它提供的信息,我们可以深入了解程序的内存使用行为,发现内存泄漏、内存过度分配等问题,并进行针对性的优化。例如,我们可以根据 Massif 的报告,调整数据结构的大小、优化内存分配策略,以减少内存的占用,提高程序的运行效率,避免因内存不足而导致的程序崩溃或性能下降。
三、安装与配置 Valgrind
3.1不同系统下的安装方法
安装 Valgrind 其实并不复杂,不过不同的操作系统下,安装方式还是略有差异的。下面,我就来给大家详细介绍一下。
在 Linux 系统下,安装 Valgrind 就像是一场轻松的旅行。以常见的 Ubuntu 系统为例,我们只需打开终端,输入以下几条命令:
sudo apt-get update
sudo apt-get install valgrind
简单几步,就能轻松搞定安装,是不是超级方便?这就好比在应用商店里一键下载安装软件一样便捷,让你快速拥有这款强大的工具。
对于 Windows 用户来说,由于 Valgrind 本身是基于 Linux 开发的,所以不能直接在 Windows 上安装原生版本。不过别担心,我们可以借助 Windows 下的 Linux 子系统(WSL)来使用它。首先,按照微软官方的教程安装 WSL,安装完成后,在 WSL 的终端中,使用和 Linux 系统下类似的命令安装 Valgrind。就像是在 Windows 系统里开辟了一块 “Linux 小天地”,让 Valgrind 在其中顺畅运行,为我们的 Windows 编程保驾护航。
Mac 用户也有自己的安装方式。我们可以使用 Homebrew 这个强大的包管理器来安装 Valgrind,只需在终端中输入:
brew install valgrind
这就像是用一把万能钥匙打开了软件安装的大门,Homebrew 会自动帮我们处理好所有的依赖关系,轻松完成安装,让我们在 Mac 上也能尽情享受 Valgrind 带来的便利。
3.2配置要点
安装好 Valgrind 后,还需要进行一些简单的配置,才能让它更好地发挥作用。在编译我们的程序时,记得要打开调试模式,这就像是给程序戴上了一个 “智能手环”,可以记录更多的运行信息,方便 Valgrind 进行分析。以 gcc 编译器为例,我们需要加上 “-g” 选项,像这样:
gcc -g -o myprog myprog.c
另外,为了避免编译优化影响 Valgrind 的检测结果,最好关闭编译优化选项。因为有些优化可能会改变程序的执行顺序,让 Valgrind 难以准确找到问题所在。在 gcc 中,我们可以使用 “-O0” 选项来关闭优化,就像这样:
gcc -g -O0 -o myprog myprog.c
完成这些配置后,我们就可以让 Valgrind 闪亮登场,开启代码的 “体检” 之旅啦。
3.3检测内存泄漏
终端进入可执行文件所在的文件夹,输入
valgrind --tool=memcheck
--leak-check=full
--show-leak-kinds=all
--undef-value-errors=no
--log-file=log ./可执行文件名
即可在终端所在文件夹下生成log文件,在log文件最后会有个summary,其中对内存泄露进行了分类,总共有五类:
- “definitely lost” 意味着你的程序一定存在内存泄露;
- ”indirectly lost”意味着你的程序一定存在内存泄露,并且泄露情况和指针结构相关
- “possibly lost” 意味着你的程序一定存在内存泄露,除非你是故意进行着不符合常规的操作,例如将指针指向某个已分配内存块的中间位置。
- “still reachable” 意味着你的程序可能是没问题的,但确实没有释放掉一些本可以释放的内存。这种情况是很常见的,并且通常基于合理的理由。
- ”suppressed” 意味着有些泄露信息被压制了。在默认的 suppression 文件中可以看到一些 suppression 相关设置。
其中,如果二叉树的根节点被判定为”definitely lost”,则其所有子节点将被判定为”indirectly lost”,而如果你正确修复了类型为 “definitely lost” 的根节点泄露,那么类型为 “indirectly lost” 的子节点泄露也会随着消失。
对于以上的情况,posslbly lost其实并没有造成内存上的影响,如果想要过滤掉该类报告信息,可以加入--show-possibly-lost=no ,而对于”still reachable” ,同样可以通过--show-reachable=yes来控制是否输出相应的信息。如果某些需要的库没有找到,用指令进行添加:
export LD_LIBRARY_PATH=/usr/local/mysql/lib:$LD_LIBRARY_PATH
查看发生泄露的具体位置
在log中由summary往上翻即可看到对应的错误,错误是不断细化的,比如:
这样的是一个错误,先告诉你出现了多少的内存泄露,然后从最里层不断往外部函数显示:先说是calloc造成的错误,然后不断往外部函数显示。可以从下往上进行查看,比如先说main()函数发生了泄露,往上看到是main()中的init()函数,再往上init()中的init_detectionmodel,如此不断细定位泄露位置。
四、Valgrind 工作原理
Memcheck 能够检测出内存问题,关键在于其建立了两个全局表。Valid-Value 表对于进程的整个地址空间中的每一个字节(byte),都有与之对应的 8 个 bits;对于CPU的每个寄存器,也有一个与之对应的bit向量。这些bits负责记录该字节或者寄存器值是否具有有效的、已初始化的值。
Valid-Address表:对于进程整个地址空间中的每一个字节(byte),还有与之对应的1个bit,负责记录该地址是否能够被读写。
检测原理:当要读写内存中某个字节时,首先检查这个字节对应的 A bit。如果该A bit显示该位置是无效位置,memcheck则报告读写错误。
内核(core)类似于一个虚拟的 CPU 环境,这样当内存中的某个字节被加载到真实的 CPU 中时,该字节对应的 V bit 也被加载到虚拟的 CPU 环境中。一旦寄存器中的值,被用来产生内存地址,或者该值能够影响程序输出,则 memcheck 会检查对应的V bits,如果该值尚未初始化,则会报告使用未初始化内存错误。
五、Valgrind 命令介绍
用法valgrind[options] prog-and-args [options]常用选项,适用于所有Valgrind工具:
- -tool=<name> 最常用的选项。运行 valgrind中名为toolname的工具。默认memcheck。
- h –help 显示帮助信息。
- -version 显示valgrind内核的版本,每个工具都有各自的版本。
- q –quiet 安静地运行,只打印错误信息。
- v –verbose 更详细的信息, 增加错误数统计。
- -trace-children=no|yes 跟踪子线程? [no]
- -track-fds=no|yes 跟踪打开的文件描述?[no]
- -time-stamp=no|yes 增加时间戳到LOG信息? [no]
- -log-fd=<number> 输出LOG到描述符文件 [2=stderr]
- -log-file=<file> 将输出的信息写入到filename.PID的文件里,PID是运行程序的进行ID
- -log-file-exactly=<file> 输出LOG信息到 file
- -log-file-qualifier=<VAR> 取得环境变量的值来做为输出信息的文件名。[none]
- -log-socket=ipaddr:port 输出LOG到socket ,ipaddr:port
LOG信息输出:
- -xml=yes 将信息以xml格式输出,只有memcheck可用
- -num-callers=<number> show <number> callers in stack traces [12]
- -error-limit=no|yes 如果太多错误,则停止显示新错误? [yes]
- -error-exitcode=<number> 如果发现错误则返回错误代码 [0=disable]
- -db-attach=no|yes 当出现错误,valgrind会自动启动调试器gdb。[no]
- -db-command=<command> 启动调试器的命令行选项[gdb -nw %f %p]
适用于Memcheck工具的相关选项:
- -leak-check=no|summary|full 要求对leak给出详细信息? [summary]
- -leak-resolution=low|med|high how much bt merging in leak check [low]
- -show-reachable=no|yes show reachable blocks in leak check? [no]
六、使用 Valgrind 进行内存调试
6.1基本命令参数解析
了解了 Valgrind 的安装和配置,下面我们就来看看如何在实战中使用它。使用 Valgrind 的基本命令格式如下:
valgrind [options] program [arguments]
其中,[options]是一系列的参数,用来控制 Valgrind 的行为,program是我们要检测的程序,[arguments]则是程序运行所需的参数。
下面,给大家介绍几个常用的参数。首先是 “--tool=memcheck”,这个参数指定使用 Memcheck 工具,它是 Valgrind 中最常用的工具,用于检测各种内存问题,如果你不确定程序具体存在哪种内存问题,使用这个参数准没错。
“--leak-check” 参数用于检测内存泄漏,它有几个可选的值,“no” 表示不检查内存泄漏,“summary” 仅显示内存泄漏的摘要信息,而 “full” 则会显示所有内存泄漏的详细信息,包括泄漏的内存位置、大小等,方便我们深入排查问题,一般在调试阶段,建议使用 “full” 模式,以获取最全面的信息。
还有 “--track-origins=yes”,这个参数非常实用,它可以帮助我们追踪未初始化内存的使用情况,让我们清楚地知道未初始化的内存是在哪里被创建的,以及在哪些地方被使用,对于找出那些因使用未初始化内存而导致的诡异问题特别有帮助。
6.2应用实践
下面通过介绍几个范例来说明如何使用Memcheck ,示例仅供参考,更多用途可在实际应用中不断探索。
⑴数组越界/内存未释放
#include<stdlib.h>
void k(void)
{
int *x = malloc(8 * sizeof(int));
x[9] = 0; //数组下标越界
} //内存未释放int main(void)
{k();
return 0;
}
①编译程序test.c
gcc -Wall test.c -g -o test#Wall提示所有告警,-g gdb,-o输出
②使用Valgrind检查程序BUG
valgrind --tool=memcheck --leak-check=full ./test
#--leak-check=full 所有泄露检查
③运行结果如下:
==2989== Memcheck, a memory error detector==2989== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Sewardet al.==2989== Using Valgrind-3.8.1 and LibVEX; rerun with -h forcopyright info==2989== Command: ./test==2989====2989== Invalid write of size 4==2989== at 0x4004E2: k (test.c:5)==2989== by 0x4004F2: main (test.c:10)==2989== Address 0x4c27064 is 4 bytes after a block of size 32 alloc'd==2989== at 0x4A06A2E: malloc (vg_replace_malloc.c:270)==2989== by 0x4004D5: k (test.c:4)==2989== by 0x4004F2: main (test.c:10)==2989====2989====2989== HEAP SUMMARY:==2989== in use at exit: 32 bytes in 1 blocks==2989== total heap usage: 1 allocs, 0 frees, 32 bytes allocated==2989====2989== 32 bytes in 1 blocks are definitely lost in loss record 1of 1==2989== at 0x4A06A2E: malloc (vg_replace_malloc.c:270)==2989== by 0x4004D5: k (test.c:4)==2989== by 0x4004F2: main (test.c:10)==2989====2989== LEAK SUMMARY:==2989== definitely lost: 32 bytes in 1 blocks==2989== indirectly lost: 0 bytes in 0 blocks==2989== possibly lost: 0 bytes in 0 blocks==2989== still reachable: 0 bytes in 0 blocks==2989==suppressed: 0 bytes in 0 blocks==2989====2989== For counts of detected and suppressed errors, rerun with: -v==2989== ERROR SUMMARY: 2 errors from 2 contexts(suppressed: 6 from 6)
⑵内存释放后读写
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *p = malloc(1); //分配
*p = 'a';
char c = *p;
printf("\n [%c]\n",c);
free(p); //释放c = *p; //取值
return 0;
}
①编译程序t2.c
gcc -Wall t2.c -g -o t2
②使用Valgrind检查程序BUG
valgrind --tool=memcheck --leak-check=full ./t2
③运行结果如下:
==3058== Memcheck, a memory error detector
==3058== Copyright (C) 2002-2012, and GNU GPL'd, by Julian
Seward et al.
==3058== Using Valgrind-3.8.1 and LibVEX; rerun with -h
for copyright info
==3058== Command: ./t2
==3058==[a]==3058== Invalid read of size 1==3058== at 0x4005A3: main (t2.c:14)==3058== Address 0x4c27040 is 0 bytes inside a block of size1 free'd==3058== at 0x4A06430: free (vg_replace_malloc.c:446)==3058== by 0x40059E: main (t2.c:13)==3058====3058====3058== HEAP SUMMARY:==3058== in use at exit: 0 bytes in 0 blocks==3058== total heap usage: 1 allocs, 1 frees, 1 bytes allocated==3058====3058== All heap blocks were freed -- no leaks are possible==3058====3058== For counts of detected and suppressed errors, rerun with:-v==3058== ERROR SUMMARY: 1 errors from 1 contexts(suppressed: 6 from 6)从上输出内容可以看到,Valgrind检测到无效的读取操作然后输出“Invalid read of size 1”。
⑶无效读写
#include <stdio.h>
#include <stdlib.h>
int main(void){char *p = malloc(1); //分配1字节*p = 'a';char c = *(p+1); //地址加1printf("\n [%c]\n",c); free(p);return 0;
}
①编译程序t3.c
gcc -Wall t3.c -g -o t3
②使用Valgrind检查程序BUG
valgrind --tool=memcheck --leak-check=full ./t3
③运行结果如下:
==3128== Memcheck, a memory error detector==3128== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.==3128== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info==3128== Command: ./t3==3128====3128== Invalid read of size 1 #无效读取==3128==at 0x400579: main (t3.c:9)==3128==Address 0x4c27041 is 0 bytes after a block of size 1 alloc'd==3128==at 0x4A06A2E: malloc (vg_replace_malloc.c:270)==3128==by 0x400565: main (t3.c:6)==3128==[]
==3128==
==3128== HEAP SUMMARY:
==3128==in use at exit: 0 bytes in 0 blocks
==3128==total heap usage: 1 allocs, 1 frees, 1 bytes allocated
==3128==
==3128== All heap blocks were freed -- no leaks are possible
==3128==
==3128== For counts of detected and suppressed errors, rerun with: -v
==3128== ERROR SUMMARY: 1 errors from 1 contexts
(suppressed: 6 from 6)
⑷内存泄露
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p = malloc(1);
*p = 'x';
char c = *p;
printf("%c\n",c); //申请后未释放return 0;
}
①编译程序t4.c
gcc -Wall t4.c -g -o t4
②使用Valgrind检查程序BUG
valgrind --tool=memcheck --leak-check=full ./t4
③运行结果如下:
==3221== Memcheck, a memory error detector==3221== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.==3221== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info==3221== Command: ./t4==3221====3221== Invalid write of size 4==3221==at 0x40051E: main (t4.c:7)==3221==Address 0x4c27040 is 0 bytes inside a block of size 1 alloc'd==3221==at 0x4A06A2E: malloc (vg_replace_malloc.c:270)==3221==by 0x400515: main (t4.c:6)==3221====3221== Invalid read of size 4==3221==at 0x400528: main (t4.c:8)==3221==Address 0x4c27040 is 0 bytes inside a block of size 1 alloc'd==3221==at 0x4A06A2E: malloc (vg_replace_malloc.c:270)==3221==by 0x400515: main (t4.c:6)==3221==x==3221====3221== HEAP SUMMARY:==3221==in use at exit: 1 bytes in 1 blocks==3221==total heap usage: 1 allocs, 0 frees, 1 bytes allocated==3221====3221== 1 bytes in 1 blocks are definitely lost in loss record 1 of 1==3221==at 0x4A06A2E: malloc (vg_replace_malloc.c:270)==3221==by 0x400515: main (t4.c:6)==3221====3221== LEAK SUMMARY:==3221==definitely lost: 1 bytes in 1 blocks==3221==indirectly lost: 0 bytes in 0 blocks==3221== possibly lost: 0 bytes in 0 blocks==3221==still reachable: 0 bytes in 0 blocks==3221== suppressed: 0 bytes in 0 blocks==3221====3221== For counts of detected and suppressed errors, rerun with: -v==3221== ERROR SUMMARY: 3 errors from 3 contexts(suppressed: 6 from 6)
从检查结果看,可以发现内存泄露。
⑸内存多次释放
#include <stdio.h>
#include <stdlib.h>
int main(void) { char *p;p=(char *)malloc(100); if(p)printf("Memory Allocated at: %s/n",p); elseprintf("Not Enough Memory!/n"); free(p); //重复释放free(p);free(p);return 0;
}
①编译程序t5.c
gcc -Wall t5.c -g -o t5
②使用Valgrind检查程序BUG
valgrind --tool=memcheck --leak-check=full ./t5
③运行结果如下:
==3294== Memcheck, a memory error detector==3294== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Sewardet al.==3294== Using Valgrind-3.8.1 and LibVEX; rerun with -h forcopyright info==3294== Command: ./t5==3294====3294== Conditional jump or move depends on uninitialised value(s)==3294== at 0x3CD4C47E2C: vfprintf (in /lib64/libc-2.12.so)==3294== by 0x3CD4C4F189: printf (in /lib64/libc-2.12.so)==3294== by 0x400589: main (t5.c:9)==3294====3294== Invalid free() / delete / delete[] / realloc()==3294== at 0x4A06430: free (vg_replace_malloc.c:446)==3294== by 0x4005B5: main (t5.c:13)==3294== Address 0x4c27040 is 0 bytes inside a block of size100 free'd==3294== at 0x4A06430: free (vg_replace_malloc.c:446)==3294== by 0x4005A9: main (t5.c:12)==3294====3294== Invalid free() / delete / delete[] / realloc()==3294== at 0x4A06430: free (vg_replace_malloc.c:446)==3294== by 0x4005C1: main (t5.c:14)==3294== Address 0x4c27040 is 0 bytes inside a block of size100 free'd==3294== at 0x4A06430: free (vg_replace_malloc.c:446)==3294== by 0x4005A9: main (t5.c:12)==3294==Memory Allocated at: /n==3294====3294== HEAP SUMMARY:==3294== in use at exit: 0 bytes in 0 blocks==3294== total heap usage: 1 allocs, 3 frees, 100 bytes allocated
从上面的输出可以看到(标注), 该功能检测到我们对同一个指针调用了3次释放内存操作。
⑹内存动态管理
常见的内存分配方式分三种:静态存储,栈上分配,堆上分配。全局变量属于静态存储,它们是在编译时就被分配了存储空间,函数内的局部变量属于栈上分配,而最灵活的内存使用方式当属堆上分配,也叫做内存动态分配了。常用的内存动态分配函数包括:malloc, alloc, realloc, new等,动态释放函数包括free, delete。
一旦成功申请了动态内存,我们就需要自己对其进行内存管理,而这又是最容易犯错误的。下面的一段程序,就包括了内存动态管理中常见的错误。
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
int i;
char* p = (char*)malloc(10);
char* pt=p;
for(i = 0;i < 10;i++){
p[i] = 'z';}
free(p);
pt[1] = 'x';
free(pt);
return 0;
}
①编译程序t6.c
gcc -Wall t6.c -g -o t6
②使用Valgrind检查程序BUG
valgrind --tool=memcheck --leak-check=full ./t6
③运行结果如下:
==3380== Memcheck, a memory error detector
==3380== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
==3380== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==3380== Command: ./t6
==3380==
==3380== Invalid write of size 1
==3380==at 0x40055C: main (t6.c:14)
==3380==Address 0x4c27041 is 1 bytes inside a block of size 10 free'd
==3380==at 0x4A06430: free (vg_replace_malloc.c:446)
==3380==by 0x400553: main (t6.c:13)
==3380==
==3380== Invalid free() / delete / delete[] / realloc()
==3380==at 0x4A06430: free (vg_replace_malloc.c:446)
==3380==by 0x40056A: main (t6.c:15)
==3380==Address 0x4c27040 is 0 bytes inside a block of size 10 free'd
==3380==at 0x4A06430: free (vg_replace_malloc.c:446)
==3380==by 0x400553: main (t6.c:13)
==3380==
==3380==
==3380== HEAP SUMMARY:
==3380==in use at exit: 0 bytes in 0 blocks
==3380==total heap usage: 1 allocs, 2 frees, 10 bytes allocated
申请内存在使用完成后就要释放。如果没有释放,或少释放了就是内存泄露;多释放也会产生问题。上述程序中,指针p和pt指向的是同一块内存,却被先后释放两次。系统会在堆上维护一个动态内存链表,如果被释放,就意味着该块内存可以继续被分配给其他部分,如果内存被释放后再访问,就可能覆盖其他部分的信息,这是一种严重的错误,上述程序第14行中就在释放后仍然写这块内存。
输出结果显示,第13行分配和释放函数不一致;第14行发生非法写操作,也就是往释放后的内存地址写值;第15行释放内存函数无效。
七、多线程程序调试
在多线程编程的世界里,线程之间的同步与竞争问题就像是隐藏在暗处的 “幽灵”,随时可能导致程序出现难以捉摸的错误。Valgrind 中的 Helgrind 和 DRD 工具,就是专门用来驱赶这些 “幽灵” 的 “守护者”。
Helgrind 致力于检查多线程程序中出现的竞争问题,它通过先进的算法,仔细监测内存中被多个线程访问的区域,一旦发现没有正确加锁或同步的情况,就会及时发出警报。比如说,我们有一个多线程程序,多个线程同时对一个共享变量进行读写操作,却没有使用任何锁来保护这个共享变量,这就很可能导致数据不一致的问题。运行 Helgrind,它就能精准地检测到这种潜在的风险,输出类似这样的报告:
==12345== Possible data race during write of size 4 at 0x... by thread #1
==12345== at 0x... increment_counter
==12345== by 0x... start_thread...
这份报告清晰地指出了在哪个线程、哪个函数中发生了可能的数据竞争,让我们能够迅速定位问题。
DRD 同样是检测多线程程序问题的得力助手,它专注于查找数据竞争、死锁等并发错误。它会对程序的执行过程进行全面的 “扫描”,一旦发现可疑的并发问题,就会给出详细的提示。例如,在一个复杂的多线程程序中,多个线程之间存在复杂的锁依赖关系,如果不小心出现了死锁的情况,DRD 就能及时察觉,帮助我们找出导致死锁的锁获取顺序,让我们能够调整代码,避免死锁的发生。
下面,我们通过一个具体的例子来看看它们的实战效果。假设我们有以下一段多线程 C++ 代码:
#include <thread>
#include <iostream>
#include <vector>
#include <mutex>std::vector<int> shared_data;
std::mutex mtx;void thread_function() {while (true) {std::unique_lock<std::mutex> lock(mtx);shared_data.push_back(rand());lock.unlock();}
}int main() {std::thread t1(thread_function);std::thread t2(thread_function);t1.join();t2.join();return 0;
}
在这段代码中,两个线程都在向共享的shared_data向量中添加随机数,虽然使用了互斥锁mtx来保护共享数据的访问,但在实际复杂的多线程环境下,可能还存在一些隐藏的问题。
我们使用 Helgrind 来检测这个程序,在终端中输入:
valgrind --tool=helgrind./test
Helgrind 运行后,可能会给出一些关于锁使用的建议,比如是否存在锁竞争、锁的粒度是否合适等信息,帮助我们进一步优化代码,确保多线程程序的稳定性。
如果我们使用 DRD 来检测,输入:
valgrind --tool=drd./test
DRD 可能会从不同的角度发现一些潜在的并发问题,比如是否存在某个线程长时间持有锁,导致其他线程阻塞,影响程序的并发性能等。通过这两个工具的双重保障,我们能够更加全面地排查多线程程序中的问题,让程序在多线程环境下稳定高效地运行。
7.1性能剖析功能
除了内存调试,Valgrind 在性能剖析方面也有着出色的表现,能帮我们深挖程序性能瓶颈,让程序 “跑” 得更快。
Callgrind 是性能剖析的得力工具,它就像程序的 “动态心电图”,能详细记录程序运行时函数的调用情况与 CPU 指令执行信息。运行程序时加上 “--tool=callgrind” 参数,它会生成一个包含丰富数据的文件,像函数调用次数、每个函数执行的指令数等。借助callgrind_annotate命令或KCachegrind图形界面工具查看分析结果,那些 “吃” CPU 资源多的函数便无所遁形。比如开发一个图形渲染程序,用 Callgrind 分析后发现某个复杂的光照计算函数占用大量 CPU 时间,对其算法优化或采用更高效的数学库后,程序渲染速度大幅提升。
Cachegrind 则专注于缓存使用分析,是优化缓存命中率的 “好帮手”。它模拟 CPU 缓存行为,精确统计缓存命中、未命中次数,还涵盖指令计数、内存引用次数等关键指标。执行程序加上 “--tool=cachegrind” 参数,会得到详细记录缓存信息的文件,用cg_annotate工具查看,能清晰知晓程序哪些部分缓存利用率低。如处理大规模图像数据时,发现频繁从内存加载数据导致缓存未命中率高,通过调整数据结构为连续存储,或优化算法减少数据跨缓存行访问,就能提高缓存命中率,让程序运行如 “闪电” 般迅速。
7.2Valgrind 的局限性
尽管 Valgrind 如此强大,但它也并非十全十美,存在一些局限性。就像再好的医生也有棘手的病症一样,Valgrind 在某些复杂的情况下,也可能会 “力不从心”。
比如说,对于一些静态分配或在堆栈上分配的数组的超出范围的读取或写入,Valgrind 可能无法检测到。这就需要我们在编写代码时,依然要保持警惕,不能完全依赖工具。另外,在检测某些复杂的内存错误场景时,可能会出现误报或漏报的情况,需要我们结合代码逻辑仔细甄别。但即便存在这些小瑕疵,也丝毫不能掩盖 Valgrind 在内存调试和性能分析领域的卓越光芒,它依然是我们编程路上最得力的助手之一。
相关文章:
深入内存调试:Valgrind工具的终极指南(转)
在软件开发的世界里,代码质量就是生命线,而内存管理又是这条生命线中最脆弱的一环。内存泄漏,哪怕只是微小的一处,日积月累,都可能对整个系统造成灾难性的打击,无论是大型企业级应用、实时性要求极高的嵌入…...
深入解析MediaPipe:强大的实时计算机视觉框架
深入解析MediaPipe:强大的实时计算机视觉框架 1. 引言 在计算机视觉应用的快速发展中,实时处理和低延迟成为了许多应用的关键需求。Google 开发的 MediaPipe 是一个强大的开源框架,它能够高效处理 手势识别、姿态估计、物体检测、语音处理 …...
DeepSeek 和 ChatGPT 在特定任务中的表现:逻辑推理与创意生成
🎁个人主页:我们的五年 🔍系列专栏:Linux网络编程 🌷追光的人,终会万丈光芒 🎉欢迎大家点赞👍评论📝收藏⭐文章 Linux网络编程笔记: https://blog.cs…...
大白话实战Sentinel
Sentinel是SpringCloudAlibaba提供的用来做服务保护的框架,而服务保护的常见手段就是限流和熔断降级。在大型分布式系统里面,由于微服务众多,所以服务之间的稳定性需要做特别关注,Sentinel的核心包就提供了从多个维度去保护服务稳定的策略,而且这些保护策略都可以连接上Se…...
【AI面板识别】
题目描述 AI识别到面板上有N(1 ≤ N ≤ 100)个指示灯,灯大小一样,任意两个之间无重叠。 由于AI识别误差,每次别到的指示灯位置可能有差异,以4个坐标值描述AI识别的指示灯的大小和位置(左上角x1,y1&#x…...
Docker安装Kafka(不依赖ZooKeeper)
创建docker-compose.yaml version: "3.9" #版本号 services:kafka:image: apache/kafka:3.9.0container_name: kafkahostname: kafkaports:- 9092:9092 # 容器内部之间使用的监听端口- 9094:9094 # 容器外部访问监听端口environment:KAFKA_NODE_ID: 1KAFKA_PROCES…...
大道至简 少字全意 易经的方式看 jvm基础 、 内存模型 、 gc、 内存异常、内存调优实战案例 、类加载机制、双亲委派模型 适用于 懂而久未用回忆 ,不懂而需明正理而用
目录 介绍 内存模型 一、线程私有区域 二、线程共享区域 1.堆Heap 2. 方法区Method Area 3.运行时常量池 Runtime constant Pool 三、直接内存(Direct Memory) 四、内存异常与调优 五、总结对比 类加载机制 一、类加载的三大阶段 二、双亲委派模型 三、类加载的特…...
【Java学习】继承
一、继承 子类继承父类,子类这个类变量的引用在原有的指向子类自己类变量空间的原有访问权限上,增加上了父类类变量空间的访问权限,此时子类类变量指向的空间变为了原来子类类变量空间加上父类类变量空间,此时子类类变量空间就变成…...
Ubuntu24安装MongoDB(解压版)
目录 0.需求说明1.环境检查2.下载软件2.1.下载MongoDB服务端2.2.下载MongoDB连接工具(可略过)2.3.检查上传或下载的安装包 3.安装MongoDB3.1.编辑系统服务3.2.启动服务3.3.客户端连接验证3.3.1.创建管理员用户 4.远程访问4.1.开启远程访问4.2.开放防火墙 0.需求说明 问&#x…...
计算机毕业设计Python考研院校推荐系统 考研分数线预测 考研推荐系统 考研可视化(代码+LW文档+PPT+讲解视频)
温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…...
Python 爬虫框架对比与推荐
Python 爬虫框架对比与推荐 Python 爬虫框架对比与推荐1. Scrapy1.1 框架介绍1.2 优点1.3 缺点1.4 适用场景 2. PySpider2.1 框架介绍2.2 优点2.3 缺点2.4 适用场景 3. Selenium3.1 框架介绍3.2 优点3.3 缺点3.4 适用场景 4. BeautifulSoup Requests(自定义方案&am…...
本地DeepSeek模型GGUF文件转换为PyTorch格式
接前文,我们在本地Windows系统上,基于GGUF文件部署了DeepSeek模型(DeepSeek-R1-Distill-Qwen-1.5B.gguf版本),但是GGUF是已经量化的版本,我们除了对其进行微调之外,无法对其训练,那么还有没有其他办法对本地的GGUF部署的DeepSeek模型进行训练呢?今天我们就反其道而行之…...
自动化测试框架搭建-单次接口执行-三部曲
目的 判断接口返回值和提前设置的预期是否一致,从而判断本次测试是否通过 代码步骤设计 第一步:前端调用后端已经写好的POST接口,并传递参数 第二步:后端接收到参数,组装并请求指定接口,保存返回 第三…...
SAP F1搜索帮助 添加自定义功能按钮
最近deepseek 比较火,好多伙伴把deep seek 调用集成到SAP 系统,集成需要方便的去查询问题,方便一点就是添加在F1搜索帮助的地方,看到有朋友问看自定义按钮怎么添加在F1的工具栏,跟踪了下代码,尝试了下&…...
Webpack,Vite打包的理解
Webpack 和 Vite 都是现代前端开发中常用的构建工具,用于打包和优化项目代码。尽管它们的目标相似,但在设计理念、工作方式和适用场景上存在显著差异。 Webpack Webpack 是一个模块打包工具,主要用于将多个模块(如 JavaScript、…...
ollama部署大模型,本地调用
Ollama是一个强大的大型语言模型平台,它允许用户轻松地下载、安装和运行各种大型语言模型。在本文中,我将指导你如何在你的本地机器上部署Ollama,并展示如何使用Python进行简单的API调用以访问这些模型。 最近很多人在学习大模型的时候&…...
【ISO 14229-1:2023 UDS诊断(ECU复位0x11服务)测试用例CAPL代码全解析⑩】
ISO 14229-1:2023 UDS诊断【ECU复位0x11服务】_TestCase10 作者:车端域控测试工程师 更新日期:2025年02月18日 关键词:UDS诊断协议、ECU复位服务、0x11服务、ISO 14229-1:2023 TC11-010测试用例 用例ID测试场景验证要点参考条款预期结果TC…...
opencv实时二维码识别的一种实现与思路分享
在嵌入式平台上比如 rk3568 这种弱鸡的平台,要做到实时视频处理就非常鸡肋,不像英伟达那种 deepstrem 什么的。 开始的时候,我们使用python 下的 pyzbar + opencv opencv 读取摄像头的数据然后每帧送到 pyzbar 二维码识别函数里面进行处理,然后打印出识别的数字。结果,非常…...
【ISO 14229-1:2023 UDS诊断(ECU复位0x11服务)测试用例CAPL代码全解析⑫】
ISO 14229-1:2023 UDS诊断【ECU复位0x11服务】_TestCase12 作者:车端域控测试工程师 更新日期:2025年02月18日 关键词:UDS诊断协议、ECU复位服务、0x11服务、ISO 14229-1:2023 TC11-012测试用例 用例ID测试场景验证要点参考条款预期结果TC…...
Jenkins同一个项目不同分支指定不同JAVA环境
背景 一些系统应用,会为了适配不同的平台,导致不同的分支下用的是不同的gradle,导致需要不同的JAVA环境来编译,比如a分支需要使用JAVA11, b分支使用JAVA17。 但是jenkins上,一般都是Global Tool Configuration 全局所有环境公用一个JAVA_HOME。 尝试过用 Build 的Execut…...
小爱音箱连接电脑外放之后,浏览器网页视频暂停播放后,音箱整体没声音问题解决
背景 22年买的小爱音箱增强版play,小爱音箱连接电脑外放之后,浏览器网页视频暂停播放后,音箱整体没声音(一边打着游戏,一边听歌,一边放视频,视频一暂停,什么声音都没了,…...
AIGC(生成式AI)试用 21 -- Python调用deepseek API
1. 安装openai pip3 install openai########################## Collecting openaiUsing cached openai-1.61.1-py3-none-any.whl.metadata (27 kB) Collecting anyio<5,>3.5.0 (from openai)Using cached anyio-4.8.0-py3-none-any.whl.metadata (4.6 kB) Collecting d…...
使用linux脚本部署discuz博客(详细注释版)
使用脚本部署一个discuzz项目 1.显示当前环境状态 防火墙状态 selinux状态 httpd状态 由上可知,虚拟机已处于最初始状态 2.脚本编写 #!/bin/bash #这是一个通过脚本来部署discuzz博客 firewalld关闭 systemctl stop firewalld if [ $? -eq 0 ];then echo "…...
Kafka的生产者和消费者模型
Kafka的生产者和消费者模型是一种消息传递模式,以下是该模型的详细描述: 一、生产者(Producer) 定义:生产者是消息的生产者,它将消息发布到Kafka的主题(Topic)中。 功能࿱…...
调用openssl实现加解密算法
由于工作中涉及到加解密,包括Hash(SHA256)算法、HMAC_SHA256 算法、ECDH算法、ECC签名算法、AES/CBC 128算法一共涉及5类算法,笔者通过查询发现openssl库以上算法都支持,索性借助openssl库实现上述5类算法。笔者用的op…...
【Python项目】信息安全领域中语义搜索引擎系统
【Python项目】信息安全领域中语义搜索引擎系统 技术简介:采用Python技术、MYSQL数据库等实现。 系统简介:系统主要是围绕着语义搜索展开的,要将输入的文字在爬取数据时能够通过深层次的内涵理解,来更好的查找到与之相关的精准信息…...
快速排序_912. 排序数组(10中排序算法)
快速排序_912. 排序数组(10中排序算法) 1 快速排序(重点)报错代码超时代码修改官方题解快速排序 1:基本快速排序快速排序 2:双指针(指针对撞)快速排序快速排序 3:三指针快…...
BS5852英国家具防火安全条款主要包括哪几个方面呢?
什么是BS5852检测? BS5852是英国针对家用家具的强制性安全要求,主要测试家具在受到燃烧香烟和火柴等火源时的可燃性。这个标准通常分为四个部分进行测试,但实际应用中主要测试第一部分和第二部分,包括烟头测试和利用乙炔火焰模拟…...
高考或者单招考试需要考物理这科目
问题:帮忙搜索一下以上学校哪些高考或者单招考试需要考物理这科目的 回答: 根据目前获取的资料,明确提及高考或单招考试需考物理的学校为湖南工业职业技术学院,在部分专业单招时要求选考物理;其他学校暂未发现明确提…...
基于vue3实现的课堂点名程序
设计思路 采用vue3实现的课堂点名程序,模拟课堂座位布局,点击开始点名按钮后,一朵鲜花在座位间传递,直到点击结束点名按钮,鲜花停留的座位被点名。 课堂点名 座位组件 seat.vue <script setup>//组合式APIimpo…...
压力传感器
压力传感器是一种用于测量气体或液体压力的设备,广泛应用于工业控制、汽车电子、医疗设备、航空航天等领域。以下是关于压力传感器的详细介绍: 一、压力传感器的分类 1. 按测量原理分类 - 压阻式压力传感器: - 原理:利用压…...
Django REST Framework (DRF) 中用于构建 API 视图类解析
Django REST Framework (DRF) 提供了丰富的视图类,用于构建 API 视图。这些视图类可以分为以下几类: 1. 基础视图类 这些是 DRF 中最基础的视图类,通常用于实现自定义逻辑。 常用类 APIView: 最基本的视图类,所有其…...
DeepSeek介绍[Cache-Through、Cache-Around、Cache-Behind、Cache-Asid]
Cache-Through、Cache-Around、Cache-Behind和Cache-Aside是几种常见的缓存策略,每种策略有其独特的工作机制和应用场景。以下是对这些缓存模式的详细介绍: 1. Cache-Through 工作原理: 读操作:应用程序首先向缓存层请求数据。…...
React 前端框架介绍
什么是 React? React 是一个由 Facebook 开发并维护的开源 JavaScript 库,用于构建用户界面。它主要用于创建交互式用户界Face(UI),尤其是当数据变化时需要更新部分视图时非常有效。React 的核心思想是组件化和声明性…...
自制简单的图片查看器(python)
图片格式:支持常见的图片格式(JPG、PNG、BMP、GIF)。 import os import tkinter as tk from tkinter import filedialog, messagebox from PIL import Image, ImageTkclass ImageViewer:def __init__(self, root):self.root rootself.root.…...
基于Electron+Vue3创建桌面应用
Electron 是一个开源框架,基于 Chromium 和 Node.js,用于开发跨平台桌面应用程序。它允许开发者使用 HTML、CSS 和 JavaScript 等 Web 技术构建原生桌面应用,支持 Windows、macOS 和 Linux。Electron 以其开发便捷性、强大的功能和丰富的生态系统而广泛应用于工具类应用、媒…...
Redis实战-扩展Redis
扩展Redis 1、扩展读性能2、扩展写性能和内存容量3、扩展复杂的查询3.1 扩展联合查询3.2 扩展分片排序 如有侵权,请联系~ 如有错误,也欢迎批评指正~ 本篇文章大部分是来自学习《Redis实战》的笔记 1、扩展读性能 单台Redis服务器…...
Vue 前端开发中的路由知识:从入门到精通
文章目录 引言1. Vue Router 简介1.1 安装 Vue Router1.2 配置 Vue Router1.3 在 Vue 实例中使用 Vue Router 2. 路由的基本用法2.1 路由映射2.2 路由视图2.3 路由链接 3. 动态路由3.1 动态路径参数3.2 访问动态参数3.3 响应路由参数的变化 4. 嵌套路由4.1 定义嵌套路由4.2 渲染…...
为AI聊天工具添加一个知识系统 之109 详细设计之50 三性三量三境
本文要点 纵观整个讨论过程 最初我提“相得益彰的三性(三性) 相提并论的三者(三量) 相映成趣的三化(三境)” “ 确定 今天的讨论题-- “我”的知识树:相得益彰的三性(即 三性&…...
51-ArrayList
51-ArrayList Collection 类型介绍 仓颉中常用的几种基础 Collection 类型,包含 Array、ArrayList、HashSet、HashMap。 可以在不同的场景中选择适合对应业务的类型: Array:如果不需要增加和删除元素,但需要修改元素ÿ…...
工业制造能耗管理新突破,漫途MTIC-ECM平台助力企业绿色转型!
在工业制造领域,能源消耗一直是企业运营成本的重要组成部分。随着“双碳”目标的推进,如何实现高效能耗管理,成为制造企业亟待解决的问题。漫途MTIC-ECM能源能耗在线监测平台,结合其自研的硬件产品,为工业制造企业提供…...
sql注入之python脚本进行时间盲注和布尔盲注
一、什么是时间盲注和布尔盲注? 答:时间盲注是攻击者通过构造恶意sql语句利用sleep()等延迟函数来观察数据库响应时间差异来进行推断信息和条件判断。如果条件为真,数据库会执行延时操作,如果为假则立即返回。响应时间较短。 SELE…...
map的使用(c++)
在了解map之前,我们先看看两个场景,通过这两个场景的对比,让我们知道为什么要存在存储双关键字的容器 场景一:判断一堆字符串中,某一个字符串是否出现过 在没学set容器之前,我们只能想到把这一堆字符串存到…...
Android13-包安装器PackageInstaller-之apk安装流程
目的 我们最终是为了搞明白安装的整个流程通过安卓系统自带的包安装器来了解PMS 安装流程实现需求定制:静默安装-安装界面定制-安装拦截验证。【核心目的】 安装流程和PMS了解不用多说了; 安装定制相关: 如 手机上安装时候弹出锁屏界面需要输入密码;安…...
前端函数在开发环境与生产环境中处理空字符串的差异及解决方案
在前端开发过程中,我们经常会遇到一些函数在开发环境中运行正常,但在生产环境中却出现报错的情况。本文将通过具体的代码示例和分析,探讨一个函数在开发环境和生产环境中处理空字符串的差异,并提供解决方案。 1. 问题描述 我们有…...
数智读书笔记系列014 MICK《SQL进阶教程》第一版和第二版对比和总结
引言 在当今数字化时代,数据已成为企业和组织的核心资产之一。而 SQL(Structured Query Language)作为管理和操作关系型数据库的标准语言,其重要性不言而喻。无论是数据查询、插入、更新还是删除,SQL 都能高效地完成任务,广泛应用于数据分析、数据挖掘、数据仓库、Web 开…...
智能猫眼实现流程图
物理端开发流程图 客户端端开发流程图 用户功能开发流程图 管理员开发流程图...
docker安装kafka,并通过springboot快速集成kafka
目录 一、docker安装和配置Kafka 1.拉取 Zookeeper 的 Docker 镜像 2.运行 Zookeeper 容器 3.拉取 Kafka 的 Docker 镜像 4.运行 Kafka 容器 5.下载 Kafdrop 6.运行 Kafdrop 7.如果docker pull wurstmeister/zookeeper或docker pull wurstmeister/kafka下载很慢&#x…...
Spring Boot 中自动装配机制的原理
Spring Boot 的自动装配机制是其核心特性之一,它简化了 Spring 应用的配置,让开发者能够快速构建应用。以下是对其原理的详细总结: 1. 核心概念 自动装配 (Auto-configuration): Spring Boot 根据应用依赖和配置,自动配置 Spring…...
python继承中super() 不是简单的“调用父类”,而是调用 MRO 里的下一个类
Python 里的一个类可以同时继承多个父类。这让我们的模型设计变得更灵 活,但同时也带来一个新问题:“在复杂的继承关系下,如何确认子类的 某个方法会用到哪个父类?” 这里有点需要理解: MRO(方法解析顺序…...