我在C++中读到了切片问题,并尝试了一些例子(我来自Java背景(。不幸的是,我不理解一些行为。目前,我被困在这个例子中(来自Efficent C++第三版的替代示例(。谁能帮我理解它?
我的家长简单类:
class Parent
{
public:
Parent(int type) { _type = type; }
virtual std::string getName() { return "Parent"; }
int getType() { return _type; }
private:
int _type;
};
我的简单儿童课:
class Child : public Parent
{
public:
Child(void) : Parent(2) {};
virtual std::string getName() { return "Child"; }
std::string extraString() { return "Child extra string"; }
};
主要:
void printNames(Parent p)
{
std::cout << "Name: " << p.getName() << std::endl;
if (p.getType() == 2)
{
Child & c = static_cast<Child&>(p);
std::cout << "Extra: " << c.extraString() << std::endl;
std::cout << "Name after cast: " << c.getName() << std::endl;
}
}
int main()
{
Parent p(1);
Child c;
printNames(p);
printNames(c);
}
在我得到的执行之后:
姓名:家长
姓名:家长
额外:子项额外字符串
演员后姓名:父级
我理解前两行,因为它是"切片"的原因。但我不明白为什么我可以通过静态强制转换将孩子转换为父母。书中写道,切片后,所有专业信息都会被切掉。所以我想,我不能将 p 转换为 c,因为我没有信息(在函数 printNames 的开头,在没有附加信息的情况下创建了一个新的 Parent 对象(。
此外,如果演员阵容成功,为什么我会得到"演员后的名字:父母"而不是"演员后的名字:孩子"?
你的结果是一次非凡的厄运。这是我得到的结果:
Name: Parent
Name: Parent
Extra: Child extra string
bash: line 8: 6391 Segmentation fault (core dumped) ./a.out
下面是代码中发生的情况:
当您将c
传递给printNames
时,会发生转换。特别是,由于传递是按值传递的,因此调用 Parent
的复制构造函数,该构造函数是隐式声明的,其代码如下所示:
Parent(Parent const& other) : _type{other._type} {}
换句话说,您复制了 c
的 _type
变量,而不是其他任何内容。您现在有了一个类型为 Parent
的新对象(其静态和动态类型都Parent
(,并且c
实际上根本没有传递到printNames
中。
在函数内部,然后强制将p
转换为Child&
。这个强制转换不能成功,因为p
根本不是一个Child
,或者可以转换为一个,但是C++并没有给你任何诊断(这实际上是一种耻辱,因为编译器可以微不足道地证明强制转换是错误的(。
我们处于不确定行为的土地上,现在一切都被允许发生。实际上,由于Child::extraString
从不访问this
(隐式或显式(,因此对该函数的调用会成功。调用是在非法对象上完成的,但由于该对象从未被触摸过,因此可以工作(但仍然是非法的!
下一个调用,Child::getName
,是一个虚拟调用,因此它需要显式访问this
(在大多数实现中,它访问虚拟方法表指针(。再说一次,因为代码无论如何都是UB,所以任何事情都可能发生。您很"幸运",代码刚刚抓住了父类的虚拟方法表指针。使用我的编译器,该访问显然失败了。
那段代码太可怕了。 发生了什么事情:
-
printNames(c)
切片c
,从嵌入在调用方c
对象中的Parent
对象复制构造本地p
,然后设置p
指向Parent
虚拟调度表的指针。 -
因为
Parent
的数据成员是从c
复制的,所以p
的类型为 2,并且输入了if
分支 -
Child & c = static_cast<Child&>(p);
有效地告诉编译器"相信我(我是一名程序员(,我知道p
实际上是一个我想引用的Child
对象",但这是一个公然的谎言,因为p
实际上是从Child
c
复制的Parent
对象- 作为程序员,如果您不确定它是否有效,则有责任不要求编译器执行此操作
-
编译器静态(在编译时(找到
c.extraString()
,因为它知道c
是Child
(或进一步派生的类型,但c.extraString
不是virtual
,因此可以静态解析;在Parent
对象上执行此操作是未定义的行为,但可能是因为extraString
不会尝试访问只有Child
对象才会具有的任何数据, 它表面上为您运行"正常"> -
c.getName()
是virtual
的,所以编译器使用对象的虚拟调度表 - 因为对象实际上是一个Parent
它动态解析(在运行时(到Parent::getName
函数并产生关联的输出- 虚拟调度的实现是定义的实现,您的未定义行为可能不会在所有C++实现中以这种方式运行,甚至在所有优化级别,使用所有编译器选项等。