C++对象切片
什么是对象切片?
定义:把一个派生类对象用传值的方式赋值或传递给一个基类对象时,这个派生类对象中所有“派生类特有”的数据成员和行为都会被“切掉”,最终只剩下和基类完全相同的部分。这个过程就叫做对象切片
eg:
#include <iostream>
#include <string>
class Base {
public:
// 虚函数,用于多态
virtual void print_type() const {
std::cout << "I am a Base object.\n";
}
};
class Derived : public Base {
private:
std::string derived_data_ = "Derived specific data"; // 派生类独有的数据
public:
// 重写基类的虚函数
void print_type() const override {
std::cout << "I am a Derived object. My data is: " << derived_data_ << "\n";
}
};
int main() {
Derived derived_obj; // 创建一个完整的派生类对象
std::cout << "Original object:\n";
derived_obj.print_type();
std::cout << "\n--- Slicing happens here ---\n";
// 触发对象切片:将派生类对象赋值给一个基类对象
Base base_obj = derived_obj;
std::cout << "Sliced object:\n";
base_obj.print_type(); // 调用print_type()
return 0;
}
/*
结果:
Original object:
I am a Derived object. My data is: Derived specific data
--- Slicing happens here ---
Sliced object:
I am a Base object.
*/
analysis:
- 执行
Base base_obj = derived_obj;时,编译器会创建一个新的、类型为Base的base_obj对象 base_obj只拥有Base类所定义的内存空间- 编译器会将
derived_obj中属于Base的部分(“基类子对象”)复制到base_obj中 derived_obj中独有的derived_data_成员因为在base_obj中没有存放的空间,所以被完全丢弃了- 更重要的是,
base_obj就是一个纯粹的Base对象,它的虚函数表指针指向Base类的虚函数表。因此,当调用base_obj.print_type()时,执行的是Base版本的函数,多态性完全失效
如何避免对象切片?
唯一方法,就是在处理多态对象时,始终使用指针或引用
指针和引用不会创建新对象,它们只是现有对象的“别名”或“地址”,因此保留了对象的完整类型信息,不会发生切片
#include <iostream>
#include <string>
// Base 和 Derived 类的定义同上
int main() {
Derived derived_obj;
std::cout << "--- Avoiding slicing with a pointer ---\n";
Base* ptr_to_base = &derived_obj; // 使用基类指针指向派生类对象
ptr_to_base->print_type(); // 通过指针调用,多态正常工作
std::cout << "\n--- Avoiding slicing with a reference ---\n";
Base& ref_to_base = derived_obj; // 使用基类引用绑定派生类对象
ref_to_base.print_type(); // 通过引用调用,多态正常工作
return 0;
}
/*--- Avoiding slicing with a pointer ---
I am a Derived object. My data is: Derived specific data
--- Avoiding slicing with a reference ---
I am a Derived object. My data is: Derived specific data*/
分析:
通过指针(Base*)和引用(Base&),没有创建任何新对象,
只是持有了原始derived_obj的地址或别名。
因此,调用虚函数时,C++的动态派发机制可以正确地找到并执行Derived类重写的版本,多态得以实现
Summarize When does slicing occur?:
- 按值赋值:Base b = d; (如上例)
- 按值传递给函数:
void my_func(Base b) { ... },然后用派生类对象调用my_func(d); - 在容器中存储对象:
std::vector<Base> vec; vec.push_back(d);最危险且最难发现的情况之一,因为容器会创建对象的副本,导致所有存入的派生类对象都被“切片”成基类对象。正确的做法是存储指针,最好是智能指针:std::vector<std::unique_ptr<Base>>