Dlaczego musimy dołączyć .h, podczas gdy wszystko działa, gdy dołączany jest tylko plik .cpp?

18

Dlaczego musimy dołączyć zarówno pliki, jak .hi .cpppliki, podczas gdy możemy sprawić, że będzie działać wyłącznie poprzez dołączenie .cpppliku?

Na przykład: tworzenie file.hdeklaracji zawierających, następnie tworzenie file.cppdefinicji zawierających i uwzględnianie obu w main.cpp.

Alternatywnie: tworzenie file.cppdeklaracji / definicji zawierających (bez prototypów), w tym main.cpp.

Oba działają dla mnie. Nie widzę różnicy. Może może pomóc wgląd w proces kompilacji i łączenia.

potwierdzać
źródło
Udostępnianie badań pomaga wszystkim . Powiedz nam, co próbowałeś i dlaczego nie spełnia twoich potrzeb. To pokazuje, że poświęciłeś trochę czasu, aby spróbować sobie pomóc, oszczędza nam to powtarzania oczywistych odpowiedzi, a przede wszystkim pomaga uzyskać bardziej konkretną i odpowiednią odpowiedź. Zobacz także How to Ask
gnat
Zdecydowanie polecam spojrzeć na powiązane pytania tutaj z boku. programmers.stackexchange.com/questions/56215/… i programmers.stackexchange.com/questions/115368/... są dobre
Dlaczego dotyczy to programistów, a nie SO?
Wyścigi lekkości z Moniką
@LightnessRacesInOrbit, ponieważ jest to kwestia stylu kodowania, a nie pytanie „jak”.
CashCow
@CashCow: Wygląda na to, że źle zrozumiałeś SO, co nie jest witryną z instrukcjami. Brzmi również, jakbyś źle zrozumiał to pytanie, które nie jest kwestią stylu kodowania.
Lekkość ściga się z Moniką

Odpowiedzi:

29

Podczas może zawierać .cpppliki, jak wspomniano, jest to zły pomysł.

Jak wspomniałeś, deklaracje należą do plików nagłówkowych. Nie powodują one problemów, gdy są zawarte w wielu jednostkach kompilacji, ponieważ nie zawierają implementacji. Wielokrotne dołączenie definicji funkcji lub elementu klasy zwykle spowoduje problem (ale nie zawsze), ponieważ linker może się pomylić i zgłosić błąd.

Co powinno się zdarzyć, gdy każdy .cppplik zawiera definicje podzbioru programu, takie jak klasa, logicznie zorganizowana grupa funkcji, globalne zmienne statyczne (należy używać oszczędnie, jeśli w ogóle) itp.

Każda jednostka kompilacji ( .cppplik) zawiera następnie wszelkie deklaracje potrzebne do skompilowania zawartych w niej definicji. Śledzi funkcje i klasy, do których się odwołuje, ale nie zawiera, więc linker może je rozwiązać później, gdy połączy kod obiektowy w plik wykonywalny lub bibliotekę.

Przykład

  • Foo.h -> zawiera deklarację (interfejs) dla klasy Foo.
  • Foo.cpp -> zawiera definicję (implementację) dla klasy Foo.
  • Main.cpp-> zawiera główną metodę, punkt wejścia programu. Ten kod tworzy instancję Foo i używa jej.

Oba Foo.cppi Main.cppmuszą zawierać Foo.h. Foo.cpppotrzebuje go, ponieważ definiuje kod wspierający interfejs klasy, więc musi wiedzieć, czym jest ten interfejs. Main.cpppotrzebuje go, ponieważ tworzy Foo i wywołuje swoje zachowanie, więc musi wiedzieć, co to za zachowanie, rozmiar Foo w pamięci i jak znaleźć jego funkcje itp., ale nie potrzebuje jeszcze faktycznej implementacji.

Kompilator wygeneruje Foo.oz Foo.cppktórego zawiera cały kod klasy Foo w skompilowanej formie. Generuje również, Main.októra zawiera główną metodę i nierozwiązane odwołania do klasy Foo.

Teraz przychodzi łącznik, który łączy dwa pliki obiektów Foo.oi Main.odo pliku wykonywalnego. Widzi nierozwiązane odwołania do Foo, Main.oale Foo.ozawiera niezbędne symbole, więc „łączy kropki”, że tak powiem. Wywołanie funkcji Main.ojest teraz połączone z faktyczną lokalizacją skompilowanego kodu, więc w czasie wykonywania program może przejść do właściwej lokalizacji.

Gdybyś włączył Foo.cppplik Main.cpp, istniałyby dwie definicje klasy Foo. Linker zobaczyłby to i powiedział „Nie wiem, który wybrać, więc to jest błąd”. Krok kompilacji się powiódł, ale połączenie nie powiodło się. (Chyba że po prostu nie kompilujesz, Foo.cppale dlaczego znajduje się w osobnym .cpppliku?)

Wreszcie idea różnych typów plików nie ma znaczenia dla kompilatora C / C ++. Kompiluje „pliki tekstowe”, które, mam nadzieję, zawierają poprawny kod dla pożądanego języka. Czasami może być w stanie powiedzieć język na podstawie rozszerzenia pliku. Na przykład skompiluj .cplik bez opcji kompilatora, a on przyjmie C, podczas gdy a .cclub .cpprozszerzenie poinformuje go o założeniu C ++. Jednak mogę łatwo powiedzieć kompilatorowi, aby skompilował plik, a .hnawet .docxplik jako C ++, i wyemituje plik object ( .o), jeśli zawiera poprawny kod C ++ w formacie zwykłego tekstu. Te rozszerzenia są bardziej korzystne dla programisty. Jeśli widzę Foo.hi Foo.cpp, od razu zakładam, że pierwsza zawiera deklarację klasy, a druga zawiera definicję.


źródło
Nie rozumiem argumentu, który tu wysyłasz. Jeśli tylko to Foo.cppsię Main.cppnie musisz dołączyć .hplik, masz jeden mniej plik, trzeba jeszcze wygrać w kodzie łupania w oddzielnych plikach dla czytelności, a komenda kompilacji jest prostsza. Chociaż rozumiem znaczenie plików nagłówkowych, nie sądzę, że o to chodzi
Benjamin Gruenbaum,
2
W przypadku trywialnego programu wystarczy umieścić wszystko w jednym pliku. Nawet nie dołączaj żadnych nagłówków projektów (tylko nagłówki bibliotek). Dołączanie plików źródłowych jest złym nawykiem, który spowoduje problemy dla programów innych niż najmniejsze, gdy masz wiele jednostek kompilacji i faktycznie kompilujesz je osobno.
2
If you had included the Foo.cpp file in Main.cpp, there would be two definitions of class Foo.Najważniejsze zdanie dotyczące pytania.
marczellm
@Snowman Right, na czym myślę, że powinieneś się skupić - wiele jednostek kompilacji. Przyklejanie kodu z / bez plików nagłówkowych do dzielenia kodu na mniejsze części jest przydatne i prostopadłe do plików nagłówkowych. Jeśli wszystko, co chcemy zrobić, to podzielić go na pliki nagłówkowe, nic nas nie kupuj - gdy tylko będziemy potrzebować dynamicznego linkowania, warunkowego linkowania i bardziej zaawansowanych kompilacji, nagle stają się bardzo przydatne.
Benjamin Gruenbaum
Istnieje wiele sposobów organizowania źródła dla kompilatora / linkera C / C ++. Pytający nie był pewien procesu, więc wyjaśniłem najlepsze praktyki organizowania kodu i jego działania. Możesz podzielić zdanie, że możliwe jest zorganizowanie kodu w inny sposób, ale faktem jest, że wyjaśniłem, w jaki sposób 99% projektów tam robi, co jest najlepszą praktyką z jakiegoś powodu. Działa dobrze i utrzymuje twoje zdrowie psychiczne.
9

Przeczytaj więcej na temat roli preprocesora C i C ++ , który jest koncepcyjnie pierwszą „fazą” kompilatora C lub C ++ (wcześniej był to osobny program /lib/cpp; teraz, ze względu na wydajność, jest zintegrowany z właściwym kompilatorem cc1 lub cc1plus). Przeczytaj w szczególności dokumentację cpppreprocesora GNU . W praktyce więc kompilator koncepcyjnie najpierw przetwarza twoją jednostkę kompilacyjną (lub jednostkę tłumaczącą ), a następnie pracuje na wstępnie przetworzonym formularzu.

Prawdopodobnie będziesz musiał zawsze dołączyć plik nagłówka, file.hjeśli zawiera (zgodnie z konwencjami i nawykami ):

  • definicje makr
  • definicje typów (np typedef, struct, classetc ...)
  • definicje static inlinefunkcji
  • deklaracje funkcji zewnętrznych.

Zauważ, że umieszczenie ich w pliku nagłówkowym jest kwestią konwencji (i wygody).

Oczywiście twoja implementacja file.cppwymaga wszystkich powyższych, więc #include "file.h" na początku chce .

To konwencja (ale bardzo powszechna). Można uniknąć plików nagłówkowych oraz skopiować i wkleić ich zawartość do plików implementacyjnych (tj. Jednostek tłumaczeniowych). Ale nie chcesz tego (z wyjątkiem być może, jeśli Twój kod C lub C ++ jest generowany automatycznie; wtedy możesz zmusić program generujący do wykonania tej operacji kopiowania i wklejania, naśladując rolę preprocesora).

Chodzi o to, że preprocesor wykonuje operacje tylko tekstowe . Można (w zasadzie) całkowicie tego uniknąć, kopiując i wklejając lub zastępując go innym „preprocesorem” lub generatorem kodu C (takim jak GPP lub M4 ).

Dodatkowym problemem jest to, że najnowsze standardy C (lub C ++) definiują kilka standardowych nagłówków. Większość implementacji tak naprawdę implementuje te standardowe nagłówki jako pliki (specyficzne dla implementacji) , ale uważam, że implementacja zgodna ze standardem może zawierać (jak w #include <stdio.h>przypadku C lub #include <vector>C ++) pewne magiczne sztuczki (np. Użycie bazy danych lub niektórych informacje w kompilatorze).

Jeśli używasz kompilatorów GCC (np. gccLub g++), możesz użyć -Hflagi, aby uzyskać informacje o każdym włączeniu, oraz -C -Eflag, aby uzyskać wstępnie przetworzony formularz. Oczywiście istnieje wiele innych flag kompilatora wpływających na przetwarzanie wstępne (np. W -I /some/dir/celu dodania /some/dir/do wyszukiwania dołączonych plików oraz w -Dcelu predefiniowania niektórych makr preprocesora itp.).

NB. Przyszłe wersje C ++ (być może C ++ 20 , może nawet później) mogą mieć moduły C ++ .

Basile Starynkevitch
źródło
1
Jeśli linki gniją, czy nadal byłaby to dobra odpowiedź?
Dan Pichelman,
4
Zredagowałem swoją odpowiedź, aby ją poprawić, i ostrożnie wybieram linki - do wikipedii lub dokumentacji GNU - które, jak sądzę, pozostaną aktualne przez długi czas.
Basile Starynkevitch,
3

Ze względu na wieloskładnikowy model kompilacji C ++, potrzebujesz sposobu, aby kod pojawiał się w twoim programie tylko raz (definicje) i potrzebujesz sposobu, aby kod pojawiał się w każdej jednostce tłumaczenia twojego programu (deklaracje).

Z tego rodzi się idiom nagłówka C ++. Z jakiegoś powodu jest to konwencja.

Państwo może zrzucić cały program w jednym urządzeniu tłumaczenia, ale wprowadza to problemy z ponownego wykorzystania kodu, testowanie jednostkowe i między modułem obsługi zależności. To także wielki bałagan.

Lekkość Wyścigi z Moniką
źródło
0

Wybrana odpowiedź z Dlaczego musimy napisać plik nagłówka? jest rozsądnym wyjaśnieniem, ale chciałem dodać dodatkowe szczegóły.

Wydaje się, że racjonalne podejście do plików nagłówkowych zwykle gubi się w nauczaniu i omawianiu C / C ++.

Plik nagłówków stanowi rozwiązanie dwóch problemów związanych z programowaniem aplikacji:

  1. Rozdzielenie interfejsu i implementacji
  2. Poprawione czasy kompilacji / łączenia dla dużych programów

C / C ++ można skalować od małych programów do bardzo dużych wielomilionowych programów wielotysięcznych. Tworzenie aplikacji może być skalowane od zespołów jednego programisty do setek programistów.

Jako deweloper możesz nosić kilka czapek. W szczególności możesz być użytkownikiem interfejsu funkcji i klas lub możesz być autorem interfejsu funkcji i klas.

Kiedy używasz funkcji, musisz znać interfejs funkcji, jakich parametrów użyć, jakie funkcje zwracają i musisz wiedzieć, co robi funkcja. Można to łatwo udokumentować w pliku nagłówkowym, nie patrząc nawet na implementację. Kiedy przeczytałeś implementację printf? Kup, używamy go na co dzień.

Gdy jesteś programistą interfejsu, kapelusz zmienia inny kierunek. Plik nagłówkowy zawiera deklarację interfejsu publicznego. Plik nagłówkowy określa, czego potrzebuje inna implementacja, aby móc korzystać z tego interfejsu. Informacje wewnętrzne i prywatne dla tego nowego interfejsu nie są (i nie powinny) deklarowane w pliku nagłówkowym. Publiczny plik nagłówkowy powinien być wszystkim, czego każdy potrzebuje do korzystania z modułu.

W przypadku projektowania na dużą skalę kompilacja i łączenie może zająć dużo czasu. Od wielu minut do wielu godzin (nawet do wielu dni!). Dzielenie oprogramowania na interfejsy (nagłówki) i implementacje (źródła) zapewnia metodę kompilacji tylko tych plików, które należy skompilować, a nie odbudować wszystko.

Ponadto pliki nagłówkowe umożliwiają programistom udostępnienie biblioteki (już skompilowanej), a także pliku nagłówkowego. Inni użytkownicy biblioteki mogą nigdy nie widzieć poprawnej implementacji, ale nadal mogą korzystać z biblioteki z plikiem nagłówka. Robisz to codziennie ze standardową biblioteką C / C ++.

Nawet jeśli tworzysz małą aplikację, dobrym zwyczajem jest używanie technik tworzenia oprogramowania na dużą skalę. Ale musimy również pamiętać, dlaczego używamy tych nawyków.

Bill Door
źródło
0

Równie dobrze możesz zapytać, dlaczego nie umieścić całego kodu w jednym pliku.

Najprostszą odpowiedzią jest utrzymanie kodu.

Są chwile, kiedy rozsądne jest utworzenie klasy:

  • wszystkie wstawione w nagłówku
  • wszystko w jednostce kompilacyjnej i bez nagłówka.

Moment, w którym uzasadnione jest całkowite wstawienie do nagłówka, to fakt, że klasa jest tak naprawdę strukturą danych z kilkoma podstawowymi modułami pobierającymi i ustawiającymi oraz być może konstruktorem, który przyjmuje wartości w celu inicjalizacji swoich elementów.

(Szablony, które muszą być wstawiane, to nieco inna kwestia).

Innym razem, aby utworzyć klasę w nagłówku, możesz użyć tej klasy w wielu projektach, a konkretnie chcesz uniknąć łączenia się z bibliotekami.

Czas, w którym możesz uwzględnić całą klasę w jednostce kompilacyjnej i w ogóle nie ujawniać jej nagłówka, to:

  • Klasa „impl”, która jest używana tylko przez klasę, którą implementuje. Jest to szczegół implementacji tej klasy i nie jest używany zewnętrznie.

  • Implementacja abstrakcyjnej klasy bazowej, która jest tworzona przez pewnego rodzaju metodę fabryczną, która zwraca wskaźnik / referencję / inteligentny wskaźnik) do klasy bazowej. Metoda fabryczna byłaby narażona przez samą klasę, nie byłaby. (Ponadto, jeśli klasa ma instancję, która rejestruje się w tabeli za pośrednictwem instancji statycznej, nie musi nawet być ujawniana za pośrednictwem fabryki).

  • Klasa typu „funktor”.

Innymi słowy, jeśli nie chcesz, aby ktokolwiek dołączał nagłówek.

Wiem, co myślisz ... Jeśli chodzi tylko o łatwość konserwacji, dołączając plik CPP (lub całkowicie wbudowany nagłówek), możesz łatwo edytować plik, aby „znaleźć” kod i po prostu go odbudować.

Jednak „łatwość konserwacji” to nie tylko uporządkowanie kodu. Jest to kwestia skutków zmian. Powszechnie wiadomo, że jeśli nie zmienisz nagłówka i po prostu zmienisz (.cpp)plik implementacji , przebudowanie innego źródła nie będzie konieczne, ponieważ nie powinny wystąpić żadne skutki uboczne.

To sprawia, że ​​„bezpieczniej” jest dokonywać takich zmian, nie martwiąc się o efekt domina, i to właśnie tak naprawdę oznacza „łatwość konserwacji”.

Dojną krową
źródło
-1

Przykład Snowmana wymaga małego rozszerzenia, aby dokładnie pokazać, dlaczego potrzebne są pliki .h.

Dodaj kolejny pasek klasy do gry, który jest również zależny od klasy Foo.

Foo.h -> zawiera deklarację dla klasy Foo

Foo.cpp -> zawiera definicję (impementację) klasy Foo

Main.cpp -> używa zmiennej typu Foo.

Bar.cpp -> również używa zmiennej typu Foo.

Teraz wszystkie pliki CPP muszą zawierać Foo.h Błędem byłoby włączenie Foo.cpp w więcej niż jednym innym pliku CPP. Linker zawiódłby, ponieważ klasa Foo zostałaby zdefiniowana więcej niż jeden raz.

SkaarjFace
źródło
1
To też nie koniec. Można to rozwiązać dokładnie tak samo jak w .hplikach - z włączonymi zabezpieczeniami i tak samo jak problem z .hplikami. W ogóle nie do tego .hsłużą pliki.
Benjamin Gruenbaum,
Nie jestem pewien, w jaki sposób użyjesz osłon włączających, ponieważ działają one tylko na plik (tak jak działa preprocessod). W tym przypadku, ponieważ Main.cpp i Bar.cpp to różne pliki Foo.cpp zostałyby dołączone tylko raz w obu z nich (chronione lub nie robi żadnej różnicy). Main.cpp i Bar.cpp zostałyby skompilowane. Ale błąd łącza nadal występuje z powodu podwójnej definicji klasy Foo. Istnieje jednak dziedzictwo, o którym nawet nie wspomniałem. Deklaracja dla modułów zewnętrznych (dll) itp.
SkaarjFace
Nie, byłaby to jedna jednostka kompilacji, żadne połączenie nie miałoby miejsca, ponieważ dołączasz .cppplik.
Benjamin Gruenbaum
Nie powiedziałem, że się nie skompiluje. Ale nie połączy zarówno main.o, jak i bar.o w tym samym pliku wykonywalnym. Bez łączenia w ogóle sensu kompilacji.
SkaarjFace
Uhh ... uruchomienie kodu? Nadal możesz go łączyć, ale po prostu nie łączyć plików ze sobą - raczej byłyby to te same jednostki kompilacji.
Benjamin Gruenbaum
-2

Jeśli napiszesz cały kod w tym samym pliku, spowoduje to, że Twój kod będzie brzydki.
Po drugie, nie będziesz mógł dzielić się swoją pisemną lekcją z innymi. Według inżynierii oprogramowania kod klienta należy pisać osobno. Klient nie powinien wiedzieć, jak działa Twój program. Potrzebują tylko danych wyjściowych. Jeśli napiszesz cały program w tym samym pliku, spowoduje to przeciek bezpieczeństwa twojego programu.

Talha Junaid
źródło