多態與面向介面程式設計是軟體工程的核心思想,並未綁定於特定語言。雖然 C 語言缺乏原生的類與虛函數支援,但透過結構體與函數指標,我們完全可以在純 C 中手動構建虛函數表,實現執行期的動態綁定。本文將透過一個極簡的「動物叫聲」範例,逐步拆解虛表的記憶體佈局、建構流程與呼叫機制。
在 C++ 中,每個含有虛函數的類別會產生一張靜態的虛函數表,表中存放虛函數的位址;每個物件實例內部則隱藏一個指標(vptr)指向該表。我們在 C 中採用相同策略:
#ifndef ANIMAL_H
#define ANIMAL_H
/* 虛函數表:對應 C++ 的 vtable */
typedef struct vtable vtable;
struct vtable {
void (*eat)();
void (*bite)();
};
/* 基底類別 Animal */
typedef struct Animal Animal;
struct Animal {
const vtable* _vptr; /* 每個物件持有一個 vptr */
};
#endif
註解中提到的替代方案——在每個物件內直接存放所有函數指標——會造成嚴重的記憶體浪費,因為同類型物件的介面函數位址完全相同,應該共享一張表。
/* dog.h */
#ifndef DOG_H
#define DOG_H
#include "animal.h"
typedef struct Dog Dog;
struct Dog {
Animal base_obj; /* 繼承基底 */
int x; /* 子類自有資料 */
};
Animal* create_dog(); /* 建構函式 */
#endif
/* dog.c */
#include
#include
#include "dog.h"
static void dog_eat() { printf("dog_eat()\n"); }
static void dog_bite() { printf("dog_bite()\n"); }
/* 編譯期確定的靜態虛表 */
static const vtable dog_vtable = { dog_eat, dog_bite };
Animal* create_dog() {
Dog* pDog = malloc(sizeof(Dog));
if (pDog) {
pDog->base_obj._vptr = &dog_vtable; /* 執行期綁定 vptr */
pDog->x = 0;
}
return (Animal*)pDog; /* 向上轉型 */
}
結構與 Dog 幾乎相同,僅虛表指向 `cat_eat`、`cat_bite`,此處省略重複程式碼。
#include
#include "animal.h"
#include "cat.h"
#include "dog.h"
void ShowBite(Animal* pAnimal) {
pAnimal->_vptr->bite(); /* 動態派發 */
}
void main() {
ShowBite(create_dog()); /* 輸出 dog_bite() */
ShowBite(create_cat()); /* 輸出 cat_bite() */
}
這個極簡範例揭示了 C++ 物件模型的核心機制:靜態表 + 物件內指標 + 間接呼叫。理解這一機制不僅有助於跨語言移植多態設計,更能深入掌握 C++ 執行期效能與記憶體佈局的本質。