2015-10-08 12 views
7

Giả sử chúng ta có nhiều hơn một hàm ảo trong lớp cha và lớp dẫn xuất. Sẽ có một vtable được tạo cho các hàm ảo này trong vtable cho cả lớp cha đẻ.Trình biên dịch biết mục nhập nào trong vtable tương ứng với một hàm ảo?

Trình biên dịch sẽ biết mục nhập nào trong vtable tương ứng với hàm ảo nào?

Ví dụ:

class Animal{ 
public: 
void fakeMethod1(){} 
virtual void getWeight(){} 
void fakeMethod2(){} 
virtual void getHeight(){} 
virtual void getType(){} 
}; 

class Tiger:public Animal{ 
public: 
void fakeMethod3(){} 
virtual void getWeight(){} 
void fakeMethod4(){} 
virtual void getHeight(){} 
virtual void getType(){} 
}; 
main(){ 
Animal a* = new Tiger(); 
a->getHeight(); // A will now point to the base address of vtable Tiger 
//How will the compiler know which entry in the vtable corresponds to the function getHeight()? 
} 

tôi đã không tìm thấy lời giải thích chính xác trong nghiên cứu của tôi -

https://stackoverflow.com/a/99341/437894 =

"Bảng này được sử dụng để giải quyết các cuộc gọi chức năng vì nó có chứa các địa chỉ của tất cả các chức năng ảo của lớp đó. "

Bảng được sử dụng để giải quyết cuộc gọi chức năng chính xác như thế nào?

https://stackoverflow.com/a/203136/437894 =

"Vì vậy, trong thời gian chạy, mã chỉ sử dụng vptr của đối tượng để xác định vị trí vtbl, và từ đó địa chỉ của hàm ghi đè thực tế."

Tôi không thể hiểu điều này. Vtable giữ địa chỉ của hàm ảo không phải là địa chỉ của hàm được ghi đè thực tế.

+2

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

+0

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

+0

@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

Trả lời

7

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ỏ thisconst đủ đ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_ageanimal_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ỏ thisTHIS 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 STYPEanimal, 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 vptrthà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.

+0

Cảm ơn bạn đã giải thích chi tiết. Điều này đã giúp. Cũng tìm thấy một ví dụ khác. Ở đây họ nói về một ví dụ với hai lớp cơ sở và các lớp dẫn xuất qua việc cưỡi một hàm ảo từ cả hai lớp cơ sở. [link] (http: //www.openrce.org/articles/full% 5Fview/23) – Ashwin

+0

Đối với lớp tiếp theo của bạn, hãy bao gồm thừa kế 'virtual' và cách mô phỏng nó trong C. :) +1 Như một bên, lợi thế của hệ thống này là bạn có thể ly dị vtable từ dữ liệu (không lưu trữ nó liên tục), có thể cho phép một số thủ thuật. Các kỹ thuật này rất hữu ích trong C++ khi bạn cần chia nhỏ dữ liệu đó (nếu bạn muốn lưu trữ dữ liệu trong bộ đệm bên trong một nơi nào đó, nhưng hành động trên nó một cách đa hình: hoặc, các vật thể nhỏ nhẹ được đóng gói trong một mảng, với độ dài chạy) mã hóa vtables ở nơi khác, mà bạn có thể sử dụng trong xử lý văn bản) – Yakk

+0

Cảm ơn bạn rất nhiều vì câu trả lời. Một câu hỏi: Làm thế nào để trình biên dịch biết ở nơi đầu tiên mà nó đã đặt một cuộc gọi chức năng ảo? Nếu tôi có: X * z = new z(); Như vậy X không có hàm ảo, Y thừa hưởng từ X và có hàm ảo, và Z kế thừa từ Y. Tất cả các cuộc gọi hàm từ "z" ở trên phải thông qua vptr, nhưng kiểu là X *, vậy trình biên dịch như thế nào biết liệu các cuộc gọi đến các hàm trên "z" có nên đi qua vptr hay không? –

2

Nó có thể giúp bạn tự thực hiện điều tương tự.

struct Bob; 
struct Bob_vtable { 
    void(*print)(Bob const*self) = 0; 
    Bob_vtable(void(*p)(Bob const*)):print(p){} 
}; 
template<class T> 
Bob_vtable const* make_bob_vtable(void(*print)(Bob const*)) { 
    static Bob_vtable const table(+print); 
    return &table; 
} 
struct Bob { 
    Bob_vtable const* vtable; 
    void print() const { 
    vtable->print(this); 
    } 
    Bob():vtable(make_bob_vtable<Bob>([](Bob const*self){ 
    std::cout << "Bob\n"; 
    })) {} 
protected: 
    Bob(Bob_vtable const* t):vtable(t){} 
}; 
struct Alice:Bob { 
    int x = 0; 
    Alice():Bob(make_bob_vtable<Alice>([](Bob const*self){ 
    std::cout << "Alice " << static_cast<Alice const*>(self)->x << '\n'; 
    })) {} 
}; 

live example.

Ở đây chúng tôi có vtable rõ ràng được lưu trữ trong Bob. Nó chỉ vào một bảng chức năng. Hàm thành viên không ảo print sử dụng hàm đó để tự động gửi đến đúng phương thức.

Hàm khởi tạo của Bob và lớp dẫn xuất Alice đặt vtable thành giá trị khác (được tạo dưới dạng địa phương tĩnh trong trường hợp này) với các giá trị khác nhau trong bảng.

Con trỏ nào được sử dụng được đưa vào định nghĩa về những gì Bob::print có nghĩa là - nó biết độ lệch vào bảng.

Nếu chúng ta thêm một hàm ảo khác vào Alice, điều đó chỉ có nghĩa là con trỏ vtable sẽ trỏ đến struct Alice_vtable:Bob_vtable trong thực tế. Tĩnh/diễn giải lại đúc sẽ giúp chúng ta có được bảng "thực", và chúng ta có thể truy cập các con trỏ hàm dễ dàng hơn.

Mọi thứ trở nên lạ khi chúng ta nói về thừa kế ảo cũng như các chức năng ảo. Tôi không đủ điều kiện để mô tả cách thức hoạt động.

Các vấn đề liên quan