Bí mật của Virtual Method và Override

399

Bài viết được cho phép bởi tác giả Nguyễn Thái Dương

Những ai từng học lập trình hướng đối tượng OOP chắc chắn đều biết đến khái niệm nạp chồng (Override). Nhưng thông thường, ít bạn quan tâm đến việc compiler đã xử lý nó như thế nào. Phía sau hậu trường hệ thống đã làm gì để tạo nên điều kì diệu? Bài viết này hi vọng sẽ giúp các bạn có câu trả lời xác đáng nhất.

Để dễ dàng trong việc hiển thị các giá trị trong vùng bộ nhớ, tôi chọn C++. Đối với Java hay các ngôn ngữ lập trình khác, tôi tin rằng các bạn cũng có thể dễ dàng luận ra được dựa trên cơ chế của C++

Trước hết,  chúng ta cần tìm hiểu về hàm virtual. Hãy xét 1 class đơn giản sau (Giả sử ta compile cho hệ vi xử lý 64 bit). Bạn có thể copy paste vào http://cpp.sh để chạy thử:

#include <iostream> 
#include <string>

class Sample1 {
public:
    virtual void vMethod1() {
        printf("This is virtual method 1\n");
    }
    virtual void vMethod2() {
        printf("This is virtual method 2\n");    
    }
    void method2() {
        printf("This is NONVIRTUAL method 2\n");    
    }
public:
    int     member1;
    int     member2;
};

//Define method type
typedef void (*MyFunc)();

int main(int argc, const char * argv[]) {
    Sample1 *a = new Sample1();
    
    printf("size of Sample1: %d\n", sizeof(Sample1));
    
    MyFunc *virtualMethodTable = (MyFunc*)(*(MyFunc*)a);    
    virtualMethodTable[0](); //call method1
    virtualMethodTable[1](); //call method2
    
    return 0;
}

Kết quả sẽ như sau:

size of Sample1: 16
This is virtual method 1
This is virtual method 2

Yeah!!. Đến đây bạn thấy không? Chúng ta có thể call được class method mà không cần gọi thông qua instance của nó. Ta hãy thử đi phân tích cấu trúc dữ liệu 16 byte của class Sample1:

# vị trí (byte) size (bytes) field
1 0 8 con trỏ trỏ tới virtual method table (VMT)
2 8 4 member1
3 12 4 member2

Chỗ này có 1 khái niệm mới là Virtual Method Table (VMT). Vậy nó là cái gì? Virtual Method Table thực chất là một mảng các con trỏ hàm chứa địa chỉ của các hàm virtual trong class. Trong ví dụ trên sẽ là:

# Vị trí (byte) Size (bytes) Field
1 0 8 địa chỉ của vMethod1
2 8 8 địa chỉ của vMethod2

  Lỗi Could not create the Java Virtual Machine khi chạy Minecraft

  Hướng dẫn cài đặt VirtualBox trên Ubuntu chi tiết nhất

OK, có 1 điều hơi lấn cấn ở đây khi bạn sử dụng member1 trong vMethod1:

#include <iostream> 
#include <string>

class Sample1 {
public:
    virtual void vMethod1() {
        printf("This is virtual method 1\n");
        printf("member1 = %d\n", member1);
    }
    virtual void vMethod2() {
        printf("This is virtual method 2\n");    
    }
    void method2() {
        printf("This is NONVIRTUAL method 2\n");    
    }
public:
    int     member1;
    int     member2;
};

//Define method type
typedef void (*MyFunc)();

int main(int argc, const char * argv[]) {
    Sample1 *a = new Sample1();
    
    a->member1 = 1000;
    printf("size of Sample1: %d\n", sizeof(Sample1));
    
    MyFunc *virtualMethodTable = (MyFunc*)(*(MyFunc*)a);    
    virtualMethodTable[0](); //call method1
    virtualMethodTable[1](); //call method2
    
    return 0;
}

Kết quả sẽ có dạng như sau:

size of Sample1: 16
This is virtual method 1
member1 = 1268843168
This is virtual method 2

giá trị member1 là 1 giá trị không phải là 1000 như mình đã gán ở trên mà là 1 giá trị rác. Nguyên nhân là do lời gọi virtualMethodTable[0](); chỉ đơn thuần là gọi 1 đoạn code của hàm mà chưa truyền con trỏ this vào trong hàm đó (vMethod1).

Thông thường, lời gọi đúng phải là: a->vMethod1();

Giờ chúng ta sẽ tìm cách truyền con trỏ a vào cho lời gọi virtualMethodTable[0]();. Giờ ta sửa lại 1 chút:

#include <iostream> 
#include <string>

class Sample1 {
public:
    virtual void vMethod1() {
        printf("This is virtual method 1\n");
        printf("member1 = %d\n", member1);
    }
    virtual void vMethod2() {
        printf("This is virtual method 2\n");    
    }
    void method2() {
        printf("This is NONVIRTUAL method 2\n");    
    }
public:
    int     member1;
    int     member2;
};

//Define method type
//Thêm tham số thisPtr ở đây
typedef void (*MyFunc)(Sample1 *thisPtr);

int main(int argc, const char * argv[]) {
    Sample1 *a = new Sample1();
    
    a->member1 = 1000;
    printf("size of Sample1: %d\n", sizeof(Sample1));
    
    MyFunc *virtualMethodTable = (MyFunc*)(*(MyFunc*)a);    
    //Lệnh trên tương đương với:
    //    memcpy(&virtualMethodTable2, b, 8);

    virtualMethodTable[0](a); //call method1
    virtualMethodTable[1](a); //call method2
    
    return 0;
}

Giờ chạy thử nhé:

size of Sample1: 16
This is virtual method 1
member1 = 1000
This is virtual method 2

Yeah. Giờ member1 đã là 1000. Đúng với giá trị chúng ta truyền vào. Giờ chúng ta thử fake lại cách 1 instance được tạo ra từ 1 class theo cách không dùng class nhé:

#include <iostream>
#include <string>

class Sample1 {
public:
    virtual void vMethod1() {
        printf("This is virtual method 1\n");
        printf("member1 = %d\n", member1);
    }
    virtual void vMethod2() {
        printf("This is virtual method 2\n");    
    }
    void method2() {
        printf("This is NONVIRTUAL method 2\n");    
    }
public:
    int     member1;
    int     member2;
};

//Define method type
//Thêm tham số thisPtr ở đây
typedef void (*MyFunc)(Sample1 *thisPtr);

struct ClassDesc {
    MyFunc* vmt;
    int     member1;
    int     member2;
};

void fakeVMethod1(ClassDesc* thisPtr) {
    printf("fake virtual method1 - member1: %d\n", thisPtr->member1);
    
}

void fakeVMethod2(ClassDesc* thisPtr) {
    printf("fake virtual method2 - member2: %d\n", thisPtr->member2);
    
}

void constructor(ClassDesc* desc) {
    //Init virtual method table
    desc->vmt = new MyFunc[2];
    desc->vmt[0] = (MyFunc)&fakeVMethod1;
    desc->vmt[1] = (MyFunc)&fakeVMethod2;
}

void destructor(ClassDesc* desc) {
    //delete virtual method table
    delete []desc->vmt;
}

int main(int argc, const char * argv[]) {
    Sample1 *a = new Sample1();
    
    a->member1 = 1000;
    printf("size of Sample1: %d\n", sizeof(Sample1));
    
    MyFunc *virtualMethodTable = (MyFunc*)(*(MyFunc*)a);    
    
    virtualMethodTable[0](a); //call method1
    virtualMethodTable[1](a); //call method2
    
    printf("\n--------------FAKE CLASS----------------\n\n");
    
    
    ClassDesc* clsDesc = new ClassDesc(); //   |
    constructor(clsDesc);                 //   | <=>  a = new Sample1();
    
    clsDesc->member1 = 2000;
    clsDesc->member2 = 3000;
    
    Sample1* b = reinterpret_cast<Sample1*>(clsDesc); // cast Class Description struct to Sample1*
        
    b->vMethod1();
    b->vMethod2();
        
    destructor(clsDesc);        //
    delete clsDesc;             //  <=> delete b;
    
    return 0;
}

Kết quả:

size of Sample1: 16
This is virtual method 1
member1 = 1000
This is virtual method 2 ————–FAKE CLASS—————-
fake virtual method1 – member1: 2000
fake virtual method2 – member2: 3000

Tham khảo việc làm Java hấp dẫn trên TopDev

Đến đây chúng ta ít nhiều đã hình dung ra được cách C++ tổ chức dữ liệu trong một class như thế nào. Giờ chúng ta sẽ cùng tìm hiểu xem cách mà C++ override một method trong Class như thế nào:

#include < iostream >
#include < string >
#include < cstring >
class Sample1 {
public:
    virtual void vMethod1() {
        printf("This is virtual method 1\n");
        printf("Sample1: member1 = %d\n", member1);
    }
    virtual void vMethod2() {
        printf("Sample1: This is virtual method 2\n");    
    }
    void method2() {
        printf("This is NONVIRTUAL method 2\n");    
    }
public:
    int     member1;
    int     member2;
};

class  Sample2: public Sample1 {
public:

    //override vMethod1()
    virtual void vMethod1() {
        printf("Sample2: This is virtual method 1\n");
    }
    virtual void vMethod3() {
        printf("Sample2: This is virtual method 3\n");
    }
};


//Define method type
//Thêm tham số thisPtr ở đây
typedef void (*MyFunc)(Sample1 *thisPtr);

int main(int argc, const char * argv[]) {
    Sample1 *a = new Sample1();
        
    MyFunc *virtualMethodTable = (MyFunc*)(*(MyFunc*)a);    
    Sample2 *b = new Sample2();    
    
    MyFunc* virtualMethodTable2;
    memcpy(&virtualMethodTable2, b, 8);
    
        
    printf("\n\n");
    printf("Sample1 - vMethod1 Addr: %p\n", virtualMethodTable[0]);
    printf("Sample1 - vMethod2 Addr: %p\n", virtualMethodTable[1]);
    
    printf("--------------------------------\n");
    printf("Sample2 - vMethod1 Addr: %p\n", virtualMethodTable2[0]);
    printf("Sample2 - vMethod2 Addr: %p\n", virtualMethodTable2[1]);
        
    return 0;
}

Kết quả:

Sample1 – vMethod1 Addr: 0x400980
Sample1 – vMethod2 Addr: 0x400950 ——————————–
Sample2 – vMethod1 Addr: 0x400970
Sample2 – vMethod2 Addr: 0x400950

Chúng ta thấy ngay, Class B được override method1 nên địa chỉ của vMethod1 trong VMT của b khác với của a, trong khi vMethod2 thì giống hết nhau vì không bị override.

Ta có thể fake lại việc override một cách đơn giản như sau:

#include <iostream>
#include <string>

class Sample1 {
public:
    virtual void vMethod1() {
        printf("This is virtual method 1\n");
        printf("member1 = %d\n", member1);
    }
    virtual void vMethod2() {
        printf("This is virtual method 2\n");    
    }
    void method2() {
        printf("This is NONVIRTUAL method 2\n");    
    }
public:
    int     member1;
    int     member2;
};

//Define method type
//Thêm tham số thisPtr ở đây
typedef void (*MyFunc)(Sample1 *thisPtr);

struct ClassDesc {
    MyFunc* vmt;
    int     member1;
    int     member2;
};

void fakeVMethod1(ClassDesc* thisPtr) {
    printf("fake virtual method1 - member1: %d\n", thisPtr->member1);
    
}

void fakeVMethod2(ClassDesc* thisPtr) {
    printf("fake virtual method2 - member2: %d\n", thisPtr->member2);
    
}

void constructor(ClassDesc* desc) {
    //Init virtual method table
    desc->vmt = new MyFunc[2];
    desc->vmt[0] = (MyFunc)&fakeVMethod1;
    desc->vmt[1] = (MyFunc)&fakeVMethod2;
}

void destructor(ClassDesc* desc) {
    //delete virtual method table
    delete []desc->vmt;
}


void override_fakeVMethod1(ClassDesc* thisPtr) {
    printf("override fake virtual method1 - member1: %d\n", thisPtr->member1);
    
}

void constructor_Extend(ClassDesc* desc) {
    constructor(desc);
    desc->vmt[0] = (MyFunc)&override_fakeVMethod1;
}

int main(int argc, const char * argv[]) {
    Sample1 *a = new Sample1();
    
    a->member1 = 1000;
    printf("size of Sample1: %d\n", sizeof(Sample1));
    
    MyFunc *virtualMethodTable = (MyFunc*)(*(MyFunc*)a);    
    
    virtualMethodTable[0](a); //call method1
    virtualMethodTable[1](a); //call method2
    
    printf("\n--------------FAKE CLASS----------------\n\n");
    
    
    ClassDesc* clsDesc = new ClassDesc(); //   |
    constructor(clsDesc);                 //   | <=>  a = new Sample1();
    
    clsDesc->member1 = 2000;
    clsDesc->member2 = 3000;
    
    Sample1* b = reinterpret_cast<Sample1*>(clsDesc); // cast Class Description struct to Sample1*
        
    b->vMethod1();
    b->vMethod2();
        
    destructor(clsDesc);        //
    delete clsDesc;             //  <=> delete b;
    
    
    printf("\n--------------FAKE INHERITANCE CLASS----------------\n\n");
    
    ClassDesc* clsDesc2 = new ClassDesc(); //   |
    constructor_Extend(clsDesc2);                 //   | <=>  a = new Sample1();
    
    clsDesc2->member1 = 2000;
    clsDesc2->member2 = 3000;
    
    b = reinterpret_cast<Sample1*>(clsDesc2); // cast Class Description struct to Sample1*
        
    b->vMethod1();
    b->vMethod2();
        
    destructor(clsDesc2);        //
    delete clsDesc2;             //  <=> delete b;
    
    return 0;
}

Kết quả:

size of Sample1: 16
This is virtual method 1 member1 = 1000
This is virtual method 2

————–FAKE CLASS—————-

fake virtual method1 – member1: 2000
fake virtual method2 – member2: 3000

————–FAKE INHERITANCE CLASS—————-

override fake virtual method1 – member1: 2000
fake virtual method2 – member2: 3000

Yeah. Ta thấy ngay khi gọi b->vMethod1() thì hàm override_fakeVMethod1 được gọi. Vậy là chúng ta đã thực hiện thành công việc override hàm method.

Bài viết này hi vọng mang đến cho các bạn 1 cái nhìn rõ hơn về khía cạnh cài đặt của virtual method và override – cái mà trình biên dịch đã che dấu khỏi developer. Chúng ta sẽ cùng nhau tìm hiểu những điều thú vị khác nằm sâu bên trong chương trình để hiểu rõ hơn cách thức mà máy tính hoạt động đằng sau những dòng code của bạn.

Xin cảm ơn và hẹn gặp lại!

Bài viết gốc được đăng tải tại codetoanbug