Dlaczego musimy dołączyć zarówno pliki, jak .h
i .cpp
pliki, podczas gdy możemy sprawić, że będzie działać wyłącznie poprzez dołączenie .cpp
pliku?
Na przykład: tworzenie file.h
deklaracji zawierających, następnie tworzenie file.cpp
definicji zawierających i uwzględnianie obu w main.cpp
.
Alternatywnie: tworzenie file.cpp
deklaracji / 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.
Odpowiedzi:
Podczas może zawierać
.cpp
pliki, 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
.cpp
plik 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 (
.cpp
plik) 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.cpp
iMain.cpp
muszą zawieraćFoo.h
.Foo.cpp
potrzebuje go, ponieważ definiuje kod wspierający interfejs klasy, więc musi wiedzieć, czym jest ten interfejs.Main.cpp
potrzebuje 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.o
zFoo.cpp
którego zawiera cały kod klasy Foo w skompilowanej formie. Generuje również,Main.o
która zawiera główną metodę i nierozwiązane odwołania do klasy Foo.Teraz przychodzi łącznik, który łączy dwa pliki obiektów
Foo.o
iMain.o
do pliku wykonywalnego. Widzi nierozwiązane odwołania do Foo,Main.o
aleFoo.o
zawiera niezbędne symbole, więc „łączy kropki”, że tak powiem. Wywołanie funkcjiMain.o
jest 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.cpp
plikMain.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.cpp
ale dlaczego znajduje się w osobnym.cpp
pliku?)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
.c
plik bez opcji kompilatora, a on przyjmie C, podczas gdy a.cc
lub.cpp
rozszerzenie poinformuje go o założeniu C ++. Jednak mogę łatwo powiedzieć kompilatorowi, aby skompilował plik, a.h
nawet.docx
plik 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.h
iFoo.cpp
, od razu zakładam, że pierwsza zawiera deklarację klasy, a druga zawiera definicję.źródło
Foo.cpp
sięMain.cpp
nie musisz dołączyć.h
plik, 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 chodziIf you had included the Foo.cpp file in Main.cpp, there would be two definitions of class Foo.
Najważniejsze zdanie dotyczące pytania.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 kompilatoremcc1
lubcc1plus
). Przeczytaj w szczególności dokumentacjęcpp
preprocesora 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.h
jeśli zawiera (zgodnie z konwencjami i nawykami ):typedef
,struct
,class
etc ...)static inline
funkcjiZauważ, że umieszczenie ich w pliku nagłówkowym jest kwestią konwencji (i wygody).
Oczywiście twoja implementacja
file.cpp
wymaga 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.
gcc
Lubg++
), możesz użyć-H
flagi, aby uzyskać informacje o każdym włączeniu, oraz-C -E
flag, 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-D
celu 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 ++ .
źródło
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.
źródło
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:
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.
źródło
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:
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”.
źródło
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.
źródło
.h
plikach - z włączonymi zabezpieczeniami i tak samo jak problem z.h
plikami. W ogóle nie do tego.h
służą pliki..cpp
plik.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.
źródło