当我们定义类指针或引用时到底发生了什么

在《C++对象模型——内存布局》中,我解释了C++对象在不同情境下的几种内存布局.

而我在读《Java核心技术·卷一》的时候看到了这样的一段话:

不能将一个超类的引用赋给子类变量

这本书中,是通过一个员工类作为基类派生出boss类来讲解的.代码示例如下:

1
2
3
4
Manager boss = new Manager();
Employee staff = new Employee();
Employee e = boss; //正确
Manager m = staff; //错误

书中提到

不是所有的雇员都是经理。如果赋值成功,m有可能引用了一个不是经理的Employee对象,当在后面调用 m.setBonus(...)时就有可能发生运行时错误.

如果根据C++对象的内存布局来看,子类的对象往往是要比父类对象大的,如果用大的来模拟小的(切片),那么会导致越界,反之,用小的来模拟大的(切片),自然就可以了.

这样解释固然很清楚,但我认为从内存分配的角度来理解这个问题会更好,这里先从C++的多态讲起,简单的来说,C++的多态就是通过父指针指向子类对象+虚函数来实现.当父类指针指向子类对象,可以调用子类从父类继承来的那部分.但如果父类中的函数声明了virtual,则会调用子类中重写的那个函数,这也就是实现了动态绑定,也就是多态.

所以定义父类指针指向对象时是将该对象的地址赋给指针,然后根据指针类型的大小限定了访问范围,可以看作是对该对象内存分布的一个切片,只能访问该范围内的成员,也就是从父类继承过来的成员.由此看来C++对象内存分布的设计真是相当的精妙呢.

程序验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>

class A {
public:
A(){}

private:
int a1;
int a2;
};
class B : public A {
public:
B(){}
private:
int b1;
int b2;
};

int main(int argc, const char * argv[]) {
A a;
B b;
A* Aptra = &a;
A* Aptrb = &b;
B* Bptrb = &b;

std::cout << Aptra << " " << Aptrb << " " << Bptrb << std::endl << std::endl;

std::cout << sizeof(a) << " " << sizeof(b) << std::endl;
std::cout << sizeof(*Aptra) << " "
<< sizeof(*Aptrb) << " " << sizeof(*Bptrb) << std::endl;
return 0;
}

输出结果:

0x7ffeefbff558 0x7ffeefbff548 0x7ffeefbff548

8 16
8 8 16
script>