- 开发工具
- 静态分析
- 代码质量
- 质量保障
【免费下载链接】cppcheck
static analysis of C/C++ code
导读
在 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)如下:
| 项目 | 值 |
|---|---|
| Message | Using 'memset' on class that contains a 'std::string'. |
| Category | Undefined Behaviour(未定义行为) |
| Severity | Error(错误) |
| Language | C++ |
从源码角度看,该报告对应 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 行),主要途径包括:
- 从
sizeof()参数推导类型:当第三个参数是sizeof(Fred)、sizeof(n1::Fred)、sizeof(struct X)等形式时,直接从 sizeof 表达式提取类型(第 1468-1480 行); - 从对象表达式推导类型:第一个参数为
&fred、fred、*ptr等时,通过变量符号解析其类型作用域(第 1483-1509 行); - 成员函数内
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 会依据符号数据库尽量解析;解析失败时可能漏报,属于静态分析工具的固有局限。
在项目中落地建议
- 持续集成接入:把
memsetClass当作 Error 级缺陷对待,在 CI 中配置cppcheck --enable=warning,style --error-exitcode=1,使此类问题直接阻断构建; - 配合库配置:检测逻辑会参考
library.podtype()(checkclass.cpp 第 1594 行)——若你的自定义类型本质是 POD,可在项目自定义.cfg库文件中标记,避免误报;同时std::array已内置豁免,因为它不具备堆指针等资源管理语义; - 代码评审要点:看到
memset(&obj, ...)/memcpy(&obj, ...)/memmove(&obj, ...)时,先确认对象类型是否为 POD;非 POD 一律改用构造、赋值或成员函数实现; - 善用变体检查器:
memsetClassReference、memsetClassFloat与memsetClass一并默认启用,覆盖了引用与浮点两种更隐蔽的场景。
小结
memsetClass是 cppcheck 针对"原始内存操作破坏 C++ 对象语义"这一未定义行为提供的高置信度静态检测。它覆盖memset/memcpy/memmove三种函数、继承与命名空间等多种对象形态,并延伸出memsetClassReference(引用成员)与memsetClassFloat(浮点成员)两个变体,配合测试套件 testclass.cpp 中的大量用例,可以可靠地在编译前拦截这类会导致内存损坏、资源泄漏与不可移植行为的经典 C 语言惯性写法。
- 开发工具
- 静态分析
- 代码质量
- 质量保障
【免费下载链接】cppcheck
static analysis of C/C++ code
相关推荐
微信聊天记录怎么完整导出?WeChatMsg 免费教程:Word/HTML/CSV 与年度报告一次搞定
微信聊天记录怎么完整导出?WeChatMsg 免费教程:Word/HTML/CSV 与年度报告一次搞定 WeChatMsg(项目名"留痕")是基于 MIT 协议
开发工具静态分析代码质量质量保障cppcheck returnReference 检查器详解:C++ 返回局部变量引用的未定义行为检测
cppcheck returnReference 检查器详解:C++ 返回局部变量引用的未定义行为检测 导读 returnReference 是 cppchec
开发工具静态分析代码质量质量保障cppcheck incompleteArrayFill 检查器深度解析:识别 memset/memcpy/memmove 字节数误用
cppcheck incompleteArrayFill 检查器深度解析:识别 memset/memcpy/memmove 字节数误用 导读 本文围绕 cppc
开发工具静态分析代码质量质量保障
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考