Wydaje mi się, że zawsze piszę kod w C, który jest głównie zorientowany obiektowo, więc powiedzmy, że mam plik źródłowy lub coś, co utworzę strukturę, a następnie przekażę wskaźnik do tej struktury do funkcji (metod) należących do tej struktury:
struct foo {
int x;
};
struct foo* createFoo(); // mallocs foo
void destroyFoo(struct foo* foo); // frees foo and its things
Czy to zła praktyka? Jak nauczyć się pisać „we właściwy sposób”.
Odpowiedzi:
Nie, to nie jest zła praktyka, nawet się do tego zachęca, chociaż można nawet stosować konwencje takie jak
struct foo *foo_new();
ivoid foo_free(struct foo *foo);
Oczywiście, jak mówi komentarz, rób to tylko w razie potrzeby. Nie ma sensu używać konstruktora dla
int
.Przedrostek
foo_
jest konwencją, po której następuje wiele bibliotek, ponieważ chroni przed kolizją z nazywaniem innych bibliotek. Inne funkcje często używają konwencjifoo_<function>(struct foo *foo, <parameters>);
. To pozwalastruct foo
być nieprzezroczystym typem.Zajrzyj do dokumentacji libcurl dla konwencji, szczególnie z „przestrzeniami nazw”, aby wywoływanie funkcji
curl_multi_*
wyglądało niepoprawnie na pierwszy rzut oka, gdy pierwszy parametr został zwróconycurl_easy_init()
.Są jeszcze bardziej ogólne podejścia, patrz Programowanie obiektowe z ANSI-C
źródło
std::string
nie mogłeśfoo::create
? Nie używam C. Może to tylko w C ++?Nie jest źle, jest doskonale. Object Oriented Programming jest dobre (chyba, że się ponieść, to może mieć za dużo tego dobrego). C nie jest najodpowiedniejszym językiem dla OOP, ale nie powinno to powstrzymywać Cię przed wykorzystaniem tego, co najlepsze.
źródło
Nie jest źle. Zaleca stosowanie RAII, które zapobiega wielu błędom (wycieki pamięci, użycie niezainicjowanych zmiennych, użycie po zwolnieniu itp., Co może powodować problemy z bezpieczeństwem).
Tak więc, jeśli chcesz skompilować kod tylko za pomocą GCC lub Clang (a nie kompilatora MS), możesz użyć
cleanup
atrybutu, który odpowiednio zniszczy twoje obiekty. Jeśli zadeklarujesz swój obiekt w ten sposób:Następnie
my_str_destructor(ptr)
zostanie uruchomiony, gdy ptr wykracza poza zakres. Pamiętaj tylko, że nie można go używać z argumentami funkcji .Pamiętaj również, aby używać
my_str_
w nazwach metod, ponieważC
nie ma przestrzeni nazw i łatwo jest zderzyć się z inną nazwą funkcji.źródło
#define
swoich nazw typów,__attribute__((cleanup(my_str_destructor)))
otrzymasz je domyślnie w całym#define
zakresie (zostanie dodane do wszystkich deklaracji zmiennych).#define
typu d lub tablic). W skrócie: To nie jest standardowy C i płacisz z dużą elastycznością w użyciu.__attribute__((cleanup()))
prawie quasi-standard. Jednak b) ic) nadal stoją ...Kod ten może mieć wiele zalet, ale niestety nie napisano standardu C, aby to ułatwić. Kompilatory w przeszłości oferowały skuteczne gwarancje behawioralne, wykraczające poza to, co wymagał standard, co pozwoliło na napisanie takiego kodu znacznie czystiej niż jest to możliwe w standardzie C, ale ostatnio kompilatory zaczęły odwoływać takie gwarancje w imię optymalizacji.
Co najważniejsze, wiele kompilatorów C ma historycznie gwarancję (na podstawie projektu, jeśli nie dokumentację), że jeśli dwa typy struktur zawierają tę samą sekwencję początkową, można użyć wskaźnika do dowolnego typu, aby uzyskać dostęp do elementów tej wspólnej sekwencji, nawet jeśli typy nie są ze sobą powiązane, a ponadto, że do celów ustanowienia wspólnej sekwencji początkowej wszystkie wskaźniki do struktur są równoważne. Kod, który wykorzystuje takie zachowanie, może być znacznie bardziej przejrzysty i bezpieczniejszy dla typu niż kod, który tego nie robi, ale niestety, mimo że Standard wymaga, aby struktury dzielące wspólną sekwencję początkową musiały być ułożone w ten sam sposób, zabrania kodowi faktycznego używania wskaźnik jednego typu, aby uzyskać dostęp do początkowej sekwencji innego.
W związku z tym, jeśli chcesz napisać obiektowy kod w C, musisz zdecydować (i powinieneś podjąć tę decyzję wcześniej), aby albo przeskoczyć przez wiele obręczy, aby przestrzegać reguł typu C wskaźnika i być przygotowanym na nowoczesne kompilatory generują nonsensowny kod, jeśli ktoś się poślizgnie, nawet jeśli starsze kompilatory wygenerowałyby kod, który działa zgodnie z przeznaczeniem, lub dokumentują wymaganie, aby kod był użyteczny tylko z kompilatorami skonfigurowanymi do obsługi zachowania wskaźnika w starym stylu (np. „-fno-ścisłe-aliasing”) Niektórzy ludzie uważają „-fno-ścisłe-aliasing” za złe, ale sugerowałbym, że bardziej pomocne jest myślenie o „-fno-ścisłym-aliasingu” jako języku, który oferuje większą moc semantyczną do niektórych celów niż „standardowe” C,ale kosztem optymalizacji, które mogą być ważne dla innych celów.
Na przykład w tradycyjnych kompilatorach historyczne kompilatory interpretują następujący kod:
wykonując następujące kroki w kolejności: zwiększ pierwszy element
*p
, uzupełnij najniższy bit pierwszego elementu*t
, a następnie zmniejsz pierwszy element*p
i uzupełnij najniższy bit pierwszego elementu*t
. Nowoczesne kompilatory przestawią sekwencję operacji w taki sposób, który kod będzie bardziej wydajny, jeślip
it
zidentyfikuje różne obiekty, ale zmieni zachowanie, jeśli tego nie zrobią.Ten przykład jest oczywiście celowo opracowany i w praktyce kod, który używa wskaźnika jednego typu, aby uzyskać dostęp do elementów wchodzących w skład wspólnej sekwencji początkowej innego typu, zwykle działa, ale niestety, ponieważ nie ma możliwości dowiedzenia się, kiedy taki kod może zawieść w ogóle nie można bezpiecznie z niego korzystać, z wyjątkiem wyłączenia analizy aliasingu opartej na typach.
Nieco wymyślony przykład miałby miejsce, gdyby ktoś chciał napisać funkcję umożliwiającą zamianę dwóch wskaźników na dowolne typy. W zdecydowanej większości kompilatorów C z lat 90. można to osiągnąć poprzez:
Jednak w standardzie C należałoby użyć:
Jeśli
*p2
jest przechowywany w przydzielonej pamięci, a wskaźnik tymczasowy nie jest przechowywany w przydzielonej pamięci, efektywny typ*p2
stanie się typem wskaźnika tymczasowego i kodu, który próbuje użyć*p2
jako dowolnego typu, który nie pasuje do wskaźnika tymczasowego type wywoła niezdefiniowane zachowanie. Na pewno jest bardzo mało prawdopodobne, aby kompilator zauważył coś takiego, ale ponieważ współczesna filozofia kompilatora wymaga, aby programiści unikali Nieokreślonego Zachowania za wszelką cenę, nie mogę wymyślić żadnego innego bezpiecznego sposobu pisania powyższego kodu bez użycia przydzielonej pamięci .źródło
foo_function(foo*, ...)
pseudo-OO w C jest tylko szczególnym stylem API, który akurat wygląda jak klasy, ale bardziej poprawnie należy go nazwać programowaniem modułowym z abstrakcyjnymi typami danych.Następnym krokiem jest ukrycie deklaracji struktury. Umieszczasz to w pliku .h:
A następnie w pliku .c:
źródło