विषय पर बढ़ें

अंतिम अद्यतन:2026-10-12

C++ vptr और vtable तंत्र

लेखक के बारे में:

व्यक्तिगत WeChat सार्वजनिक अकाउंट:

wechat

1.बुनियादी सिद्धांत

वर्चुअल फ़ंक्शन implement करने के लिए, C++ late binding के एक खास रूप का उपयोग करता है जिसे virtual table कहते हैं। यह virtual table dynamic/late binding तरीके से फ़ंक्शन कॉल हल करने के लिए lookup table है। virtual table को कभी-कभी दूसरे नामों से भी जाना जाता है, जैसे "vtable", "虚函数表", "虚方法表" या "dispatch table"।

virtual table सचमुच बहुत सरल है, भले ही शब्दों में बताना थोड़ा जटिल है। सबसे पहले, वर्चुअल फ़ंक्शन उपयोग करने वाली हर क्लास (या वर्चुअल फ़ंक्शन उपयोग करने वाली क्लास से derive हुई) का अपना virtual table होता है। यह table सिर्फ़ कंपाइलर द्वारा compile time पर set किया गया static array है। virtual table में क्लास के ऑब्जेक्ट द्वारा कॉल किए जा सकने वाले हर वर्चुअल फ़ंक्शन के लिए एक entry होती है। इस table की हर entry सिर्फ़ एक function pointer है, जो उस क्लास द्वारा एक्सेस किए जा सकने वाले सबसे derived फ़ंक्शन की ओर पॉइंट करता है।

दूसरा, कंपाइलर एक छिपा हुआ बेस क्लास की ओर पॉइंटर भी जोड़ता है, जिसे हम vptr कहते हैं। vptr क्लास इंस्टेंस बनते समय अपने-आप set होता है, ताकि वह उस क्लास के virtual table की ओर पॉइंट करे। this पॉइंटर से अलग — this पॉइंटर दरअसल कंपाइलर द्वारा self-reference resolve करने का function parameter है — vptr एक असली पॉइंटर है।

इसलिए यह हर क्लास ऑब्जेक्ट के allocation को एक पॉइंटर के साइज़ ज़्यादा बड़ा कर देता है। इसका मतलब यह भी है कि vptr derived क्लास द्वारा inherit होता है, जो महत्वपूर्ण है।

2.इम्प्लीमेंटेशन और आंतरिक स्ट्रक्चर

नीचे हम vptr के साथ automatic और manual तरीके से पता निकालकर वर्चुअल फ़ंक्शन कॉल करते देखेंगे!

कोड देखने से पहले, समझने की सुविधा के लिए, यहाँ call graph दिया गया है:

base

कोड पूरी तरह standard comment style का पालन करता है, देखते ही समझ आ जाएगा, न समझ आए तो कमेंट करें!

/**
 * @file vptr1.cpp
 * @brief C++虚函数vptr和vtable
 * 编译:g++ -g -o vptr vptr1.cpp -std=c++11
 * @author 光城
 * @version v1
 * @date 2019-07-20
 */

#include <iostream>
#include <stdio.h>
using namespace std;

/**
 * @brief 函数指针
 */
typedef void (*Fun)();

/**
 * @brief 基类
 */
class Base
{
    public:
        Base(){};
        virtual void fun1()
        {
            cout << "Base::fun1()" << endl;
        }
        virtual void fun2()
        {
            cout << "Base::fun2()" << endl;
        }
        virtual void fun3(){}
        ~Base(){};
};

/**
 * @brief 派生类
 */
class Derived: public Base
{
    public:
        Derived(){};
        void fun1()
        {
            cout << "Derived::fun1()" << endl;
        }
        void fun2()
        {
            cout << "DerivedClass::fun2()" << endl;
        }
        ~Derived(){};
};
/**
 * @brief 获取vptr地址与func地址,vptr指向的是一块内存,这块内存存放的是虚函数地址,这块内存就是我们所说的虚表
 *
 * @param obj
 * @param offset
 *
 * @return 
 */
Fun getAddr(void* obj,unsigned int offset)
{
    cout<<"======================="<<endl;
    void* vptr_addr = (void *)*(unsigned long *)obj;  //64位操作系统,占8字节,通过*(unsigned long *)obj取出前8字节,即vptr指针
    printf("vptr_addr:%p\n",vptr_addr);

    /**
     * @brief 通过vptr指针访问virtual table,因为虚表中每个元素(虚函数指针)在64位编译器下是8个字节,因此通过*(unsigned long *)vptr_addr取出前8字节,
     * 后面加上偏移量就是每个函数的地址!
     */
    void* func_addr = (void *)*((unsigned long *)vptr_addr+offset);
    printf("func_addr:%p\n",func_addr);
    return (Fun)func_addr;
}
int main(void)
{
    Base ptr;
    Derived d;
    Base *pt = new Derived(); // 基类指针指向派生类实例
    Base &pp = ptr; // 基类引用指向基类实例
    Base &p = d; // 基类引用指向派生类实例
    cout<<"基类对象直接调用"<<endl;
    ptr.fun1();
    cout<<"基类引用指向基类实例"<<endl;
    pp.fun1(); 
    cout<<"基类指针指向派生类实例并调用虚函数"<<endl;
    pt->fun1();
    cout<<"基类引用指向派生类实例并调用虚函数"<<endl;
    p.fun1();

    // 手动查找vptr 和 vtable
    Fun f1 = getAddr(pt, 0);
    (*f1)();
    Fun f2 = getAddr(pt, 1);
    (*f2)();
    delete pt;
    return 0;
}

रन का परिणाम:

基类对象直接调用
Base::fun1()
基类引用指向基类实例
Base::fun1()
基类指针指向派生类实例并调用虚函数
Derived::fun1()
基类引用指向派生类实例并调用虚函数
Derived::fun1()
=======================
vptr_addr:0x401130
func_addr:0x400ea8
Derived::fun1()
=======================
vptr_addr:0x401130
func_addr:0x400ed4
DerivedClass::fun2()

हम पाते हैं कि C++ का dynamic polymorphism वर्चुअल फ़ंक्शन से हासिल होता है। सरल शब्दों में, virtual फ़ंक्शन के ज़रिए, चाइल्ड क्लास की ओर पॉइंट करने वाला बेस क्लास पॉइंटर चाइल्ड क्लास का फ़ंक्शन कॉल कर सकता है। उदाहरण के लिए, ऊपर बेस क्लास पॉइंटर द्वारा derived क्लास इंस्टेंस की ओर पॉइंट करके वर्चुअल फ़ंक्शन कॉल किया, उस कोड को सरल करने पर:

Base *pt = new Derived(); // 基类指针指向派生类实例
cout<<"基类指针指向派生类实例并调用虚函数"<<endl;
pt->fun1();

इसकी प्रक्रिया है: सबसे पहले प्रोग्राम पहचानता है कि fun1() वर्चुअल फ़ंक्शन है, फिर प्रोग्राम pt->vptr से Derived का virtual table लेता है। तीसरा, वह Derived virtual table में खोजता है कि fun1() का कौन-सा वर्ज़न कॉल हो। यहाँ पाया जाता है कि कॉल होने वाला Derived::fun1() है। इसलिए pt->fun1() को Derived::fun1() की तरह resolve किया जाता है!

इसके अलावा, ऊपर के कोड में आप देखेंगे, manual तरीके से vptr का पता लेकर vtable के फ़ंक्शन कॉल करना भी शामिल है, तो आइए साथ जाँचें कि ऊपर वाला पता और वास्तव में automatic रूप से vtable का वर्चुअल फ़ंक्शन कॉल करते समय, जैसे ऊपर pt->fun1() के समय, एक जैसा है या नहीं!

यहाँ gdb debugging उपयोग करते हैं, compile करते समय -g लगाना न भूलें।

gdb vptr से gdb debugging पेज में जाएँ, फिर b Derived::fun1 लिखकर fun1 पर breakpoint लगाएँ, फिर r लिखकर प्रोग्राम को breakpoint तक चलाएँ, इस समय हमें call stack का मेमोरी पता देखना है, disassemable fun1 से fun1 से जुड़ा current assembly कोड देखा जा सकता है, हमें 0x0000000000400ea8 दिखा, फिर ऊपर के परिणाम से तुलना करने पर manual कॉल किए fun1 से मेल खाता है, fun2 भी ऐसा ही, इससे कोड की सही होने का प्रमाण मिलता है!

gdb debugging जानकारी नीचे है:

(gdb) b Derived::fun1
Breakpoint 1 at 0x400eb4: file vptr1.cpp, line 23.
(gdb) r
Starting program: /home/light/Program/CPlusPlusThings/virtual/pure_virtualAndabstract_class/vptr 
基类对象直接调用
Base::fun1()
基类引用指向派生类实例
Base::fun1()
基类指针指向派生类实例并调用虚函数

Breakpoint 1, Derived::fun1 (this=0x614c20) at vptr1.cpp:23
23              cout << "Derived::fun1()" << endl;
(gdb) disassemble fun1
Dump of assembler code for function Derived::fun1():
   0x0000000000400ea8 <+0>: push   %rbp
   0x0000000000400ea9 <+1>: mov    %rsp,%rbp
   0x0000000000400eac <+4>: sub    $0x10,%rsp
   0x0000000000400eb0 <+8>: mov    %rdi,-0x8(%rbp)
=> 0x0000000000400eb4 <+12>:    mov    $0x401013,%esi
   0x0000000000400eb9 <+17>:    mov    $0x602100,%edi
   0x0000000000400ebe <+22>:    callq  0x4009d0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
   0x0000000000400ec3 <+27>:    mov    $0x400a00,%esi
   0x0000000000400ec8 <+32>:    mov    %rax,%rdi
   0x0000000000400ecb <+35>:    callq  0x4009f0 <_ZNSolsEPFRSoS_E@plt>
   0x0000000000400ed0 <+40>:    nop
   0x0000000000400ed1 <+41>:    leaveq 
   0x0000000000400ed2 <+42>:    retq   
End of assembler dump.
(gdb) disassemble fun2
Dump of assembler code for function Derived::fun2():
   0x0000000000400ed4 <+0>: push   %rbp
   0x0000000000400ed5 <+1>: mov    %rsp,%rbp
   0x0000000000400ed8 <+4>: sub    $0x10,%rsp
   0x0000000000400edc <+8>: mov    %rdi,-0x8(%rbp)
   0x0000000000400ee0 <+12>:    mov    $0x401023,%esi
   0x0000000000400ee5 <+17>:    mov    $0x602100,%edi
   0x0000000000400eea <+22>:    callq  0x4009d0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
   0x0000000000400eef <+27>:    mov    $0x400a00,%esi
   0x0000000000400ef4 <+32>:    mov    %rax,%rdi
   0x0000000000400ef7 <+35>:    callq  0x4009f0 <_ZNSolsEPFRSoS_E@plt>
   0x0000000000400efc <+40>:    nop
   0x0000000000400efd <+41>:    leaveq 
   0x0000000000400efe <+42>:    retq   
End of assembler dump.

संबंधित विषय

टिप्पणियाँ