Bí mật của Virtual Method và Override
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 |
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
- M Mới ra trường không kinh nghiệm, sao làm BA?
- C Chuyển đổi SA key sang Workload Identity
- U Use Case Diagram và 5 sai lầm thường gặp
- K Kinh nghiệm xử lý câu lệnh điều kiện trong JavaScript
- D Dart là gì? Ứng dụng của ngôn ngữ lập trình Dart
- C Chuẩn Hóa CV, Nhận Ngay Phím Chất
- S So sánh MySQL và MongoDB: Nên chọn CSDL nào?
- D Dân BA có cần phải rành về kỹ thuật???
- C CSS Selector là gì? Cách sử dụng các loại CSS Selector phổ biến
- P Phân biệt Sketch, Wireframe, Mockup, và Prototype?