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>classSample1{public:virtualvoid vMethod1(){
printf("This is virtual method 1n");}virtualvoid vMethod2(){
printf("This is virtual method 2n");}void method2(){
printf("This is NONVIRTUAL method 2n");}public:int member1;int member2;};//Define method typetypedefvoid(*MyFunc)();int main(int argc,constchar* argv[]){Sample1*a =newSample1();
printf("size of Sample1: %dn",sizeof(Sample1));MyFunc*virtualMethodTable =(MyFunc*)(*(MyFunc*)a);
virtualMethodTable[0]();//call method1
virtualMethodTable[1]();//call method2return0;}
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à:
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>classSample1{public:virtualvoid vMethod1(){
printf("This is virtual method 1n");
printf("member1 = %dn", member1);}virtualvoid vMethod2(){
printf("This is virtual method 2n");}void method2(){
printf("This is NONVIRTUAL method 2n");}public:int member1;int member2;};//Define method type//Thêm tham số thisPtr ở đâytypedefvoid(*MyFunc)(Sample1*thisPtr);int main(int argc,constchar* argv[]){Sample1*a =newSample1();
a->member1 =1000;
printf("size of Sample1: %dn",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 method2return0;}
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>classSample1{public:virtualvoid vMethod1(){
printf("This is virtual method 1n");
printf("member1 = %dn", member1);}virtualvoid vMethod2(){
printf("This is virtual method 2n");}void method2(){
printf("This is NONVIRTUAL method 2n");}public:int member1;int member2;};//Define method type//Thêm tham số thisPtr ở đâytypedefvoid(*MyFunc)(Sample1*thisPtr);structClassDesc{MyFunc* vmt;int member1;int member2;};void fakeVMethod1(ClassDesc* thisPtr){
printf("fake virtual method1 - member1: %dn", thisPtr->member1);}void fakeVMethod2(ClassDesc* thisPtr){
printf("fake virtual method2 - member2: %dn", thisPtr->member2);}voidconstructor(ClassDesc* desc){//Init virtual method table
desc->vmt =newMyFunc[2];
desc->vmt[0]=(MyFunc)&fakeVMethod1;
desc->vmt[1]=(MyFunc)&fakeVMethod2;}void destructor(ClassDesc* desc){//delete virtual method tabledelete[]desc->vmt;}int main(int argc,constchar* argv[]){Sample1*a =newSample1();
a->member1 =1000;
printf("size of Sample1: %dn",sizeof(Sample1));MyFunc*virtualMethodTable =(MyFunc*)(*(MyFunc*)a);
virtualMethodTable[0](a);//call method1
virtualMethodTable[1](a);//call method2
printf("n--------------FAKE CLASS----------------nn");ClassDesc* clsDesc =newClassDesc();// |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;return0;}
Đế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 >classSample1{public:virtualvoid vMethod1(){
printf("This is virtual method 1n");
printf("Sample1: member1 = %dn", member1);}virtualvoid vMethod2(){
printf("Sample1: This is virtual method 2n");}void method2(){
printf("This is NONVIRTUAL method 2n");}public:int member1;int member2;};classSample2:publicSample1{public://override vMethod1()virtualvoid vMethod1(){
printf("Sample2: This is virtual method 1n");}virtualvoid vMethod3(){
printf("Sample2: This is virtual method 3n");}};//Define method type//Thêm tham số thisPtr ở đâytypedefvoid(*MyFunc)(Sample1*thisPtr);int main(int argc,constchar* argv[]){Sample1*a =newSample1();MyFunc*virtualMethodTable =(MyFunc*)(*(MyFunc*)a);Sample2*b =newSample2();MyFunc* virtualMethodTable2;
memcpy(&virtualMethodTable2, b,8);
printf("nn");
printf("Sample1 - vMethod1 Addr: %pn", virtualMethodTable[0]);
printf("Sample1 - vMethod2 Addr: %pn", virtualMethodTable[1]);
printf("--------------------------------n");
printf("Sample2 - vMethod1 Addr: %pn", virtualMethodTable2[0]);
printf("Sample2 - vMethod2 Addr: %pn", virtualMethodTable2[1]);return0;}
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>classSample1{public:virtualvoid vMethod1(){
printf("This is virtual method 1n");
printf("member1 = %dn", member1);}virtualvoid vMethod2(){
printf("This is virtual method 2n");}void method2(){
printf("This is NONVIRTUAL method 2n");}public:int member1;int member2;};//Define method type//Thêm tham số thisPtr ở đâytypedefvoid(*MyFunc)(Sample1*thisPtr);structClassDesc{MyFunc* vmt;int member1;int member2;};void fakeVMethod1(ClassDesc* thisPtr){
printf("fake virtual method1 - member1: %dn", thisPtr->member1);}void fakeVMethod2(ClassDesc* thisPtr){
printf("fake virtual method2 - member2: %dn", thisPtr->member2);}voidconstructor(ClassDesc* desc){//Init virtual method table
desc->vmt =newMyFunc[2];
desc->vmt[0]=(MyFunc)&fakeVMethod1;
desc->vmt[1]=(MyFunc)&fakeVMethod2;}void destructor(ClassDesc* desc){//delete virtual method tabledelete[]desc->vmt;}void override_fakeVMethod1(ClassDesc* thisPtr){
printf("override fake virtual method1 - member1: %dn", thisPtr->member1);}void constructor_Extend(ClassDesc* desc){constructor(desc);
desc->vmt[0]=(MyFunc)&override_fakeVMethod1;}int main(int argc,constchar* argv[]){Sample1*a =newSample1();
a->member1 =1000;
printf("size of Sample1: %dn",sizeof(Sample1));MyFunc*virtualMethodTable =(MyFunc*)(*(MyFunc*)a);
virtualMethodTable[0](a);//call method1
virtualMethodTable[1](a);//call method2
printf("n--------------FAKE CLASS----------------nn");ClassDesc* clsDesc =newClassDesc();// |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----------------nn");ClassDesc* clsDesc2 =newClassDesc();// |
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;return0;}
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.