简体   繁体   中英

Assigning derived class array to base class pointer

#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; 
}

The problem is in line 2 of main function. I expected that when sum1() function was called with argument arrD( which is an array of the Derived class objects), it would simply "cut off" the D::d, but in this case it rearranges the order in arrD, and the summing goes like this: 10+11+11+12+12+13+13+14+14+15 It seems to be alternating between b and d fields of arrD[i], and it should be summing up only b fields. Can someone please explain why? Thanks in advance.

You have been unlucky enough to hit one of the sweet spots of the type system that allows to compile perfectly invalid code.

The function int sum1 (B* arr) takes a pointer to a B object as argument according to the signature, but semantically it really takes a pointer to an array of B objects. When you call sum1(arrD) you are violating that contract by passing not an array of B objects, but rather an array of D objects. How do they differ? Pointer arithmetic is done based on the size of the type of the pointer, and a B object and a D object have different sizes.

An array of D is not an array of B

In general, a container of a derived type is not a container of the base type. If you think about it, the contract of a container of D is that it holds, well, D objects, but if a container of D was a container of B , then you would be able to add B objects (if the argument was extending, you might even consider adding D1 objects --also derived from B !).

If instead of raw arrays you were using higher order constructs, like std::vector the compiler would have blocked you from passing a std::vector<D> in place of a std::vector<B> , but why did it not stop you in the case of an array?

If an array of D is not an array of B , why did the program compile at all?

The answer to this predates C++. In C, all arguments to functions are passed by value. Some people consider that you can also pass-by-pointer , but that is just passing a pointer by-value . But arrays are large , and it would be very expensive to pass arrays by value. At the same time, when you dynamically allocate memory you use pointers , although conceptually, when you malloc 10 int s you are allocating an array of int . The designers of the C language considered this and made an exception to the pass by value rules: if you try to pass an array by value, a pointer to the first element is obtained, and that pointer is passed instead of the array (a similar rule exists for functions, you cannot copy a function, so passing a function implicitly obtains a pointer to the function and passes that instead). The same rules have been in C++ since the beginning.

Now, the next problem is that the type system does not differentiate from a pointer to an element when that is all there is, and a pointer to an element that is part of an array. And this has consequences. A pointer to a D object can be implicitly converted to a pointer to B , since B is a base of D , and the whole object of OO programming is being able to use derived types in place of base objects (well, that for the purpose of polymorphism).

Now going back to your original code, when you write sum1( arrD ) , arrD is used as an rvalue , and that means that the array decays to a pointer to the first element, so it effectively is translated to sum1( &arrD[0] ) . The subexpression &arrD[0] is a pointer, and a pointer is just a pointer... sum1 takes a pointer to a B , and a pointer to D is implicitly convertible to a pointer to B , so the compiler gladly does that conversion for you: sum1( static_cast<B*>(&arrD[0]) ) . If the function just took the pointer and used it as a single element, that would be fine, as you can pass a D in place of a B , but an array of D is not an array of B ... even if the compiler allowed you to pass it as such.

The size of a B is smaller than a size of a D . So when sum1 is iterating over the pointer arr , arr[1] is pointing at what it thinks is the 2nd B element in the array, which will actually be in the middle of the 1st D element.

So (assuming no padding), arrD has a layout like this:

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

But, you set a B *arr to it, making sum1 think it is an array of B. So sum1 will think that the parameter has a layout like this:

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

So, arr[1] is actually the d member of arrD[0] .

Your arr is of type B* , what this means is that arr[i] or (arr + i) would advance sizeof(B) * i in memory. The memory looks like this:

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

And the for loop adds

10 11 11 12 12 13 13 14 14 15

which is exactly what is the first elements in memory are, instead of advancing by the sizeof(D) * i like you want it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM