前言
std::memset声明在<cstring>(C 里是<string.h>),签名是void* memset(void* dest, int ch, size_t count),语义是「把dest指向的内存起始的count个字节,每个字节都设置为ch转换成unsigned char后的值」。注意单位是字节,不是元素;注意它只认识字节,不认识类型。这两点是绝大多数误用的根源。
最常见的误解是把memset当成「给变量赋初值」的通用工具。memset(&x, 1, sizeof x)并不会把int变成 1,而是把四个字节都写成0x01,在常见的 32 位补码实现上得到0x01010101,即 16843009。类似地,memset(&d, 0, sizeof d)把double的所有比特位清零,在 IEEE 754 平台上恰好等于0.0,但标准并不保证所有平台都用这种表示;把指针清零也依赖「空指针的全零表示」,同样不是标准保证。
第二个误解是认为memset可以用于任何类型。对std::string、std::vector、含虚函数的类、含指针成员并拥有资源的类调用memset,会破坏对象的不变量:内部指针被清零、虚表指针被覆盖,后续任何操作都是未定义行为(undefined behavior, UB)。析构时崩溃只是最幸运的结果。
第三个误解是忽略编译器的优化。用于清除敏感数据(如密码缓冲区)的memset,如果编译器判断这块内存之后不会再被读取,可能把这次写入优化掉。安全场景要使用不会被优化掉的清除手段,并遵循平台与库的官方建议。
本文先讲清memset的字节语义,再说明哪些类型可以安全清零、哪些绝对不行,然后给出 C++ 中更合适的替代写法,最后列出常见坑点。示例以 C++17 为基准。
一、字节语义:为什么不能用来设非零值
memset的第二个参数虽然是int,但只有低 8 位被使用,函数把每个字节都写成这个值。因此它天然只能表达「每个字节都相同」的模式。常见的可用模式只有两种:全0x00和全0xFF。前者用于把一块内存清零,后者在某些平台上表示「全部位为 1」,但对有符号整数而言,0xFFFFFFFF是 -1,这只在补码表示下成立。
任何「按元素赋值」的需求都不属于memset的适用范围。把int数组设为 1、把double数组设为 0.5、把结构体成员的某个字段设为特定值,都必须用循环、std::fill或构造函数。memset没有类型信息,它不知道一个int占几个字节,也不知道字节序。
第三个参数count是字节数。对数组写sizeof array是正确的;对指针写sizeof ptr只得到指针本身的大小,通常是 4 或 8,远小于数组长度。这是 C 语言里最经典的错误之一,编译器通常只能给出警告,不会阻止编译。
二、哪些类型可以安全地清零
对「可平凡复制」(trivially copyable)且「全零表示合法值」的类型,用memset清零是可行的。典型的例子是char、unsigned char、int、long long等整数类型的数组,以及只含这些成员的简单结构体。C++ 标准对整数类型的补码表示没有强制要求,但所有主流实现都用补码,全零就是 0;写跨平台代码时,更稳妥的做法是值初始化。
浮点类型要谨慎。IEEE 754 规定全零比特表示+0.0,主流平台都遵循,但标准 C++ 并不保证浮点表示一定是 IEEE 754。对double数组清零,用std::fill或值初始化更符合标准,也更清晰。
指针类型同样要谨慎。空指针的表示不保证全零,虽然在几乎所有现代平台上都是。用nullptr赋值,或者用值初始化,才是标准保证的写法。
对含有非平凡成员的类型,memset不仅「不保证正确」,而是直接 UB。std::string内部有指针与长度,清零后长度字段变成 0、指针变成空,看起来「像空串」,但它的不变量已经被破坏,后续调用size()、析构、拷贝都可能访问非法内存。含虚函数的类更严重:对象头部的虚表指针被清零,任何虚调用都会跳到空地址。
三、C++ 中的替代写法
清零一个数组,优先用值初始化:int arr[10]{};或std::array<int, 10> arr{};,标准保证所有元素被初始化为 0。已经存在的数组要重新清零,用std::fill(std::begin(arr), std::end(arr), 0);只需前 n 个元素时用std::fill_n。
清零一个结构体,用Pod p{};或者p = Pod{};。清零一个容器,用v.assign(v.size(), 0)或std::fill(v.begin(), v.end(), 0);新建容器直接std::vector<int> v(n, 0);。
清零一块原始内存(例如malloc得到的缓冲区),memset仍然是合适工具,因为那块内存本来就没有类型,只有字节。calloc也可以,它在分配的同时清零,并且对溢出做了处理。
C++ 里还有std::ranges::fill(C++20)与std::bit_cast等工具,但日常清零用值初始化与std::fill已经足够。
四、完整示例
下面的示例演示正确与错误的用法。错误用法只放在注释与说明里,不在main中执行未定义行为。
// 适用于 C++17
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstring>
#include <iostream>
#include <string>
#include <vector>
struct Pod {
int id;
double score;
};
int main() {
// 正确:整数数组清零,字节全零即数值 0
int arr[5];
std::memset(arr, 0, sizeof arr);
std::cout << arr[0] << ' ' << arr[4] << '\n';
// 正确:C++ 风格的值初始化,不依赖 memset
std::array<int, 5> arr2{};
std::fill(arr2.begin(), arr2.end(), 7);
std::cout << arr2.front() << ' ' << arr2.back() << '\n';
// 正确:结构体值初始化
Pod p{};
std::cout << p.id << ' ' << p.score << '\n';
// 正确:容器构造时指定初值
std::vector<int> v(4, 0);
std::cout << v.size() << ' ' << v[0] << '\n';
// 错误示范(仅说明,不要执行):
// int x = 0;
// std::memset(&x, 1, sizeof x); // 每个字节为 0x01,不是数值 1
//
// std::string s = "hello";
// std::memset(&s, 0, sizeof s); // 破坏 string 的不变量,UB
//
// double d = 1.0;
// std::memset(&d, 0, sizeof d); // 依赖 IEEE 754,标准不保证
//
// void* ptr = &x;
// std::memset(&ptr, 0, sizeof ptr); // 依赖空指针的全零表示
// 正确:原始缓冲区清零
constexpr std::size_t kSize = 16;
unsigned char raw[kSize];
std::memset(raw, 0, sizeof raw);
std::cout << static_cast<int>(raw[0]) << '\n';
// 正确:只清零前 n 个字节
std::memset(raw, 0xFF, 4);
std::cout << static_cast<int>(raw[3]) << '\n'; // 255
return 0;
}按标准语义推演:std::memset(arr, 0, sizeof arr)把 20 个字节全部置零,int元素读取为 0;std::fill把五个元素都设为 7;Pod{}的值初始化把id设为 0、score设为 0.0;std::memset(raw, 0xFF, 4)把前四个字节设为 255,raw[3]读取为 255。注释中的错误用法不参与执行。以上只按标准库语义推导,未经编译运行核对。
五、正确与错误用法对照
| 场景 | 错误写法 | 正确写法 | 原因 |
|---|
| 整数数组清零 | memset(a, 0, n)(n 是元素数) | memset(a, 0, sizeof a)或std::fill | 第三个参数是字节数 |
| 整数设为 1 | memset(&x, 1, sizeof x) | x = 1; | 逐字节写入不等于赋值 |
| 浮点清零 | memset(&d, 0, sizeof d) | d = 0.0; | 不依赖浮点表示 |
| 指针置空 | memset(&p, 0, sizeof p) | p = nullptr; | 不依赖空指针表示 |
| 字符串清空 | memset(&s, 0, sizeof s) | s.clear(); | 破坏类的不变量 |
| 结构体清零 | memset(&obj, 0, sizeof obj) | obj = Pod{}; | 非平凡类型是 UB |
| 清除敏感数据 | memset(buf, 0, n) | 使用平台提供的安全清除接口 | 可能被优化掉 |
常见坑点
一、用memset给整数赋非零值
❌int x; std::memset(&x, 1, sizeof x);以为得到 1,实际每个字节都是0x01。
✅ 直接x = 1;;数组批量赋值用std::fill或std::fill_n。
二、对非平凡类型清零
❌std::string s; std::memset(&s, 0, sizeof s);或对含虚函数的对象清零,破坏不变量,后续操作是 UB。
✅ 用s.clear()、重新赋值、或值初始化;类类型一律不用memset。
三、第三个参数传元素个数
❌int a[10]; std::memset(a, 0, 10);只清零了 10 个字节,不是 10 个int。
✅ 传字节数:sizeof a,或者sizeof(int) * 10,更推荐sizeof a。
四、对指针用sizeof求数组长度
❌void f(int* a) { std::memset(a, 0, sizeof a); }只清零了一个指针大小的字节数。
✅ 把元素个数或字节数作为参数传入;在数组定义所在作用域内才可用sizeof a。
五、在构造函数里memset(this, 0, sizeof *this)
❌ 对含有虚函数、基类、或非平凡成员的对象这样做,虚表指针与成员的不变量被破坏。
✅ 用成员初始化列表与类内初始值;需要整体清零时用*this = Widget{};(要求可赋值)。
六、认为memset清零后对象就可以安全使用
❌ 对std::vector对象清零后继续push_back,内部指针为空、容量为 0,行为不可预期。
✅ 容器的清空用clear();要释放内存用shrink_to_fit或交换空容器。
七、用memset清除密码等敏感数据
❌std::memset(password, 0, len);之后缓冲区不再被读取,编译器可能把写入优化掉。
✅ 使用平台或库提供的「安全清零」接口,并按其官方文档使用;不要把普通memset当作安全擦除。
八、把memset的返回值当成新指针
❌ 以为memset返回清零后的缓冲区,或在表达式里链式使用导致类型混淆。
✅memset返回的就是传入的dest,直接使用原指针即可;需要类型安全时用std::fill。
总结
memset是一个字节级工具,它能可靠完成的只有「把一块原始内存的每个字节设为同一个值」。整数数组清零是它最正当的用途;浮点、指针、类类型都不应由它初始化。C++ 提供了值初始化、std::fill、容器构造函数等类型安全的替代品,应当优先使用。
判断能不能用memset,只需问三个问题:这块内存有类型吗?全零字节对该类型是合法值吗?第三个参数是字节数吗?三问有一个答不上来,就换std::fill或值初始化。
| 目标 | 全零字节是否合法 | 建议 |
|---|
char数组 | 是 | 可用memset |
| 整数数组清零 | 是 | 可用memset |
float/double | 否,非 0.0 | 用值初始化或std::fill |
| 指针数组 | 否,空指针不保证全零 | 逐个赋nullptr |
| 含虚函数的类 | 否 | 用构造函数 |