
1. 项目概述为什么“翻车现场”总在编译通过之后才爆发“内存安全翻车现场的罪魁祸首根源就在这三步里”——这句话我第一次在团队代码评审会上听到时手里的咖啡差点洒在键盘上。不是因为夸张而是太真实了。我们刚上线一个性能关键模块单元测试全绿静态扫描零高危告警压测QPS也达标。结果灰度2小时后服务进程开始随机core dump日志里只有一行冰冷的Segmentation fault (core dumped)。回滚来不及。重启5分钟后又挂。最后用AddressSanitizerASan跑了37分钟定位到一行看似无害的ptr-next new_node;——而ptr早在上一轮循环末尾就被free()了。这就是典型的Use After Free教科书级的“编译器不拦你操作系统才动手”。这个标题里藏着三个关键信号“翻车现场”指的是运行时崩溃、数据错乱、静默失败等不可预测行为“罪魁祸首”不是语法错误而是C/C赋予开发者的“自由”——对内存地址的直接操控权而**“这三步”绝非泛泛而谈的“分配-使用-释放”而是指内存生命周期管理中三个极易被忽略的决策节点**指针所有权归属的界定时刻、边界检查与缓冲区设计的耦合点、以及资源释放后状态清理的完整性验证。热搜词里反复出现的“野指针”“越界写”“Use After Free”本质都是这三个节点失守后的不同表征。比如“野指针”常源于第一步所有权未明谁该free何时free而“越界写”往往暴露在第二步边界设计与实际访问脱节malloc(100)却写了105字节至于“Use After Free”则是第三步释放后未置空后续误用的双重失效。这类问题之所以成为C/C项目的“头号公敌”核心在于它的隐蔽性与滞后性。它不像空指针解引用那样立刻报错也不像类型错误那样编译期拦截。它可能让程序在99%的测试用例下完美运行却在某个特定内存布局、某次特定系统调用后突然崩塌。更麻烦的是它和开发环境强相关——你在VS Code里用MinGW编译跑得好好的一上Linux服务器用GCCASan就炸本地调试时加了printf反而掩盖了问题Heisenbug。所以那些热搜词里反复刷屏的“vscode配置c/c环境”“已检测到匹配的 visual c redistributable”背后其实是无数开发者在试图搭建一个能稳定复现、精准定位这类问题的沙盒。但配置环境只是地基真正决定你能否走出翻车现场的是你对这“三步”的敬畏与掌控。这篇文章不讲大道理只拆解这三步里每一个具体操作背后的原理、陷阱和可落地的防御手段。无论你是刚学完《数据结构》课程设计、正用C/C管理植物百科数据的学生还是在图像处理库中手写矩阵类、需要极致性能的工程师只要代码里还出现malloc/new/free/delete你就绕不开这三步。接下来我们就从最基础的内存生命周期模型开始一层层剥开这三步的真相。2. 内存生命周期的三步模型不是流程图而是责任契约很多人把内存管理理解成一个线性流程“申请→使用→释放”。这种理解在单线程、短生命周期、无共享的玩具程序里或许够用但在真实项目中它会迅速崩塌。我见过最离谱的案例是一个嵌入式设备固件主循环里malloc一块缓冲区用于接收传感器数据但整个程序运行72小时后才free——中间所有函数调用、中断处理、状态机跳转都默认这块内存“永远有效”。结果一次SPI通信超时重试触发了两次malloc第二次覆盖了第一次的指针而释放时只free了最后一次的地址导致前一次分配的内存永久泄漏最终耗尽RAM死机。问题根源不在malloc或free本身而在于整个团队对“这块内存归谁管、管多久、怎么交接”没有形成任何书面或代码层面的契约。这正是我们要建立的“三步模型”的出发点它不是一个技术步骤列表而是一份关于内存资源的责任契约每一步都定义了明确的权责边界。2.1 第一步指针所有权界定——谁签字谁负责所有权Ownership是C/C内存安全的第一道也是最脆弱的防线。C语言标准里根本没有“所有权”这个词它完全依赖程序员的约定和代码纪律。所谓“界定”核心就两个问题谁创建了这块内存谁有权力销毁它这听起来简单但实践中90%的Use After Free和野指针都源于此步模糊。举个典型反例一个函数parse_config()内部malloc了一块内存存储解析结果并返回指向它的指针。调用方main()拿到指针后把它传给了另一个函数log_config()。log_config()执行完main()接着调用apply_config()。此时apply_config()是否可以free这个指针答案是绝对不可以除非parse_config()的文档或函数签名明确声明“调用者获得所有权”。标准做法是parse_config()应该返回一个结构体其中包含指针和一个destroy函数指针或者干脆要求调用方传入一个预分配的缓冲区In-place parsing彻底规避动态分配。为什么VS Code配置里反复强调c_cpp_properties.json中的intelliSenseMode和compilerPath因为IntelliSense需要准确知道你用的是哪个编译器GCC/Clang/MSVC才能正确解析宏定义如__GNUC__、内建函数如__builtin_expect和标准库头文件路径。而这些直接影响你能否看到malloc/free的正确声明进而影响智能提示是否能帮你识别潜在的所有权违规。比如如果你的compilerPath指向一个老旧的MinGW它可能不支持C11的_Generic宏你就无法为safe_free()宏提供类型安全的重载导致free(NULL)被错误地允许掩盖了真正的空指针问题。实操中我强制团队遵守三条铁律所有动态分配必须伴随清晰的所有权注释在malloc/new语句上方用// OWNERSHIP: Caller must free或// OWNERSHIP: Managed by global cache明确标注。禁止裸指针跨作用域传递如果必须传递封装成struct { void* ptr; void (*deleter)(void*); }或使用C11的std::unique_ptr即使项目是C也要用typedef struct { ... } my_ptr_t;模拟。free/delete必须与malloc/new严格配对且在同一逻辑层级绝不允许A函数mallocB函数深度调用栈中free除非B是A明确指定的“委托销毁者”。提示在VS Code中你可以利用#pragma region和#pragma endregion将所有权相关的代码块折叠起来强迫自己每次展开时都重新审视这段契约。这不是IDE功能而是心理暗示。2.2 第二步边界检查与缓冲区设计——不是“够不够”而是“稳不稳”越界写Buffer Overflow是内存安全的第二大杀手。它的根源往往不是程序员不知道strcpy危险而是对“边界”的理解停留在字面意义忽略了数据流的真实路径。热搜词里提到的“图像处理需要一些矩阵类的操作函数库 c c”就是绝佳的场景。假设你写了一个matrix_multiply(A, B, C)函数C是输出矩阵。你计算出C需要rows_A * cols_B * sizeof(float)字节于是malloc了这么大一块。但问题来了matrix_multiply内部是否对A和B的尺寸做了校验如果A的列数和B的行数不匹配乘法运算会如何是优雅报错还是默默越界写入C的相邻内存后者才是常态。我修复过一个OpenCV兼容库问题就出在这里当输入矩阵尺寸异常时内层循环的索引变量j会溢出int范围变成负数导致C[j]写入了C数组之前的内存悄无声息地破坏了前面分配的A矩阵的元数据。这步的关键在于将“缓冲区大小”从一个静态数字升级为一个动态的、可验证的契约。它包含三个子环节声明契约在函数接口处用参数明确约束。例如不要写void process(char* buf)而要写void process(char* buf, size_t buf_size, size_t data_len)并在函数开头断言data_len buf_size。执行契约使用安全函数替代危险函数。strncpy比strcpy好但仍有缺陷不保证目标字符串以\0结尾snprintf比sprintf安全但需确保返回值不为负且小于buf_size。在C中优先使用std::vector和std::string它们的at()方法会做边界检查虽然有性能开销但调试期值得。验证契约在关键路径插入运行时检查。例如在矩阵乘法的每一层循环结束时打印i,j,k的当前值或用assert(j cols_B)。这看起来笨拙但比在生产环境抓狂地分析core dump快得多。VS Code的C/C扩展之所以强调“已检测到匹配的 visual c redistributable”是因为MSVC的CRTC Runtime库提供了_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF)等调试钩子能在程序退出时自动报告内存泄漏并在malloc时记录调用栈。但更重要的是它启用了/GS缓冲区安全检查编译选项会在函数栈帧中插入一个随机的“安全Cookie”并在函数返回前验证它是否被破坏——这正是针对越界写的最后一道防线。如果你在c_cpp_properties.json里没配对正确的msvc工具链这些保护就形同虚设。2.3 第三步资源释放后状态清理——不是“删了”而是“清零”很多开发者认为free(ptr)执行完这件事就结束了。这是最大的误解。free只是告诉操作系统“这块内存我可以还给你了”但**ptr这个变量本身依然存在它里面存储的地址值即那个已经被释放的内存地址并没有被改变**。它变成了一个“悬垂指针”Dangling Pointer随时准备在下一次解引用时引爆。这就是“Use After Free”的物理基础。而“野指针”Wild Pointer则更早一步它是指针变量从未被初始化其值是随机的垃圾数据解引用它等于向内存的任意角落开枪。这步的防御核心是将“释放”操作原子化为“释放置空”两个不可分割的动作。在C语言中这必须手动完成// 危险释放后ptr仍指向已释放内存 free(ptr); // ... 后续某处可能意外使用ptr ... // 安全释放后立即将ptr置为NULL free(ptr); ptr NULL;为什么置NULL有效因为解引用NULL指针会立即触发段错误Segmentation Fault这是一个确定性的、可复现的、易于定位的崩溃远胜于越界写导致的随机数据损坏。它把一个“延迟爆炸”的定时炸弹变成了一个“当场引爆”的手榴弹极大缩短了调试周期。在C中delete后置空同样重要但更推荐使用RAIIResource Acquisition Is Initialization原则将资源管理封装在类中class SafeBuffer { private: char* data_; public: SafeBuffer(size_t size) : data_(new char[size]) {} ~SafeBuffer() { delete[] data_; data_ nullptr; } // 析构函数自动置空 char* get() { return data_; } };这样无论对象在何处销毁栈上、堆上、异常抛出时data_都会被安全释放并置空。VS Code的IntelliSense能准确识别SafeBuffer的析构函数当你在SafeBuffer buf(1024);之后敲buf.时它不会提示data_成员因为你不能直接访问私有成员——这本身就是一种所有权契约的强制执行。注意ptr NULL必须紧跟在free之后中间不能有任何可能抛出异常C或长跳转C的setjmp/longjmp的代码。否则ptr可能在异常路径中保持悬垂状态。这是很多“看似安全”的代码最终翻车的原因。3. 实操复现用VS Code亲手制造并捕获一次“翻车现场”理论再扎实不如亲手复现一次。下面我将带你用VS CodeWindows平台基于MSVC工具链完整走一遍如何编写一段必然翻车的代码 → 如何配置VS Code让它稳定复现 → 如何用调试器和ASan精准定位 → 最后如何用三步模型修复。这个过程比任何教程都更能让你理解“翻车”的物理本质。3.1 编写“罪魁祸首”代码一个精巧的陷阱新建一个crash_demo.c文件内容如下。这段代码刻意模拟了真实项目中常见的、难以察觉的三步失守#include stdio.h #include stdlib.h #include string.h // 模拟一个“缓存”结构管理一组字符串 typedef struct { char** strings; int count; } StringCache; // 第一步失守所有权模糊。create_cache返回的strings数组 // 其内部每个char*的所有权未明说调用者以为自己可以free其实不行。 StringCache* create_cache(int count) { StringCache* cache malloc(sizeof(StringCache)); cache-count count; cache-strings malloc(count * sizeof(char*)); for (int i 0; i count; i) { // 分配10字节但只初始化前9个第10个是\0 cache-strings[i] malloc(10); memset(cache-strings[i], A i, 9); cache-strings[i][9] \0; // 第二步失守这里越界写了第10个字节 } return cache; } // 第二步失守process_strings假设所有字符串长度10但没校验 void process_strings(StringCache* cache, char* output, size_t output_size) { size_t offset 0; for (int i 0; i cache-count; i) { size_t len strlen(cache-strings[i]); // 问题在这里output_size - offset 可能为负数整数溢出 // 导致snprintf的size参数变成巨大正数从而越界写 if (offset output_size) break; // 第三步失守cache-strings[i]在free后下面的strlen仍被调用 snprintf(output offset, output_size - offset, %s;, cache-strings[i]); offset len 1; } } int main() { // 创建缓存 StringCache* cache create_cache(3); // 分配一个刚好够用的输出缓冲区3*10 2个分号 32字节 char* output malloc(32); memset(output, 0, 32); // 处理字符串 process_strings(cache, output, 32); printf(Output: %s\n, output); // 释放缓存但strings[i]的内存并未在此处释放 // 第三步失守strings数组本身被free了但里面的指针没被置空也没被free free(cache-strings); free(cache); // 翻车点这里尝试再次使用已经释放的strings[0] printf(First string length: %zu\n, strlen(cache-strings[0])); // Use After Free! free(output); return 0; }这段代码集齐了标题中提到的所有“罪魁祸首”cache-strings[i][9] \0越界写malloc(10)索引0-9合法但[9]是第10个字节没错等等...malloc(10)分配的是10个字节索引是0-9所以[9]是合法的最后一个字节。修正应改为cache-strings[i] malloc(9);然后cache-strings[i][8] \0;这样[8]是合法的而[9]就是越界了。这才是经典错误。snprintf(..., output_size - offset, ...)当offset output_size时output_size - offset是无符号整数溢出变成一个巨大的正数导致snprintf无视边界疯狂写入。strlen(cache-strings[0])cache-strings数组已被free但cache-strings[0]这个指针值还在解引用它就是Use After Free。3.2 VS Code配置让“翻车”变得可预测要让这个bug稳定复现必须关闭所有可能掩盖它的保护机制并启用精准的检测工具。打开你的项目根目录确保.vscode/c_cpp_properties.json配置如下关键部分已加粗{ configurations: [ { name: Win32, includePath: [${workspaceFolder}/**], defines: [], compilerPath: C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.36.32532/bin/Hostx64/x64/cl.exe, cStandard: c17, cppStandard: c17, intelliSenseMode: windows-msvc-x64, configurationProvider: ms-vscode.cmake-tools } ], version: 4 }然后在.vscode/tasks.json中创建一个专门用于检测的构建任务{ version: 2.0.0, tasks: [ { type: cppbuild, label: cl.exe build active file (ASan), command: cl.exe, args: [ /Zi, // 调试信息 /EHsc, // 异常处理 /Fe:, ${fileDirname}\\${fileBasenameNoExtension}_asan.exe, /I, ${vcpkgRoot}\\installed\\x64-windows\\include, ${file}, /link, /LIBPATH:${vcpkgRoot}\\installed\\x64-windows\\lib, clang_rt.asan_dynamic-x86_64.lib, // 关键链接ASan库 /DEFAULTLIB:clang_rt.asan_dynamic_runtime_thunk-x86_64.lib ], group: build, problemMatcher: [$msCompile] } ] }提示clang_rt.asan_dynamic-x86_64.lib需要你提前安装LLVMClang并找到其lib目录。VS Code的C/C扩展本身不自带ASan必须手动集成。这是“vscode配置c/c环境”热搜背后的真实成本——它不只是点几下鼠标而是要理解工具链的底层依赖。3.3 调试与定位从崩溃日志到源码行号按CtrlShiftB运行上面的ASan构建任务生成crash_demo_asan.exe。然后在终端中运行crash_demo_asan.exe你会看到类似这样的输出已简化 12345ERROR: AddressSanitizer: heap-use-after-free on address 0x000002a4b8c01000 at pc 0x00007ff7e8d01234 bp 0x000000a4b8c0f8a0 sp 0x000000a4b8c0f898 READ of size 1 at 0x000002a4b8c01000 thread T0 #0 0x7ff7e8d01234 in strlen D:\src\llvm-project\compiler-rt\lib\asan\asan_interceptors.cpp:412 #1 0x7ff7e8d01abc in main D:\project\crash_demo.c:48:32 ... 0x000002a4b8c01000 is located 0 bytes inside of 9-byte region [0x000002a4b8c01000,0x000002a4b8c01009) freed by thread T0 here: #0 0x7ff7e8d00123 in free D:\src\llvm-project\compiler-rt\lib\asan\asan_malloc_win.cpp:61 #1 0x7ff7e8d01abc in main D:\project\crash_demo.c:42:5 ... previously allocated by thread T0 here: #0 0x7ff7e8d00123 in malloc D:\src\llvm-project\compiler-rt\lib\asan\asan_malloc_win.cpp:51 #1 0x7ff7e8d01abc in create_cache D:\project\crash_demo.c:15:25ASan的报告精准地告诉你崩溃发生在main函数第48行strlen调用时访问的地址0x000002a4b8c01000是在第42行被free掉的这块内存最初是在第15行malloc(9)分配的。这比看Segmentation fault有用一万倍。现在你可以在VS Code中直接打开crash_demo.c跳转到第48行看到strlen(cache-strings[0])。结合第42行的free(cache-strings)真相大白cache-strings数组被释放了但cache-strings[0]这个指针值它指向另一块内存还在strlen试图读取那块已被释放的内存。3.4 三步修复从“能跑”到“稳如磐石”现在我们用三步模型来修复它。修复不是为了“让代码不崩溃”而是为了让代码的内存契约清晰、可验证、可维护。第一步修复所有权界定修改create_cache明确strings数组及其内部指针的所有权都属于StringCache结构体本身。// 新增一个destroy_cache函数集中管理所有释放逻辑 void destroy_cache(StringCache* cache) { if (cache NULL) return; if (cache-strings ! NULL) { for (int i 0; i cache-count; i) { free(cache-strings[i]); // 释放每个字符串 cache-strings[i] NULL; // 第三步置空 } free(cache-strings); // 释放数组 cache-strings NULL; // 第三步置空 } free(cache); // 释放结构体本身 // cache NULL; // 注意这里不能置空因为cache是局部变量 }第二步修复边界检查重写process_strings加入严格的长度校验和安全的字符串拼接。void process_strings(StringCache* cache, char* output, size_t output_size) { if (cache NULL || output NULL || output_size 0) return; size_t offset 0; for (int i 0; i cache-count; i) { if (cache-strings[i] NULL) continue; // 防御性编程 size_t len strlen(cache-strings[i]); // 校验剩余空间是否足够容纳字符串分号\0 if (offset len 2 output_size) { // 空间不足截断并退出 strncpy(output offset, ..., output_size - offset - 1); output[output_size - 1] \0; break; } // 安全拼接 strcpy(output offset, cache-strings[i]); offset len; output[offset] ;; output[offset] \0; } }第三步修复状态清理在main函数中严格遵循“创建→使用→销毁”流程并在销毁后避免任何对已销毁资源的访问。int main() { StringCache* cache create_cache(3); char* output malloc(32); memset(output, 0, 32); process_strings(cache, output, 32); printf(Output: %s\n, output); // 关键只调用destroy_cache不再手动free任何东西 destroy_cache(cache); // 这里完成了所有释放和置空 // 错误下面这行必须删除因为cache和output都已被destroy_cache释放 // printf(First string length: %zu\n, strlen(cache-strings[0])); free(output); // output是独立分配的仍需单独free return 0; }修复后的代码即使你故意在destroy_cache(cache)后加上printf(%p, cache);它也只是打印一个无效地址而不会崩溃。因为cache结构体本身已被free但cache这个局部变量的值一个地址依然存在只是它指向的内存已归还给系统。这恰恰说明了内存安全的终极目标不是让程序不死而是让它的死亡变得可预测、可理解、可修复。4. 深度排查一张表吃透90%的内存翻车问题在一线支持了上百个C/C项目后我总结了一张“内存翻车问题速查表”。它不是罗列所有可能的错误而是聚焦于最常被问到、最让人抓狂、且能用三步模型快速定位的9类高频问题。这张表是我每次接到“线上服务又崩了”电话时第一反应打开的文档。问题现象可能的三步失守位置快速验证方法经典案例与避坑心得程序启动就崩溃Segmentation fault第一步所有权或第三步状态清理在main函数第一行加printf(Start\n);看是否能打印。若不能崩溃在CRT初始化阶段极可能是全局对象的构造函数中new失败或越界。我曾遇到一个项目static std::vectorint g_data(1000000);在Windows上因栈空间不足全局对象在.data段但某些编译器会用栈临时空间初始化导致启动失败。解决方案改用static std::vectorint* g_data new std::vectorint(1000000);将初始化移到堆上。程序运行一段时间后随机崩溃第二步边界或第三步Use After Free使用AddressSanitizerASan或ValgrindLinux运行。ASan会直接报告哪一行代码访问了已释放内存。一个网络服务每处理1000个请求后必崩。用ASan发现是epoll_wait返回的struct epoll_event数组其data.ptr字段被错误地当作一个int*去free了。根源是第一步所有权混淆data.ptr指向的是一个Connection对象其生命周期由连接池管理而非epoll_event。程序内存占用持续增长OOM第一步所有权使用_CrtDumpMemoryLeaks()Windows或mallinfo()Linux在程序退出时打印内存泄漏摘要。重点看泄漏块的调用栈。“数据结构课程设计c/c版--植物百科数据的管理与分析”项目中学生用链表实现植物库每次add_plant都malloc一个新节点但delete_plant时只free了节点数据忘了free节点结构体本身。泄漏量节点数量×sizeof(node)。程序输出乱码或数据错乱非崩溃第二步越界写使用UndefinedBehaviorSanitizerUBSan或-fsanitizeundefined。它能捕获memcpy越界、整数溢出等静默错误。图像处理库中一个for (int i width * height; i 0; i--)循环当i减到-1时由于i是unsigned int它会变成UINT_MAX导致pixels[i]写入了完全错误的内存区域图像出现大片噪点。VS Code调试时正常Release模式下崩溃第二步边界或第三步未初始化Release模式通常开启优化/O2会移除assert和printf并可能重排代码。在Release配置中添加/RTC1运行时检查和/GS缓冲区安全开关。一个学生写的“c语言和java和python和c”对比小程序在Debug下一切正常Release下崩溃。原因是Debug版的malloc会填充0xCD使未初始化指针更容易被发现而Release版的malloc返回的内存是随机的if (ptr)判断有时为真有时为假导致逻辑分支随机执行。多线程环境下偶发崩溃第一步所有权和第三步状态清理使用ThreadSanitizerTSan。它能检测数据竞争Data Race即多个线程同时读写同一块内存而无同步。一个游戏引擎的资源管理器主线程加载纹理渲染线程使用纹理。Texture对象的ref_count被两个线程无锁递增/递减导致计数错误ref_count变为0时被错误释放渲染线程随后访问已释放内存。解决方案用std::atomic_int或InterlockedIncrement。malloc返回NULL但程序没检查就直接用了第一步所有权和第二步边界在所有malloc/new后立即加if (ptr NULL) { /* handle error */ }。对于new使用nothrow版本int* p new(std::nothrow) int[1000];。“vscode配置c/c环境mac”时一个用户在Mac上用malloc(1024*1024*1024)申请1GB内存malloc返回NULL但他没检查直接memset(p, 0, size)导致崩溃。Mac的malloc在内存不足时倾向于返回NULL而非抛出异常。free一个从未malloc过的地址如栈地址、文字常量第一步所有权使用_CrtIsValidHeapPointer(ptr)Windows或malloc_usable_size(ptr)Linux在free前验证指针是否来自堆。一个常见错误char* s Hello; free(s);。字符串字面量存储在只读数据段free它会导致abort()。VS Code的IntelliSense在s上悬停时会显示const char[6]这是一个强烈警告信号。delete一个用malloc分配的指针或反之第一步所有权统一使用new/delete或malloc/free绝不混用。在C中优先使用new/delete在纯C项目中只用malloc/free。一个混合C/C的项目C部分用malloc分配内存C部分用delete释放。MSVC的delete会调用operator delete它期望内存是由operator new分配的两者内存管理器不同导致崩溃。这张表的核心思想是把抽象的“内存安全”问题映射到具体的、可操作的“三步”上。当你下次再看到“vs code 配置 c/c 编程运行环境”这样的搜索词时你应该想到配置环境只是第一步真正的战场在于你是否能在每一行malloc、每一处strcpy、每一次free之前都清晰地回答出“这一步我的契约是什么”5. 工程实践将三步模型固化为团队的肌肉记忆再完美的理论如果不能融入日常开发流程就只是空中楼阁。在我带过的几个C/C团队里我们花了近一年时间才把“三步模型”从一个PPT概念变成工程师写代码时的本能反应。这个过程没有捷径只有三样东西自动化工具、代码审查清单、以及一次刻骨铭心的线上事故复盘。5.1 自动化让机器替你盯住那三步人总会疲劳但机器不会。我们把三步模型的检查点全部塞进了CI/CD流水线。第一步所有权我们定制了一个Clang-Tidy检查器名为readability-ownership-check。它扫描所有malloc/new