C++uninitialized_fill_n填充N未初
uninitialized_fill_n:给“空地”批量撒上默认种子
写C++时,你有没有遇到过这种场景:刚用operator new或malloc划了一块原始内存,指针指着一片“荒地”,既没对象、也没构造——这时候你想快速填满它,但又不想一个个new (p+i) T{}手动调用构造函数?uninitialized_fill_n就是为这种“开荒式初始化”而生的。
它不负责分配内存,也不检查类型是否可默认构造;它只做一件事:在已知起始地址的原始内存上,连续构造N个默认初始化的对象。听起来简单,但用错地方,轻则行为未定义,重则程序悄无声息地崩在深夜调试现场。
先看最简用法:
#include <memory>
#include <iostream>
struct Widget {
int x = 42;
Widget() { std::cout << "ctor\n"; }
};
int main() {
char* raw = new char[sizeof(Widget) * 3];
auto ptr = reinterpret_cast<Widget*>(raw);
// 在ptr开始的3个位置,逐个调用Widget()构造
std::uninitialized_fill_n(ptr, 3, Widget{});
// 别忘了显式析构 + 释放
for (int i = 0; i < 3; ++i) ptr[i].~Widget();
delete[] raw;
}
注意:uninitialized_fill_n第三个参数是“值”,不是“类型”。它会把该值拷贝构造到每个目标位置。如果你传的是临时对象(如Widget{}),它会被复制三次——所以Widget必须可拷贝(或移动)。这点常被忽略,尤其当类禁用了拷贝时,编译直接报错,但错误信息可能绕得人晕头转向。
更典型的用例,其实是配合std::vector底层或自定义容器。比如实现一个简易SmallVector,内部缓冲区用std::aligned_storage_t预留空间:
template<typename T>
class SmallVec {
alignas(T) char storage[16];
T* begin_ = nullptr;
size_t size_ = 0;
public:
void resize(size_t n) {
if (n > 16 / sizeof(T)) return; // 简化处理
if (n > size_) {
// 对新增的[n - size_)段,执行默认构造
std::uninitialized_fill_n(
begin_ + size_,
n - size_,
T{} // 注意:这里T{}是右值,触发移动构造(若可用)
);
}
size_ = n;
}
};
这里的关键在于:uninitialized_fill_n真正起作用的前提,是目标内存区域尚未构造任何对象。如果某处已经构造过T,再对同一地址调用它,就等于在活对象身上二次构造——标准明确称之为未定义行为(UB)。这不是“可能出错”,而是“任何事都可能发生”,包括看起来正常运行数月后突然崩溃。
那它和std::fill_n有什么本质区别?
std::fill_n假定目标已是有效对象,它调用的是赋值操作符(operator=);
std::uninitialized_fill_n则假定目标是“未出生”的原始字节,它调用的是构造函数。
一个修房子,一个盖房子。修一栋没打地基的楼,结果可想而知。
还有一点实战中容易踩坑:uninitialized_fill_n对POD类型(比如int、double)的行为,和你直觉可能相反。它仍会调用int{}进行值初始化(即零初始化),而不是单纯填0字节。如果你想跳过构造、直接按字节填充(比如全填0xff),那就该用std::memset——但得确保类型是平凡可复制的,且你清楚自己在绕过类型系统。
最后说个真实调试经历:有次同事在对象池里反复复用内存块,每次uninitialized_fill_n前忘了先析构旧对象,结果某些带std::string成员的类,第二次构造时内部指针指向了已释放的堆内存,core dump前连日志都没来得及打。后来加了一行std::destroy_n(begin_, size_),问题消失。所以记住:uninitialized_fill_n从不自动清理旧状态,它只负责“生”,不负责“死”。
总结一下使用心法:
✅ 明确内存是原始的、未构造的;
✅ 确保T可拷贝/可移动(取决于你传的值);
✅ 构造后,必须配对调用std::destroy_n或手动析构;
❌ 别把它当成memset的高级替代品;
❌ 别在已有对象的内存上重复调用。
它不是万能钥匙,但当你需要精准控制对象生命周期的起点时,它是少数几个能让你把“内存”和“对象”真正拆开操作的工具之一。用得克制,它可靠;用得随意,它沉默地埋下雷。


还没有评论,来说两句吧...