将派生类数组赋值给基类指针

3
#include <iostream> 
class B { 
public: 
 B () : b(bCounter++) {} 
int b; 
static int bCounter;  
}; 
int B::bCounter = 0; 
class D : public B { 
public: 
D () : d(bCounter) {} 
int d; 
}; 
const int N = 10; 
B arrB[N]; 
D arrD[N]; 
int sum1 (B* arr) { 
    int s = 0; 
    for (int i=0; i<N; i++) 
         s+=arr[i].b; 
    return s; 
} 
int sum2 (D* arr) { 
     int s = 0; 
     for (int i=0; i<N; i++) s+=arr[i].b+arr[i].d; 
     return s; 
} 
int main() { 
    std::cout << sum1(arrB) << std::endl; 
    std::cout << sum1(arrD) << std::endl; 
    std::cout << sum2(arrD) << std::endl; 
    return 0; 
}

问题出现在主函数的第二行。我原本期望当使用参数arrD(一个Derived类对象数组)调用sum1()函数时,它会简单地“剪掉”D::d,但是在这种情况下,它重新排列了arrD中的顺序,并且求和如下: 10+11+11+12+12+13+13+14+14+15 看起来它在交替使用arrD[i]的b和d字段进行求和,而应该只对b字段求和。 能否有人解释一下为什么? 提前感谢您。
3个回答

7
您不幸地碰到了类型系统中的一个甜点,它允许编译出完全无效的代码。
根据函数签名,函数int sum1 (B* arr) 接受一个指向 B对象的指针作为参数,但从语义上讲,它实际上接受一个指向 B对象数组的指针。当您调用sum1(arrD)时,您正在违反这个约定,因为您传递的不是B对象数组,而是D对象数组。它们有什么不同呢?指针算术是基于指针类型的大小完成的,而B对象和D对象的大小不同。
“一个D数组不是一个B数组”
通常,派生类型的容器不是基类型的容器。如果您仔细思考一下,D容器的约定是它保存D对象,但如果D容器是B容器,则可以添加B对象(如果参数是扩展的,则甚至可以考虑添加D1对象——也是从B派生的!)。
如果您使用高阶结构而不是原始数组,比如std :: vector,编译器将阻止您将std :: vector < D >放在std :: vector < B >的位置,但是为什么在数组的情况下它没有阻止您?
“如果一个D数组不是一个B数组,那么程序为什么能够编译?”
这个答案早于C ++。在C中,所有函数参数都是按值传递的。有些人认为您也可以通过指针传递,但那只是通过值传递指针。但数组很大,按值传递数组将非常昂贵。同时,在动态分配内存时使用指针,尽管在概念上,当您malloc 10个int时,您正在分配一个int数组。C语言的设计者们考虑到了这一点,并对传递值规则做了一个例外:如果您尝试按值传递数组,则会获得指向第一个元素的指针,并传递该指针而不是数组(类似的规则也存在于函数中,您无法复制函数,因此隐式传递指向函数的指针)。自C ++开始就是相同的规则。
现在,下一个问题是当只有一个元素时,类型系统无法区分指向该元素的指针和指向数组中的元素的指针。这会产生后果。因为B是D的基类且面向对象编程的整个目的就是能够使用派生类型替代基类对象(这是多态的用途),所以D对象的指针可以隐式转换为B对象的指针。
现在回到原始代码,当你写sum1(arrD)时,arrD被用作rvalue,这意味着数组会退化为指向第一个元素的指针,所以它实际上被翻译成sum1(&arrD[0])。子表达式&arrD[0]是一个指针,而指针只是指针...sum1接受一个指向B的指针,而指向D的指针隐式地可转换为指向B的指针,所以编译器很乐意为你执行该转换:sum1(static_cast<B*>(&arrD[0]))。如果函数只接受指针并将其用作单个元素,那么这将是可以的,因为你可以在B的位置上使用D,但是D的数组不是B的数组...即使编译器允许你将其作为B的数组传递也不行。

因此,根据C++核心指南,应该使用向量或跨度。链接 - http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c152-never-assign-a-pointer-to-an-array-of-derived-class-objects-to-a-pointer-to-its-base - jack_1729

2
B 的大小比 D 的大小要小。因此,当 sum1 在迭代指针 arr 时,arr[1] 指向的是数组中它认为是第二个 B 元素,但实际上位于第一个 D 元素的中间位置。

因此(假设没有填充),arrD 的布局如下:

arrD: | 2 ints    | 2 ints    | 2 ints    | ...

但是,你将一个B *arr赋值给它,使得sum1认为它是一个B类型的数组。因此,sum1会认为参数的布局如下:

arr:  | int | int | int | int | int | int | ...

所以,arr[1] 实际上是 arrD[0]d 成员。

迭代为什么不会在vtable中遇到值呢? - YePhIcK
@YePhIcK:由于没有虚拟方法,因此不存在虚函数表。 - jxh
确实,你是正确的。我太傻了。我自动假设基类的析构函数是“虚拟的”,但实际上应该从派生类中继承 :) - YePhIcK
@YePhIcK:在这种情况下,基类中的虚函数并不能帮助你。我怀疑它甚至会更糟。 - PiotrNycz

2
您的arr属于B*类型,这意味着arr[i](arr + i)会在内存中向前移动sizeof(B) * i。内存看起来像这样:

10 11 11 12 12 13 13 14 14 15 15 16 16 17 17 18 18 19 19 20

而for循环添加的是:

10 11 11 12 12 13 13 14 14 15

这正好是内存中第一个元素的内容,而不是按照您想要的sizeof(D) * i向前移动。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接