我有一个非常特殊的情况......我继承了一些旧的C ++代码(前C ++ 11),这些代码很长,很复杂,一半用C语言编写,另一半用C语言编写 - 类心态(即:具有更多数据成员的类,没有太多的类方法,直接操纵数据成员......这个项目肯定需要一些重构,这就是我现在正在进行的工作)。但是这些代码暴露了我发现令人费解的一些问题。
首先让我们采取以下非常简单的方法(无错误):
#include <iostream>
static int c = 0;
struct Bar
{
Bar() : base_id(++c) { std::cout << "Bar "<< base_id << std::endl;}
int base_id;
};
struct Foo : public Bar
{
Foo() : x(c) { std::cout << "Foo "<< x << std::endl;}
int x;
};
int main()
{
Bar* b = new Foo[200];
Foo *p;
for(p = (Foo*)b; p - (Foo*)b < 200; p ++ )
{
std::cout << p->base_id << " " << ((Foo*)p)->x
<< " p-b=" << (unsigned)(p-(Foo*)b) << std::endl;
}
delete[] b;
}
或text version:我们通过new Derived
创建一个基类对象数组。到目前为止一切都很好,这就是大型复杂应用程序所做的(那里的类更加层次化,构造函数做得更多),但是全局静态变量(c
)也存在于大型复杂应用程序中,它使用它作为唯一对象标识符。然后大型复杂的应用程序开始工作,等等....
然后在某个时间点,我们遍历对象数组,做一些工作。迭代看起来与我在这里写的一个完全相同,在for
循环中找到,具有详尽的指针算法。我的示例只是在大型复杂应用程序中打印对象id(m
),完成了更多的工作。
但是在大型复杂应用程序的某个地方发生了一些魔术......在列表的一半之后的某个时间,对象(通过p
指针获得)不再有效,它们的base_id
s显示数据对我来说几乎看起来像指针和其他数据成员的值。但是,如果我检查特定索引处的数组成员,那么该值是有效的(即:正确增加对象ID)。
我还检查了以下内容:
所以......我得出了一个结论:
Bar
(e ...一些不同命名的基类)所以我可能需要更多地挖掘找到这个的代码。但是这是一个庞大的代码库,该数组有数千个引用,在此之前我想问:(这就是问题所在:)
我不知道C ++对象可以在运行时改变它的大小(如果有可能,请与我们分享),所以除了我已经确定的可能问题之外,社区中的任何人都可能有任何想法为什么指针算术表现得如此奇怪?
(是的,项目将转换为带有标准容器等的正确的c ++ 11 ......但是现在我只对这个特定问题感兴趣)
答案 0 :(得分:2)
你的问题在这里:
Bar* b = new Foo[200];
b
指向Bar
数组的第一个元素中的Foo
子对象,但不能用作访问数组的方法;大小错了。不断地将它强制转换回Foo
指针似乎有效,但很容易失败。 (当Bar
子对象甚至与它所属的Foo
对象的地址相同时,面对多重继承会变得更糟。)
在发布指针算法之前,您发布的代码小心谨慎地将b
强制转换为Foo*
。 (即使过于谨慎:((Foo*)p)->x
可能只是p->x
。)但是,如果在任何地方,任何地方都有人忘记这样做(例如尝试b[i]
或b+i
,或者(Foo*)(b+i)
...),它将导致您描述的行为。
或者,随着所有这些向下倾斜的进行,有人将Bar*
向下倾斜
Baz*
,其中Baz
也继承自Bar
,但其大小与Foo
不同。
这也会以不稳定的方式覆盖字段。
此外,delete[] b
is undefined behavior。因此,编译器得出结论b
确实指向Bar
个实例,并且将强制转换或者强制转换为Foo*
- 编译器没有可能的范围 - 编译器会这样做这些天的事情。
总而言之,Bar* b = new Foo[200];
是行不通的。使用
Foo* fp = new Foo[200];
代替。如果由于某种原因,您需要Bar*
,则可以使用
Bar* b = fp;
但不清楚你为什么需要这个;只要需要fp
,您就可以使用Bar*
。