2010-07-21 27 views
50

Cách thích hợp/ưu tiên để cấp phát bộ nhớ trong API C là gì?Thiết kế API C: Ai nên phân bổ?

tôi có thể thấy, lúc đầu, hai lựa chọn:

1) Hãy để cho người gọi làm tất cả những (bên ngoài xử lý bộ nhớ):

myStruct *s = malloc(sizeof(s)); 
myStruct_init(s); 

myStruct_foo(s); 

myStruct_destroy(s); 
free(s); 

Các _init_destroy chức năng là cần thiết vì một số bộ nhớ hơn có thể được phân bổ bên trong, và nó phải được xử lý ở đâu đó.

này có những bất lợi của việc lâu hơn, mà còn là malloc thể được loại bỏ trong một số trường hợp (ví dụ, nó có thể được thông qua một cấu trúc ngăn xếp phân bổ:

int bar() { 
    myStruct s; 
    myStruct_init(&s); 

    myStruct_foo(&s); 

    myStruct_destroy(&s); 
} 

Ngoài ra, nó là cần thiết cho người gọi đến biết kích thước của cấu trúc.

2) Ẩn malloc s trong _initfree s trong _destroy.

Ưu điểm: mã ngắn hơn, vì các hàm sẽ được gọi. Cấu trúc hoàn toàn mờ đục.

Nhược điểm: Không thể chuyển giao cấu trúc được phân bổ theo cách khác.

myStruct *s = myStruct_init(); 

myStruct_foo(s); 

myStruct_destroy(foo); 

Tôi hiện đang nghiêng về trường hợp đầu tiên; sau đó một lần nữa, tôi không biết về thiết kế C API.

+1

btw tôi nghĩ đây sẽ là một câu hỏi phỏng vấn tuyệt vời, để so sánh và đối chiếu hai thiết kế. – frankc

+1

Đây là bài viết của Armin Ronacher về cách làm cho các cấu trúc mờ đục nhưng vẫn cho phép tùy chỉnh phân bổ: http://lucumr.pocoo.org/2013/8/18/beautiful-native-libraries/ –

Trả lời

8

Ví dụ yêu thích của tôi về API C thiết kế đẹp là GTK+ sử dụng phương pháp # 2 mà bạn mô tả.

Mặc dù lợi thế khác của phương pháp # 1 không chỉ là bạn có thể phân bổ đối tượng trên ngăn xếp, mà còn có thể sử dụng lại cùng một phiên bản nhiều lần. Nếu đó không phải là một trường hợp sử dụng phổ biến, thì sự đơn giản của # 2 có lẽ là một lợi thế.

Tất nhiên, đó chỉ là ý kiến ​​của tôi :)

+0

Bây giờ, đây là một nhận xét thú vị . Tôi đã nghe nhiều người nói chính xác điều ngược lại, rằng GTK + là một API khủng khiếp. Tôi đã không may chỉ sử dụng nó một chút, tôi thường lên trong những đám mây của C + +, và sử dụng Gtkmm. Tuy nhiên, kinh nghiệm của tôi ghi nhớ các con trỏ được tính toán lại và các hàm _new và _free, dường như phù hợp với tùy chọn thứ 3 hơn. Tôi muốn được tò mò như lý do của bạn để ý kiến ​​của bạn. – Thanatos

+2

Triết lý thiết kế chung của GLib/Gtk có vẻ là "chúng tôi sẽ không sử dụng C++ về nguyên tắc, vì vậy chúng tôi sẽ viết tay tất cả các công cụ tương tự". Cách tiếp cận này có một số lợi thế theo nghĩa là nó vẫn là một API C thuần túy, làm cho nó dễ sử dụng hơn với nhiều FFI C khác nhau ... nhưng từ một quan điểm C/C++ thuần túy, có vẻ là không thực tế. –

1

Cả hai đều được chấp nhận - có sự cân bằng giữa chúng, như bạn đã lưu ý.

Có các ví dụ thực tế lớn về cả hai - như Dean Harding cho biết, GTK + sử dụng phương pháp thứ hai; OpenSSL là một ví dụ sử dụng đầu tiên.

14

Một bất lợi khác của số 2 là người gọi không có quyền kiểm soát cách mọi thứ được phân bổ. Điều này có thể được làm việc xung quanh bằng cách cung cấp một API cho khách hàng để đăng ký các chức năng phân bổ/deallocation của riêng mình (như SDL), nhưng thậm chí có thể không đủ chi tiết.

Điểm bất lợi của số 1 là nó không hoạt động tốt khi bộ đệm đầu ra không cố định (ví dụ: chuỗi). Tốt nhất, sau đó bạn sẽ cần phải cung cấp một chức năng khác để có được chiều dài của bộ đệm đầu tiên để người gọi có thể phân bổ nó. Tệ nhất, nó chỉ đơn giản là không thể làm như vậy hiệu quả (tức là chiều dài máy tính trên một con đường riêng biệt là quá đắt so với tính toán và sao chép trong một lần).

Lợi thế của số 2 là nó cho phép bạn hiển thị kiểu dữ liệu của bạn đúng như một con trỏ mờ (tức là khai báo cấu trúc nhưng không xác định cấu trúc và sử dụng con trỏ một cách nhất quán).Sau đó, bạn có thể thay đổi định nghĩa của cấu trúc như bạn thấy phù hợp trong các phiên bản tương lai của thư viện của bạn, trong khi khách hàng vẫn tương thích ở cấp nhị phân. Với # 1, bạn phải làm điều đó bằng cách yêu cầu máy khách chỉ định phiên bản bên trong struct theo một cách nào đó (ví dụ: tất cả các trường cbSize trong Win32 API), và sau đó viết mã thủ công có thể xử lý cả phiên bản cũ và mới hơn của cấu trúc duy trì tương thích nhị phân khi thư viện của bạn phát triển.

Nói chung, nếu cấu trúc của bạn là dữ liệu trong suốt sẽ không thay đổi với bản sửa đổi nhỏ trong tương lai của thư viện, tôi sẽ đi với # 1. Nếu nó là một đối tượng dữ liệu phức tạp hơn hoặc ít hơn và bạn muốn đóng gói đầy đủ để đánh lừa nó để phát triển trong tương lai, hãy đi với # 2.

+0

+1 cho điểm về trừu tượng và con trỏ mờ đục - đây là một lợi thế lớn vì nó hoàn toàn tách rời việc thực hiện của bạn từ mã gọi –

4

Cả hai đều có chức năng tương đương. Tuy nhiên, theo ý kiến ​​của tôi, phương pháp # 2 dễ sử dụng hơn. Một vài lý do cho việc thích 2 trên 1 là:

  1. Điều này trực quan hơn. Tại sao tôi phải gọi free trên đối tượng sau khi tôi (rõ ràng) đã phá hủy nó bằng cách sử dụng myStruct_Destroy.

  2. Ẩn chi tiết của myStruct từ người dùng. Anh ta không phải lo lắng về kích thước của nó, v.v.

  3. Trong phương pháp # 2, myStruct_init không phải lo lắng về trạng thái ban đầu của đối tượng.

  4. Bạn không phải lo lắng về rò rỉ bộ nhớ từ người dùng quên gọi free.

Tuy nhiên, phương pháp triển khai API của bạn đang được gửi đi như một thư viện chia sẻ riêng biệt. Để cô lập mô-đun của bạn khỏi bất kỳ sự không khớp nào trong việc triển khai các phiên bản malloc/newfree/delete trên các phiên bản trình biên dịch, bạn nên giữ phân bổ bộ nhớ và phân bổ lại cho chính mình. Lưu ý, điều này đúng hơn với C++ so với C.

+1

Cả hai đều * không * tương đương, vì sau này yêu cầu phân bổ động và trước đó không có. – Tom

+0

Vâng ... vâng. Nên có chức năng tương đương. Đã cập nhật. – 341008

3

Vấn đề tôi có với phương pháp đầu tiên không quá nhiều vì nó dài hơn đối với người gọi, đó là api hiện bị còng tay khi có thể mở rộng số tiền bộ nhớ nó đang sử dụng chính xác bởi vì nó không biết bộ nhớ mà nó nhận được được phân bổ như thế nào. Người gọi không phải lúc nào cũng biết trước bao nhiêu bộ nhớ nó sẽ cần (hãy tưởng tượng nếu bạn đang cố gắng thực hiện một vectơ).

Một tùy chọn khác mà bạn không đề cập đến, điều này sẽ vượt quá phần lớn thời gian, là chuyển một con trỏ hàm mà api sử dụng làm cấp phát. Điều này không cho phép bạn sử dụng ngăn xếp, nhưng cho phép bạn làm điều gì đó như thay thế việc sử dụng malloc với một nhóm bộ nhớ, mà vẫn giữ api kiểm soát khi nào nó muốn phân bổ.

Đối với phương pháp nào là thiết kế api phù hợp, nó được thực hiện theo cả hai cách trong thư viện chuẩn C. strdup() và stdio sử dụng phương thức thứ hai trong khi sprintf và strcat sử dụng phương thức đầu tiên. Cá nhân tôi thích phương pháp thứ hai (hoặc thứ ba) trừ khi 1) Tôi biết tôi sẽ không bao giờ cần realloc và 2) Tôi mong đợi tuổi thọ của các đối tượng của tôi là ngắn và do đó sử dụng ngăn xếp là rất convienent

chỉnh sửa: Có thực sự là 1 tùy chọn khác và đó là một tùy chọn tồi tệ với tiền lệ nổi bật. Bạn có thể làm điều đó theo cách strtok() hiện nó với statics. Không tốt, chỉ đề cập đến vì lợi ích đầy đủ.

2

Cả hai cách đều ổn, tôi có xu hướng thực hiện theo cách đầu tiên như nhiều C tôi làm cho các hệ thống nhúng và tất cả bộ nhớ là các biến nhỏ trên ngăn xếp hoặc phân bổ tĩnh.Bằng cách này có thể không có hết bộ nhớ, hoặc bạn có đủ lúc đầu hoặc bạn đang hơi say từ đầu. Tốt để biết khi bạn có 2K Ram :-) Vì vậy, tất cả các thư viện của tôi giống như # 1 nơi bộ nhớ được giả định được phân bổ.

Nhưng đây là trường hợp cạnh của phát triển C.

Có nói rằng, tôi chắc chắn sẽ đi với # 1 vẫn còn. Có lẽ sử dụng init và finalize/dispose (thay vì phá hủy) cho tên.

2

Điều đó có thể đưa ra một số yếu tố phản xạ:

trường hợp # 1 mimick chương trình cấp phát bộ nhớ của C++, với ít nhiều lợi ích tương tự:

  • phân bổ dễ dàng là tạm thời trên stack (hoặc trong mảng tĩnh hoặc như vậy để viết bạn phân bổ struct riêng thay thế malloc).
  • dễ dàng miễn phí bộ nhớ nếu bất cứ điều gì đi sai trong init

trường hợp # 2 ẩn biết thêm thông tin về cơ cấu sử dụng và cũng có thể được sử dụng cho các cấu trúc đục, thường khi cấu trúc theo cách nhìn của người sử dụng không phải là chính xác giống như được sử dụng nội bộ bởi lib (nói rằng có thể có thêm một số trường ẩn ở cuối cấu trúc).

API hỗn hợp giữa trường hợp # 1 và trường hợp 2 cũng phổ biến: có trường được sử dụng để truyền con trỏ tới cấu trúc đã được khởi tạo, nếu nó được phân bổ (và con trỏ luôn được trả về). Với API như vậy, miễn phí thường là trách nhiệm của người gọi ngay cả khi init thực hiện phân bổ.

Trong hầu hết các trường hợp, có thể tôi sẽ tìm trường hợp số 1.

11

Tại sao không cung cấp cả hai, để tận dụng tối đa cả hai thế giới?

Sử dụng hàm _init và _terminate để sử dụng phương pháp # 1 (hoặc bất kỳ tên nào bạn thấy phù hợp).

Sử dụng các hàm _create và _destroy bổ sung cho phân bổ động. Kể từ _init và _terminate đã tồn tại, nó có hiệu quả boils xuống:

myStruct *myStruct_create() 
{ 
    myStruct *s = malloc(sizeof(*s)); 
    if (s) 
    { 
     myStruct_init(s); 
    } 
    return (s); 
} 

void myStruct_destroy (myStruct *s) 
{ 
    myStruct_terminate(s); 
    free(s); 
} 

Nếu bạn muốn nó được đục, sau đó hãy _init và _terminate static và không tiếp xúc với họ trong API, chỉ cung cấp _create và _destroy. Nếu bạn cần phân bổ khác, ví dụ: với một cuộc gọi lại cụ thể, cung cấp một tập hợp các hàm khác cho điều này, ví dụ: _createcalled, _destroycalled.

Điều quan trọng là theo dõi phân bổ, nhưng bạn vẫn phải thực hiện việc này. Bạn phải luôn luôn sử dụng các đối tác của phân bổ được sử dụng cho deallocation.

+1

Có thư viện C nổi tiếng nào đã thực hiện phương pháp này không? – cubuspl42

1

Tôi sẽ đi cho (1) với một phần mở rộng đơn giản, đó là để có chức năng _init của bạn luôn trả về con trỏ đến đối tượng. Khởi tạo con trỏ của bạn sau đó có thể chỉ đọc:

myStruct *s = myStruct_init(malloc(sizeof(myStruct))); 

Như bạn có thể thấy phía bên tay phải, chỉ có tham chiếu đến loại và không còn biến nữa.Một macro đơn giản sau đó cung cấp cho bạn (2) ít nhất một phần

#define NEW(T) (T ## _init(malloc(sizeof(T)))) 

và khởi tạo con trỏ của bạn đọc

myStruct *s = NEW(myStruct); 
+0

Làm thế nào để bạn xử lý một thất bại malloc? – Secure

+0

@ Bảo mật: Điểm tốt. Tôi nghĩ rằng '_init' chức năng nên được thực hiện mạnh mẽ để đi qua trong một con trỏ' NULL' và chỉ cần vượt qua điều này thông qua ngày trở lại. Việc kiểm tra cho rằng là hơn trái cho người sử dụng của con trỏ, như thường lệ. –

+0

Triết lý thiết kế khác trong lĩnh vực này là hầu hết các hàm đều mong đợi các con trỏ hợp lệ (với ngoại lệ rõ ràng của các trình deallocators) và khẳng định() chúng không phải là NULL. Mà sẽ làm cho cách tiếp cận của bạn để có hiệu quả sử dụng khẳng định cho logic chương trình, mà là một lớn không đi. Nó phụ thuộc vào thiết kế tổng thể của chương trình của bạn, chắc chắn, nhưng cá nhân tôi thích được rõ ràng với xử lý lỗi. I E. malloc được sử dụng riêng biệt và được kiểm tra tính hợp lệ trước khi bất cứ điều gì khác được thực hiện với con trỏ. – Secure

13

Phương pháp số 2 mỗi lần.

Tại sao? bởi vì với phương pháp số 1 bạn phải rò rỉ chi tiết triển khai cho người gọi. Người gọi phải biết ít nhất cấu trúc lớn đến mức nào. Bạn không thể thay đổi việc thực hiện nội bộ của đối tượng mà không biên dịch lại bất kỳ mã nào sử dụng nó.

+2

Điều đó có nghĩa là # 2 có thể được triển khai dưới dạng giao diện tương thích nhị phân, với các bổ sung API phiên bản nhỏ, cải tiến, v.v. không vi phạm mã khách hàng khi được gửi bằng .so hoặc .dll Câu trả lời này cần nhiều phiếu vượt trội hơn – kert

+1

Người gọi không cần biết kích thước của đối tượng (và có lẽ là liên kết?), nhưng điều đó không có nghĩa là nó phải biết nó _statically_: bạn có thể có 'myStruct_size (void)' và 'myStruct_alignment (void)'. Xem [câu hỏi này] (http: // stackoverflow.com/questions/26471718/dynamic-allocate-aligned-aligned-memory-là-the-new-expression-on-char-arra). – Kalrish

+0

@Kalrish Tại sao người gọi phải biết kích thước? Tôi đồng ý rằng nếu người gọi cần biết kích thước, bạn có thể thêm các phương pháp mà bạn đề xuất, nhưng API được thiết kế phù hợp không yêu cầu người gọi biết bất kỳ điều gì về nội bộ của đối tượng - bao gồm kích thước và căn chỉnh . – JeremyP

0

Xem phương pháp của bạn # 2 nói

myStruct *s = myStruct_init(); 

myStruct_foo(s); 

myStruct_destroy(s); 

Bây giờ xem nếu myStruct_init() nhu cầu quay trở lại một số mã lỗi vì lý do khác nhau sau đó cho phép đi theo con đường này.

myStruct *s; 
int ret = myStruct_init(&s); // int myStruct_init(myStruct **s); 

myStruct_foo(s); 

myStruct_destroy(s); 
Các vấn đề liên quan