☰
cppcheck 的 memsetClass 检查器:如何在 C++ 中检测对非 POD 类使用 memset/memcpy 的未定义行为
2026/10/5 1:45:42 网站建设 项目流程
  • 开发工具
  • 静态分析
  • 代码质量
  • 质量保障

【免费下载链接】cppcheck

static analysis of C/C++ code

项目地址:https://gitcode.com/gh_mirrors/cpp/cppcheck
点击查看免费下载

导读

在 C++ 中,对包含std::string、std::vector、虚函数等非平凡成员的类型使用memset()、memcpy()或memmove(),会绕过构造函数、析构函数与拷贝语义,直接按字节覆写对象内部状态,属于未定义行为。cppcheck 提供的memsetClass检查器专门识别这类隐患,并在默认配置下以Error(错误)级别报告。本文以 cppcheck 仓库中的 memsetClass 检查器文档 为主线,结合 checkclass.cpp 源码与 testclass.cpp 测试用例,讲解该检查器的触发条件、检测原理、修复方案及其三个变体(memsetClassReference、memsetClassFloat、mallocOnClassError)的差异,帮助读者在实际项目中快速定位并修复此类未定义行为。

检查器速览

memsetClass的核心元信息(见 man/checkers/memsetClass.md)如下:

项目值
MessageUsing 'memset' on class that contains a 'std::string'.
CategoryUndefined Behaviour(未定义行为)
SeverityError(错误)
LanguageC++

从源码角度看,该报告对应 CWE 编号CWE-762(Mismatched Memory Management Routines,内存管理例程不匹配),见 checkclass.cpp 第 48 行 的 CWE 定义与 第 1642 行 的reportError调用,上报确定性为Certainty::normal。

为什么要禁止对非 POD 类型使用 memset

原始内存函数(memset等)对 C++ 对象语义一无所知——它们只负责把一段内存按字节覆写为指定值。而std::string内部维护着指针/长度等簿记信息(heap 指针、size、capacity),其构造、析构和拷贝都需要执行特定的代码逻辑来维持自身不变量。

当用memset(&fred, 0, sizeof(Fred))这类调用覆写整个对象时:

  • 构造函数被绕过:对象从未被正确初始化,成员处于"半构造"状态;
  • 析构函数被绕过:std::string原本在析构时释放的堆内存将泄漏;
  • 拷贝/赋值语义被绕过:引用计数、指针管理等内部机制全部失效;
  • 对象状态被破坏:把 string 内部的指针清零后,后续任何访问该成员的操作(析构、赋值、比较)都会触发崩溃或内存损坏。

这正是 cppcheck 将其归类为Undefined Behaviour的原因:对象被按字节抹零后,其生命周期语义已完全被破坏。

触发场景与检测原理

检测入口

该检查由CheckClassImpl::checkMemset()实现(checkclass.cpp 第 1450 行),逐一遍历符号数据库中的函数作用域,匹配形如memset|memcpy|memmove (的调用(第 1456 行),因此三个函数都会被检测到。

如何判定对象类型

判定逻辑集中在checkMemsetType()(第 1546 行),主要途径包括:

  1. 从sizeof()参数推导类型:当第三个参数是sizeof(Fred)、sizeof(n1::Fred)、sizeof(struct X)等形式时,直接从 sizeof 表达式提取类型(第 1468-1480 行);
  2. 从对象表达式推导类型:第一个参数为&fred、fred、*ptr等时,通过变量符号解析其类型作用域(第 1483-1509 行);
  3. 成员函数内this:识别memset(this, 0, sizeof(*this))模式,通过findFunctionOf()找到所属类(第 1440-1448 行、第 1481-1482 行)。

触发条件清单

checkMemsetType()对目标类型递归检查基类(第 1556-1560 行),并遍历成员变量(第 1573 行),满足以下任一条件即报错:

  • 含有非静态、非 const 的 STL 类型成员(std::string、std::vector等),排除std::array以及被库配置标记为 POD 的类型(第 1594 行);
  • 含有虚函数(virtual关键字,如虚析构函数),因为虚表指针会被覆写(第 1563-1569 行);
  • 含有引用成员:走memsetClassReference变体(见下文);
  • 含有浮点成员:走memsetClassFloat变体(仅对memset,见下文)。

同时存在豁免规则(测试用例 testclass.cpp 第 3055-3064 行 验证):

  • 空类(无成员)不会报错;
  • 成员为指针类型(如std::string * b;)不会报错——指针本身是 POD,不会破坏对象语义;
  • 静态成员、const 成员、数组元素为指针的成员会被跳过(第 1578-1579 行)。

复现与验证:最小示例

原文档给出的"Before"示例就是最直接的复现(man/checkers/memsetClass.md):

#include <cstring> #include <string> class Fred { public: std::string b; }; void f() { Fred fred; memset(&fred, 0, sizeof(Fred)); // <- memsetClass: bypasses std::string's own management }

cppcheck 对这段代码的输出为:

[test.cpp:8:5]: (error) Using 'memset' on class that contains a 'std::string'. [memsetClass]

这与测试套件 testclass.cpp 第 3075 行 中checkNoMemset用例断言的输出完全一致。同类场景还有:

  • 在成员函数内memset(this, 0, sizeof(*this))(测试 3088-3095 行);
  • 通过基类继承获得非 POD 成员(class Pebbles : public Fred {};,测试 3118-3128 行);
  • 命名空间限定类型n1::Fred(测试 3182-3193 行);
  • 含虚析构函数的类(测试 3130-3139 行);
  • 同一std::vector结构体上多处 memset 各报一次(测试 3268-3273 行)。

命令行验证方式

在已构建 cppcheck 的机器上,可直接对示例文件运行(默认即启用 error 级别):

cppcheck --enable=all --language=c++ --std=c++11 test.cpp

也可单独指定该检查器相关的 id(error 级别默认开启,无需额外开关)。

如何修复

原文档给出的"After"示例体现了修复的核心思路:直接用对象语义完成"清零"需求——依靠构造函数创建已初始化的Fred对象,而不是用字节覆写:

#include <string> class Fred { public: std::string b; }; void f() { Fred fred; }

针对不同场景的推荐修复方式:

场景推荐做法
需要"重置"一个对象为初始状态使用赋值fred = Fred();或fred = {};(走拷贝/移动语义)
需要清空容器使用str.clear()、vec.clear()等成员函数
需要重新初始化在对象作用域内重新构造,或使用placement new(需配合显式析构调用)
仅清零 POD 部分只对std::memset于 POD 子对象/数组成员,不覆盖非 POD 成员

三个关联变体:同源问题的不同侧面

原文档末尾列出的两个关联检查器,与主检查器共享相同的底层失误(用字节覆写破坏类型不变量),但针对不同的成员类型、使用不同的严重级别与 CWE:

memsetClassReference:含引用成员的类

  • 报告 id:memsetClassReference,Severity 为Error(checkclass.cpp 第 1645-1650 行);
  • 对应 CWE-665(Improper Initialization,不正确的初始化);
  • 触发路径:成员变量isReference() && !isStatic()(第 1574-1576 行);
  • 消息形如:Using 'memset' on class that contains a reference.
  • 文档见 man/checkers/memsetClassReference.md。

memsetClassFloat:含浮点成员的类

  • 报告 id:memsetClassFloat,Severity 为Portability(可移植性)(checkclass.cpp 第 1652-1658 行);
  • 对应 CWE-758(Reliance on Undefined, Unspecified, or Implementation-Defined Behavior,依赖未定义/未指定/实现定义行为);
  • 仅对memset触发,memcpy/memmove不在其列(第 1606 行);
  • 原因是浮点数的内存表示是实现定义的,全字节置零在 IEEE-754 兼容实现上恰好等于0.0,但不可移植;
  • 文档见 man/checkers/memsetClassFloat.md。

关联:mallocOnClassError / mallocOnClassWarning

同一源码函数还处理为类实例分配内存的相关场景(checkMemsetType的allocation参数为 true 时的分支):

  • mallocOnClassError(CWE-665):malloc分配的内存未调用构造函数(第 1622-1631 行),报告中提示"Consider using 'new' instead";
  • mallocOnClassWarning(warning 级别,需--enable=warning):类提供构造函数但仍使用malloc(第 1612-1620 行)。

三者的完整报告消息均可在 checkclass.cpp 第 1633-1658 行 中查看,其中的$symbol:占位符会在输出时替换为实际的函数名、类型名等信息。

测试覆盖与边界行为

memsetClass的测试集中在 test/testclass.cpp 的checkNoMemset系列用例中,覆盖了相当全面的边界情况:

  • 类型识别方式多样:sizeof(Fred)、sizeof(fred)(通过变量)、sizeof(n1::Fred)(命名空间限定)、sizeof(*this)(成员函数内);
  • 成员形态差异:std::string、std::vector、mutable成员(测试 3077-3086 行)均会触发;
  • 豁免路径:空类、指针成员、数组指针成员不触发(测试 3097-3105 行);
  • 继承与虚函数:多级继承中任一基类含非 POD 成员或虚函数都会触发(测试 3152-3165 行);
  • 静态存储期对象:对static Fred fred;同样触发(测试 3141-3150 行);
  • 跨文件/未知类型:类定义不在当前翻译单元时,cppcheck 会依据符号数据库尽量解析;解析失败时可能漏报,属于静态分析工具的固有局限。

在项目中落地建议

  1. 持续集成接入:把memsetClass当作 Error 级缺陷对待,在 CI 中配置cppcheck --enable=warning,style --error-exitcode=1,使此类问题直接阻断构建;
  2. 配合库配置:检测逻辑会参考library.podtype()(checkclass.cpp 第 1594 行)——若你的自定义类型本质是 POD,可在项目自定义.cfg库文件中标记,避免误报;同时std::array已内置豁免,因为它不具备堆指针等资源管理语义;
  3. 代码评审要点:看到memset(&obj, ...)/memcpy(&obj, ...)/memmove(&obj, ...)时,先确认对象类型是否为 POD;非 POD 一律改用构造、赋值或成员函数实现;
  4. 善用变体检查器:memsetClassReference、memsetClassFloat与memsetClass一并默认启用,覆盖了引用与浮点两种更隐蔽的场景。

小结

memsetClass是 cppcheck 针对"原始内存操作破坏 C++ 对象语义"这一未定义行为提供的高置信度静态检测。它覆盖memset/memcpy/memmove三种函数、继承与命名空间等多种对象形态,并延伸出memsetClassReference(引用成员)与memsetClassFloat(浮点成员)两个变体,配合测试套件 testclass.cpp 中的大量用例,可以可靠地在编译前拦截这类会导致内存损坏、资源泄漏与不可移植行为的经典 C 语言惯性写法。

  • 开发工具
  • 静态分析
  • 代码质量
  • 质量保障

【免费下载链接】cppcheck

static analysis of C/C++ code

项目地址:https://gitcode.com/gh_mirrors/cpp/cppcheck
点击查看免费下载

相关推荐

上一篇:突破实时监控瓶颈:RTSP Simple Server实现RPi相机微秒级时间戳叠加全指南
下一篇:3分钟上手!Text-Generation-Inference本地化部署全攻略(NVIDIA/AMD通用)

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询