类非常简单地存储在内存中 - 几乎与结构相同。如果您检查存储类实例的位置的内存,您会注意到它的字段只是一个接一个地打包。
但是,如果您的类具有虚拟方法,则会有所不同。在这种情况下,存储在类实例中的第一件事是指向虚拟方法表的指针,它允许虚拟方法正常工作。您可以在 Internet 上阅读更多相关信息,这是一个更高级的主题。幸运的是,您不必担心,编译器会为您完成所有工作(我的意思是,处理 VMT,不用担心)。
让我们来看看方法。当你看到:
void MyClass::myFunc(int i, int j) { }
实际上编译器将它转换成类似的东西:
void myFunc(MyClass * this, int i, int j) { }
当你打电话时:
myClassInstance->myFunc(1, 2);
编译器生成以下代码:
myFunc(myClassInstance, 1, 2);
请记住,这是一种简化——有时它比这更复杂一些(尤其是当我们讨论虚拟方法调用时),但它或多或少地显示了编译器如何处理类。如果您使用诸如 WinDbg 之类的低级调试器,您可以检查方法调用的参数,您会看到,第一个参数通常是指向您调用该方法的类实例的指针。
现在,所有相同类型的类共享它们的方法的二进制文件(编译代码)。因此,为每个类实例复制它们是没有意义的,因此内存中只有一个副本,所有实例都使用它。现在应该清楚了,为什么即使没有类的实例,也可以获得指向方法的指针。
但是,如果要调用保存在变量中的方法,则始终必须提供一个类实例,该实例可以通过隐藏的“this”参数传递。
编辑:回应评论
您可以在另一个 SO question中阅读有关指针成员的更多信息。我猜,指向成员的指针存储了类实例的开头和指定字段之间的差异。当您尝试使用指向成员的指针检索字段的值时,编译器会定位类实例的开头并移动存储在指向成员的指针中的字节数以到达指定的字段。
每个类实例都有自己的非静态字段副本——否则它们对我们没有多大用处。
请注意,与指向方法的指针类似,您不能直接使用指向成员的指针,您必须再次提供类实例。
我所说的证明是有序的,所以这里是:
class C
{
public:
int a;
int b;
};
// Disassembly of fragment of code:
int C::*pointerToA = &C::a;
00DB438C mov dword ptr [pointerToA],0
int C::*pointerToB = &C::b;
00DB4393 mov dword ptr [pointerToB],4
你能看到pointerToA和pointerToB中存储的值吗?字段a
与类实例的开头相距 0 个字节,因此值 0 存储在 pointerToA 中。另一方面, fieldb
存储在 field 之后a
,它有 4 个字节长,因此 value 4 存储在 pointerToB 中。