Jak myśleć jako programista C po uprzedzeniu w języku OOP? [Zamknięte]

38

Wcześniej używałem tylko języków programowania obiektowego (C ++, Ruby, Python, PHP), a teraz uczę się C. Trudno mi znaleźć właściwy sposób robienia rzeczy w języku bez pojęcia 'Obiekt'. Zdaję sobie sprawę, że można używać paradygmatów OOP w C, ale chciałbym nauczyć się C-idiomatycznego sposobu.

Podczas rozwiązywania problemu programistycznego, pierwszą rzeczą, którą robię, jest wyobrażenie sobie obiektu, który rozwiąże problem. Jakimi krokami mam to zastąpić, gdy używam paradygmatu programowania imperatywnego innego niż OOP?

Mas Bagol
źródło
15
Muszę jeszcze znaleźć język, który ściśle pasuje do mojego sposobu myślenia, więc muszę „skompilować” swoje myśli dla każdego języka, którego używam. Jedną z koncepcji, którą uważam za przydatną, jest „jednostka kodu”, bez względu na to, czy jest to etykieta, podprogram, funkcja, obiekt, moduł lub struktura: każda z nich powinna być zamknięta w sobie i ujawniać dobrze zdefiniowany interfejs. Jeśli korzystasz z odgórnego podejścia na poziomie obiektu, w C możesz zacząć od przygotowania zestawu funkcji, które zachowują się tak, jakby problem został rozwiązany. Często dobrze zaprojektowane C API wyglądają jak OOP, ale qux = foo.bar(baz)stają się qux = Foo_bar(foo, baz).
amon
Echo amon , koncentrują się na: wykres podobny do struktury danych, wskaźniki, algorytmy, wykonanie (przepływ kontrolna) kodu (funkcje), wskaźniki funkcyjnych.
rwong
1
LibTiff (kod źródłowy na github) jest przykładem organizacji dużych programów w języku C.
rwong
1
Jako programista C # tęsknię za delegatami (wskaźnikami funkcji z jednym powiązanym parametrem) znacznie bardziej niż obiektami.
CodesInChaos
Osobiście uważałem, że większość C jest łatwa i jednoznaczna, z godnym uwagi wyjątkiem preprocesora. Gdybym musiał ponownie nauczyć się języka C, byłby to obszar, na którym skoncentrowałbym swój wysiłek.
biziclop

Odpowiedzi:

53
  • Program AC to zbiór funkcji.
  • Funkcja to zbiór instrukcji.
  • Możesz enkapsulować dane za pomocą struct.

Otóż ​​to.

Jak napisałeś lekcję? Tak właśnie piszesz plik .C. To prawda, że ​​nie dostajesz takich rzeczy jak polimorfizm metod i dziedziczenie, ale i tak możesz symulować te o różnych nazwach funkcji i składzie .

Aby utorować drogę, przestudiuj programowanie funkcjonalne. To naprawdę niesamowite, co możesz zrobić bez zajęć, a niektóre rzeczy faktycznie działają lepiej bez narzutu zajęć.

Dalsza lektura
Orientacja obiektowa w ANSI C.

Robert Harvey
źródło
9
możesz typedefto structzrobić i stworzyć coś klasowego . i typedef-ed typy mogą być zawarte w innych, structktóre same mogą być typedef-ed. to, czego nie dostaniesz w C, to przeciążenie operatora i pozornie proste dziedziczenie klas i członków w C ++. i nie dostajesz dużo dziwnej i nienaturalnej składni, którą dostajesz w C ++. Naprawdę podoba mi się koncepcja OOP, ale myślę, że C ++ jest brzydką realizacją OOP. lubię C, ponieważ jest to mniejszy język i pomija składnię języka, który najlepiej pozostawić funkcjom.
Robert Bristol-Johnnson
22
Jako osoba, której pierwszym językiem był / to C, zaryzykuję stwierdzenie tego . a lot of things actually work better without the overhead of classes
haneefmubarak
1
Aby się rozwinąć, wiele rzeczy zostało opracowanych bez OOP: systemy operacyjne, serwery protokołów, programy ładujące, przeglądarki i tak dalej. Komputery nie myślą w kategoriach przedmiotów i nie muszą tego robić. Rzeczywiście, jest to często dość powolne dla nich zmusić tego.
edmz
Kontrapunkt: a lot of things actually work better with addition of class-based OOP. Źródło: TypeScript, Dart, CoffeeScript i wszystkie inne sposoby, w jakie przemysł próbuje uciec od funkcjonalnego / prototypowego języka OOP.
Den
Aby rozwinąć, wiele rzeczy zostały opracowane z OOP: wszystkiego innego. Ludzie naturalnie myślą w kategoriach przedmiotów, a programy są napisane dla innych ludzi do czytania i rozumienia.
Den
18

Przeczytaj SICP i poznaj schemat oraz praktyczną ideę abstrakcyjnych typów danych . Zatem kodowanie w C jest łatwe (ponieważ z SICP, trochę C, trochę PHP, Ruby itp. ... twoje myślenie byłoby wystarczająco poszerzone i zrozumiałbyś, że programowanie obiektowe może nie być najlepszym stylem w wszystkie przypadki, ale tylko w przypadku niektórych programów). Uważaj na dynamiczną alokację pamięci C , która jest prawdopodobnie najtrudniejszą częścią. C99 lub C11 standardowy język programowania, a jej Biblioteka standardowa języka C jest rzeczywiście dość słaba (nie wiem o TCP lub katalogów!), I będziesz często potrzebować zewnętrznych bibliotek lub interfejsów (npPOSIX , libcurl dla biblioteki klienta HTTP, libonion dla biblioteki serwera HTTP, GMPlib dla bignums, niektóre biblioteki takie jak libunistring dla UTF-8 itp.).

Twoje „obiekty” często znajdują się w C niektórych pokrewnych structi definiujesz zestaw funkcji, które na nich działają. W przypadku krótkich lub bardzo prostych funkcji rozważ ich zdefiniowanie za pomocą odpowiedniego struct, jak static inlinew niektórych plikach nagłówkowych, które foo.hmają być #include-d gdzie indziej.

Zauważ, że programowanie obiektowe nie jest jedynym paradygmatem programowania . W niektórych przypadkach warto zastosować inne paradygmaty ( programowanie funkcjonalne à la Ocaml lub Haskell, a nawet Scheme lub Commmon Lisp, programowanie logiczne à la Prolog itp. Itd. ... Przeczytaj także blog J.Pitrat o deklaratywnej sztucznej inteligencji). Zobacz książkę Scotta: Programming Language Pragmatics

W rzeczywistości programista w C lub Ocaml zwykle nie chce kodować w obiektowym stylu programowania. Nie ma powodu, aby zmuszać się do myślenia o przedmiotach, gdy nie jest to przydatne.

Zdefiniujesz niektóre structi działające na nich funkcje (często przez wskaźniki). Możesz potrzebować niektórych oznaczonych związków (często structze znacznikiem, często niektórych enumi niektórych unionwewnątrz), a może być przydatne posiadanie elastycznego elementu tablicy na końcu niektórych struct-s.

Zajrzyj do kodu źródłowego istniejącego wolnego oprogramowania w C ( aby znaleźć trochę, zobacz github i sourceforge ). Prawdopodobnie przydatna byłaby instalacja i używanie dystrybucji Linuksa: jest ona zbudowana prawie wyłącznie z wolnego oprogramowania, ma świetne kompilatory C wolnego oprogramowania ( GCC , Clang / LLVM ) i narzędzia programistyczne. Zobacz także Zaawansowane programowanie w systemie Linux, jeśli chcesz programować dla systemu Linux.

Nie zapomnij skompilować z wszystkich ostrzeżeń i informacji debugowania, np gcc -Wall -Wextra -g-notably trakcie rozwoju i debugowania phases- i nauczyć się korzystać z pewnych narzędzi, np Valgrind na polowanie wycieki pamięci , do gdbdebuggera itp Zadbaj, aby zrozumieć dobrze, co jest niezdefiniowane zachowanie i mocno go uniknąć (należy pamiętać, że program może mieć jakiś UB i czasami wydaje się „praca”).

Gdy naprawdę potrzebujesz konstrukcji obiektowych (w szczególności dziedziczenia ), możesz użyć wskaźników do powiązanych struktur i funkcji. Możesz mieć własną maszynę vtable , mieć każdy „obiekt” zaczynający się od wskaźnika do structzawierających wskaźniki funkcji. Korzystasz z możliwości rzutowania typu wskaźnika na inny typ wskaźnika (oraz z faktu, że możesz rzutować z struct super_stpola zawierającego te same typy pól, co te rozpoczynające się struct sub_stna emulacji dziedziczenia). Zauważ, że C wystarcza do zaimplementowania dość wyrafinowanych systemów obiektowych - w szczególności przestrzegając pewnych konwencji - jak pokazuje GObject (z GTK / Gnome).

Kiedy rzeczywiście trzeba zamknięć , będziesz często naśladować ich wywołania zwrotne , z konwencją , że każda funkcja przy użyciu wywołania zwrotnego jest przekazywana zarówno wskaźnik funkcji i niektóre dane klienta (zużywanej przez wskaźnik funkcji, gdy nazywa to). Możesz także (konwencjonalnie) mieć swoje własne zamknięcia struct(zawierające pewien wskaźnik funkcji i wartości zamknięte).

Ponieważ C jest językiem bardzo niskiego poziomu, ważne jest, aby zdefiniować i udokumentować własne konwencje (inspirowane praktyką w innych programach C), w szczególności dotyczące zarządzania pamięcią i prawdopodobnie także niektórych konwencji nazewnictwa. Przydaje się pomysł na architekturę zestawu instrukcji . Nie zapominaj, że C kompilator może zrobić wiele optymalizacje na kodzie (jeśli zapytać go do), więc nie obchodzi zbytnio robi mikro optymalizacje ręcznie, zostawiam to do kompilatora ( dla optymalnego zestawienia zwolniony oprogramowanie). Jeśli zależy Ci na testach porównawczych i wydajności, powinieneś włączyć optymalizacje (po debugowaniu programu).gcc -Wall -O2

Nie zapominaj, że czasami metaprogramowanie jest przydatne . Dość często duże oprogramowanie napisane w C zawiera skrypty lub programy ad-hoc do generowania kodu C używanego gdzie indziej (możesz także grać w brudne sztuczki preprocesora C , np. Makra X ). Istnieje kilka przydatnych generatorów programów C (np. Yacc lub gnu bison do generowania parserów, gperf do generowania doskonałych funkcji skrótu itp.). W niektórych systemach (zwłaszcza Linux i POSIX) możesz nawet wygenerować kod C w czasie wykonywania w generated-001.cpliku, skompilować go do współdzielonego obiektu, uruchamiając komendę (np. gcc -O -Wall -shared -fPIC generated-001.c -o generated-001.so) W czasie wykonywania, dynamicznie ładować ten obiekt współdzielony za pomocą dlopeni pobierz wskaźnik funkcji z nazwy za pomocą dlsym . Robię takie sztuczki w MELT (język specyficzny dla domeny Lisp, który może ci się przydać, ponieważ umożliwia dostosowanie kompilatora GCC ).

Pamiętaj o koncepcjach i technikach odśmiecania pamięci ( zliczanie referencji jest często techniką zarządzania pamięcią w C, a IMHO jest słabą formą odśmiecania, która nie radzi sobie dobrze z referencjami cyklicznymi ; możesz mieć słabe wskaźniki, które mogą w tym pomóc ale może to być trudne). Czasami możesz rozważyć użycie konserwatywnego urządzenia do usuwania śmieci Boehm .

Basile Starynkevitch
źródło
7
Szczerze mówiąc, niezależnie od tego pytania, czytanie SICP jest niewątpliwie dobrą radą, ale dla PO prawdopodobnie doprowadzi to do następnego pytania „Jak myśleć jako programista C po uprzedzeniu SICP”.
Doc Brown
1
Nie, ponieważ schemat z SICP i PHP (lub Ruby lub Python) są tak różne, że OP zyskałoby znacznie szersze myślenie; a SICP dość dobrze wyjaśnia, czym jest abstrakcyjny typ danych w praktyce, i jest to bardzo przydatne do zrozumienia, w szczególności do kodowania w C.
Basile Starynkevitch
1
SICP to dziwna sugestia. Schemat jest bardzo różny od C.
Brian Gordon
Ale SICP uczy wielu dobrych nawyków, a znajomość Schematu pomaga przy kodowaniu w C (dla koncepcji zamknięć, abstrakcyjnych typów danych itp.)
Basile Starynkevitch
5

Sposób budowy programu polega w zasadzie na określeniu, jakie akcje (funkcje) należy wykonać, aby rozwiązać problem (dlatego nazywa się je językiem proceduralnym). Każda akcja będzie odpowiadać funkcji. Następnie musisz określić, jakie informacje będą odbierane przez każdą funkcję i jakie informacje muszą zwrócić.

Program jest zwykle rozdzielony na pliki (moduły), każdy plik zwykle ma grupę powiązanych funkcji. Na początku każdego pliku deklarujesz (poza jakąkolwiek funkcją) zmienne, które będą używane przez wszystkie funkcje w tym pliku. Jeśli użyjesz kwalifikatora „statycznego”, te zmienne będą widoczne tylko w tym pliku (ale nie w innych plikach). Jeśli nie użyjesz kwalifikatora „statycznego” dla zmiennych zdefiniowanych poza funkcjami, będą one również dostępne z innych plików, a te inne pliki powinny zadeklarować zmienną jako „zewnętrzną” (ale jej nie zdefiniować), aby kompilator poszukał ich w innych plikach.

Krótko mówiąc, najpierw musisz pomyśleć o procedurach (funkcjach), a następnie upewnić się, że wszystkie funkcje mają dostęp do potrzebnych informacji.

Mandryl
źródło
3

Interfejsy API C często - może nawet zwykle - mają interfejs zorientowany obiektowo, jeśli spojrzy się na nie we właściwy sposób.

W C ++:

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

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

W C:

typedef struct {
    int x;
} foo;

void bar (foo*, int param);

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

Jak być może wiesz, w C ++ i różnych innych oficjalnych językach OO, pod maską metoda obiektowa przyjmuje pierwszy argument, który jest wskaźnikiem do obiektu, podobnie jak bar()powyższa wersja C. Na przykład, gdzie pojawia się to na powierzchni w C ++, zastanów się, jak std::bindmożna dopasować metody obiektowe do funkcji podpisów:

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

Jak zauważyli inni, prawdziwa różnica polega na tym, że formalne języki OO mogą implementować polimorfizm, kontrolę dostępu i różne inne ciekawe funkcje. Ale istota programowania obiektowego, tworzenie i manipulowanie dyskretnymi, złożonymi strukturami danych jest już podstawową praktyką w C.

Złotowłosa
źródło
2

Jednym z głównych powodów zachęcania ludzi do nauki języka C jest to, że jest to jeden z najniższych języków programowania wysokiego poziomu. Języki OOP ułatwiają myślenie o modelach danych oraz szablonowaniu kodu i przekazywaniu wiadomości, ale pod koniec dnia mikroprocesor wykonuje kod krok po kroku, wskakując i wychodząc z bloków kodu (funkcje w C) i przenosząc odniesienia do zmiennych (wskaźników w C) wokół, aby różne części programu mogły współużytkować dane. Pomyśl o C jako języku asemblera w języku angielskim - dostarczającym instrukcje krok po kroku mikroprocesorowi twojego komputera - i nie pomylisz się. Jako bonus, większość interfejsów systemu operacyjnego działa jak wywołania funkcji C, a nie paradygmaty OOP,

Gauraw
źródło
2
IMHO C jest językiem niskiego poziomu, ale znacznie wyższym niż asembler lub kod maszynowy, ponieważ kompilator C może wykonać wiele optymalizacji niskiego poziomu.
Basile Starynkevitch
Kompilatory C również, w imię „optymalizacji”, zmierzają w kierunku abstrakcyjnego modelu maszyny, który może negować prawa czasu i przyczynowości, gdy otrzyma dane wejściowe, które spowodują Nieokreślone zachowanie, nawet jeśli naturalne zachowanie kodu na maszynie, gdzie jest ono uruchamiany spełniłby w przeciwnym razie wymagania. Na przykład funkcja uint16_t blah(uint16_t x) {return x*x;}będzie działać identycznie na komputerach, na których unsigned intjest 16 bitów lub na 33 lub więcej bitów. Niektóre kompilatory do maszyn, w których unsigned intjest od 17 do 32 bitów, mogą jednak odwoływać się do tej metody ...
supercat
... jako udzielenie kompilatorowi uprawnienia do wnioskowania, że ​​nie może wystąpić łańcuch zdarzeń, który spowodowałby, że metoda otrzyma wartość przekraczającą 46340. Mimo że pomnożenie 65533u * 65533u na dowolnej platformie dałoby wartość, która po rzutowaniu uint16_tdałaby 9, Standard nie nakazuje takich zachowań przy pomnożeniu wartości typu uint16_tna platformach 17- i 32-bitowych.
supercat
-1

Jestem także ojczystym OO (ogólnie C ++), który czasami musi przetrwać w świecie C. Dla mnie podstawową największą przeszkodą jest obsługa błędów i zarządzanie zasobami.

W C ++ musimy rzucić błąd, aby przekazać błąd z miejsca, w którym zdarza się to aż do najwyższego poziomu, gdzie możemy sobie z tym poradzić, i mamy destruktory, aby automatycznie zwolnić naszą pamięć i inne zasoby.

Można zauważyć, że wiele interfejsów API C zawiera funkcję init, która zapewnia typedef'd void *, który tak naprawdę jest wskaźnikiem struktury. Następnie przekazujesz to jako pierwszy argument dla każdego wywołania API. Zasadniczo staje się twoim wskaźnikiem „ten” z C ++. Przyzwyczaja się do wszystkich ukrytych wewnętrznych struktur danych (koncepcja bardzo OO). Możesz także użyć go do zarządzania pamięcią, np. Mieć funkcję o nazwie myapiMalloc, która mallocuje twoją pamięć i zapisuje malloc w twojej wersji C tego wskaźnika, abyś mógł się upewnić, że zostanie zwolniony po powrocie API. Jak niedawno odkryłem, możesz go używać do przechowywania kodów błędów i używać setjmp i longjmp, aby nadać ci zachowanie bardzo podobne do rzucania catch. Połączenie obu koncepcji daje wiele funkcji programu C ++.

Teraz powiedziałeś, że nie chcesz nauczyć się zmuszać C do C ++. Tak naprawdę nie opisuję (przynajmniej nie celowo). Jest to po prostu (miejmy nadzieję) dobrze zaprojektowana metoda wykorzystania funkcji C. Okazuje się, że ma pewne smaki OO - być może dlatego opracowano języki OO, były one sposobem na sformalizowanie / egzekwowanie / ułatwienie koncepcji, które niektóre osoby uznały za najlepszą praktykę.

Jeśli uważasz, że jest to dla ciebie odczucie OO, to alternatywą jest, aby prawie każda funkcja zwróciła kod błędu, który musisz religijnie upewnić się po każdym wywołaniu funkcji i propagować stos wywołań. Musisz upewnić się, że wszystkie zasoby są zwalniane nie tylko na końcu każdej funkcji, ale w każdym punkcie powrotu (co potencjalnie może nastąpić po każdym wywołaniu funkcji, które może zwrócić błąd wskazujący, że nie możesz kontynuować). Może stać się bardzo żmudny i prowadzi do myślenia, że ​​prawdopodobnie nie muszę radzić sobie z tym potencjalnym błędem alokacji pamięci (lub odczytem pliku lub połączeniem portu ...), po prostu założę, że to zadziała lub Napiszę teraz „interesujący” kod i wrócę, zajmując się obsługą błędów - co nigdy się nie zdarza.

Phil Rosenberg
źródło