Thực hành tốt nhất OO cho các chương trình C [đã đóng]


19

"Nếu bạn thực sự muốn OO sugar - hãy sử dụng C ++" - là câu trả lời ngay lập tức tôi nhận được từ một trong những người bạn của mình khi tôi hỏi điều này. Tôi biết hai điều đã chết sai ở đây. OO thứ nhất KHÔNG phải là 'đường' và thứ hai, C ++ chưa hấp thụ C.

Chúng ta cần phải viết một máy chủ bằng C (phần đầu sẽ có trong Python), và vì vậy tôi đang khám phá những cách tốt hơn để quản lý các chương trình C lớn.

Mô hình hóa một hệ thống lớn về các đối tượng và các tương tác đối tượng làm cho nó dễ quản lý hơn, có thể duy trì và mở rộng hơn. Nhưng khi bạn cố gắng dịch mô hình này sang C mà không mang các đối tượng (và mọi thứ khác), bạn sẽ bị thách thức với một số quyết định quan trọng.

Bạn có tạo một thư viện tùy chỉnh để cung cấp các tóm tắt OO mà hệ thống của bạn cần không? Những thứ như đối tượng, đóng gói, kế thừa, đa hình, ngoại lệ, pub / sub (sự kiện / tín hiệu), không gian tên, hướng nội, v.v. (ví dụ GObject hoặc COS ).

Hoặc, bạn chỉ cần sử dụng các cấu trúc C ( structvà các hàm) cơ bản để xấp xỉ tất cả các lớp đối tượng của bạn (và các khái niệm trừu tượng khác) theo các cách đặc biệt. (ví dụ: một số câu trả lời cho câu hỏi này trên SO )

Cách tiếp cận đầu tiên cung cấp cho bạn một cách có cấu trúc để thực hiện toàn bộ mô hình của bạn trong C. Nhưng nó cũng thêm một lớp phức tạp mà bạn phải duy trì. (Hãy nhớ rằng, độ phức tạp là những gì chúng tôi muốn giảm bằng cách sử dụng các đối tượng ở vị trí đầu tiên).

Tôi không biết về cách tiếp cận thứ hai và hiệu quả của nó là gần đúng với tất cả những điều trừu tượng mà bạn có thể yêu cầu.

Vì vậy, câu hỏi đơn giản của tôi là: các thực tiễn tốt nhất trong việc hiện thực hóa một thiết kế hướng đối tượng trong C. Hãy nhớ rằng tôi không yêu cầu CÁCH làm điều đó. Đâyđây câu hỏi nói về nó, và có ngay cả một cuốn sách về vấn đề này. Điều tôi quan tâm hơn là một số lời khuyên / ví dụ thực tế giải quyết các vấn đề thực sự xuất hiện khi tiền này.

Lưu ý: vui lòng không tư vấn tại sao C không nên được sử dụng để ủng hộ C ++. Chúng ta đã đi qua giai đoạn đó.


3
Bạn có thể viết máy chủ C ++ để giao diện bên ngoài của nó extern "C"và có thể được sử dụng từ python. Bạn có thể làm điều đó bằng tay hoặc bạn có thể nhờ SWIG giúp bạn điều đó. Vì vậy, mong muốn cho frontend python là không có lý do để không sử dụng C ++. Điều đó không có nghĩa là không có lý do chính đáng để muốn ở lại với C.
Jan Hudec

1
Câu hỏi này cần làm rõ. Hiện tại, các đoạn 4 và 5 về cơ bản nên hỏi cách tiếp cận nào, nhưng sau đó bạn nói rằng bạn "không yêu cầu LÀM THẾ NÀO" và thay vào đó muốn (một danh sách?) Thực hành tốt nhất. Nếu bạn không tìm kiếm CÁCH để làm điều đó trong C, thì bạn có đang yêu cầu một danh sách "các thực tiễn tốt nhất" liên quan đến OOP nói chung không? Nếu vậy, hãy nói điều đó, nhưng lưu ý rằng câu hỏi có thể sẽ bị đóng vì chủ quan .
Caleb

:) Tôi đang yêu cầu các ví dụ thực tế (mã hoặc cách khác) nơi nó đã được thực hiện - và các vấn đề họ gặp phải khi thực hiện nó.
treecoder

4
Yêu cầu của bạn có vẻ khó hiểu. Bạn khăng khăng sử dụng hướng đối tượng mà không có lý do nào tôi có thể thấy (trong một số ngôn ngữ, đó là cách để làm cho chương trình dễ bảo trì hơn, nhưng không phải bằng C) và nhấn mạnh vào việc sử dụng C. Định hướng đối tượng là phương tiện, không phải là kết thúc hay là thuốc chữa bách bệnh . Hơn nữa, nó được hưởng lợi rất nhiều bởi sự hỗ trợ ngôn ngữ. Nếu bạn thực sự muốn OO, bạn nên xem xét điều đó trong quá trình lựa chọn ngôn ngữ. Một câu hỏi về cách làm cho một hệ thống phần mềm lớn với C sẽ có ý nghĩa hơn nhiều.
David Thornley

Bạn có thể muốn xem "Thiết kế và mô hình hướng đối tượng". (Rumbaugh et al.): Có phần về ánh xạ các thiết kế OO sang các ngôn ngữ như C.
Giorgio

Câu trả lời:


16

Từ câu trả lời của tôi đến Làm thế nào tôi nên cấu trúc các dự án phức tạp trong C (không phải OO mà là về việc quản lý độ phức tạp trong C):

Chìa khóa là tính mô đun. Điều này dễ dàng hơn để thiết kế, thực hiện, biên dịch và bảo trì.

  • Xác định các mô-đun trong ứng dụng của bạn, như các lớp trong ứng dụng OO.
  • Giao diện và cách thực hiện riêng cho từng mô-đun, chỉ đưa vào giao diện những gì cần thiết cho các mô-đun khác. Hãy nhớ rằng không có không gian tên trong C, vì vậy bạn phải làm cho mọi thứ trong giao diện của mình trở nên độc đáo (ví dụ: có tiền tố).
  • Ẩn các biến toàn cục trong triển khai và sử dụng các hàm truy cập để đọc / ghi.
  • Đừng nghĩ về mặt thừa kế, nhưng về mặt thành phần. Theo nguyên tắc chung, đừng cố bắt chước C ++ trong C, điều này sẽ rất khó đọc và duy trì.

Từ câu trả lời của tôi đến các quy ước đặt tên điển hình cho các chức năng công khai và riêng tư của OO C (Tôi nghĩ rằng đây là một cách thực hành tốt nhất):

Quy ước tôi sử dụng là:

  • Chức năng công khai (trong tệp tiêu đề):

    struct Classname;
    Classname_functionname(struct Classname * me, other args...);
  • Hàm riêng (tĩnh trong tệp thực hiện)

    static functionname(struct Classname * me, other args...)

Hơn nữa, nhiều công cụ UML có thể tạo mã C từ các sơ đồ UML. Một nguồn mở là Topcasing .



1
Câu trả lời chính xác. Nhằm mục đích cho mô-đun. OO được cho là cung cấp nó, nhưng 1) trong thực tế, tất cả đều quá phổ biến để kết thúc với spaghetti OO và 2) đó không phải là cách duy nhất. Đối với một số ví dụ trong cuộc sống thực, hãy xem nhân linux (mô đun kiểu C) và các dự án sử dụng glib (kiểu OO kiểu C). Tôi đã có cơ hội làm việc với cả hai phong cách và chiến thắng mô đun kiểu IMO C.
Joh

Và tại sao chính xác thành phần là một cách tiếp cận tốt hơn so với thừa kế? Cơ sở lý luận và tài liệu tham khảo hỗ trợ được chào đón. Hoặc bạn chỉ đề cập đến các chương trình C?
Alexanderr Blekh

1
@AleksandrBlekh - Có, tôi chỉ nói đến C.
mouviciel

16

Tôi nghĩ bạn cần phân biệt OO và C ++ trong cuộc thảo luận này.

Việc triển khai các đối tượng là có thể trong C, và nó khá dễ dàng - chỉ cần tạo các cấu trúc với các con trỏ hàm. Đó là "cách tiếp cận thứ hai" của bạn, và tôi sẽ thực hiện theo. Một tùy chọn khác là không sử dụng các con trỏ hàm trong cấu trúc, mà chuyển cấu trúc dữ liệu cho các hàm trong các cuộc gọi trực tiếp, như một con trỏ "ngữ cảnh". Điều đó tốt hơn, IMHO, vì nó dễ đọc hơn, dễ theo dõi hơn và cho phép thêm các chức năng mà không thay đổi cấu trúc (dễ dàng kế thừa nếu không có dữ liệu nào được thêm vào). Thực tế đó là cách C ++ thường thực hiện thiscon trỏ.

Đa hình trở nên phức tạp hơn vì không có hỗ trợ kế thừa và trừu tượng tích hợp, do đó, bạn sẽ phải đưa cấu trúc cha vào lớp con hoặc thực hiện nhiều thao tác sao chép, một trong những tùy chọn đó thật sự kinh khủng, mặc dù về mặt kỹ thuật đơn giản. Rất nhiều lỗi đang chờ để xảy ra.

Các chức năng ảo có thể dễ dàng đạt được thông qua các con trỏ hàm trỏ đến các chức năng khác nhau theo yêu cầu, một lần nữa - rất dễ bị lỗi khi thực hiện thủ công, rất nhiều công việc tẻ nhạt trong việc khởi tạo các con trỏ đó một cách chính xác.

Đối với không gian tên, ngoại lệ, mẫu, v.v. - Tôi nghĩ rằng nếu bạn bị giới hạn ở C - bạn chỉ nên từ bỏ những thứ đó. Tôi đã từng viết OO bằng C, sẽ không làm điều đó nếu tôi có sự lựa chọn (tại nơi làm việc đó, tôi thực sự đã chiến đấu để giới thiệu C ++, và các nhà quản lý đã "ngạc nhiên" về việc tích hợp nó dễ dàng như thế nào với phần còn lại của các mô-đun C ở cuối.).

Đi mà không nói rằng nếu bạn có thể sử dụng C ++ - sử dụng C ++. Không có lý do thực sự để không.


Trên thực tế, bạn có thể kế thừa từ một cấu trúc và thêm dữ liệu: chỉ cần khai báo mục đầu tiên của cấu trúc con là một biến có kiểu là cha cấu trúc. Sau đó, đúc như bạn cần.
mouviciel

1
@mouviciel - vâng. Tôi đã nói thế. " ... Vì vậy, bạn sẽ phải bao gồm cấu trúc cha mẹ trong lớp con của mình hoặc ... "
littleadv

5
Không có lý do để cố gắng thực hiện thừa kế. Là một phương tiện để đạt được việc sử dụng lại mã, đó là một ý tưởng hoàn hảo để bắt đầu. Thành phần đối tượng là dễ dàng hơn và tốt hơn.
KaptajnKold

@KaptajnKold - đồng ý.
littleadv

8

Dưới đây là những điều cơ bản về cách tạo hướng đối tượng trong C

1. Tạo đối tượng và đóng gói

Thông thường - người ta tạo ra một đối tượng như

object_instance = create_object_typex(parameter);

Các phương thức có thể được định nghĩa theo một trong hai cách ở đây.

object_type_method_function(object_instance,parameter1)
OR
object_instance->method_function(object_instance_private_data,parameter1)

Lưu ý rằng trong hầu hết các trường hợp, object_instance (or object_instance_private_data)được trả về là loại void *.Ứng dụng không thể tham chiếu các thành viên riêng lẻ hoặc các chức năng của điều này.

Hơn nữa, mỗi phương thức sử dụng các object_instance này cho phương thức tiếp theo.

2. Đa hình

Chúng ta có thể sử dụng nhiều chức năng và con trỏ hàm để ghi đè chức năng nhất định trong thời gian chạy.

ví dụ - tất cả các object_method được định nghĩa là một con trỏ hàm có thể được mở rộng cho các phương thức công khai cũng như riêng tư.

Chúng ta cũng có thể áp dụng quá tải hàm theo nghĩa hạn chế, bằng cách sử dụng var_args rất giống với cách số lượng đối số được xác định trong printf. vâng, điều này không linh hoạt bằng C ++ - nhưng đây là cách gần nhất.

3. Xác định thừa kế

Xác định thừa kế là một chút khó khăn nhưng người ta có thể làm như sau với các cấu trúc.

typedef struct { 
     int age,
     int sex,
} person; 

typedef struct { 
     person p,
     enum specialty s;
} doctor;

typedef struct { 
     person p,
     enum subject s;
} engineer;

// use it like
engineer e1 = create_engineer(); 
get_person_age( (person *)e1); 

ở đây doctorengineercó nguồn gốc từ người và có thể đánh máy nó lên cấp cao hơn, nói person.

Ví dụ tốt nhất về điều này được sử dụng trong GObject và các đối tượng dẫn xuất từ ​​nó.

4. Tạo các lớp ảo Tôi đang trích dẫn một ví dụ thực tế bởi một thư viện có tên libjpeg được sử dụng bởi tất cả các trình duyệt để giải mã jpeg. Nó tạo ra một lớp ảo gọi là error_manager mà ứng dụng có thể tạo cá thể cụ thể và cung cấp lại -

struct djpeg_dest_struct {
  /* start_output is called after jpeg_start_decompress finishes.
   * The color map will be ready at this time, if one is needed.
   */
  JMETHOD(void, start_output, (j_decompress_ptr cinfo,
                               djpeg_dest_ptr dinfo));
  /* Emit the specified number of pixel rows from the buffer. */
  JMETHOD(void, put_pixel_rows, (j_decompress_ptr cinfo,
                                 djpeg_dest_ptr dinfo,
                                 JDIMENSION rows_supplied));
  /* Finish up at the end of the image. */
  JMETHOD(void, finish_output, (j_decompress_ptr cinfo,
                                djpeg_dest_ptr dinfo));

  /* Target file spec; filled in by djpeg.c after object is created. */
  FILE * output_file;

  /* Output pixel-row buffer.  Created by module init or start_output.
   * Width is cinfo->output_width * cinfo->output_components;
   * height is buffer_height.
   */
  JSAMPARRAY buffer;
  JDIMENSION buffer_height;
};

Lưu ý ở đây rằng JMETHOD mở rộng trong một con trỏ hàm thông qua một macro cần được tải với các phương thức đúng tương ứng.


Tôi đã cố gắng nói rất nhiều điều mà không có quá nhiều lời giải thích riêng lẻ. Nhưng tôi hy vọng mọi người có thể thử những thứ của riêng họ. Tuy nhiên, ý định của tôi chỉ là chỉ ra cách bản đồ.

Ngoài ra, sẽ có nhiều tranh luận rằng đây sẽ không phải là tài sản chính xác của C ++. Tôi biết rằng OO trong C - sẽ không nghiêm ngặt với định nghĩa của nó. Nhưng làm việc như thế người ta sẽ hiểu một số nguyên tắc cốt lõi.

Điều quan trọng không phải là OO nghiêm ngặt như trong C ++ và JAVA. Đó là người ta có thể tổ chức cấu trúc mã với suy nghĩ OO và vận hành nó theo cách đó.

Tôi đặc biệt khuyên mọi người nên xem thiết kế thực sự của libjpeg và các tài nguyên sau

a. Lập trình hướng đối tượng trong C
b. đây là một nơi tốt để mọi người trao đổi ý tưởng
c. và đây là cuốn sách đầy đủ


3

Hướng đối tượng nắm bắt được ba điều:

1) Thiết kế chương trình mô-đun với các lớp tự trị.

2) Bảo vệ dữ liệu với đóng gói riêng.

3) Tính kế thừa / đa hình và các cú pháp hữu ích khác như hàm tạo / hàm hủy, mẫu, v.v.

1 là quan trọng nhất cho đến nay, và nó cũng hoàn toàn độc lập với ngôn ngữ, tất cả là về thiết kế chương trình. Trong C, bạn thực hiện điều này bằng cách tạo các "mô-đun mã" tự trị bao gồm một tệp .h và một tệp .c. Coi đây là tương đương với một lớp OO. Bạn có thể quyết định những gì nên được đặt bên trong mô-đun này thông qua ý nghĩa thông thường, UML hoặc bất kỳ phương pháp thiết kế OO nào bạn đang sử dụng cho các chương trình C ++.

2 cũng khá quan trọng, không chỉ để bảo vệ quyền truy cập có chủ ý vào dữ liệu riêng tư, mà còn để bảo vệ chống truy cập không chủ ý, tức là "sự lộn xộn không gian tên". C ++ thực hiện điều này theo những cách tao nhã hơn C, nhưng nó vẫn có thể đạt được trong C bằng cách sử dụng từ khóa tĩnh. Tất cả các biến bạn đã khai báo là riêng tư trong một lớp C ++, nên được khai báo là tĩnh trong C và được đặt ở phạm vi tệp. Họ chỉ có thể truy cập từ bên trong mô-đun mã riêng của họ (lớp). Bạn có thể viết "setters / getters" giống như trong C ++.

3 là hữu ích nhưng không cần thiết. Bạn có thể viết các chương trình OO mà không cần kế thừa hoặc không có hàm tạo / hàm hủy. Những thứ này là tốt đẹp để có, chúng chắc chắn có thể làm cho các chương trình thanh lịch hơn và có lẽ cũng an toàn hơn (hoặc ngược lại nếu sử dụng bất cẩn). Nhưng chúng không cần thiết. Vì C không hỗ trợ các tính năng hữu ích này, nên bạn chỉ cần thực hiện mà không có chúng. Hàm xây dựng có thể được thay thế bằng các hàm init / hủy.

Kế thừa có thể được thực hiện thông qua các thủ thuật cấu trúc khác nhau, nhưng tôi sẽ khuyên bạn, vì nó có thể sẽ làm cho chương trình của bạn phức tạp hơn mà không có bất kỳ lợi ích nào (nói chung nên được áp dụng cẩn thận, không chỉ trong C mà bằng bất kỳ ngôn ngữ nào).

Cuối cùng, mọi thủ thuật OO đều có thể được thực hiện trong cuốn sách "Lập trình hướng đối tượng với ANSI C" của C. Axel-Tobias từ đầu những năm 90 đã chứng minh điều này. Tuy nhiên, tôi sẽ không giới thiệu cuốn sách đó cho bất kỳ ai: nó thêm một sự phức tạp khó chịu, kỳ lạ vào các chương trình C của bạn, thứ không đáng để bận tâm. (Cuốn sách có sẵn miễn phí ở đây cho những người vẫn quan tâm bất chấp cảnh báo của tôi.)

Vì vậy, lời khuyên của tôi là thực hiện 1) và 2) ở trên và bỏ qua phần còn lại. Đó là một cách viết các chương trình C đã được chứng minh thành công trong hơn 20 năm.


2

Mượn một số kinh nghiệm từ các thời gian chạy Objective-C khác nhau, viết một khả năng OO đa hình, năng động trong C không quá khó (mặt khác, nó nhanh và dễ sử dụng, dường như vẫn đang tiếp diễn sau 25 năm). Tuy nhiên, nếu bạn triển khai khả năng đối tượng kiểu Objective-C mà không mở rộng cú pháp ngôn ngữ thì mã bạn kết thúc khá lộn xộn:

  • mỗi lớp được định nghĩa bởi một cấu trúc khai báo siêu lớp của nó, các giao diện mà nó tuân thủ, các thông điệp mà nó thực hiện (như một bản đồ của "bộ chọn", tên thông báo, đến "triển khai", hàm cung cấp hành vi) và thể hiện của lớp bố trí biến.
  • mọi thể hiện được định nghĩa bởi một cấu trúc có chứa một con trỏ tới lớp của nó, sau đó là các biến đối tượng của nó.
  • gửi tin nhắn được thực hiện (đưa hoặc lấy một số trường hợp đặc biệt) bằng cách sử dụng một chức năng trông như thế nào objc_msgSend(object, selector, …). Bằng cách biết lớp nào của đối tượng là một thể hiện của nó, nó có thể tìm thấy việc triển khai khớp với bộ chọn và do đó thực hiện đúng chức năng.

Đó là tất cả một phần của thư viện OO đa năng được thiết kế để cho phép nhiều nhà phát triển sử dụng và mở rộng các lớp của nhau, do đó có thể là quá mức cần thiết cho dự án của riêng bạn. Tôi thường thiết kế các dự án C là các dự án định hướng lớp "tĩnh" bằng cách sử dụng các cấu trúc và hàm: - mỗi lớp là định nghĩa của cấu trúc C chỉ định bố cục ivar - mỗi trường hợp chỉ là một thể hiện của cấu trúc tương ứng - các đối tượng không thể "Bối rối", nhưng các hàm giống như phương thức trông giống như MyClass_doSomething(struct MyClass *object, …)được định nghĩa. Điều này làm cho mọi thứ rõ ràng hơn trong mã so với cách tiếp cận ObjC nhưng có độ linh hoạt kém hơn.

Việc đánh đổi sự dối trá phụ thuộc vào dự án của chính bạn: có vẻ như các lập trình viên khác sẽ không sử dụng giao diện C của bạn để lựa chọn tùy thuộc vào sở thích nội bộ. Tất nhiên, nếu bạn quyết định bạn muốn một cái gì đó như thư viện thời gian chạy objc, thì có các thư viện thời gian chạy objc đa nền tảng sẽ phục vụ.


1

GObject không thực sự che giấu bất kỳ sự phức tạp nào và giới thiệu một số sự phức tạp của chính nó. Tôi sẽ nói rằng điều đặc biệt dễ dàng hơn GObject trừ khi bạn cần những thứ GObject tiên tiến như tín hiệu hoặc máy móc giao diện.

Điều này hơi khác với COS vì nó đi kèm với một bộ tiền xử lý mở rộng cú pháp C với một số cấu trúc OO. Có một tiền xử lý tương tự cho GObject, các G Object Builder .

Bạn cũng có thể thử ngôn ngữ lập trình Vala , ngôn ngữ cấp cao đầy đủ biên dịch thành C và theo cách cho phép sử dụng các thư viện Vala từ mã C đơn giản. Nó có thể sử dụng GObject, khung đối tượng của riêng nó hoặc cách quảng cáo (với các tính năng hạn chế).


1

Trước hết, trừ khi đây là bài tập về nhà hoặc không có trình biên dịch C ++ cho thiết bị đích, tôi không nghĩ bạn phải sử dụng C, bạn có thể cung cấp giao diện C với liên kết C từ C ++ một cách dễ dàng.

Thứ hai, tôi sẽ xem bạn sẽ dựa vào mức độ đa hình và ngoại lệ như thế nào và các tính năng khác mà khung có thể cung cấp, nếu không có nhiều cấu trúc đơn giản với các chức năng liên quan sẽ dễ dàng hơn nhiều so với khung có tính năng đầy đủ, nếu một phần đáng kể của bạn thiết kế cần chúng sau đó cắn viên đạn và sử dụng khung để bạn không phải tự thực hiện các tính năng.

Nếu bạn chưa thực sự có một thiết kế nào để đưa ra quyết định từ đó thì hãy tăng đột biến và xem những gì mã nói với bạn.

cuối cùng, nó không phải là một hoặc một sự lựa chọn nào khác khi cần thiết

EDIT: Vì vậy, quyết định của ban quản lý cho phép bỏ qua điểm đầu tiên sau đó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.