Làm thế nào để suy nghĩ như một lập trình viên C sau khi thiên vị với ngôn ngữ OOP? [đóng cửa]


38

Trước đây, tôi chỉ sử dụng các ngôn ngữ lập trình hướng đối tượng (C ++, Ruby, Python, PHP) và hiện đang học C. Tôi cảm thấy khó khăn khi tìm ra cách thức phù hợp để thực hiện mọi thứ bằng ngôn ngữ không có khái niệm về một ngôn ngữ 'Vật'. Tôi nhận ra rằng có thể sử dụng mô hình OOP trong C, nhưng tôi muốn tìm hiểu cách thức thành ngữ C.

Khi giải quyết một vấn đề lập trình, điều đầu tiên tôi làm là tưởng tượng một đối tượng sẽ giải quyết vấn đề. Những bước nào để tôi thay thế điều này bằng, khi sử dụng mô hình Lập trình mệnh lệnh không OOP?


15
Tôi vẫn chưa tìm thấy một ngôn ngữ phù hợp với cách suy nghĩ của mình, vì vậy tôi phải biên dịch lại những suy nghĩ của tôi cho bất kỳ ngôn ngữ nào tôi đang sử dụng. Một khái niệm tôi thấy hữu ích là một đơn vị mã của Cameron, cho dù đây là nhãn, chương trình con, hàm, đối tượng, mô-đun hoặc khung: mỗi một trong số chúng phải được đóng gói và hiển thị giao diện được xác định rõ. Nếu bạn được sử dụng một cách tiếp cận cấp đối tượng từ trên xuống, trong C, bạn có thể bắt đầu bằng cách tạo ra một tập hợp các hàm hoạt động như thể vấn đề đã được giải quyết. Thông thường, các API C được thiết kế tốt trông giống như OOP, nhưng qux = foo.bar(baz)trở thành qux = Foo_bar(foo, baz).
amon

Để lặp lại amon , hãy tập trung vào các mục sau: cấu trúc dữ liệu giống như đồ thị, con trỏ, thuật toán, thực thi (luồng điều khiển) của mã (hàm), con trỏ hàm.
rwong

1
LibTiff (mã nguồn trên github) là một ví dụ về cách tổ chức các chương trình C lớn.
rwong

1
Là một lập trình viên C #, tôi sẽ bỏ lỡ các đại biểu (con trỏ hàm với một tham số ràng buộc) nhiều hơn nhiều so với việc tôi bỏ lỡ các đối tượng.
CodeInChaos

Cá nhân tôi thấy hầu hết C dễ dàng và đơn giản, ngoại trừ đáng chú ý là bộ xử lý trước. Nếu tôi phải học lại C, đó sẽ là một lĩnh vực tôi tập trung rất nhiều công sức.
biziclop

Câu trả lời:


53
  • Chương trình AC là một tập hợp các chức năng.
  • Một chức năng là một tập hợp các câu lệnh.
  • Bạn có thể đóng gói dữ liệu với a struct.

Đó là nó.

Làm thế nào bạn viết một lớp học? Đó là khá nhiều cách bạn viết một tập tin .C. Cấp, bạn không nhận được những thứ như đa hình phương thức và kế thừa, nhưng bạn có thể mô phỏng những thứ có tên hàm và thành phần khác nhau .

Để mở đường, nghiên cứu Lập trình chức năng. Nó thực sự khá tuyệt vời với những gì bạn có thể làm mà không cần đến các lớp học, và một số thứ thực sự hoạt động tốt hơn mà không cần đến các lớp học.

Đọc thêm
Định hướng đối tượng trong ANSI C


9
bạn cũng có thể typedefthấy structvà làm một cái gì đó tầm cỡ như . và typedefloại -ed có thể được bao gồm trong các loại khác structmà bản thân chúng có thể được typedef-ed. những gì bạn không nhận được với C là quá tải toán tử và sự kế thừa đơn giản bề ngoài của các lớp và các thành viên trong đó bạn có trong C ++. và bạn không nhận được nhiều cú pháp kỳ lạ và không tự nhiên mà bạn nhận được với C ++. Tôi thực sự thích khái niệm OOP, nhưng tôi nghĩ C ++ là một nhận thức xấu về OOP. Tôi thích C vì nó một ngôn ngữ nhỏ hơn và bỏ qua cú pháp từ ngôn ngữ tốt nhất còn lại cho các chức năng.
robert bristow-johnson

22
Là một người có ngôn ngữ đầu tiên là / là C, tôi muốn nói điều đó . a lot of things actually work better without the overhead of classes
haneefmubarak

1
Để mở rộng, rất nhiều thứ đã được phát triển mà không có OOP: hệ điều hành, máy chủ giao thức, bộ tải khởi động, trình duyệt, v.v. Máy tính không nghĩ về các đối tượng và chúng cũng không cần. Thật vậy, nó thường khá chậm đối với họ để ép buộc điều đó.
edmz

Phản biện : a lot of things actually work better with addition of class-based OOP. Nguồn: TypeScript, Dart, CoffeeScript và tất cả các cách khác mà ngành công nghiệp đang cố gắng thoát khỏi ngôn ngữ OOP chức năng / nguyên mẫu.
Den

Để mở rộng, rất nhiều thứ đã được phát triển với OOP: mọi thứ khác. Con người tự nhiên nghĩ về các đối tượng và chương trình được viết cho người khác đọc và hiểu.
Den

18

Đọc SICP và tìm hiểu Đề án, và ý tưởng thực tế về các loại dữ liệu trừu tượng . Sau đó, mã hóa bằng C rất dễ dàng (vì với SICP, một chút C và một chút PHP, Ruby, v.v ... suy nghĩ của bạn sẽ được mở rộng đủ và bạn sẽ hiểu rằng lập trình hướng đối tượng có thể không phải là phong cách tốt nhất trong tất cả các trường hợp, nhưng chỉ cho một số loại chương trình). Hãy cẩn thận về phân bổ bộ nhớ động C , có lẽ là phần khó nhất. Các C99 hoặc C11 tiêu chuẩn ngôn ngữ lập trình và nó thư viện chuẩn C thực sự là khá nghèo (nó không biết về TCP hoặc thư mục!), Và bạn thường sẽ cần một số thư viện bên ngoài hoặc các giao diện (ví dụPOSIX , libcurl cho thư viện máy khách HTTP, libonion cho thư viện máy chủ HTTP, GMPlib cho các bignums, một số thư viện như libunistring cho UTF-8, v.v ...).

Các "đối tượng" của bạn thường ở C một số struct-s liên quan và bạn xác định tập hợp các hàm hoạt động trên chúng. Đối với các hàm ngắn hoặc rất đơn giản, hãy xem xét việc xác định chúng, với các hàm có liên quan struct, như static inlinetrong một số tệp tiêu đề foo.hsẽ là #include-d ở nơi khác.

Lưu ý rằng lập trình hướng đối tượng không phải là mô hình lập trình duy nhất . Trong một số trường hợp, các mô hình khác đáng giá ( lập trình chức năng à Ocaml hoặc Haskell hoặc thậm chí Scheme hoặc Commmon Lisp, lập trình logic à la Prolog, v.v ... Đọc thêm blog của J.Pitrat về trí tuệ nhân tạo khai báo). Xem cuốn sách của Scott: Ngôn ngữ lập trình thực dụng

Trên thực tế, một lập trình viên ở C, hoặc trong Ocaml, thường không muốn viết mã theo kiểu lập trình hướng đối tượng. Không có lý do gì để ép bản thân nghĩ về đồ vật khi điều đó không hữu ích.

Bạn sẽ xác định một số structvà các chức năng hoạt động trên chúng (thường thông qua con trỏ). Bạn có thể cần một số công đoàn được gắn thẻ (thường là, structvới một thành viên thẻ, thường là một số enumvà một số unionbên trong) và bạn có thể thấy hữu ích khi có một thành viên mảng linh hoạt ở cuối một số struct-s của bạn .

Nhìn vào bên trong mã nguồn của một số phần mềm miễn phí hiện có trong C (xem github & sourceforge để tìm một số). Có lẽ, cài đặt và sử dụng bản phân phối Linux sẽ hữu ích: nó được tạo ra hầu như chỉ bằng phần mềm miễn phí, nó có trình biên dịch phần mềm C miễn phí tuyệt vời ( GCC , Clang / LLVM ) và các công cụ phát triển. Xem thêm Lập trình Linux nâng cao nếu bạn muốn phát triển cho Linux.

Đừng quên để biên dịch với tất cả các cảnh báo và thông tin gỡ lỗi, ví dụ như gcc -Wall -Wextra -g-notably trong sự phát triển & gỡ lỗi phases- và học cách sử dụng một số công cụ, ví dụ như valgrind để săn rò rỉ bộ nhớ , các gdbchương trình gỡ rối, vv Hãy chăm sóc để hiểu rõ những gì đang undefined hành vi và mạnh mẽ tránh nó (hãy nhớ rằng một chương trình có thể có một số UB và đôi khi dường như "hoạt động").

Khi bạn thực sự cần các cấu trúc hướng đối tượng (đặc biệt là kế thừa ), bạn có thể sử dụng các con trỏ tới các cấu trúc liên quan và các hàm. Bạn có thể có máy móc vtable của riêng mình , có mỗi "đối tượng" bắt đầu bằng một con trỏ tới một structcon trỏ hàm chứa. Bạn tận dụng khả năng truyền một loại con trỏ sang một loại con trỏ khác (và thực tế là bạn có thể truyền từ một loại struct super_stcó chứa các loại trường giống như các loại bắt đầu struct sub_stđể mô phỏng thừa kế). Lưu ý rằng C là đủ để thực hiện các hệ thống đối tượng khá tinh vi - cụ thể bằng cách tuân theo một số quy ước -, như GObject (từ GTK / Gnome) chứng minh.

Khi bạn thực sự cần đóng , bạn sẽ thường mô phỏng chúng bằng các cuộc gọi lại , với quy ước rằng mọi chức năng sử dụng một cuộc gọi lại đều được truyền cả con trỏ hàm và một số dữ liệu máy khách (được con trỏ hàm sử dụng khi nó gọi). Bạn cũng có thể có (thông thường) các đóng cửa giống như của bạn struct(chứa một số con trỏ hàm và các giá trị đóng).

Vì C là ngôn ngữ cấp độ rất thấp, điều quan trọng là phải xác định và ghi lại các quy ước của riêng bạn (lấy cảm hứng từ thực tiễn trong các chương trình C khác), đặc biệt là về quản lý bộ nhớ và có thể cả một số quy ước đặt tên. Nó rất hữu ích để có một số ý tưởng về kiến trúc tập lệnh . Đừng quên rằng một C trình biên dịch có thể làm được nhiều việc tối ưu trên mã của bạn (nếu bạn hỏi nó), do đó, không quan tâm quá nhiều về việc làm vi tối ưu bằng tay, nghỉ đó để trình biên dịch của bạn ( gcc -Wall -O2cho biên soạn được tối ưu hóa của phát hành phần mềm). Nếu bạn quan tâm đến điểm chuẩn và hiệu suất thô, bạn nên kích hoạt tối ưu hóa (một khi chương trình của bạn đã được gỡ lỗi).

Đừng quên rằng đôi khi siêu lập trình là hữu ích . Rất thường xuyên, phần mềm lớn được viết bằng C chứa một số tập lệnh hoặc chương trình đặc biệt để tạo một số mã C được sử dụng ở nơi khác (và bạn cũng có thể chơi một số thủ thuật tiền xử lý C bẩn , ví dụ như X-macro ). Tồn tại một số trình tạo chương trình C hữu ích (ví dụ: yacc hoặc gnu bison để tạo trình phân tích cú pháp, gperf để tạo các hàm băm hoàn hảo, v.v ...). Trên một số hệ thống (đặc biệt là Linux & POSIX), bạn thậm chí có thể tạo một số mã C khi chạy trong generated-001.ctệp, biên dịch nó thành một đối tượng chia sẻ bằng cách chạy một số lệnh (như gcc -O -Wall -shared -fPIC generated-001.c -o generated-001.so) trong thời gian chạy, tải động đối tượng được chia sẻ đó bằng dlopen& nhận được một con trỏ hàm từ một tên bằng cách sử dụng dlsym . Tôi đang thực hiện các thủ thuật như vậy trong MELT (ngôn ngữ dành riêng cho miền giống Lisp có thể hữu ích cho bạn, vì nó cho phép tùy chỉnh trình biên dịch GCC ).

Lưu ý về các khái niệm và kỹ thuật thu gom rác ( đếm tham chiếu thường là một kỹ thuật để quản lý bộ nhớ trong C và IMHO là một dạng thu gom rác kém, không xử lý tốt các tham chiếu vòng tròn ; bạn có thể có các con trỏ yếu để giúp về điều đó, nhưng nó có thể là khó khăn). Đôi khi, bạn có thể cân nhắc sử dụng công cụ thu gom rác bảo thủ của Boehm .


7
Thành thật mà nói, độc lập với câu hỏi này, đọc SICP chắc chắn là một lời khuyên tốt, nhưng đối với OP, điều này có thể sẽ dẫn đến câu hỏi tiếp theo "Làm thế nào để suy nghĩ như một lập trình viên C sau khi thiên vị với SICP".
Doc Brown

1
Không, bởi vì Lược đồ từ SICP & PHP (hoặc Ruby hoặc Python) khác nhau đến mức OP sẽ có suy nghĩ rộng hơn nhiều; và SICP giải thích khá rõ loại dữ liệu trừu tượng trong thực tế là gì và điều đó rất hữu ích để hiểu, đặc biệt là mã hóa trong C.
Basile Starynkevitch

1
SICP là một gợi ý lạ. Lược đồ rất khác với C.
Brian Gordon

Nhưng SICP đang dạy rất nhiều thói quen tốt và biết Scheme sẽ giúp ích khi mã hóa bằng C (đối với các khái niệm về đóng cửa, các loại dữ liệu trừu tượng, v.v ...)
Basile Starynkevitch

5

Cách thức chương trình được xây dựng về cơ bản là xác định hành động (chức năng) nào phải được thực hiện để giải quyết vấn đề (đó là lý do tại sao nó được gọi là ngôn ngữ thủ tục). Mỗi hành động sẽ tương ứng với một chức năng. Sau đó, bạn cần xác định loại thông tin mà mỗi chức năng sẽ nhận được và thông tin nào họ cần trả về.

Chương trình thường được phân tách trong các tệp (mô-đun), mỗi tệp thường sẽ có một nhóm các chức năng có liên quan. Trong phần đầu của mỗi tệp, bạn khai báo (bên ngoài bất kỳ hàm) các biến sẽ được sử dụng bởi tất cả các hàm trong tệp đó. Nếu bạn sử dụng vòng loại "tĩnh", các biến đó sẽ chỉ hiển thị bên trong tệp đó (chứ không phải từ các tệp khác). Nếu bạn không sử dụng vòng loại "tĩnh" trên các biến được xác định bên ngoài hàm, chúng cũng có thể truy cập được từ các tệp khác và các tệp khác sẽ khai báo biến là "extern" (nhưng không xác định nó) để trình biên dịch sẽ tìm chúng trong các tập tin khác.

Vì vậy, trong ngắn hạn, trước tiên bạn nghĩ về các thủ tục (chức năng) sau đó bạn đảm bảo tất cả các chức năng có quyền truy cập vào thông tin họ cần.


3

API C thường - có lẽ, thậm chí, thường - có giao diện hướng đối tượng cơ bản nếu bạn nhìn chúng đúng cách.

Trong C ++:

class foo {
    public:
        foo (int x);
        void bar (int param);
    private:
        int x;
};

// Example use:
foo f(42);
f.bar(23);

Trong C:

typedef struct {
    int x;
} foo;

void bar (foo*, int param);

// Example use:
foo f = { .x = 42 };
bar(&f, 23);

Như bạn có thể biết, trong C ++ và các ngôn ngữ OO chính thức khác, dưới mui xe, một phương thức đối tượng lấy một đối số đầu tiên là một con trỏ tới đối tượng, giống như phiên bản C bar()ở trên. Để biết ví dụ về nơi xuất hiện trên bề mặt trong C ++, hãy xem xét cách std::bindsử dụng để khớp các phương thức đối tượng với chữ ký hàm:

new function<void(int)> (
    bind(&foo::bar, this, placeholders::_1)
//                  ^^^^ object pointer as first arg
);

Như những người khác đã chỉ ra, sự khác biệt thực sự là các ngôn ngữ OO chính thức có thể thực hiện đa hình, kiểm soát truy cập và các tính năng tiện lợi khác. Nhưng bản chất của lập trình hướng đối tượng, tạo và thao tác các cấu trúc dữ liệu phức tạp, rời rạc, đã là một thực tiễn cơ bản trong C.


2

Một trong những lý do lớn khiến mọi người được khuyến khích học C là đó là một trong những ngôn ngữ lập trình cấp thấp nhất. Các ngôn ngữ OOP giúp dễ dàng suy nghĩ về các mô hình dữ liệu và mã khuôn mẫu và thông báo truyền đi, nhưng vào cuối ngày, một bộ vi xử lý thực hiện mã từng bước, nhảy vào và ra khỏi các khối mã (chức năng trong C) và di chuyển tham chiếu đến các biến (con trỏ trong C) xung quanh để các phần khác nhau của chương trình có thể chia sẻ dữ liệu. Hãy nghĩ về C như ngôn ngữ lắp ráp bằng tiếng Anh - cung cấp hướng dẫn từng bước cho bộ vi xử lý của máy tính của bạn - và bạn sẽ không đi quá xa. Như một phần thưởng, hầu hết các giao diện hệ điều hành hoạt động như các lệnh gọi hàm C thay vì các mô hình OOP,


2
IMHO C là ngôn ngữ cấp thấp, nhưng cao hơn nhiều so với trình biên dịch mã hoặc mã máy, vì trình biên dịch C có thể thực hiện rất nhiều tối ưu hóa cấp thấp.
Basile Starynkevitch

Trình biên dịch C, nhân danh "tối ưu hóa", chuyển sang mô hình máy trừu tượng có thể phủ định quy luật thời gian và quan hệ nhân quả khi đưa ra đầu vào sẽ gây ra Hành vi không xác định, ngay cả khi hành vi tự nhiên của mã trên máy ở đó được chạy sẽ đáp ứng yêu cầu khác. Ví dụ, chức năng uint16_t blah(uint16_t x) {return x*x;}sẽ hoạt động giống hệt nhau trên các máy có unsigned int16 bit hoặc lớn hơn 33 bit. unsigned intTuy nhiên, một số trình biên dịch cho các máy có 17 đến 32 bit, tuy nhiên, có thể liên quan đến một cuộc gọi đến phương thức đó ...
supercat

... khi cấp quyền cho trình biên dịch suy ra rằng không có chuỗi sự kiện nào có thể khiến phương thức được đưa ra một giá trị vượt quá 46340 có thể xảy ra. Mặc dù việc nhân 65533u * 65533u trên bất kỳ nền tảng nào sẽ mang lại giá trị, khi được truyền tới uint16_t, sẽ mang lại 9, Tiêu chuẩn không bắt buộc các hành vi đó khi nhân các giá trị loại uint16_ttrên nền tảng 17 đến 32 bit.
supercat

-1

Tôi cũng là người bản địa OO (nói chung là C ++) đôi khi phải sống sót trong một thế giới C. Đối với tôi, trở ngại lớn nhất về cơ bản là xử lý lỗi và quản lý tài nguyên.

Trong C ++, chúng tôi đã đưa ra một lỗi từ nơi nó xảy ra hoàn toàn trở lại mức cao nhất nơi chúng tôi có thể xử lý và chúng tôi có các hàm hủy để tự động giải phóng bộ nhớ và các tài nguyên khác.

Bạn có thể nhận thấy rằng nhiều API C bao gồm một hàm init cung cấp cho bạn một typedef'd void * thực sự là một con trỏ tới một cấu trúc. Sau đó, bạn chuyển điều này thành đối số đầu tiên cho mọi lệnh gọi API. Về cơ bản, nó trở thành con trỏ "này" của bạn từ C ++. Nó được sử dụng cho tất cả các cấu trúc dữ liệu nội bộ được ẩn đi (một khái niệm rất OO). Bạn cũng có thể sử dụng nó để quản lý bộ nhớ, ví dụ: có một hàm gọi là myapiMalloc giúp mallocs bộ nhớ của bạn và ghi lại malloc trong phiên bản C của con trỏ này để bạn có thể chắc chắn rằng nó được giải phóng khi API của bạn trở lại. Ngoài ra, gần đây tôi phát hiện ra rằng bạn có thể sử dụng nó để lưu trữ mã lỗi và sử dụng setjmp và longjmp để cung cấp cho bạn hành vi rất giống với ném bắt. Kết hợp cả hai khái niệm cung cấp cho bạn rất nhiều chức năng của chương trình C ++.

Bây giờ bạn đã nói rằng bạn không muốn học cách buộc C vào C ++. Đó thực sự không phải là những gì tôi mô tả (ít nhất là không cố ý). Đây chỉ đơn giản là một phương pháp (hy vọng) được thiết kế tốt để khai thác chức năng C. Hóa ra có một số hương vị OO - có lẽ đó là lý do tại sao các ngôn ngữ OO phát triển, chúng là một cách để chính thức hóa / thực thi / tạo điều kiện cho các khái niệm mà một số người thấy là thực tiễn tốt nhất.

Nếu bạn nghĩ rằng điều này là cảm giác OO cho bạn thì giải pháp thay thế là có khá nhiều chức năng trả về mã lỗi mà bạn phải đảm bảo rằng bạn kiểm tra sau mỗi lần gọi hàm và truyền lên ngăn xếp cuộc gọi. Bạn phải đảm bảo rằng tất cả các tài nguyên được giải phóng không chỉ ở cuối mỗi chức năng, mà tại mọi điểm trả về (có thể xảy ra sau bất kỳ lệnh gọi chức năng nào có thể trả về lỗi cho biết bạn không thể tiếp tục). Nó có thể trở nên rất tẻ nhạt và có xu hướng dẫn đến bạn nghĩ rằng tôi có lẽ không cần phải đối phó với lỗi phân bổ bộ nhớ tiềm năng đó (hoặc đọc tệp hoặc kết nối cổng ...), tôi sẽ cho rằng nó sẽ hoạt động hoặc tôi sẽ viết mã "thú vị" ngay bây giờ và quay lại và xử lý lỗi xử lý - điều không bao giờ xảy ra.

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.