4

我阅读了 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);
}

执行后我得到:

姓名:家长

姓名:家长

Extra:子额外字符串

演员后姓名:家长

我理解前两行,因为它是“切片”的原因。但我不明白为什么我可以通过静态演员把孩子变成父母。书中写到,切片后,所有的专业信息都会被切掉。所以我想,我不能将p转换为c,因为我没有信息(在函数 printNames 的开头,创建了一个没有附加信息的新 Parent 对象)。

另外,如果演员表成功,为什么我得到“演员表后姓名:父母”而不是“演员表后姓名:孩子”?

4

2 回答 2

8

你的结果是厄运的非凡中风。这是我得到的结果:

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} {}

换句话说,你只复制了of 的_type变量,c 没有别的。您现在有一个的类型对象Parent(它的静态和动态类型都是Parent),c实际上根本没有传入printNames

在函数内部,您然后强制转换pChild&. 这种强制转换不能成功,因为p根本不是 aChild或可转换为一个,但 C++ 没有为此提供任何诊断(这实际上是一种耻辱,因为编译器可以简单地证明强制转换是错误的)。

现在我们处于未定义行为的领域,现在一切都被允许发生。在实践中,由于Child::extraString从不访问this(隐式或显式),对该函数的调用就会成功。调用是在一个非法对象上完成的,但由于该对象从未被触及,所以它有效(但仍然是非法的!)。

下一个调用 toChild::getName是一个虚拟调用,因此它需要显式访问this(在大多数实现中,它访问虚拟方法表指针)。再一次,因为代码无论如何都是UB,任何事情都可能发生。你很“幸运”,代码刚刚抓取了父类的虚方法表指针。使用我的编译器,该访问显然失败了。

于 2014-07-31T09:06:44.257 回答
2

那个代码太可怕了。发生了什么:

  • printNames(c)slices ,从嵌入在调用者对象中的对象c复制构造本地,然后设置' 指向虚拟调度表的指针。pParentcpParent

  • 因为 的数据成员Parent是从 复制过来的c,所以 的类型p是 2 并且if进入了分支

  • Child & c = static_cast<Child&>(p);有效地告诉编译器“相信我(我是一名程序员),我知道这p实际上是一个Child我想要引用的对象”,但这是一个公然的谎言,因为p实际上是一个ParentChild c

    • 如果您不确定它是否有效,则作为程序员的您有责任不要求编译器执行此操作
  • c.extraString()由编译器静态(在编译时)发现,因为它知道c是一个Child(或进一步派生的类型,但c.extraString不是virtual这样可以静态解析的;在对象上执行此操作是未定义的行为Parent,但可能是因为extraString不尝试要访问只有Child对象才能拥有的任何数据,它表面上已经为您运行“ok”

  • c.getName()virtual,所以编译器使用对象的虚拟调度表 - 因为对象实际上是一个Parent它动态地(在运行时)解析到Parent::getName函数并产生相关的输出

    • 虚拟调度的实现虽然是实现定义的,但您未定义的行为可能不会在所有 C++ 实现上以这种方式运行,甚至在所有优化级别上,以及所有编译器选项等。
于 2014-07-31T09:07:40.803 回答