【问题标题】:How does the virtual keyword affect memory locations?virtual 关键字如何影响内存位置?
【发布时间】:2021-04-29 20:08:29
【问题描述】:

我之前有一个工作面试,被问到以下代码的输出是什么:

struct A {
    int data[2];
    A(int x, int y) { data[0] = x; data[1] = y; }

    virtual void f() {}
};

int main(){
    A a(22, 33);
    int* data = (int*)&a;
    cout << data[2] << endl;
}

我已经讨论过了,但无法完全弄清楚。他提到虚函数是一个提示。之后我编译它并得到了输出:

22

然后我想到了虚函数,把它去掉了:

struct A {
    int data[2];
    A(int x, int y) { data[0] = x; data[1] = y; }

    //virtual void f() {}
};

int main(){
    A a(22, 33);
    int* data = (int*)&a;
    cout << data[2] << endl;
}

导致输出:

0

我的问题是:看似无关紧要的虚函数调用如何影响结果对象的内存布局导致这种情况?

【问题讨论】:

  • data[2] 是未定义的行为。无需多言。回答问题 - 对象将有一个 vtable
  • 你的面试官依赖于他们选择的编译器的行为。面试官可能没有意识到这一点。 C++ 语言在调用未定义行为时很清楚。也就是说,possible dupe.
  • 这两种情况下的行为都是未定义的。如果面试官期望特定的输出 - 可能与公司使用的特定编译器一致 - 那么你的面试官是一名黑客,他编写特定于一个编译器的代码,却不了解未定义行为的重要性。
  • 也许你申请的工作与逆向工程或黑客有关? Richard Critten 和 Drew Dorman 从“法律”的角度来看都是正确的。 “这是未定义的行为。”是正确答案。然而,已经为那些寻求定义这种“未定义行为”的人创造了很多工作,从这个角度来看,您观察到的行为将几乎在所有现代编译器中普遍存在。正如其他人所指出的,您刚刚发现了 vtables。尝试打印 data[0] 或 data[1] 以查看更多信息。
  • @Roguebantha - 实际上假设 vtable 的大小(或它的表示,在类类型中)是 int 大小的两倍。这不是一个有效的普遍假设。如果问题要求对可能适合逆向工程工作的某些观察到的行为(将特定于特定实现)进行可能解释。如果在这两种情况下都需要特定的输出,则表明缺乏理解。

标签: c++ memory virtual-functions


【解决方案1】:

向一个类添加 1 个或多个 virtual 方法会导致(在本例中为 1)该类的对象实例包含指向编译器管理的“虚拟方法表”的隐藏指针( vtable) 在对象内存的前面,例如:

int *data = &a;     A a;
   data -> --------------------
           | vtable           | -> [0]: &A::f
           | (8 bytes)        |
           |------------------|
data[2] -> | data[0]: 22      |
           |------------------|
           | data[1]: 33      |
           --------------------

假设sizeof(int) 是4(通常是这种情况),能够通过指向对象的int* 指针访问对象的data[0] 成员,该指针正在索引第三个int,告诉我们如果代码正在编译为 64 位(如果它被编译为 32 位,则 vtable 指针将是 4 个字节,则在对象的前面存在额外的 8 个字节,这可以由 vtable 指针来解释,而data[2] 将访问A::data[1] = 33)。

没有任何virtual 方法,不存在vtable,因此额外的8 个字节不存在,因此从对象前面开始索引到第三个int 将超出对象的边界进入周围内存,例如:

int *data = &a;     A a;
   data -> --------------------
           | data[0]: 22      |
           |------------------|
           | data[1]: 33      |
           --------------------
data[2] -> 

1:这是编译器的实现细节。 C++ 标准没有规定如何实现虚拟方法。不过,大多数编译器都会使用 vtable。

【讨论】:

  • 所有的答案都很棒,但是这个真的帮助我理解了,谢谢。有一次我被问到是否知道虚拟方法如何知道调用它们正确的覆盖版本。我现在意识到他正在测试我对 vtables 的了解,这是我以前不知道的。感谢您帮助我解决这个问题!
【解决方案2】:

virtual 关键字如何影响内存位置?

由于该类没有任何其他虚函数,因此添加virtual 函数会添加指向该类的指针。指针指向一个vtable,对于该类来说是唯一的。

指针可能是int 的大小,但也可能不是。在 64 位系统上,它通常是 int 大小的两倍。但不一定是这样。

指针可能位于类内存布局的开头,但也可能不在。开头是一个常见的位置,但流行的商业 C++ 编译器有时会将其放在其他位置。

我之前有一个工作面试,被问到下面代码的输出是什么

这是未定义的行为——这意味着对程序可能会做的任何声明都是危险的。

具体来说,这段代码:

int* data = (int*)&a;
cout << data[2] << endl;

声称指向A的指针是指向int的指针。这不是真的。 A 不是 int

由于它是未定义的行为,因此无法保证从一次运行中观察到的行为将与从任何其他运行中观察到的行为相匹配。正因为如此,UB是要避免的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-08-01
    • 1970-01-01
    • 2021-04-04
    • 2016-10-11
    • 1970-01-01
    • 1970-01-01
    • 2010-10-25
    相关资源
    最近更新 更多