2009-10-26 38 views
8

xem xét:C++ nhớ bảng chức năng ảo tốn

class A 
{ 
    public: 
     virtual void update() = 0; 
} 

class B : public A 
{ 
    public: 
     void update() { /* stuff goes in here... */ } 

    private: 
     double a, b, c; 
} 

class C { 
    // Same kind of thing as B, but with different update function/data members 
} 

Tôi bây giờ thực hiện:

A * array = new A[1000]; 
array[0] = new B(); 
array[1] = new C(); 
//etc., etc. 

Nếu tôi gọi sizeof(B), kích thước trở lại là kích thước theo yêu cầu của 3 thành viên tăng gấp đôi, cộng với một số chi phí cần thiết cho bảng con trỏ hàm ảo. Bây giờ, trở lại mã của tôi, nó chỉ ra rằng 'sizeof (myclass)' là 32; có nghĩa là, tôi đang sử dụng 24 byte cho các thành viên dữ liệu của tôi, và 8 byte cho bảng chức năng ảo (4 chức năng ảo). Câu hỏi của tôi là: có cách nào tôi có thể sắp xếp hợp lý điều này không? Chương trình của tôi cuối cùng sẽ sử dụng rất nhiều bộ nhớ, và tôi không thích âm thanh 25% của nó được ăn bởi các con trỏ hàm ảo.

+3

Đừng quên trình phá hủy ảo. – GManNickG

+0

Nhìn vào chiến lược phân bổ của bạn, bạn có thể tốt hơn khi điều tra bằng cách sử dụng phân bổ tùy chỉnh từ mỗi 'B',' C', v.v. Sử dụng một 'mới' cơ bản cho một số lượng lớn các đối tượng đơn lẻ có thể được thêm nhiều chi phí hơn bạn nhận ra . –

+0

Khi câu trả lời được ghi chú, bạn chỉ trả một con trỏ cho mỗi cá thể (4 byte trên x86 và 8 byte trên x64). Tuy nhiên, liên kết bộ nhớ có thể thay đổi hình ảnh một chút (ví dụ, nếu bạn có một lớp với một char và phương thức ảo, nó thường sẽ mất 8 byte trên x86). Ngoài ra, trình biên dịch Microsoft C++ đã biết lỗi với việc sắp xếp vfptr có thể thêm 4 hoặc nhiều byte vào kích cỡ lớp. – sbk

Trả lời

10

Thông thường, mọi trường hợp của một lớp có ít nhất một hàm ảo sẽ có thêm một con trỏ được lưu trữ với các thành viên dữ liệu rõ ràng của nó.

Không có cách nào làm tròn điều này, nhưng hãy nhớ rằng (một lần nữa) mỗi bảng chức năng ảo được chia sẻ giữa tất cả các phiên bản của lớp, vì vậy không có phí tuyệt vời để có nhiều chức năng ảo hoặc thừa kế thừa trả "thuế vptr" (chi phí nhỏ của con trỏ vtable).

Đối với các lớp học lớn hơn, chi phí sẽ trở nên nhỏ hơn nhiều so với phần trăm.

Nếu bạn muốn chức năng hoạt động giống như chức năng ảo, bạn sẽ phải trả tiền theo cách nào đó. Trên thực tế sử dụng các chức năng ảo gốc cũng có thể là tùy chọn rẻ nhất.

38

Bảng v là cho mỗi lớp chứ không phải cho mỗi đối tượng. Mỗi đối tượng chỉ chứa một con trỏ vào bảng v. Vì vậy, chi phí cho mỗi trường hợp là sizeof(pointer) (thường là 4 hoặc 8 byte). Không quan trọng bạn có bao nhiêu hàm ảo cho kích thước của đối tượng lớp. Xem xét điều này, tôi nghĩ bạn không nên lo lắng quá nhiều về nó.

+0

@ 280Z28: cảm ơn cho chỉnh sửa .. – Ponting

+1

chỉ để thêm nó - vẫn còn một v-table mỗi lớp, chứa ít nhất một con trỏ cho mỗi hàm ảo. –

+1

Bạn vẫn phải trả thêm chi phí cho - trong nhiều trường hợp hoàn toàn không cần thiết - vtable. Nó chỉ lấp đầy không gian cho mỗi lớp trong các bit 'toàn cầu' của tệp thực thi, không phải cho mỗi trường hợp. Không gian nói có thể rất quan trọng, đặc biệt nếu kế thừa khi bạn không cần, từ các lớp w/số funcs ảo cao. Trong những trường hợp như vậy, việc chuyển sang chế phẩm có thể làm giảm đáng kể số nhị phân. Tôi đã làm mỏng một hệ nhị phân của dự án bằng 1/6 bằng cách dừng kế thừa không cần thiết từ kiểu thư viện của ai đó. Thêm vào đó, nó có lợi ích bình thường khi giải phóng thiết kế lớp của bạn khỏi tất cả các ràng buộc khác được ghi chép đầy đủ của thừa kế –

6

Bạn có hai tùy chọn.

1) Đừng lo lắng về điều đó.

2) Không sử dụng các chức năng ảo. Tuy nhiên, không sử dụng các chức năng ảo chỉ có thể di chuyển kích thước vào mã của bạn, vì mã của bạn trở nên phức tạp hơn.

6

Chi phí không gian của vtable là một con trỏ (căn chỉnh modulo). Bản thân bảng không được đặt vào mỗi thể hiện của lớp.

+0

Bằng cách đặt con trỏ vtable vào đầu lớp không có vấn đề liên kết (đó là lý do tại sao hầu hết các trình biên dịch đặt nó ở đó). –

+3

Sự cố liên kết vẫn còn ở đó nếu kích thước lớp học của bạn nhỏ hơn con trỏ vtable để bắt đầu. Nói 'struct C {char c; } 'có thể sẽ có kích thước 1, và bất kỳ mảng nào như vậy sẽ có chúng được đóng gói chặt chẽ. Nếu bạn thêm một thành viên ảo, trên nền tảng 32 bit, nó có thể sẽ tạo ra 5 ký tự lưu trữ thành 8, vì vậy bây giờ bạn đột nhiên có 3 ký tự bị lãng phí cho mỗi đối tượng. –

+1

Câu trả lời này đề cập rõ ràng "căn chỉnh" và thực sự đây là vấn đề đối với các mục tiêu có ít RAM (vi điều khiển). Nó giống như Pavel mô tả nó, một char + con trỏ vtable (4 byte) kết quả trong tám byte trên 32-bit Cortex-Mx ARM (có một padding của ba byte có thể/hy vọng trước khi ptr vtable). – Beryllium

1

Nếu bạn biết tất cả các loại dẫn xuất và chức năng cập nhật tương ứng của chúng, bạn có thể lưu trữ kiểu dẫn xuất trong A và thực hiện gửi thủ công cho phương thức cập nhật.

Tuy nhiên, như những người khác đang chỉ ra, bạn thực sự không trả tiền nhiều cho vtable, và sự cân bằng là mã phức tạp (và tùy thuộc vào sự liên kết, bạn có thể không tiết kiệm bộ nhớ nào cả!). Ngoài ra, nếu bất kỳ thành viên dữ liệu nào của bạn có một destructor, thì bạn cũng phải lo lắng về việc gửi đi thủ công destructor.

Nếu bạn vẫn muốn đi tuyến đường này, nó có thể trông như thế này:

class A; 
void dispatch_update(A &); 

class A 
{ 
public: 
    A(char derived_type) 
     : m_derived_type(derived_type) 
    {} 
    void update() 
    { 
     dispatch_update(*this); 
    } 
    friend void dispatch_update(A &); 
private: 
    char m_derived_type; 
}; 

class B : public A 
{ 
public: 
    B() 
     : A('B') 
    {} 
    void update() { /* stuff goes in here... */ } 

private: 
    double a, b, c; 
}; 

void dispatch_update(A &a) 
{ 
    switch (a.m_derived_type) 
    { 
    case 'B': 
     static_cast<B &> (a).update(); 
     break; 
    // ... 
    } 
} 
2

Có bao nhiêu trường hợp của lớp A có nguồn gốc từ sao bạn mong đợi? Bạn mong đợi bao nhiêu lớp học có nguồn gốc A khác biệt?

Lưu ý rằng ngay cả với hàng triệu trường hợp, chúng tôi đang nói về tổng cộng 32MB. Lên đến 10 triệu, không đổ mồ hôi nó.

Thông thường, bạn cần thêm một con trỏ cho mỗi trường hợp, (nếu bạn đang chạy trên nền tảng 32 bit, 4 byte cuối cùng là do căn chỉnh). Mỗi lớp tiêu thụ thêm (Number of virtual functions * sizeof(virtual function pointer) + fixed size) byte cho VMT của nó.

Lưu ý rằng, xem xét căn chỉnh cho đôi, thậm chí một byte đơn là định danh loại sẽ làm tăng kích thước phần tử mảng lên 32. Vì vậy, giải pháp của Stjepan Rajko hữu ích trong một số trường hợp, nhưng không phải trong của bạn.

Ngoài ra, đừng quên chi phí của một vùng chung cho rất nhiều đối tượng nhỏ. Bạn có thể có 8 byte khác cho mỗi đối tượng. Với trình quản lý heap tùy chỉnh - chẳng hạn như đối tượng/kích thước cụ thể pool allocator - bạn có thể tiết kiệm nhiều hơn tại đây và sử dụng giải pháp chuẩn.

1

Bạn đang thêm một con trỏ vào vtable cho mỗi đối tượng - nếu bạn thêm một số hàm ảo mới, kích thước của từng đối tượng sẽ không tăng. Lưu ý rằng ngay cả khi bạn đang ở trên nền tảng 32 bit, nơi con trỏ là 4 byte, bạn sẽ thấy kích thước của đối tượng tăng lên 8 có thể do yêu cầu căn chỉnh tổng thể của cấu trúc (ví dụ: bạn nhận được 4 byte của padding).

Vì vậy, ngay cả khi bạn đã tạo lớp không ảo, việc thêm một thành viên char đơn lẻ có thể sẽ thêm 8 byte đầy đủ vào kích thước của từng đối tượng.

Tôi nghĩ rằng cách duy nhất bạn sẽ có thể giảm kích thước của bạn đối tượng sẽ được:

  • làm cho họ không ảo (? Bạn có thực sự cần hành vi đa hình)
  • sử dụng thay vì tăng gấp đôi cho một hoặc nhiều thành viên dữ liệu nếu bạn không cần độ chính xác
  • nếu bạn có khả năng thấy nhiều đối tượng có cùng giá trị cho thành viên dữ liệu, bạn có thể tiết kiệm dung lượng bộ nhớ đối với một số phức tạp trong việc quản lý các đối tượng bằng cách sử dụng Flyweight design pattern
1

Không phải là câu trả lời cho câu hỏi trực tiếp, mà còn xem xét thứ tự khai báo của các thành viên dữ liệu có thể tăng hoặc giảm mức tiêu thụ bộ nhớ thực của bạn cho mỗi đối tượng lớp. Điều này là do hầu hết các trình biên dịch không thể (đọc: không) tối ưu hóa thứ tự mà các thành viên lớp được đặt ra trong bộ nhớ để giảm phân mảnh nội bộ do các tai ương liên kết.

+0

Tôi muốn nói rằng họ không thể làm điều đó. C++ cho phép so sánh con trỏ giữa các địa chỉ của các thành viên dữ liệu của cùng một đối tượng (với một số hạn chế), đòi hỏi kết quả của sự so sánh đó để phản ánh thứ tự khai báo. Tôi muốn nói rằng bất kỳ loại "ma thuật biên dịch" nào cần thiết để hỗ trợ yêu cầu này với các thành viên dữ liệu được sắp xếp lại sẽ chứng minh là quá đắt. – AnT

+0

Tôi muốn chống lại rằng bất kỳ hai con trỏ có thể so sánh theo số học, nhưng gõ của C++ đôi khi yêu cầu bạn cast. Dù bằng cách nào, "ma thuật biên dịch" sẽ đi vào một hệ thống như vậy sẽ được tính toán để giảm thiểu sự phân mảnh nội bộ của các đối tượng lớp như một tổng thể, không được thực hiện trên cơ sở mỗi cá thể. Đây là tất cả giả định rằng tôi đã không hiểu lầm bình luận của bạn. –

2

Nếu bạn sắp có hàng triệu thứ này và bộ nhớ là mối quan tâm nghiêm trọng đối với bạn, thì có thể bạn không nên làm cho chúng trở thành đối tượng. Chỉ cần khai báo chúng như một cấu trúc hoặc một mảng gồm 3 đôi (hoặc bất kỳ thứ gì), và đặt các hàm để thao tác dữ liệu ở một nơi khác.

Nếu bạn thực sự cần những hành vi đa hình, có lẽ bạn không thể giành chiến thắng, kể từ khi loại thông tin bạn muốn có để lưu trữ trong cấu trúc của bạn sẽ kết thúc chiếm một lượng tương tự của không gian ...

Is có khả năng bạn sẽ có nhiều nhóm đối tượng cùng loại? Trong trường hợp đó, bạn có thể đặt các loại thông tin một cấp "lên" từ cá nhân "A" lớp ...

Cái gì như:

class A_collection 
{ 
    public: 
     virtual void update() = 0; 
} 

class B_collection : public A_collection 
{ 
    public: 
     void update() { /* stuff goes in here... */ } 

    private: 
     vector<double[3]> points; 
} 

class C_collection { /* Same kind of thing as B_collection, but with different update function/data members */ 
0

Với tất cả các câu trả lời rằng đã ở đây, tôi nghĩ rằng Tôi phải điên rồ, nhưng điều này có vẻ đúng với tôi vì vậy tôi đăng nó dù sao đi nữa.Khi tôi lần đầu tiên nhìn thấy ví dụ mã của bạn, tôi nghĩ bạn đã cắt các trường hợp của BC, nhưng sau đó tôi nhìn gần hơn một chút. Tôi bây giờ hợp lý chắc chắn ví dụ của bạn sẽ không biên dịch ở tất cả, nhưng tôi không có một trình biên dịch trên hộp này để kiểm tra.

A * array = new A[1000]; 
array[0] = new B(); 
array[1] = new C(); 

Với tôi, điều này có vẻ như dòng đầu tiên phân bổ một mảng 1000 A. Hai dòng tiếp theo hoạt động trên các phần tử thứ nhất và thứ hai của mảng đó, tương ứng, là các cá thể của A, không trỏ đến A. Do đó bạn không thể gán một con trỏ tới A cho các phần tử đó (và new B() trả về một con trỏ như vậy). Các loại không giống nhau, do đó nó sẽ không thành công tại thời gian biên dịch (trừ khi A có một toán tử gán phải mất A*, trong trường hợp này nó sẽ làm bất cứ điều gì bạn đã nói nó làm).

Vì vậy, tôi hoàn toàn có cơ sở không? Tôi mong muốn tìm ra những gì tôi đã bỏ lỡ.

5

Di chuyển ra khỏi vấn đề không của con trỏ vtable trong đối tượng của bạn:

Mã của bạn có vấn đề khác:

A * array = new A[1000]; 
array[0] = new B(); 
array[1] = new C(); 

Vấn đề bạn đang gặp phải là vấn đề cắt.
Bạn không thể đặt đối tượng của lớp B vào khoảng trống được đặt trước cho đối tượng của lớp A.
Bạn sẽ chỉ cắt phần B (hoặc C) của đối tượng sạch sẽ để lại bạn chỉ với phần A.

Điều bạn muốn làm. Có một mảng của một con trỏ để nó giữ mỗi mục bằng con trỏ.

A** array = new A*[1000]; 
array[0] = new B(); 
array[1] = new C(); 

Bây giờ bạn có một vấn đề khác về sự hủy diệt. Được. Điều này có thể đi vào cho các lứa tuổi.
ngắn câu trả lời sử dụng boost: ptr_vector <>

boost:ptr_vector<A> array(1000); 
array[0] = new B(); 
array[1] = new C(); 

Never allocte mảng như vậy, trừ khi bạn phải (nó quá Java Giống như là hữu ích).

+4

Nói đúng, phiên bản gốc sẽ không biên dịch. Nó cố gắng gán các giá trị * pointer * cho các phần tử mảng, khi các phần tử mảng không phải là con trỏ. Còn quá sớm để khẳng định rằng nó đã "cắt đứt vấn đề", cho đến khi mã trở thành compilable. – AnT

+0

@AndretT: Đúng vậy. Nhảy súng. –

1

Như những người khác đã nói, trong một cách tiếp cận phổ biến thực hiện phổ biến, một khi một lớp trở nên đa hình, mỗi cá thể phát triển bởi kích thước của một con trỏ dữ liệu thông thường. Không quan trọng bạn có bao nhiêu chức năng ảo trong lớp của mình. Trên nền tảng 64 bit, kích thước sẽ tăng 8 byte. Nếu bạn quan sát sự tăng trưởng 8 byte trên nền tảng 32-bit, nó có thể gây ra bởi đệm được thêm vào con trỏ 4 byte để căn chỉnh (nếu lớp của bạn có yêu cầu căn chỉnh 8 byte).

Ngoài ra, có thể cần lưu ý rằng thừa kế ảo có thể đưa thêm con trỏ dữ liệu vào các phiên bản lớp (con trỏ cơ sở ảo). Tôi chỉ quen thuộc với một vài triển khai và trong ít nhất một số con trỏ cơ sở ảo giống như số lượng cơ sở ảo trong lớp, có nghĩa là thừa kế ảo có thể thêm nhiều con trỏ dữ liệu nội bộ vào mỗi cá thể.

-1

Nếu bạn thực sự muốn lưu bộ nhớ của con trỏ bảng ảo trong từng đối tượng thì bạn có thể triển khai mã theo kiểu C ...

Ví dụ:

struct Point2D { 
int x,y; 
}; 

struct Point3D { 
int x,y,z; 
}; 

void Draw2D(void *pThis) 
{ 
    Point2D *p = (Point2D *) pThis; 
    //do something 
} 

void Draw3D(void *pThis) 
{ 
    Point3D *p = (Point3D *) pThis; 
//do something 
} 

int main() 
{ 

    typedef void (*pDrawFunct[2])(void *); 

    pDrawFunct p; 
    Point2D pt2D; 
    Point3D pt3D; 

    p[0] = &Draw2D; 
    p[1] = &Draw3D;  

    p[0](&pt2D); //it will call Draw2D function 
    p[1](&pt3D); //it will call Draw3D function 
    return 0; 
} 
+1

Đây không phải là câu trả lời. Làm thế nào là một hướng dẫn về funcptrs có liên quan? Làm thế nào người ta có thể thay thế điều này trong các tình huống thực tế trong đó các chức năng ảo là mong muốn, ví dụ: OP của ví dụ về việc tạo/iterating một container của các đối tượng liên quan không đồng nhất? Làm thế nào mỗi obj biết chức năng nào nên được gọi cho nó? Nó sẽ cần một số loại ... bảng chức năng. Bất cứ ai cố gắng để thực hiện đa hình hoặc rộng hơn OOP a la C kết thúc lên @ cùng một mức phí trên như thể họ chỉ sử dụng các công cụ C++ gốc như vfuncs, ngoại trừ bây giờ không ai có thể hiểu được mã của họ. (tức là C OOP libs: ấn tượng nhưng một công việc thực sự để đọc) –

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