2009-02-18 61 views
10

Mọi đối tượng của lớp ảo đều có con trỏ đến vtable không?Mọi đối tượng của lớp ảo đều có con trỏ đến vtable không?

Hoặc chỉ đối tượng của lớp cơ sở có chức năng ảo có nó?

Vtable được lưu trữ ở đâu? phần mã hoặc phần dữ liệu của quá trình?

+0

Sao chép? http://stackoverflow.com/questions/99297/at-as-deep-of-a-level-as-possible-how-are-virtual-functions-implemented – Anonymous

+0

Không có thứ gì như "lớp ảo" trong C++. – curiousguy

Trả lời

1

Tất cả các lớp ảo thường có vtable, nhưng nó không được yêu cầu bởi tiêu chuẩn C++ và phương thức lưu trữ phụ thuộc vào trình biên dịch.

4

Vtable là một cá thể lớp, tức là, nếu tôi có 10 đối tượng của một lớp có phương thức ảo thì chỉ có một vtable được chia sẻ giữa tất cả 10 đối tượng.

Tất cả 10 đối tượng trong trường hợp này trỏ đến cùng vtable.

+0

Điều gì về Vptr, sẽ có 10 vptr liên kết với mỗi đối tượng hoặc như vtable duy nhất sẽ chỉ có một vptr? – Rndp13

0

Mọi đối tượng thuộc loại đa hình sẽ có con trỏ đến Vtable.

Nơi lưu trữ VTable phụ thuộc vào trình biên dịch.

15

Tất cả các lớp có phương thức ảo sẽ có một vtable được chia sẻ bởi tất cả các đối tượng của lớp.

Mỗi thể hiện đối tượng sẽ có con trỏ đến vtable đó (đó là cách vtable được tìm thấy), thường được gọi là vptr. Trình biên dịch ngầm tạo mã để khởi tạo vptr trong hàm tạo.

Lưu ý rằng không ai trong số này được bắt buộc bởi ngôn ngữ C++ - việc triển khai có thể xử lý công văn ảo theo cách khác nếu muốn. Tuy nhiên, đây là việc thực hiện được sử dụng bởi tất cả các trình biên dịch tôi quen thuộc với. Cuốn sách của Stan Lippman, "Bên trong mô hình đối tượng C++" mô tả cách hoạt động này rất độc đáo.

+2

+1 Và bạn vui lòng giải thích tại sao con trỏ ảo là một đối tượng chứ không phải mỗi lớp? Cảm ơn bạn. – Viet

+1

@Viet Bạn có thể nghĩ vPtr là một bootstrap với định nghĩa thời gian chạy của một đối tượng. Chỉ sau khi vPtr được thiết lập, đối tượng có thể biết loại thực tế của nó là gì. Trong khái niệm này, làm cho một vPtr cho mỗi lớp (tĩnh) không có ý nghĩa. Suy nghĩ về cách này, nếu một đối tượng không cần vPtr thì nó phải đã biết về định nghĩa thời gian chạy của nó trong suốt thời gian biên dịch mà mâu thuẫn với nó là một đối tượng được giải quyết động. –

0

Không nhất thiết

Khá nhiều mọi đối tượng mà có một chức năng ảo sẽ có một con trỏ v-bàn. Không cần phải là một con trỏ v-table cho mỗi lớp có một hàm ảo mà đối tượng xuất phát từ đó.

Trình biên dịch mới phân tích mã đầy đủ có thể loại bỏ v-bảng trong một số trường hợp. Ví dụ, trong một trường hợp đơn giản: nếu bạn chỉ thực hiện một lớp cơ sở trừu tượng, trình biên dịch biết rằng nó có thể thay đổi các cuộc gọi ảo thành các cuộc gọi hàm bình thường bởi vì bất cứ khi nào hàm ảo được gọi, nó sẽ luôn luôn giải quyết cho cùng một chức năng.

Ngoài ra, nếu chỉ có một vài chức năng cụ thể khác nhau, trình biên dịch có thể thay đổi hiệu quả trang web gọi để nó sử dụng 'if' để chọn đúng chức năng cụ thể để gọi.

Vì vậy, trong các trường hợp như thế này bảng v là không cần thiết và các đối tượng có thể sẽ không có.

+0

Hmm. Tôi đã chỉ cố gắng tìm một trình biên dịch mà loại bỏ con trỏ bảng v-bảng. Không giống như hiện tại có bất kỳ. Tuy nhiên, việc chia sẻ thông tin giữa các trình biên dịch và các trình liên kết đang tăng lên cao đến nỗi chúng đang hợp nhất với nhau. Với sự phát triển liên tục, điều này có thể xảy ra. –

+0

Điều này có thể là do thực sự loại bỏ vptr sẽ có nghĩa là vi phạm nghiêm trọng ABI - và điều này sẽ cần đảm bảo rằng bất kỳ đối tượng nào của lớp được đề cập không bao giờ được thấy bên ngoài mô-đun - chỉ với 4 byte bộ nhớ. không thực sự được lưu – jpalecek

+0

OTOH, chỉ cần không gọi các phương thức thông qua ngắt công văn ảo chỉ có giao diện của phương thức cụ thể đó, và trình biên dịch có thể giải quyết điều này bằng cách phát ra một phiên bản mã khác. Nó cũng mang lại lợi thế lớn hơn, đặc biệt nếu fuction sau đó có thể được inline – jpalecek

4

Hãy thử điều này ở nhà:

#include <iostream> 
struct non_virtual {}; 
struct has_virtual { virtual void nop() {} }; 
struct has_virtual_d : public has_virtual { virtual void nop() {} }; 

int main(int argc, char* argv[]) 
{ 
    std::cout << sizeof non_virtual << "\n" 
      << sizeof has_virtual << "\n" 
      << sizeof has_virtual_d << "\n"; 
} 
+1

Câu trả lời cho VS2005: 1, 4, 4 –

+1

Rút ra kết luận cần thiết là 'còn lại như một nhiệm vụ' cho OP;) – dirkgently

+0

Những con số này là điển hình, nhưng không bắt buộc. Nó không nói bao nhiêu vtables tồn tại, hoặc những gì 4 byte được chi tiêu trên. – jalf

2

một vtable là một chi tiết thực hiện không có gì trong định nghĩa ngôn ngữ nói rằng nó tồn tại được. Trong thực tế, tôi đã đọc về các phương pháp thay thế để thực hiện các chức năng ảo.

NHƯNG: Tất cả các trình biên dịch thông thường (ví dụ: các trình biên dịch tôi biết) đều sử dụng VTabels.
Sau đó, Có. Bất kỳ lớp nào có một phương thức ảo hoặc có nguồn gốc từ một lớp (trực tiếp hoặc gián tiếp) có một phương thức ảo sẽ có các đối tượng với một con trỏ tới một VTable.

Tất cả các câu hỏi khác mà bạn yêu cầu sẽ phụ thuộc vào trình biên dịch/phần cứng không có câu trả lời thực sự cho các câu hỏi đó.

11

Giống như người khác đã nói, tiêu chuẩn C++ không ủy nhiệm bảng phương thức ảo, nhưng cho phép sử dụng bảng phương pháp ảo. Tôi đã thực hiện các bài kiểm tra của tôi sử dụng gcc và mã này và một trong những kịch bản đơn giản nhất có thể:

class Base { 
public: 
    virtual void bark() { } 
    int dont_do_ebo; 
}; 

class Derived1 : public Base { 
public: 
    virtual void bark() { } 
    int dont_do_ebo; 
}; 

class Derived2 : public Base { 
public: 
    virtual void smile() { } 
    int dont_do_ebo; 
}; 

void use(Base*); 

int main() { 
    Base * b = new Derived1; 
    use(b); 

    Base * b1 = new Derived2; 
    use(b1); 
} 

gia tăng dữ liệu thành viên để ngăn chặn các trình biên dịch để cung cấp cho các cơ sở lưu một kích thước-of của zero (nó được gọi là tối ưu hóa lớp cơ sở trống). Đây là cách bố trí mà GCC chọn: (in bằng cách sử dụng -fdump-class-hierarchy)

Vtable for Base 
Base::_ZTV4Base: 3u entries 
0  (int (*)(...))0 
4  (int (*)(...))(& _ZTI4Base) 
8  Base::bark 

Class Base 
    size=8 align=4 
    base size=8 base align=4 
Base (0xb7b578e8) 0 
    vptr=((& Base::_ZTV4Base) + 8u) 

Vtable for Derived1 
Derived1::_ZTV8Derived1: 3u entries 
0  (int (*)(...))0 
4  (int (*)(...))(& _ZTI8Derived1) 
8  Derived1::bark 

Class Derived1 
    size=12 align=4 
    base size=12 base align=4 
Derived1 (0xb7ad6400) 0 
    vptr=((& Derived1::_ZTV8Derived1) + 8u) 
    Base (0xb7b57ac8) 0 
     primary-for Derived1 (0xb7ad6400) 

Vtable for Derived2 
Derived2::_ZTV8Derived2: 4u entries 
0  (int (*)(...))0 
4  (int (*)(...))(& _ZTI8Derived2) 
8  Base::bark 
12 Derived2::smile 

Class Derived2 
    size=12 align=4 
    base size=12 base align=4 
Derived2 (0xb7ad64c0) 0 
    vptr=((& Derived2::_ZTV8Derived2) + 8u) 
    Base (0xb7b57c30) 0 
     primary-for Derived2 (0xb7ad64c0) 

Như bạn thấy mỗi lớp có một vtable. Hai mục đầu tiên là đặc biệt. Điểm thứ hai trỏ đến dữ liệu RTTI của lớp. Người đầu tiên - tôi biết nhưng quên. Nó có một số sử dụng trong các trường hợp phức tạp hơn. Vâng, như cách bố trí hiển thị, nếu bạn có một đối tượng của lớp Derived1, thì vptr (v-table-pointer) sẽ trỏ đến bảng v của lớp Derived1 tất nhiên, có chính xác một mục cho vỏ chức năng trỏ đến Phiên bản của Derived1. Vptr của Derived2 trỏ đến vtable của Derived2, có hai mục. Phương pháp khác là phương pháp mới được thêm vào, cười. Nó lặp lại mục nhập cho Base :: bark, nó sẽ trỏ tới phiên bản của hàm cơ bản của khóa học, bởi vì nó là phiên bản có nguồn gốc nhất của nó.

Tôi cũng đã đổ cây được tạo ra bởi GCC sau khi một số tối ưu được thực hiện (hàm dựng nội tuyến, ...), với -fdump-tree-optimization. Kết quả này được sử dụng GCC của ngôn ngữ trung cấp GIMPL đó là front-end độc lập, thụt vào một số cấu trúc khối C như:

;; Function virtual void Base::bark() (_ZN4Base4barkEv) 
virtual void Base::bark() (this) 
{ 
<bb 2>: 
    return; 
} 

;; Function virtual void Derived1::bark() (_ZN8Derived14barkEv) 
virtual void Derived1::bark() (this) 
{ 
<bb 2>: 
    return; 
} 

;; Function virtual void Derived2::smile() (_ZN8Derived25smileEv) 
virtual void Derived2::smile() (this) 
{ 
<bb 2>: 
    return; 
} 

;; Function int main() (main) 
int main()() 
{ 
    void * D.1757; 
    struct Derived2 * D.1734; 
    void * D.1756; 
    struct Derived1 * D.1693; 

<bb 2>: 
    D.1756 = operator new (12); 
    D.1693 = (struct Derived1 *) D.1756; 
    D.1693->D.1671._vptr.Base = &_ZTV8Derived1[2]; 
    use (&D.1693->D.1671); 
    D.1757 = operator new (12); 
    D.1734 = (struct Derived2 *) D.1757; 
    D.1734->D.1682._vptr.Base = &_ZTV8Derived2[2]; 
    use (&D.1734->D.1682); 
    return 0;  
} 

Như chúng ta có thể thấy độc đáo, nó chỉ thiết lập một con trỏ - các vptr - mà sẽ trỏ đến vtable thích hợp mà chúng ta đã thấy trước đây khi tạo đối tượng. Tôi cũng đã bỏ mã lắp ráp để tạo ra Derived1 và gọi để sử dụng ($ 4 là thanh ghi đối số đầu tiên, $ 2 là thanh ghi giá trị trả về, $ 0 là luôn-0-đăng ký) sau khi demangling các tên trong đó bằng công cụ c++filt:)

 # 1st arg: 12byte 
    add  $4, $0, 12 
     # allocate 12byte 
    jal  operator new(unsigned long)  
     # get ptr to first function in the vtable of Derived1 
    add  $3, $0, vtable for Derived1+8 
     # store that pointer at offset 0x0 of the object (vptr) 
    stw  $3, $2, 0 
     # 1st arg is the address of the object 
    add  $4, $0, $2 
    jal  use(Base*) 

gì sẽ xảy ra nếu chúng ta muốn gọi bark:

void doit(Base* b) { 
    b->bark(); 
} 

GIMPL mã:

;; Function void doit(Base*) (_Z4doitP4Base) 
void doit(Base*) (b) 
{ 
<bb 2>: 
    OBJ_TYPE_REF(*b->_vptr.Base;b->0) (b) [tail call]; 
    return; 
} 

OBJ_TYPE_REF là một GIMP L xây dựng được khá in vào (đó là tài liệu trong gcc/tree.def trong SVN mã nguồn gcc)

OBJ_TYPE_REF(<first arg>; <second arg> -> <third arg>) 

Nó có nghĩa là: Sử dụng các biểu *b->_vptr.Base trên đối tượng b, và lưu trữ các frontend (C++) giá trị cụ thể 0 (đó là chỉ mục vào vtable). Cuối cùng, nó vượt qua b làm đối số "này". Chúng tôi sẽ gọi một hàm xuất hiện ở chỉ mục thứ 2 trong vtable (lưu ý, chúng tôi không biết loại vtable nào thuộc loại nào!), Các GIMPL sẽ trông như thế này:

OBJ_TYPE_REF(*(b->_vptr.Base + 4);b->1) (b) [tail call]; 

Tất nhiên, ở đây mã lắp ráp lại (chồng-frame thứ cắt đứt):

# load vptr into register $2 
    # (remember $4 is the address of the object, 
    # doit's first arg) 
ldw  $2, $4, 0 
    # load whatever is stored there into register $2 
ldw  $2, $2, 0 
    # jump to that address. note that "this" is passed by $4 
jalr $2 

Hãy nhớ rằng những điểm vptr chính xác tại các chức năng đầu tiên . (Trước khi đó, khe RTTI đã được lưu trữ). Vì vậy, bất cứ điều gì xuất hiện tại khe đó được gọi. Nó cũng đánh dấu cuộc gọi là cuộc gọi đuôi, vì nó xảy ra như câu lệnh cuối cùng trong hàm doit của chúng tôi.

1

Để trả lời câu hỏi về các đối tượng (trường hợp từ bây giờ trở đi) có vtables và ở đâu, bạn nên suy nghĩ khi nào bạn cần một con trỏ vtable.

Đối với bất kỳ phân cấp thừa kế nào, bạn cần một vtable cho mỗi bộ hàm ảo được xác định bởi một lớp cụ thể trong phân cấp đó. Nói cách khác, đưa ra những điều sau đây:

class A { virtual void f(); int a; }; 
class B: public A { virtual void f(); virtual void g(); int b; }; 
class C: public B { virtual void f(); virtual void g(); virtual void h(); int c; }; 
class D: public A { virtual void f(); int d; }; 
class E: public B { virtual void f(); int e; }; 

Kết quả là bạn cần năm vtables: A, B, C, D và E đều cần vtables của riêng mình.

Tiếp theo, bạn cần phải biết những gì vtable để sử dụng cho một con trỏ hoặc tham chiếu đến một lớp cụ thể. Ví dụ: được cung cấp con trỏ đến A, bạn cần biết đủ về bố cục của A sao cho bạn có thể nhận được vtable cho bạn biết nơi gửi A :: f(). Cho một con trỏ tới B, bạn cần biết đủ về bố cục của B để gửi B :: f() và B :: g(). Và vân vân.

Một triển khai có thể có thể đặt con trỏ vtable làm thành viên đầu tiên của bất kỳ lớp nào. Điều đó sẽ có nghĩa là cách bố trí của một thể hiện của A sẽ là:

A's vtable; 
int a; 

Và một thể hiện của B sẽ là:

A's vtable; 
int a; 
B's vtable; 
int b; 

Và bạn có thể tạo mã dispatching ảo đúng từ cách bố trí này.

Bạn cũng có thể tối ưu hóa bố cục bằng cách kết hợp các con trỏ vtable của vtables có cùng bố cục hoặc nếu bố cục con là tập hợp con của bố cục còn lại. Vì vậy, trong ví dụ trên, bạn cũng có thể bố trí B dưới dạng:

B's vtable; 
int a; 
int b; 

Do vtable của B là bộ siêu của A. B's vtable có các mục cho A :: f và B :: g, và vtable của A có các mục cho A :: f.

Để hoàn chỉnh, đây là cách bạn sẽ bố trí tất cả các vtables chúng tôi đã nhìn thấy cho đến nay:

A's vtable: A::f 
B's vtable: A::f, B::g 
C's vtable: A::f, B::g, C::h 
D's vtable: A::f 
E's vtable: A::f, B::g 

Và thực tế mục sẽ là:

A's vtable: A::f 
B's vtable: B::f, B::g 
C's vtable: C::f, C::g, C::h 
D's vtable: D::f 
E's vtable: E::f, B::g 

Đối với đa kế thừa, bạn làm cùng phân tích:

class A { virtual void f(); int a; }; 
class B { virtual void g(); int b; }; 
class C: public A, public B { virtual void f(); virtual void g(); int c; }; 

Và bố cục kết quả sẽ là:

A: 
A's vtable; 
int a; 

B: 
B's vtable; 
int b; 

C: 
C's A vtable; 
int a; 
C's B vtable; 
int b; 
int c; 

Bạn cần một con trỏ đến vtable tương thích với A và con trỏ đến vtable tương thích với B vì tham chiếu đến C có thể được chuyển đổi thành tham chiếu A hoặc B và bạn cần gửi hàm ảo cho C.

Từ đây bạn có thể thấy rằng số lượng con trỏ vtable một lớp cụ thể có ít nhất số lượng lớp gốc mà nó xuất phát từ (trực tiếp hoặc do siêu lớp). Lớp gốc là một lớp có vtable không kế thừa từ một lớp cũng có vtable.

Thừa kế ảo ném một bit gián tiếp khác vào bản trộn, nhưng bạn có thể sử dụng cùng một chỉ số để xác định số lượng con trỏ có thể tìm thấy.

+0

Hãy chỉ ra những gì là sai trong câu trả lời khi bạn bỏ phiếu. Nếu không, không có cách nào để chúng tôi cải thiện nội dung! Cảm ơn. –

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