Object slicing(对象切片)

关于对象切片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指向派生对象,派生对象具有基类部分和派生部分。由于refptr的类型为Base,因此refptr只能看到derivedBase部分。

但是derivedDerived部分仍然存在,只是根本无法通过refptr看到。但是,通过使用虚函数,我们可以访问函数的最衍生版本。因此,以上程序打印:
在这里插入图片描述
但是,如果我们仅将派生对象分配给基类对象,而不是设置派生对象的基类引用或指针,
会发生什么情况呢?

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;
}
  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
在 C++ 中,子类可以使用父类对象进行构造,这个过程叫做“对象切片”(Object Slicing)。对象切片的情况通常发生在以下场景中: 1. 子类继承了父类,并且子类没有定义自己的构造函数。 2. 子类定义了自己的构造函数,并且在构造函数中使用了父类的构造函数进行构造。 在第一种情况下,子类使用父类对象进行构造时,只会保留父类对象中的成员变量,而子类的成员变量则会被忽略。这是因为父类对象只包含父类的成员变量,而不包含子类的成员变量。 在第二种情况下,子类的构造函数必须调用父类的构造函数进行初始化,可以使用初始化列表或在构造函数的函数体中调用父类的构造函数。这样做的目的是为了保证父类的构造函数能够正确地初始化父类的成员变量。 以下是一个使用父类对象进行构造的示例代码: ```cpp class Parent { public: int value; Parent(int v) : value(v) {} }; class Child : public Parent { public: Child(Parent p) : Parent(p) {} }; int main() { Parent p(10); Child c(p); std::cout << c.value << std::endl; // 输出:10 return 0; } ``` 在上面的代码中,子类 `Child` 继承了父类 `Parent`,并且定义了一个使用父类对象进行构造的构造函数。在 `main` 函数中,创建了一个父类对象 `p`,然后用它来创建一个子类对象 `c`。因为子类使用了父类对象进行构造,所以子类对象 `c` 中只包含了父类的成员变量 `value`,而没有子类的成员变量。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值