关于对象切片Thinking in C++中有这么一段话:
If you upcast to an object instead of a pointer or reference, something will happen that may surprise you: the object is “sliced” until all that remains is the subobject that corresponds to the destination type of your cast.
在C ++编程,将派生类类型(subclass type)的对象复制到基类类型(superclass)的对象时,将发生对象切片:基类副本将没有在派生类中定义的任何成员变量。实际上,这些变量已被“分割”。
更巧妙地,当基类的赋值运算符将派生类类型的对象复制到相同类型的对象时,对象切片同样会发生,在这种情况下,目标对象的某些成员变量将保留其原始值,而不是从源对象复制过来。
这个问题并不是C ++固有的,但在大多数其他面向对象的语言中也不是自然发生的-甚至在C ++的亲戚(例如D,Java和C#)中也不是这样 — 因为对象的复制不是那些语言的基本操作。
相反,这些语言更喜欢通过隐式引用来操纵对象,这样仅复制引用是一项基本操作。
另外,由于C ++中缺少垃圾回收(garbage collection),因此当不清楚单个共享对象的所有权和生存期时,程序将经常复制该对象。例如,将对象插入标准库集合(例如std :: vector)实际上涉及将副本复制并插入到集合中。
Example
example:
#include <iostream>
class Base
{
protected:
int m_value{};
public:
Base(int value)
: m_value{ value }
{
}
virtual const char* getName() const { return "Base"; }
int getValue() const { return m_value; }
};
class Derived: public Base
{
public:
Derived(int value)
: Base{ value }
{
}
virtual const char* getName() const { return "Derived"; }
};
int main()
{
Derived derived{ 5 };
std::cout << "derived is a " << derived.getName() << " and has value " << derived.getValue() << '\n';
Base &ref{ derived };
std::cout << "ref is a " << ref.getName() << " and has value " << ref.getValue() << '\n';
Base *ptr{ &derived };
std::cout << "ptr is a " << ptr->getName() << " and has value " << ptr->getValue() << '\n';
return 0;
}
在上面的示例中,&ref
和 *ptr
指向派生对象,派生对象具有基类部分和派生部分。由于ref
和ptr
的类型为Base
,因此ref
和ptr
只能看到derived
的Base
部分。
但是derived
的Derived
部分仍然存在,只是根本无法通过ref
或ptr
看到。但是,通过使用虚函数,我们可以访问函数的最衍生版本。因此,以上程序打印:
但是,如果我们仅将派生对象分配给基类对象,而不是设置派生对象的基类引用或指针,
会发生什么情况呢?
int main()
{
Derived derived{ 5 };
Base base{ derived }; // what happens here?
std::cout << "base is a " << base.getName() << " and has value " << base.getValue() << '\n';
return 0;
}
请记住,派生具有基类部分和派生部分。 当我们将派生对象分配给基类对象时,仅复制派生对象的基类部分,而不复制派生部分。在上面的示例中,base
接收了派生的Base
部分的副本,但没有 Derived
部分。该派生部分已被有效地 “sliced off”。因此,将派生类对象分配给基类对象称为对象切片(或简称切片)。
因为变量base
没有派生部分,所以base.getName()
解析为Base :: getName()
。
认真使用,切片可能是良性的。但是,如果使用不当,切片会以多种不同方式导致意外结果。让我们研究其中一些情况。
Slicing and functions
现在,您可能会认为上面的示例有点愚蠢。毕竟,您为什么要像这样分配派生给基础?你可能不会。但是,切片很可能会偶然发生在函数上。
考虑以下功能:
void printName(const Base base) // note: base passed by value, not reference
{
std::cout << "I am a " << base.getName() << '\n';
}
这是一个非常简单的函数,带有const基础对象参数,该参数按值传递。如果我们这样调用此函数:
int main()
{
Derived d{ 5 };
printName(d); // oops, didn't realize this was pass by value on the calling end
return 0;
}
在编写该程序时,您可能没有注意到base是一个值参数,而不是引用。因此,当调用printName(d)
,我们可能期望base.getName()
调用virtual函数getName()
并显示“I am a Derived”,但是它没有发生。相反,派生对象d被切片并且仅将基类部分复制到基类参数中。执行base.getName()
时,即使getName()
函数已被虚拟化,该类也没有派生部分可解析。因此,该程序将打印:
当然,修改方法是通过将函数参数作为引用而不是按值传递(这也是按引用而不是按值传递类参数是个好主意的另一个原因),可以轻松避免在此处切片。
void printName(const Base &base) // note: base now passed by reference
Slicing vectors
新程序员在切片方面遇到麻烦的另一个领域是尝试使用std :: vector
实现多态。考虑以下程序
#include <vector>
int main()
{
std::vector<Base> v{}; // std :: vector 被声明为 Base类型的向量
v.push_back(Base{ 5 }); // add a Base object to our vector
v.push_back(Derived{ 6 }); // add a Derived object to our vector
// Print out all of the elements in our vector
for (const auto& element : v)
std::cout << "I am a " << element.getName() << " with value " << element.getValue() << '\n';
return 0;
}
与前面的示例相似,因为std :: vector
被声明为Base
类型的向量,所以当将Derived(6)
添加到该向量时,将其切片。
解决这个问题要困难一些。许多新程序员尝试创建对对象的引用的std :: vector
,如下所示:
#include <iostream>
#include <vector>
int main()
{
std::vector<Base*> v{};
Base b{ 5 }; // b and d can't be anonymous objects
Derived d{ 6 };
v.push_back(&b); // add a Base object to our vector
v.push_back(&d); // add a Derived object to our vector
// Print out all of the elements in our vector
for (const auto* element : v)
std::cout << "I am a " << element->getName() << " with value " << element->getValue() << '\n';
return 0;
}
哪个有效? 有关于此的一些评论。首先,nullptr现在是一个有效的选项,可能不希望如此。其次,您现在必须处理指针语义,这可能很尴尬。但是从好的方面来说,这也允许动态内存分配,如果您的对象可能超出范围,这将很有用。
The Frankenobject
在上面的示例中,我们看到了由于划分出派生类而导致切片导致错误结果的情况。现在,让我们看一下派生对象仍然存在的另一种危险情况!
int main()
{
Derived d1{ 5 };
Derived d2{ 6 };
Base &b{ d2 };
b = d1; // this line is problematic
return 0;
}
函数的前三行非常简单。创建两个“派生”对象,并将“base”引用设置为第二个。第四行是误入歧途的地方。
如果b
是派生的话,由于b
指向d2
,我们将d1
分配给b
,您可能会认为结果将是d1
将被复制到d2
中 。但是b是一个基类对象,默认情况下C ++为类提供的operator =
不是virtual
的。因此,只有d1
的基本部分被复制到d2
中。
结果,您将发现d2
现在具有d1
的基类部分和d2
本身的派生部分。在这个特定示例中,这不是问题(因为Derived类没有自己的数据),但是在大多数情况下,您将创建一个Frankenobject
(它由多个对象的一部分组成)。
Conclusion
尽管C ++支持通过对象切片将派生对象分配给基础对象,但是一般来说,这很可能会令人头疼,因此通常应避免切片。确保您的函数参数是引用(或指针),并在涉及派生类时尽量避免传递任何值。
参考文献:
All code :
#include <iostream>
#include <vector>
class Base
{
protected:
int m_value{};
public:
Base(int value)
: m_value{ value }
{
}
virtual const char* getName() const { return "Base"; }
int getValue() const { return m_value; }
};
class Derived : public Base
{
public:
Derived(int value)
: Base{ value }
{
}
virtual const char* getName() const { return "Derived"; }
};
//void printName(const Base base) // note: base passed by value, not reference
void printName(const Base& base)
{
std::cout << "I am a " << base.getName() << '\n';
}
int main()
{
Derived derived{ 5 };
std::cout << "derived is a " << derived.getName() << " and has value " << derived.getValue() << '\n';
Base &ref{ derived };
std::cout << "ref is a " << ref.getName() << " and has value " << ref.getValue() << '\n';
Base *ptr{ &derived };
std::cout << "ptr is a " << ptr->getName() << " and has value " << ptr->getValue() << '\n';
Base base{ derived }; // what happens here?
std::cout << "base is a " << base.getName() << " and has value " << base.getValue() << '\n';
Derived d{ 5 };
printName(d); // oops, didn't realize this was pass by value on the calling end
//std::vector<Base> v{};
//v.push_back(Base{ 5 }); // add a Base object to our vector
//v.push_back(Derived{ 6 }); // add a Derived object to our vector
// Print out all of the elements in our vector
//for (const auto& element : v)
//std::cout << "I am a " << element.getName() << " with value " << element.getValue() << '\n';
std::vector<Base*> v{};
Base b{ 5 }; // b and d can't be anonymous objects
//Derived d{ 6 };
v.push_back(&b); // add a Base object to our vector
v.push_back(&d); // add a Derived object to our vector
// Print out all of the elements in our vector
for (const auto* element : v)
std::cout << "I am a " << element->getName() << " with value " << element->getValue() << '\n';
return 0;
}