Có phải là xấu khi viết đối tượng C? [đóng cửa]


14

Tôi dường như luôn viết mã trong C chủ yếu là hướng đối tượng, vì vậy giả sử tôi có một tệp nguồn hoặc một cái gì đó tôi sẽ tạo một cấu trúc sau đó chuyển con trỏ đến cấu trúc này đến các hàm (phương thức) thuộc sở hữu của cấu trúc này:

struct foo {
    int x;
};

struct foo* createFoo(); // mallocs foo

void destroyFoo(struct foo* foo); // frees foo and its things

Đây có phải là thực hành xấu? Làm thế nào để tôi học viết C "cách thích hợp".


10
Phần lớn Linux (kernel) được viết theo cách này, trên thực tế, nó thậm chí còn mô phỏng các khái niệm giống OO hơn như gửi phương thức ảo. Tôi cho rằng khá đúng.
Kilian Foth

13
" [T] anh ấy đã xác định Lập trình viên thực sự có thể viết các chương trình FORTRAN bằng bất kỳ ngôn ngữ nào. " - Ed Post, 1983
Ross Patterson

4
Có bất kỳ lý do tại sao bạn không muốn chuyển sang C ++? Bạn không phải sử dụng các phần của nó mà bạn không thích.
Svick

5
Điều này thực sự đặt ra câu hỏi, "'hướng đối tượng' là gì?" Tôi sẽ không gọi đối tượng này theo định hướng. Tôi muốn nói đó là thủ tục. (Bạn không có tính kế thừa, không đa hình, không đóng gói / khả năng che giấu trạng thái và có thể thiếu các dấu hiệu khác của OO không xuất hiện trên đầu tôi.) , Tuy nhiên.
jpmc26

3
@ jpmc26: Nếu bạn là người theo chủ nghĩa ngôn ngữ, bạn nên lắng nghe Alan Kay, anh ấy đã phát minh ra thuật ngữ này, anh ấy sẽ nói ý nghĩa của nó và anh ấy nói OOP là tất cả về Nhắn tin . Nếu bạn là người mô tả ngôn ngữ, bạn sẽ khảo sát việc sử dụng thuật ngữ này trong cộng đồng phát triển phần mềm. Cook đã làm chính xác điều đó, anh ta đã phân tích các tính năng của các ngôn ngữ mà họ yêu cầu hoặc được coi là OO và anh ta thấy rằng họ có một điểm chung: Nhắn tin .
Jörg W Mittag

Câu trả lời:


24

Không, đây không phải là thực hành xấu, thậm chí còn được khuyến khích làm như vậy, mặc dù người ta thậm chí có thể sử dụng các quy ước như struct foo *foo_new();void foo_free(struct foo *foo);

Tất nhiên, như một nhận xét nói, chỉ làm điều này khi thích hợp. Không có ý nghĩa trong việc sử dụng một hàm tạo cho mộtint .

Tiền tố foo_là một quy ước được theo sau bởi rất nhiều thư viện, bởi vì nó bảo vệ chống lại sự xung đột với việc đặt tên của các thư viện khác. Các chức năng khác thường có quy ước để sử dụngfoo_<function>(struct foo *foo, <parameters>); . Điều này cho phép bạn struct foolà một loại mờ đục.

Hãy xem tài liệu libcurl cho quy ước, đặc biệt là với "tên con", để gọi hàmcurl_multi_* vẻ sai ngay từ cái nhìn đầu tiên khi tham số đầu tiên được trả về curl_easy_init().

Thậm chí còn có những cách tiếp cận chung chung hơn, xem Lập trình hướng đối tượng với ANSI-C


11
Luôn luôn cảnh báo "khi thích hợp". OOP không phải là viên đạn bạc.
Ded repeatator

Không C có không gian tên trong đó bạn có thể khai báo các hàm này? Tương tự như std::string, bạn không thể có foo::create? Tôi không sử dụng C. Có lẽ đó chỉ là trong C ++?
Chris Cirefice

@ChrisCirefice Không có không gian tên trong C, đó là lý do tại sao nhiều tác giả thư viện sử dụng tiền tố cho các chức năng của họ.
Cư dân

2

Nó không tệ, nó rất tuyệt. Lập trình hướng đối tượng là một điều tốt (trừ khi bạn bị mang đi, bạn có thể có quá nhiều thứ tốt). C không phải là ngôn ngữ phù hợp nhất cho OOP, nhưng điều đó không thể ngăn bạn tận dụng tốt nhất ngôn ngữ đó.


4
Không phải tôi có thể không đồng ý, nhưng ý kiến ​​của bạn thực sự cần được hỗ trợ bởi một số chi tiết.
Ded repeatator

1

Không tệ. Nó xác nhận sử dụng RAII để ngăn chặn nhiều lỗi (rò rỉ bộ nhớ, sử dụng các biến chưa được khởi tạo, sử dụng sau khi miễn phí, vv có thể gây ra sự cố bảo mật).

Vì vậy, nếu bạn muốn biên dịch mã của mình chỉ bằng GCC hoặc Clang (chứ không phải với trình biên dịch MS), bạn có thể sử dụng cleanupthuộc tính, điều đó sẽ phá hủy chính xác các đối tượng của bạn. Nếu bạn khai báo đối tượng của bạn như thế:

my_str __attribute__((cleanup(my_str_destructor))) ptr;

Sau đó my_str_destructor(ptr)sẽ được chạy khi ptr đi ra khỏi phạm vi. Chỉ cần lưu ý rằng nó không thể được sử dụng với các đối số hàm .

Ngoài ra, hãy nhớ sử dụng my_str_trong tên phương thức của bạn, vì Ckhông có không gian tên và rất dễ va chạm với một số tên hàm khác.


2
Afaik, RAII nói về việc sử dụng cách gọi ngầm của hàm hủy đối với các đối tượng trong C ++ để đảm bảo dọn dẹp, tránh việc phải thêm các lệnh gọi giải phóng tài nguyên rõ ràng. Vì vậy, nếu tôi không nhầm nhiều, RAII và C loại trừ lẫn nhau.
cmaster - phục hồi monica

@cmaster Nếu bạn #definesử dụng tên đánh máy của mình __attribute__((cleanup(my_str_destructor)))thì bạn sẽ hiểu nó là ẩn trong toàn bộ #definephạm vi (nó sẽ được thêm vào tất cả các khai báo biến của bạn).
Marqin

Điều đó hoạt động nếu a) bạn sử dụng gcc, b) nếu bạn chỉ sử dụng loại trong các biến cục bộ của hàm và c) nếu bạn chỉ sử dụng loại trong phiên bản trần trụi (không có con trỏ đến loại #define'd hoặc mảng của nó). Tóm lại: Đó không phải là tiêu chuẩn C và bạn phải trả nhiều tiền khi sử dụng.
cmaster - phục hồi monica

Như đã đề cập trong câu trả lời của tôi, điều đó cũng hoạt động trong tiếng kêu.
Marqin

À, tôi không để ý điều đó. Điều đó thực sự làm cho yêu cầu a) ít nghiêm trọng hơn đáng kể, vì điều đó làm cho __attribute__((cleanup()))khá nhiều tiêu chuẩn gần đúng. Tuy nhiên, b) và c) vẫn đứng ...
cmaster - phục hồi monica

-2

Có thể có nhiều lợi thế cho mã như vậy, nhưng thật không may, Tiêu chuẩn C không được viết để tạo điều kiện thuận lợi cho nó. Các trình biên dịch trong lịch sử đã đưa ra các đảm bảo hành vi hiệu quả vượt quá những gì Tiêu chuẩn yêu cầu để có thể viết mã đó sạch hơn nhiều so với Tiêu chuẩn C, nhưng các trình biên dịch gần đây đã bắt đầu thu hồi các bảo đảm đó dưới tên tối ưu hóa.

Đáng chú ý nhất, nhiều trình biên dịch C đã được bảo đảm về mặt lịch sử (theo thiết kế nếu không phải là tài liệu) rằng nếu hai loại cấu trúc có cùng trình tự ban đầu, một con trỏ tới một trong hai loại có thể được sử dụng để truy cập các thành viên của chuỗi chung đó, ngay cả khi các loại không liên quan, và hơn nữa với mục đích thiết lập một chuỗi ban đầu chung, tất cả các con trỏ tới các cấu trúc là tương đương. Mã sử ​​dụng hành vi đó có thể sạch hơn và an toàn hơn nhiều so với mã không, nhưng thật không may, mặc dù Tiêu chuẩn yêu cầu các cấu trúc chia sẻ một chuỗi ban đầu chung phải được trình bày theo cùng một cách, nhưng nó cấm mã thực sự sử dụng một con trỏ của một loại để truy cập vào chuỗi ban đầu của loại khác.

Do đó, nếu bạn muốn viết mã hướng đối tượng bằng C, bạn sẽ phải quyết định (và nên đưa ra quyết định này sớm) để vượt qua rất nhiều vòng để tuân theo quy tắc loại con trỏ của C và được chuẩn bị để có trình biên dịch hiện đại tạo mã vô nghĩa nếu một trình duyệt bị trượt, ngay cả khi trình biên dịch cũ hơn sẽ tạo mã hoạt động như dự định, hoặc tài liệu khác yêu cầu mã sẽ chỉ có thể sử dụng được với trình biên dịch được cấu hình để hỗ trợ hành vi con trỏ kiểu cũ (ví dụ: sử dụng một "-fno -rict-aliasing") Một số người coi "-fno -rict-aliasing" là xấu xa, nhưng tôi sẽ đề nghị rằng sẽ hữu ích hơn khi nghĩ về "-fno -rict-aliasing" C là một ngôn ngữ cung cấp sức mạnh ngữ nghĩa lớn hơn cho một số mục đích so với "tiêu chuẩn" C,nhưng với chi phí tối ưu hóa có thể quan trọng cho một số mục đích khác.

Ví dụ, trên các trình biên dịch truyền thống, các trình biên dịch lịch sử sẽ diễn giải đoạn mã sau:

struct pair { int i1,i2; };
struct trio { int i1,i2,i3; };

void hey(struct pair *p, struct trio *t)
{
  p->i1++;
  t->i1^=1;
  p->i1--;
  t->i1^=1;
}

khi thực hiện các bước sau theo thứ tự: tăng thành viên đầu tiên của *p, bổ sung bit thấp nhất của thành viên đầu tiên *t, sau đó giảm thành viên đầu tiên *pvà bổ sung bit thấp nhất của thành viên đầu tiên *t. Các trình biên dịch hiện đại sẽ sắp xếp lại chuỗi các hoạt động theo kiểu mã nào sẽ hiệu quả hơn nếu ptxác định các đối tượng khác nhau, nhưng sẽ thay đổi hành vi nếu chúng không hoạt động.

Ví dụ này tất nhiên là cố tình chiếm đoạt và trong mã thực tế sử dụng một con trỏ của một loại để truy cập các thành viên là một phần của chuỗi ban đầu chung của loại khác thường sẽ hoạt động, nhưng thật không may vì không biết khi nào mã đó có thể thất bại hoàn toàn không thể sử dụng nó một cách an toàn trừ khi vô hiệu hóa phân tích răng cưa dựa trên loại.

Một ví dụ ít gây tranh cãi sẽ xảy ra nếu người ta muốn viết một hàm để làm một cái gì đó như hoán đổi hai con trỏ thành các kiểu tùy ý. Trong phần lớn các trình biên dịch "thập niên 1990 C", điều đó có thể được thực hiện thông qua:

void swap_pointers(void **p1, void **p2)
{
  void *temp = *p1;
  *p1 = *p2;
  *p2 = temp;
}

Tuy nhiên, trong Tiêu chuẩn C, người ta sẽ phải sử dụng:

#include "string.h"
#include "stdlib.h"
void swap_pointers2(void **p1, void **p2)
{
  void **temp = malloc(sizeof (void*));
  memcpy(temp, p1, sizeof (void*));
  memcpy(p1, p2, sizeof (void*));
  memcpy(p2, temp, sizeof (void*));
  free(temp);
}

Nếu *p2được giữ trong bộ lưu trữ được phân bổ và con trỏ tạm thời không được lưu trữ trong bộ lưu trữ được phân bổ, loại hiệu quả *p2sẽ trở thành loại con trỏ tạm thời và mã cố gắng sử dụng *p2như bất kỳ loại nào không khớp với con trỏ tạm thời loại sẽ gọi Hành vi không xác định. Chắc chắn là rất khó có khả năng trình biên dịch sẽ nhận thấy điều đó, nhưng vì triết lý trình biên dịch hiện đại yêu cầu các lập trình viên tránh Hành vi không xác định bằng mọi giá, tôi không thể nghĩ ra bất kỳ phương tiện an toàn nào khác để viết mã ở trên mà không sử dụng lưu trữ được phân bổ .


Downvoters: Quan tâm để bình luận? Một khía cạnh quan trọng của lập trình hướng đối tượng là khả năng có nhiều loại chia sẻ các khía cạnh chung và có một con trỏ tới bất kỳ loại nào như vậy có thể sử dụng để truy cập các khía cạnh chung đó. Ví dụ của OP không làm điều đó, nhưng nó hầu như không làm trầy xước bề mặt của "hướng đối tượng". Trình biên dịch C lịch sử sẽ cho phép mã đa hình được viết theo kiểu sạch hơn nhiều so với tiêu chuẩn C. Thiết kế mã hướng đối tượng trong C do đó yêu cầu người ta xác định chính xác ngôn ngữ nào đang nhắm mục tiêu. Với khía cạnh nào để mọi người không đồng ý?
supercat

Hừm ... bạn có cho thấy các đảm bảo mà tiêu chuẩn đưa ra không cho phép bạn truy cập sạch các thành viên của chuỗi con ban đầu chung không? Bởi vì tôi nghĩ đó là những gì bạn muốn nói về những tệ nạn dám tối ưu hóa trong giới hạn của hành vi hợp đồng bản lề vào thời điểm này? (Đó là suy đoán của tôi về những gì mà hai kẻ hạ bệ thấy phản cảm.)
Ded repeatator

OOP không nhất thiết yêu cầu sự kế thừa, do đó khả năng tương thích giữa hai cấu trúc không phải là vấn đề lớn trong thực tế. Tôi có thể nhận được OO thực bằng cách đặt các con trỏ hàm vào một cấu trúc và gọi các hàm đó theo một cách cụ thể. Tất nhiên, foo_function(foo*, ...)giả OO trong C này chỉ là một kiểu API cụ thể trông giống như các lớp, nhưng nó nên được gọi đúng hơn là lập trình mô-đun với các kiểu dữ liệu trừu tượng.
amon

@Ded repeatator: Xem ví dụ được chỉ định. Trường "i1" là thành viên của chuỗi ban đầu chung của cả hai cấu trúc, nhưng trình biên dịch hiện đại sẽ thất bại nếu mã cố sử dụng "cặp cấu trúc *" để truy cập thành viên ban đầu của "bộ ba cấu trúc".
supercat

Trình biên dịch C hiện đại nào thất bại trong ví dụ đó? Bất kỳ lựa chọn thú vị cần thiết?
Ded repeatator

-3

Bước tiếp theo là ẩn khai báo struct. Bạn đặt nó trong tệp .h:

typedef struct foo_s foo_t;

foo_t * foo_new(...);
void foo_destroy(foo_t *foo);
some_type foo_whatever(foo_t *foo, ...);
...

Và sau đó trong tệp .c:

struct foo_s {
    ...
};

6
Đó có thể là bước tiếp theo, tùy thuộc vào mục tiêu. Dù có hay không, điều này thậm chí không cố gắng trả lời câu hỏi từ xa.
Ded repeatator
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.