我有这段代码:
class Class {
public:
virtual void first() {};
virtual void second() {};
};
Class* object = new Class();
object->first();
object->second();
delete object;
我使用/ C2使用Visual C ++ 10进行编译并进行反汇编:
282: Class* object = new Class();
00403953 push 4
00403955 call dword ptr [__imp_operator new (4050BCh)]
0040395B add esp,4
0040395E test eax,eax
00403960 je wmain+1Ch (40396Ch)
00403962 mov dword ptr [eax],offset Class::`vftable' (4056A4h)
00403968 mov esi,eax
0040396A jmp wmain+1Eh (40396Eh)
0040396C xor esi,esi
283: object->first();
0040396E mov eax,dword ptr [esi]
00403970 mov edx,dword ptr [eax]
00403972 mov ecx,esi
00403974 call edx
284: object->second();
00403976 mov eax,dword ptr [esi]
00403978 mov edx,dword ptr [eax+4]
0040397B mov ecx,esi
0040397D call edx
285: delete object;
0040397F push esi
00403980 call dword ptr [__imp_operator delete (405138h)]
请注意,在00403968
处,对象的地址开始(存储vptr
的地址)被复制到esi
寄存器中。然后在0040396E
,此地址用于检索vptr
,vptr
值用于检索first()
的地址。然后在00403976
再次检索vptr
并用于检索second()
的地址。
为什么vptr检索两次?对象是否可能在调用之间更改vptr
,还是仅仅是优化不足?
答案 0 :(得分:9)
为什么vptr检索两次?对象是否可能在调用之间更改其vptr,或者它只是一个欠优化?
考虑:
object->first();
此调用可能会破坏对象并在同一块内存中创建一个新对象。因此,在此调用之后,不能对状态做出任何假设。 E.g:
#include <new>
struct Class {
virtual void first();
virtual void second() {}
virtual ~Class() {}
};
struct OtherClass : Class {
void first() {}
void second() {}
};
void Class::first() {
void* p = this;
static_assert(sizeof(Class) == sizeof(OtherClass), "Oops");
this->~Class();
new (p) OtherClass;
}
int main() {
Class* object = new Class();
object->first();
object->second();
delete object;
}
如果该函数是内联函数和/或使用链接时代码生成,编译器可以优化掉不必要的寄存器加载。
由于DeadMG和Steve Jessop注意到上述代码表现出未定义的行为。根据C ++ 2003标准的3.8 / 7:
如果在对象的生命周期结束之后并且在重用或释放对象占用的存储之前,则在原始对象占用的存储位置创建新对象,指向原始对象的指针,引用原始对象的引用,或者原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,就可以用于操作新对象,如果:
- 新对象的存储空间正好覆盖原始对象占用的存储位置,
- 新对象与原始对象的类型相同(忽略顶级cv限定符),
- 原始对象的类型不是const限定的,如果是类类型,则不包含任何类型为const-qualified或引用类型的非静态数据成员,并且
- 原始对象是类型为T的派生程度最高的对象(1.8),新对象是类型为T的派生程度最高的对象(即,它们不是基类子对象)。
上述代码不符合上述清单中的要求2.
答案 1 :(得分:2)
它存储在esi
中,以便在对不同功能的调用之间保存。
微软的惯例是
编译器生成prolog和epilog代码以保存和恢复ESI,EDI,EBX和EBP寄存器(如果它们在函数中使用)。
因此存储在esi
中的指针将保留,但this
中的ecx
指针可能不会。
答案 2 :(得分:2)
首先回答标题中的问题:
是的,派生类中的对象在构造和销毁期间更改其类型。这是唯一的情况。
问题正文中的代码是不同的。但正如Maxim正确指出的那样,你只需要一个指针。该指针可以指向(在不同时间)驻留在同一地址的两个不同对象。