2

我试图看看如果我们尝试以类似的方式“破坏”一个对象数组,在 C++ 中会发生什么,我们可以尝试在 Java 中这样做。

在 Java 中,我们可以有一个 Double[] 类型的数组,例如,将其向上转换为 Number[](因为 Double 是 Number 的子类),并尝试将 Number 的另一个子类添加到数组中,例如 Integer。代码会编译,但我们会在运行时得到 ArrayStoreException,因为在运行时会根据数组的实际类型(恰好是 Double)检查 Integer 类型,当然会出现不匹配。代码可能如下所示:

Double[] ds = new Double[12];
Number[] ns = ds;
ns[0] = 2.3;              // OK
ns[1] = new Integer(1);   // compiles, but we have ArrayStoreException in runtime

所以我想 - C++ 呢?我们可以尝试执行相同的技巧吗?运行时会发生什么?

这是我尝试过的代码和输出。

#include <iostream>

class A
{
public:
    A(int v = 0): val(v) {}
    virtual void g() {std::cout << val << " in A\n";}
    void setVal(int v) {val = v;}
protected:
    int val;
};

class B : public A
{
public:
    virtual void g() {std::cout << val << " in B\n";}
};

class C : public A
{
public:
    C(int v = 0): A(v) {}
    virtual void g() {std::cout << val << " in C\n";}
private:
    int stuff[10];
};


void f(A* as)
{
    as[1] = *(new A(12));
}

void f2(A* as)
{
    as[1] = *(new C(22));
}

int main()
{
    A* bs = new B[5];
    for (int i=0 ; i<5; ++i)
    {
        bs[i].setVal(i);
        bs[i].g();
    }

    std::cout << std::endl;

    f(bs);
    for (int i=0 ; i<5; ++i)
    {
        bs[i].g();
    }

    std::cout << std::endl;

    f2(bs);
    for (int i=0 ; i<5; ++i)
    {
        bs[i].g();
    }
}

输出:

0 in B
1 in B
2 in B
3 in B
4 in B

0 in B
12 in B
2 in B
3 in B
4 in B

0 in B
22 in B
2 in B
3 in B
4 in B

看到创建 A 或 C 然后将其复制到 B 的数组中都会复制数据(并且,正如预期的那样,在 C 的情况下,仅复制属于 A 的数据 - 复制元素后没有内存损坏),但是选择了 B 的方法,意味着 vptr 一定没有被复制。

所以我的问题是:

  • 我猜 vptr 没有在默认赋值运算符中复制。是这样吗?这是我们从 B 调用方法但从 C 对象调用数据的唯一可能原因吗?

  • 我们真的可以想出一个例子来让一些不好的或意想不到的事情发生,一些运行时失败吗?我猜我所说的坏的意思是有一个Bs 数组,但其中有一个 A 或 C 的对象(不是 B 或 B 的子类型),一个对 B 来说是“外星人”的对象?

  • 或者,也许 C++ 语言确实通过其特性的某些组合显式或隐式地保证这不会发生(比如当 Java 显式引发 ArrayStoreException 时)?

升级版:

A* bs = new B[5];

我实际上是在最后一刻更改了这一行,以强调B方法的运行时选择(不应该这样做,因为方法是虚拟的,所以很明显)。我最初有B* bs = new B[5];并且输出是相同的。

4

2 回答 2

1

1) 是的,vptr没有在赋值运算符中复制。赋值运算符不会更改对象的运行时类型。

2)A* bs = new B[5]; 危险的。如果B包含一些A不包含的数据怎么办?然后sizeof(B) > sizeof(A),并通过访问项目bs[i]导致未定义的行为(我的系统上的分段错误)。

于 2013-06-29T20:32:38.313 回答
1

实际上,当您 rite A* bs = new B[5] 时,您做错了什么。如果 B 元素的大小与 B 元素的大小不同,则会导致问题。因为您可以将 B* 向上转换为 A*,所以转换是安全的……如果只有一个元素。

假设 A 的长度为 32 位,B 的长度为 64。那么,当您浏览 bs 数组时,就会发生奇怪的事情。(bs[1] 将是第一个 B 元素的结尾)。

如果您有多个虚拟继承并且编译器需要更改指向的地址,那么它只会对第一个元素进行。

所以 :

  1. 无需复制任何内容,如果需要,只需更改第一个元素的指针地址。

  2. 是的,只需在 B 中添加一些元素,使 B 大于 A,您就可以在未定义行为的奇妙世界中。您还可以将 C 对象的 A 部分复制到 B 对象中。

注意:如果你想在同一个数组中管理一些 B 和 C,你可以使用 A** (或 std::whatever ),然后它将是安全的。(但你可能已经知道了)。

于 2013-06-29T20:37:41.290 回答