Tôi sẽ sửa đổi ví dụ của bạn một chút để nó hiển thị các khía cạnh thú vị hơn về hướng đối tượng.
Giả sử chúng ta có những điều sau đây:
#include <iostream>
struct Animal
{
int age;
Animal(int a) : age {a} {}
virtual int setAge(int);
virtual void sayHello() const;
};
int
Animal::setAge(int a)
{
int prev = this->age;
this->age = a;
return prev;
}
void
Animal::sayHello() const
{
std::cout << "Hello, I'm an " << this->age << " year old animal.\n";
}
struct Tiger : Animal
{
int stripes;
Tiger(int a, int s) : Animal {a}, stripes {s} {}
virtual void sayHello() const override;
virtual void doTigerishThing();
};
void
Tiger::sayHello() const
{
std::cout << "Hello, I'm a " << this->age << " year old tiger with "
<< this->stripes << " stripes.\n";
}
void
Tiger::doTigerishThing()
{
this->stripes += 1;
}
int
main()
{
Tiger * tp = new Tiger {7, 42};
Animal * ap = tp;
tp->sayHello(); // call overridden function via derived pointer
tp->doTigerishThing(); // call child function via derived pointer
tp->setAge(8); // call parent function via derived pointer
ap->sayHello(); // call overridden function via base pointer
}
Tôi phớt lờ những lời khuyên tốt mà các lớp học với virtual
thành viên chức năng nên có một destructor virtual
với mục đích ví dụ này. Tôi sẽ rò rỉ vật thể.
Hãy xem cách chúng tôi có thể dịch ví dụ này thành C cũ tốt khi không có chức năng thành viên, để lại một mình với virtual
. Tất cả các mã sau đây là C, không phải C++.
Các struct animal
rất đơn giản:
struct animal
{
const void * vptr;
int age;
};
Ngoài các age
thành viên, chúng tôi đã bổ sung thêm một vptr
đó sẽ là con trỏ đến vtable. Tôi đang sử dụng một con trỏ void
cho điều này bởi vì chúng tôi sẽ phải làm xấu xí phôi anyway và sử dụng void *
làm giảm sự xấu xí một chút.
Tiếp theo, chúng tôi có thể triển khai các chức năng thành viên.
static int
animal_set_age(void * p, int a)
{
struct animal * this = (struct animal *) p;
int prev = this->age;
this->age = a;
return prev;
}
Lưu ý đối số thứ 0 bổ sung: con trỏ this
được truyền hoàn toàn bằng C++. Một lần nữa, tôi đang sử dụng một con trỏ void *
vì nó sẽ đơn giản hóa mọi thứ sau này. Lưu ý rằng bên trong bất kỳ chức năng nào của thành viên, chúng tôi luôn luôn biết loại con trỏ this
tĩnh để diễn viên không có vấn đề gì. (Và ở cấp độ máy, nó không làm gì cả.)
Thành viên sayHello
được định nghĩa tương tự, ngoại trừ con trỏ this
là const
đủ điều kiện lần này.
static void
animal_say_hello(const void * p)
{
const struct animal * this = (const struct animal *) p;
printf("Hello, I'm an %d year old animal.\n", this->age);
}
Thời gian cho động vật vtable. Đầu tiên chúng ta phải cho nó một loại, mà là thẳng về phía trước.
struct animal_vtable_type
{
int (*setAge)(void *, int);
void (*sayHello)(const void *);
};
Sau đó, chúng tôi tạo một phiên bản duy nhất của vtable và thiết lập đúng chức năng thành viên. Nếu Animal
đã có thành viên virtual
tinh khiết, mục nhập tương ứng sẽ có giá trị NULL
và tốt hơn không bị hủy đăng ký.
static const struct animal_vtable_type animal_vtable = {
.setAge = animal_set_age,
.sayHello = animal_say_hello,
};
Lưu ý rằng animal_set_age
và animal_say_hello
đã được tuyên bố static
. Đó là onkay bởi vì họ sẽ không bao giờ được gọi bằng tên nhưng chỉ thông qua vtable (và vtable chỉ thông qua các vptr
vì vậy nó có thể được static
quá).
Bây giờ chúng ta có thể thực hiện các nhà xây dựng cho Animal
...
void
animal_ctor(void * p, int age)
{
struct animal * this = (struct animal *) p;
this->vptr = &animal_vtable;
this->age = age;
}
... và tương ứng operator new
:
void *
animal_new(int age)
{
void * p = malloc(sizeof(struct animal));
if (p != NULL)
animal_ctor(p, age);
return p;
}
Về điều duy nhất thú vị là dòng nơi vptr
được thiết lập trong constructor.
Hãy chuyển sang hổ.
Tiger
được thừa hưởng từ Animal
để nó nhận được một đối tượng phụ struct tiger
. Tôi đang làm điều này bằng cách đặt số struct animal
làm thành viên đầu tiên. Điều quan trọng là đây là thành viên đầu tiên bởi vì nó có nghĩa là thành viên đầu tiên của đối tượng đó - vptr
- có cùng địa chỉ với đối tượng của chúng tôi. Chúng tôi sẽ cần điều này sau khi chúng tôi sẽ làm một số đúc khó khăn.
struct tiger
{
struct animal base;
int stripes;
};
Chúng tôi cũng có thể chỉ đơn giản là sao chép các thành viên của struct animal
lexically vào đầu của định nghĩa của struct tiger
nhưng điều đó có thể là khó khăn hơn để duy trì. Trình biên dịch không quan tâm đến các vấn đề kiểu cách như vậy.
Chúng tôi đã biết cách triển khai các hàm thành viên cho hổ.
void
tiger_say_hello(const void * p)
{
const struct tiger * this = (const struct tiger *) p;
printf("Hello, I'm an %d year old tiger with %d stripes.\n",
this->base.age, this->stripes);
}
void
tiger_do_tigerish_thing(void * p)
{
struct tiger * this = (struct tiger *) p;
this->stripes += 1;
}
Lưu ý rằng chúng ta đang đúc con trỏ this
để struct tiger
thời gian này. Nếu một hàm hổ được gọi, con trỏ this
có điểm tốt hơn đối với một con hổ, ngay cả khi chúng ta được gọi thông qua một con trỏ cơ sở.
Next để vtable:
struct tiger_vtable_type
{
int (*setAge)(void *, int);
void (*sayHello)(const void *);
void (*doTigerishThing)(void *);
};
Lưu ý rằng hai thành viên đầu tiên là chính xác giống như cho animal_vtable_type
. Điều này là cần thiết và về cơ bản là câu trả lời trực tiếp cho câu hỏi của bạn. Nó sẽ rõ ràng hơn, có lẽ, nếu tôi đã đặt struct animal_vtable_type
làm thành viên đầu tiên. Tôi muốn nhấn mạnh rằng bố cục đối tượng sẽ là giống hệt nhau ngoại trừ việc chúng tôi không thể chơi các thủ đoạn truyền khó chịu trong trường hợp này. Một lần nữa, đây là những khía cạnh của ngôn ngữ C, không có mặt ở mức máy vì vậy trình biên dịch không bị làm phiền bởi điều này.
Tạo một trường hợp vtable:
static const struct tiger_vtable_type tiger_vtable = {
.setAge = animal_set_age,
.sayHello = tiger_say_hello,
.doTigerishThing = tiger_do_tigerish_thing,
};
Và thực hiện các constructor:
void
tiger_ctor(void * p, int age, int stripes)
{
struct tiger * this = (struct tiger *) p;
animal_ctor(this, age);
this->base.vptr = &tiger_vtable;
this->stripes = stripes;
}
Việc đầu tiên các nhà xây dựng hổ không đang kêu gọi các nhà xây dựng gia súc. Hãy nhớ cách hàm tạo động vật đặt vptr
thành &animal_vtable
? Đây là lý do tại sao gọi hàm virtual
thành viên từ một hàm khởi tạo lớp cơ sở sẽ làm mọi người ngạc nhiên. Chỉ sau khi hàm tạo lớp cơ sở đã chạy, chúng tôi gán lại vptr
cho loại có nguồn gốc và sau đó thực hiện khởi tạo riêng của chúng ta.
operator new
chỉ là bản mẫu.
void *
tiger_new(int age, int stripes)
{
void * p = malloc(sizeof(struct tiger));
if (p != NULL)
tiger_ctor(p, age, stripes);
return p;
}
Chúng tôi đã hoàn tất. Nhưng làm thế nào để chúng ta gọi một chức năng thành viên ảo? Đối với điều này, tôi sẽ xác định một macro trợ giúp.
#define INVOKE_VIRTUAL_ARGS(STYPE, THIS, FUNC, ...) \
(*((const struct STYPE ## _vtable_type * *) (THIS)))->FUNC(THIS, __VA_ARGS__)
Bây giờ, điều này thật xấu. Những gì nó làm là lấy kiểu tĩnh STYPE
, một con trỏ this
THIS
và tên của hàm thành viên FUNC
và bất kỳ đối số bổ sung nào để truyền cho hàm.
Sau đó, nó sẽ tạo tên loại vtable từ loại tĩnh. (Các ##
là nhà điều hành dán thẻ của tiền xử lý. Ví dụ, nếu STYPE
là animal
, sau đó STYPE ## _vtable_type
sẽ mở rộng để animal_vtable_type
.)
Tiếp theo, con trỏ THIS
được đúc một con trỏ đến một con trỏ đến kiểu vtable chỉ có nguồn gốc. Điều này hoạt động bởi vì chúng tôi đã đảm bảo đặt vptr
là thành viên đầu tiên vào mọi đối tượng để nó có cùng địa chỉ. Điều này là rất cần thiết.
Khi việc này được thực hiện, chúng ta có thể dereference con trỏ (để có được vptr
thực tế) và sau đó yêu cầu thành viên FUNC
của nó và cuối cùng gọi nó. (__VA_ARGS__
mở rộng đến các đối số macro Variad bổ sung.) Lưu ý rằng chúng ta cũng chuyển con trỏ THIS
làm đối số thứ 0 cho hàm thành viên. Bây giờ, sự thật acatual là tôi đã phải xác định một vĩ mô gần như giống hệt nhau một lần nữa cho các chức năng mà không có đối số bởi vì preprocessor không cho phép một gói đối số vĩ mô variadic để có sản phẩm nào. Nó sẽ như vậy.
#define INVOKE_VIRTUAL(STYPE, THIS, FUNC) \
(*((const struct STYPE ## _vtable_type * *) (THIS)))->FUNC(THIS)
Và nó hoạt động:
#include <stdio.h>
#include <stdlib.h>
/* Insert all the code from above here... */
int
main()
{
struct tiger * tp = tiger_new(7, 42);
struct animal * ap = (struct animal *) tp;
INVOKE_VIRTUAL(tiger, tp, sayHello);
INVOKE_VIRTUAL(tiger, tp, doTigerishThing);
INVOKE_VIRTUAL_ARGS(tiger, tp, setAge, 8);
INVOKE_VIRTUAL(animal, ap, sayHello);
return 0;
}
Bạn có thể tự hỏi điều gì sẽ xảy ra trong cuộc gọi
INVOKE_VIRTUAL_ARGS(tiger, tp, setAge, 8);
. Những gì chúng tôi đang làm là gọi thành viên không bị ghi đè setAge
của Animal
trên đối tượng Tiger
được đề cập qua con trỏ struct tiger
. Con trỏ này đầu tiên được đúc một cách hoàn toàn đến một con trỏ void
và như vậy được chuyển thành con trỏ this
đến animal_set_age
. Hàm đó sau đó chuyển nó thành con trỏ struct animal
. Điều này có đúng không? Đó là vì chúng tôi đã cẩn thận đặt struct animal
làm thành viên đầu tiên trong số struct tiger
để địa chỉ của đối tượng struct tiger
giống với địa chỉ cho đối tượng phụ struct animal
. Đó là cùng một mẹo (chỉ có một cấp độ ít hơn), chúng tôi đã chơi với vptr
.
Không có gì quy định bố cục vtbl. Nhưng một cách tự nhiên là để trình biên dịch đánh số các hàm ảo trong lớp theo thứ tự liên tiếp. Những con số này phục vụ như là chỉ số vào vtbl, đó là hiệu quả một mảng các con trỏ hàm. – Gene
Trình biên dịch biết những gì trong vtable bởi vì nó tạo ra vtable. Không rõ những gì bạn đang thực sự yêu cầu ở đây. – EJP
@Gene khó chịu, MSVC cũng nhóm quá tải với nhau ngay cả khi chúng không được khai báo theo thứ tự đó. (Oh, và những điều tự nhiên lạ với thừa kế ảo, v.v.) – Yakk