1.2 对象切片:将派生类按值赋给基类,派生部分被截断 ——C++踩坑排雷手册:派生类对象按值赋值给基类时为什么会丢失派生成员数据
1.2 C实战100例——对象切片将派生类按值赋给基类派生部分被截断拷贝构造触发类型擦除指针/引用是唯一回避手段受众在 C 容器、工厂函数、回调接口里用值语义传多态对象的开发者。会写继承和拷贝构造就行不需要提前懂内存布局。场景你把派生类对象按值赋给基类变量或者存进std::vectorBase之后调虚函数全是基类版本派生类成员全部丢了。这不是编译器优化是值语义的硬约束。这篇给你三条命令看清楚切片在哪一刻发生再给你三种回避手段。摘要对象切片是以值语义处理继承时最隐蔽的数据截断。原因很简单按值拷贝时目标对象的大小是sizeof(Base)。源对象是派生类sizeof(Derived) sizeof(Base)。拷贝只复制内存里和基类重叠的前缀部分派生类独有的成员和虚表指针从来没有被复制过——不是“丢了”是从未复制。你只需要记住一句话值语义装不下多态对象。对话正文先做一件事看看基类和派生类大小差多少。g-stdc11-O0test_slice.cpp-otest_slice./test_slice gdb-exbreak main-exrun-exprint sizeof(Component)-exprint sizeof(PlayerComponent)-exquit./test_slice如果sizeof(PlayerComponent) sizeof(Component)而且程序输出基类版本那就是这个坑。读者 我在一个游戏引擎的组件系统里把派生类PlayerComponent按值存进std::vectorComponent遍历调virtual void update()结果全是Component::update()不是PlayerComponent::update()。代码如下structComponent{virtualvoidupdate(){std::coutComponent\n;}};structPlayerComponent:publicComponent{inthealth100;virtualvoidupdate()override{std::coutPlayer: health\n;}};intmain(){std::vectorComponentcomponents;components.push_back(PlayerComponent());for(autoc:components)c.update();}输出Component不是Player: 100。我遍历用了引用为什么还是不行派生类的health去哪了总工 切片发生在push_back的拷贝构造阶段后面用引用也救不回来。第一步看内存大小g-stdc11-O0test_slice.cpp-otest_slice./test_slice gdb-exbreak main-exrun-exprint sizeof(Component)-exprint sizeof(PlayerComponent)-exquit./test_slice输出sizeof(Component) 8vptr 8 字节sizeof(PlayerComponent) 16vptr int padding。派生类比基类大 8 字节。第二步看拷贝现场push_back(PlayerComponent())实际做的事构造临时PlayerComponent16 字节vptr 指向派生类虚表。push_back在 vector 里分配sizeof(Component) 8 字节的槽位。调用Component的合成拷贝构造——把PlayerComponent的前 8 字节基类子对象含 vptr按位复制到槽位。后面 8 字节health从未被复制因为目标只有 8 字节。槽位里的 vptr 指向基类虚表。所以health不是丢了是从未进入 vector。后面用引用访问访问的是一个 8 字节的基类对象派生部分根本不存在。验证拷贝构造g-stdc11-O0-Stest_slice.cppcattest_slice.s|grep-B5-A5Component::Component(Component const)合成拷贝构造编译出来是memcpy或逐字节复制只复制 8 字节。读者 我明白了vector 存的是值所以切片。但我的 API 必须返回Component类型不能改成指针。有什么办法在接收端检测或防止切片总工 按值返回Component意味着切片必然发生这是值语义的铁律。三个层面可以防御防御一编译期禁用拷贝。删除基类拷贝构造和赋值操作符让切片编不过。structComponent{Component()default;Component(constComponent)delete;Componentoperator(constComponent)delete;virtualvoidupdate(){}};vectorComponent编译就报错从源头阻止。要存多态对象强制用指针容器。防御二运行时检测——这个不行。在拷贝构造里加typeid检查是无效的Component(constComponentother){if(typeid(*this)!typeid(other)){...}// 无效}拷贝构造执行时other已经是基类typeid(other)返回Component检测不到派生类型。别用这个方法。防御三正确设计。用std::unique_ptrComponent或std::shared_ptrComponent存多态对象。std::vectorstd::unique_ptrComponentcomponents;components.push_back(std::make_uniquePlayerComponent());for(autoup:components)up-update();// Player: 100验证g-stdc11-O0test_unique_ptr.cpp-otest_unique./test_unique输出Player: 100sizeof(unique_ptrComponent) 8对象在堆上完整保留。读者 我的容器是第三方库定义的std::vectorComponent改不了。除了改设计还有别的办法吗比如std::variant或者模板总工 容器类型改不了唯一可行路径是用std::variant替代继承。这不是修切片是从根上把运行时多态换成编译期联合体。usingComponentVariantstd::variantComponent,PlayerComponent;std::vectorComponentVariantcomponents;components.push_back(PlayerComponent());for(autov:components){std::visit([](autoc){c.update();},v);}std::visit为每个类型生成独立分发代码没有 vptr、没有切片、没有堆分配。每个元素占max(sizeof(Component), sizeof(PlayerComponent)) 16 字节所有数据完整保留。代价是每个新派生类都要加进 variant 的类型列表失去运行期动态扩展能力。如果既不能改容器也不能用 variant那就不存在解法。你必须直面设计决策要么让容器存指针推荐unique_ptr要么接受切片。验证g-stdc17-O0test_variant.cpp-otest_variant./test_variant输出Player: 100sizeof(ComponentVariant) 16。抄作业清单步骤执行命令预期输出/生效标志1g -stdc11 -O0 test_slice.cpp -o test_slice ./test_slice输出Component基类版本2gdb -ex break main -ex run -ex print sizeof(Component) -ex print sizeof(PlayerComponent) -ex quit ./test_slice输出8和163g -stdc11 -O0 -S test_slice.cpp cat test_slice.s | grep -A 10 Component::Component(Component const)合成拷贝构造只复制 8 字节4删除拷贝构造后g -stdc11 -O0 test_slice_deleted.cpp -o test_slice_deleted 21 | head -20编译报错提示 deleted function5unique_ptr版本g -stdc11 -O0 test_unique_ptr.cpp -o test_unique ./test_unique输出Player: 1006variant版本g -stdc17 -O0 test_variant.cpp -o test_variant ./test_variant输出Player: 100sizeof(ComponentVariant) 16常见卡点卡点1切片后dynamic_cast返回nullptr现象想把已切片的基类对象转回派生类失败。原因切片后对象里没有派生类虚表信息RTTI 随派生数据一起丢了。切片不可恢复必须在切片发生前用指针/引用。验证g-stdc11-O0-D_GLIBCXX_USE_RTTItest_dynamic_cast.cpp./test_dynamic_cast卡点2按值返回时切片现象Component factory() { return PlayerComponent(); }返回的对象已切片。原因返回值构造时调用基类拷贝构造切片发生在 return 语句。修复返回unique_ptrComponent或shared_ptrComponent。验证g-stdc11-O0-fno-elide-constructors test_return.cpp./test_return卡点3reserve不切片push_back/emplace_back切片现象reserve(10)正常push_back切片。换emplace_back也切片。原因reserve只分配内存不构造。emplace_back也执行拷贝/移动构造值语义不变。唯一解法components.push_back(std::make_uniquePlayerComponent())。验证对比push_back和emplace_back的汇编都调用基类拷贝构造。卡点4移动语义不避免切片现象components.push_back(std::move(PlayerComponent()))还是输出Component。原因移动构造也按类型大小工作。vectorComponent的移动构造参数是Component依然只移动基类部分。验证g-stdc11-O0test_move_slice.cpp./test_move_slice文末声明本题为公开工程技术难题不含任何企业商业秘密、未披露数据或专利陷阱。作者华夏之光永存标签#C #对象切片 #继承 #值语义 #多态 #内存布局 #拷贝构造 #unique_ptr #variant
上一篇/下一篇内容由系统自动关联
返回资讯列表 →