导读:本期聚焦于书生创作的《C++怎么把类对象指针写入文件?为什么直接write指针是个致命坑》,敬请观看详情。把类对象存进文件时,新手常以为用fwrite把对象地址写进去就完事了,程序下次读回来却直接崩溃。根本原因在于指针保存的是进程虚拟地址,文件落到磁盘后地址空间已失效,反序列化时解引用就是访问野指针。本文从内存布局讲清对象与指针的区别,给出重写operator、手动序列化成员、使用Boost.Serialization三种落地方案,并对比Plain Old Data与含虚表对象的差异,帮你避开把指针当数据写的低级错误。

在C++开发中,把运行时的数据持久化到文件是最常见的需求之一。当我们需要保存一个类实例时,很多人第一反应是拿到对象的指针,然后调用fwrite或者ofstream.write把这段内存直接写下去。这种做法在表面上看似省事,实际上埋下了极难排查的隐患。真正安全的做法不是写指针,而是写对象所包含的真实数据,也就是完成一次明确的序列化过程。

C++怎么把类对象指针写入文件?为什么直接write指针是个致命坑

为什么直接write类对象指针是致命错误

要理解这个坑,必须先弄清楚指针到底是什么。在64位程序里,一个指针变量占用8字节,它里面存储的值并不是对象本身,而是该对象在当前进程虚拟地址空间里的地址,例如0x00007ffd3c4a0010。当你执行write(fd, &objPtr, sizeof(objPtr))时,磁盘上多出来的只是这串数字。等到下次程序启动,操作系统会分配完全不同的虚拟地址布局,原来那个地址要么指向无效内存,要么属于别人的数据。

更麻烦的是,如果类里还包含指向堆内存的成员变量指针(比如char* name或者std::vector内部指针),即使你自作聪明把整个对象内存write(&obj, sizeof(obj)),写进去的也仅仅是对象栈部分里存的那些指针值。文件里根本没有字符串内容、没有vector元素,只有几个地址。读回时构造出的对象成了“悬空引用聚合体”,一访问就段错误。

下面这段代码就是典型的错误示范:把指针自身而非指向内容写文件,读回后解引用必崩。

#include <fstream>
#include <iostream>

class User {
public:
    int id;
    char* name;
    User(int i, const char* n) : id(i) {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
    }
    ~User() { delete[] name; }
};

int main() {
    User* u = new User(1, "Alice");
    std::ofstream out("bad.dat", std::ios::binary);
    // 错误:写的是指针变量地址,不是对象数据
    out.write(reinterpret_cast<char*>(&u), sizeof(u));
    out.close();

    User* loaded = nullptr;
    std::ifstream in("bad.dat", std::ios::binary);
    in.read(reinterpret_cast<char*>(&loaded), sizeof(loaded));
    // 灾难:loaded里的地址是上次进程的,访问loaded->id直接崩溃
    std::cout << loaded->id;
    return 0;
}

正确的序列化思路:只写原始数据不写地址

所谓序列化,就是约定一种与内存布局无关的字节表达形式,把对象的状态变成纯数据。对于 Plain Old Data(POD)类型且不含任何指针的简单结构,可以直接写对象内存,但前提是确保读回的程序有完全一致的编译环境。一旦类里有虚函数、有动态分配、有STL容器,就必须手写字段级的写出逻辑。

最基础的手段是重载operator<<或写两个成员函数saveload。写文件时把每个基本类型依次输出,字符串先写长度再写内容;读文件时按相同顺序还原。这样磁盘上存的是“1,5,Alice”这类真实信息,而不是0x开头的地址。下面给出一个安全的手写版本:

#include <fstream>
#include <cstring>
#include <iostream>

class User {
public:
    int id;
    char* name;
    User() : id(0), name(nullptr) {}
    User(int i, const char* n) : id(i) {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
    }
    void save(std::ofstream& out) {
        out.write(reinterpret_cast<char*>(&id), sizeof(id));
        int len = strlen(name);
        out.write(reinterpret_cast<char*>(&len), sizeof(len));
        out.write(name, len);
    }
    void load(std::ifstream& in) {
        in.read(reinterpret_cast<char*>(&id), sizeof(id));
        int len = 0;
        in.read(reinterpret_cast<char*>(&len), sizeof(len));
        name = new char[len + 1];
        in.read(name, len);
        name[len] = '\0';
    }
    ~User() { delete[] name; }
};

int main() {
    User u(1, "Alice");
    std::ofstream out("good.dat", std::ios::binary);
    u.save(out);
    out.close();

    User loaded;
    std::ifstream in("good.dat", std::ios::binary);
    loaded.load(in);
    std::cout << loaded.id << " " << loaded.name;
    return 0;
}

这种写法虽然啰嗦,但跨进程、跨时间都安全。它的缺点是每加一个字段就要改saveload,维护成本高。在真实项目中,如果对象关系复杂,建议改用成熟的序列化库。

借助Boost.Serialization与标准库方案避开深坑

当类层级变深、包含容器和嵌套对象时,手写字段读写容易漏写指针指向的子对象。Boost.Serialization通过宏和模板自动遍历成员,并能正确处理指针所指向的真实对象,它会维护一个对象跟踪表,保证同一指针不被重复写、读回后关系不丢失。使用方式是在类里声明friend class boost::serialization::access,再写serialize方法。

如果不想引入第三方库,C++标准里也可以用文本化方式,例如把对象转成JSON再写文件,虽然体积大一点,但可读性和跨语言兼容性极好。对于只是临时缓存的场景,还可以用std::ostringstream配合std::string_view做内存映射文件的定制布局。无论选哪种,核心原则不变:落盘的是数据值,不是虚拟地址。下面的片段展示Boost风格的基本结构:

#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/binary_iarchive.hpp>
#include <fstream>

class User {
    friend class boost::serialization::access;
    int id;
    std::string name;
    template<class Archive>
    void serialize(Archive& ar, const unsigned int version) {
        ar & id;
        ar & name;
    }
public:
    User() = default;
    User(int i, std::string n) : id(i), name(n) {}
};

void demo() {
    User u(2, "Bob");
    std::ofstream ofs("user.bin", std::ios::binary);
    boost::archive::binary_oarchive oa(ofs);
    oa << u;
}

从工程角度看,直接write指针永远不该出现在生产代码里。它既不是序列化也不是持久化,只是把进程内部的临时地图坐标抄给了另一天完全不同的城市。养成“写数据不写地址”的习惯,才能从根上消灭这一类悬空指针崩溃。

C++序列化类对象指针二进制文件读写修改时间:2026-08-20 18:09:03

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。