让我们举例如下:
class Base{
virtual string function1(){ return "Base - function1"; };
virtual string function2(){ return "Base - function2"; };
};
class Derived : public Base {
virtual string function2(){ return "Derived - function2"; };
virtual string function1(){ return "Derived - function1"; };
string function3() { return "Derived - function3"; };
};
所以vtable结构就像
Base-vTable
-----------------------
name_of_function address_of_function
function1 &function1
function2 &function2
-----------------------
-----------------------
Derived-vTable
-----------------------
name_of_function address_of_function
function1 &function1
function2 &function2
或者就像
Base-vTable
-----------------------
Offset function
+0 function1
+4 function2
-----------------------
-----------------------
Derived-vTable
-----------------------
Offset function
+0 function1
+4 function2
如果是后者?然后是什么偏移?在哪里使用?
功能名称: 它是错误的功能名称吗?如果它被破坏,那么基础和派生的受损名称将不匹配,并且vtable查找不起作用。 编译器确实破坏了所有虚函数名称,因此它必须是一个受损的名称,它是否意味着基数和基数的错位名称。如果是虚函数,则派生相同。
答案 0 :(得分:8)
虚拟表只是函数指针的数组,就像你的第二个代码片段一样。编译器通过指针将对虚函数的调用转换为调用,例如
Base * b = /* ... */;
b->function2();
被翻译为
b->__vtable[1]();
我使用名称__vtable
来引用虚拟表(但请注意,虚拟表通常无法直接访问)。
表中条目的顺序由在类中声明函数的顺序决定。请记住,类定义始终在调用时可用。
答案 1 :(得分:3)
我正在解释以下代码。我想它会让你清楚
Base *p = new Derived;
p->function2();
在编译时,创建VTable,类Base的VTable与Derived类的VTable相同。我的意思是两个函数都有你在第一种情况中提到的两个函数。编译器插入代码来初始化正确对象的vptr。
当编译器看到语句p-> function2();时,它不会对被调用函数进行任何绑定,因为t只知道Base对象。从类Base的VTable来了解function2的位置(这是VTable中的第二个位置)。
在运行时,Dervied类的VTable被分配给vptr。调用VTable的第二个位置中的函数。
答案 2 :(得分:3)
清除这个的最简单方法是查找实际的实现。
请考虑以下代码:
struct Base { virtual void foo() = 0; };
struct Derived { virtual void foo() { } };
Base& base();
void bar() {
Base& b = base();
b.foo(); // virtual call
}
现在,将其提供给Clang的Try Out页面以获取LLVM IR:
; ModuleID = '/tmp/webcompile/_6336_0.bc'
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
target triple = "x86_64-unknown-linux-gnu"
%struct.Base = type { i32 (...)** }
define void @_Z3barv() {
%1 = tail call %struct.Base* @_Z4basev()
%2 = bitcast %struct.Base* %1 to void (%struct.Base*)***
%3 = load void (%struct.Base*)*** %2, align 8
%4 = load void (%struct.Base*)** %3, align 8
tail call void %4(%struct.Base* %1)
ret void
}
declare %struct.Base* @_Z4basev()
由于我认为你可能还不了解IR,我们一点一点地回顾它。
首先来看一些你不应该担心的事情。它标识了编译它的体系结构(处理器和系统)及其属性。
; ModuleID = '/tmp/webcompile/_6336_0.bc'
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
target triple = "x86_64-unknown-linux-gnu"
然后,LLVM会讲授类型:
%struct.Base = type { i32 (...)** }
从结构上分析类型。所以在这里我们只得到Base
将由单个元素i32 (...)**
组成:这实际上是“臭名昭着”的v-table指针。为什么这种奇怪的类型?因为我们将在v-table中存储许多不同类型的函数指针。这意味着我们将拥有异构数组(这是不可能的),因此我们将其视为“通用”未知元素的数组(以标记我们确保存在的内容)并且由应用程序构建在实际使用之前指向适当的函数指针类型的指针(或者更确切地说,如果我们在C或C ++中,则IR的级别要低得多)。
跳到最后:
declare %struct.Base* @_Z4basev()
这声明了一个函数(_Z4basev
,名称被修复),它返回一个指向Base
的指针:在IR引用中,指针都用指针表示。
好的,让我们看看bar
(或_Z3barv
的定义,因为它被破坏了)。这就是有趣的事情所在:
%1 = tail call %struct.Base* @_Z4basev()
对base
的调用,它返回指向Base
的指针(返回类型总是在调用站点处精确,更容易分析),这存储在一个名为{{1的常量中}}
%1
一个奇怪的bitcast,它将我们的 %2 = bitcast %struct.Base* %1 to void (%struct.Base*)***
转换为指向奇怪事物的指针......本质上,我们正在获得v-table指针。它没有被“命名”,我们只是在类型的定义中确保它是第一个元素。
Base*
我们首先加载v-table(由 %3 = load void (%struct.Base*)*** %2, align 8
%4 = load void (%struct.Base*)** %3, align 8
指向),然后加载函数指针(由%2
指向)。目前,%3
因此%4
。
&Derived::foo
最后,我们调用该函数,并将 tail call void %4(%struct.Base* %1)
元素传递给它,在此明确说明。
答案 3 :(得分:2)
第二种情况 - 假设指针占用4个字节(32位机器)。
函数名永远不会存储在可执行文件中(调试除外)。 虚拟表只是函数指针的向量,由运行代码直接访问。
答案 4 :(得分:1)
实际上由编译器决定,标准没有指定内存表示的工作方式。该标准规定多态性必须始终有效(即使在inline
函数的情况下,就像你的一样)。您的函数可以内联,具体取决于编译器的上下文和智能,因此有时甚至可能不会出现call
或jmp
。但是,在大多数编译器中,很可能会遇到第二个变体。
对于你的情况:
class Base{
virtual string function1(){ return "Base - function1"; };
virtual string function2(){ return "Base - function2"; };
};
class Derived : public Base {
virtual string function2(){ return "Derived - function2"; };
virtual string function1(){ return "Derived - function1"; };
};
假设你有:
Base* base = new Base;
Base* derived = new Derived;
base->function1();
derived->function2();
对于第一次调用,编译器将获取vftable
的{{1}}的地址,并在Base
中调用第一个函数。对于第二个调用,vftable
位于不同的位置,因为该对象实际上是vftable
类型。它搜索第二个函数,跳转到遇到函数的vftable开始的偏移量(意味着Derived
- 最可能是4个字节,但同样取决于平台)。
答案 5 :(得分:1)
当在类中添加虚函数时,编译器会创建一个隐藏指针(称为v-ptr)作为类的成员。[您可以通过获取sizeof(类)来检查它,sizeof由sizeof增加(指针)]此外,编译器在构造函数的开头内部添加一些代码,以将v-ptr初始化为类的v-table的基本偏移量。现在当这个类是由其他类派生的时候,这个v-ptr也是由Derived类派生的。对于Derived类,此v-ptr初始化为Derived类的v-table的基本偏移量。我们已经知道各个类的v表将存储其虚函数版本的地址。 [请注意,如果未在派生类中重写虚函数,则层次结构中函数的基本版本或大多数派生版本(用于多级继承)的地址将存储在v表格中。因此,在运行时它只是通过这个v-ptr调用函数。因此,如果基类指针存储基础对象,那么v-ptr的基本版本就会生效。由于它指向v-table的基本版本,因此将自动调用该函数的基本版本。 Derived对象的情况也是如此。