一、虚函数与序列化:多态对象的序列化挑战
序列化,说白了就是把内存里的对象变成一串字节流。存到文件也好,发到网络也罢,总之得能再变回来。
但问题来了——当对象带有虚函数时,事情就没那么简单了。
我刚开始做序列化时,天真地以为只要把成员变量挨个存进去就行。结果反序列化回来,调用虚函数直接崩了。为什么?因为虚函数表指针(vptr)丢了。
1.1 多态对象的序列化困境
你想想看,一个 Base* 指针,实际指向的是 Derived 对象。序列化时,我们只知道它是 Base*,但实际类型是 Derived。如果只存 Base 部分,反序列化回来就变成 Base 对象了——多态性彻底丢失。
举个例子:
class Animal {
public:
virtual void speak() { cout << "Animal speaks\n"; }
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() override { cout << "Dog barks\n"; }
};
// 序列化时只存了 Animal 部分
// 反序列化回来,speak() 调用的是 Animal::speak(),不是 Dog::speak()
我在项目中遇到过类似问题。当时做一个游戏引擎,需要保存场景中的所有 GameObject。每个对象可能是 Player、Enemy 或 Item。序列化时没记录类型,结果读回来全是 GameObject,所有角色都变成了一模一样的默认模型。嗯,那场面挺尴尬的。
1.2 解决方案:记录类型标识
解决思路其实很直接——序列化时额外存一个类型ID,反序列化时根据这个ID创建正确的对象。
enum class TypeID {
ANIMAL,
DOG,
CAT
};
class Animal {
public:
virtual TypeID getTypeID() const = 0;
virtual void serialize(ostream& os) const {
TypeID id = getTypeID();
os.write(reinterpret_cast<const char*>(&id), sizeof(id));
}
};
class Dog : public Animal {
public:
TypeID getTypeID() const override { return TypeID::DOG; }
void serialize(ostream& os) const override {
Animal::serialize(os);
// 序列化 Dog 特有的成员
}
};
这样反序列化时,先读类型ID,再根据ID创建对应对象。但这里有个新问题——怎么创建?你不能直接 new Dog(),因为反序列化代码不知道具体类型。
二、虚函数与对象持久化
持久化,就是把对象存到磁盘上,下次程序启动还能用。虚函数的存在让持久化变得棘手——因为虚函数表是编译器生成的,不同编译器、不同版本都可能不一样。
我记得有一次升级编译器版本,之前序列化的数据全读不出来了。查了半天,发现是虚函数表布局变了,vptr指向的位置不同了。从那以后,我养成了一个习惯:永远不要直接序列化内存布局。
2.1 持久化的正确姿势
正确的做法是:只序列化数据成员,不碰虚函数表。反序列化时,通过构造函数或工厂方法重建对象,虚函数表自然就正确了。
class Shape {
public:
virtual double area() const = 0;
virtual void serialize(ostream& os) const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius_;
public:
Circle(double r = 0) : radius_(r) {}
double area() const override { return 3.14159 * radius_ * radius_; }
void serialize(ostream& os) const override {
os.write(reinterpret_cast<const char*>(&radius_), sizeof(radius_));
}
};
反序列化时,先读类型ID,再调用对应的构造函数:
unique_ptr<Shape> deserialize(istream& is) {
TypeID id;
is.read(reinterpret_cast<char*>(&id), sizeof(id));
switch (id) {
case TypeID::CIRCLE: {
double r;
is.read(reinterpret_cast<char*>(&r), sizeof(r));
return make_unique<Circle>(r);
}
// 其他类型...
}
}
你看,这里的关键是:反序列化时通过构造函数重建对象,虚函数表由编译器自动设置,完全不需要我们操心。
三、工厂模式在反序列化中的应用
上面那个 switch-case 写法,说实话,很丑。每加一个新类型,就得改 switch 语句。这违反了开闭原则——对扩展开放,对修改关闭。
我个人的习惯是用工厂模式来解决这个问题。工厂模式说白了就是一个对象创建中心,你给它一个类型ID,它返回一个对应类型的对象。
3.1 简单工厂模式
class ShapeFactory {
public:
static unique_ptr<Shape> create(TypeID id) {
switch (id) {
case TypeID::CIRCLE: return make_unique<Circle>();
case TypeID::RECT: return make_unique<Rect>();
case TypeID::TRIANGLE: return make_unique<Triangle>();
default: throw runtime_error("Unknown type");
}
}
};
这样反序列化代码就变成了:
unique_ptr<Shape> deserialize(istream& is) {
TypeID id;
is.read(reinterpret_cast<char*>(&id), sizeof(id));
auto obj = ShapeFactory::create(id);
obj->deserialize(is); // 虚函数调用,读取具体数据
return obj;
}
嗯,比之前清爽多了。但 switch-case 还是得改,对吧?
3.2 注册式工厂模式
我曾经在一个大型项目中维护过几十种消息类型的序列化。每次加新消息都要改工厂函数,烦不胜烦。后来我改用注册式工厂——类型自己注册到工厂里,工厂不需要知道具体有哪些类型。
class ShapeFactory {
using Creator = function<unique_ptr<Shape>()>;
static map<TypeID, Creator>& registry() {
static map<TypeID, Creator> reg;
return reg;
}
public:
static void registerType(TypeID id, Creator creator) {
registry()[id] = move(creator);
}
static unique_ptr<Shape> create(TypeID id) {
return registry().at(id)();
}
};
// 在全局或静态初始化时注册
// Circle.cpp
bool circleRegistered = []{
ShapeFactory::registerType(TypeID::CIRCLE,
[]{ return make_unique<Circle>(); });
return true;
}();
3.3 工厂模式与虚函数序列化的完整流程
把上面所有东西串起来,一个完整的多态对象序列化/反序列化流程是这样的:
// 序列化
void serialize(ostream& os, const Shape& shape) {
TypeID id = shape.getTypeID(); // 虚函数调用
os.write(reinterpret_cast<const char*>(&id), sizeof(id));
shape.serialize(os); // 虚函数调用,序列化具体数据
}
// 反序列化
unique_ptr<Shape> deserialize(istream& is) {
TypeID id;
is.read(reinterpret_cast<char*>(&id), sizeof(id));
auto shape = ShapeFactory::create(id); // 工厂创建
shape->deserialize(is); // 虚函数调用,读取具体数据
return shape;
}
四、知识体系总览
下面这张图总结了虚函数与序列化的核心逻辑:
说白了,整个流程就三件事:存类型、建对象、读数据。类型ID解决「是什么」的问题,工厂模式解决「怎么建」的问题,虚函数解决「怎么读写」的问题。三者缺一不可。
我个人觉得,工厂模式是这里面最巧妙的一环。它把「创建对象」这个职责从反序列化代码中剥离出来,让代码更干净、更可扩展。如果你在项目中遇到多态对象的序列化问题,不妨试试这个组合拳。
type_info 的 name() 返回值。结果不同编译器返回的字符串格式不一样,导致跨平台反序列化失败。后来改用自定义的整数ID,配合注册表,才彻底解决这个问题。
好了,关于虚函数与序列化的内容就聊到这里。记住:序列化存的是数据,不是代码。只要把握住这个原则,多态对象的持久化其实没那么可怕。