address changes when i assign to a pointer a pointer from a derived class in C++

Viewed 144

I'm a brand new in C++ and I'm experimenting with Polymorphism. I have the following code:

#include <iostream>

class Base1
{
protected:
    int b1;

public:
    int m() { return 1; }
};

class Base2
{
protected:
    int b2;

public:
    int n() { return 2; }
};

class Der : public Base1, public Base2
{
protected:
    int d1;

public:
    int m() { return 11; }
    int n() { return 21; }
};

int main()
{
    Der *ptr = new Der();
    Base1 *b1 = ptr;
    Base2 *b2 = ptr;

    std::cout << "d: " << ptr << ", m: " << ptr->m() << ", n: " << ptr->n() << "\n";
    std::cout << "b1: " << b1 << ", m: " << b1->m() << "\n";
    std::cout << "b2: " << b2 << ", n: " << b2->n() << "\n";

    delete ptr;
    return 0;
}

When i run this code the interesting thing is that b2 is shifted by 4 bytes, here my output:

d: 0x564eab6cbeb0, m: 11, n: 21
b1: 0x564eab6cbeb0, m: 1
b2: 0x564eab6cbeb4, n: 2

Why this happens only with b2? I guess it's related on how things are stored in memory because if I remove the int field in b1 b2 is not affected. Is there a way to see easily stack and heap? I would like to see what happens. (I'm using Virtual Studio Code)

2 Answers

The sample of OP (simplified a bit)

struct Base1 {
  int b1;
};

struct Base2 {
  int b2;
};

struct Der: Base1, Base2 { };

may result in the following memory layout:

// start of Der
// start of Base1
0000: Base1::b1 // type int
// start of Base2
0004: Base2::b2 // type int

So, when struct Der is instanced part of its contents is an instance of struct Base2 but it doesn't start at the same address of the Der instance.

With

Der *ptr = new Der();

the initialization

Base2 *b2 = ptr;

doesn't result in a plain copy of the address in ptr to b2. There is also an implicit conversion from Der* to Base2* involved. The compiler is aware of the relation of classes Der and Base2. Hence, the conversion results in a silent addition of the offset of Base2 in Der.

To show this in action, I made a small demo. (I'm not sure how convincing it is.):

#include <iostream>

struct Base1 {
  int b1 = 1;
};

struct Base2 {
  int b2 = 2;
};

struct Der: Base1, Base2 { };

int main()
{
  Der *ptr = new Der;
  Base2 *b2;
  std::cout << "ptr:" << ptr << ", ptr1->b2: " << ptr->b2 << '\n';
  b2 = ptr;
  std::cout << "b2: " << b2 << ", b2->b2: " << b2->b2 << '\n';
}

Compiled with gcc 4.1.2, you can find the following code where the actual assignment happens:

        mov     %rax, QWORD PTR [%rbp-24] # %rbp-24 <- storage of ptr on stack
        add     %rax, 4
        mov     QWORD PTR [%rbp-32], %rax # %rbp-32 <- storage of b2 on stack

Live Demo on CompilerExplorer

Note:

Compiling with most modern compiler version will emit a similar add command but also a lot of other stuff which is not as easy to decipher (by eyes) than the generated code of the older version. Hence, I've chosen the oldest gcc I could find.

Yes, it's related on how things are stored in memory.

Class Der includes Base1 and Base2 as a subobject.

Look this derived_class.

Related