Dlaczego C ++ potrzebuje oddzielnego pliku nagłówkowego?

138

Nigdy tak naprawdę nie rozumiałem, dlaczego C ++ potrzebuje oddzielnego pliku nagłówkowego z tymi samymi funkcjami, co w pliku .cpp. Utrudnia to tworzenie klas i ich refaktoryzację, a także dodaje do projektu niepotrzebne pliki. A potem pojawia się problem z koniecznością dołączania plików nagłówkowych, ale z koniecznością jawnego sprawdzenia, czy zostały już uwzględnione.

C ++ został ratyfikowany w 1998 roku, dlaczego więc został zaprojektowany w ten sposób? Jakie zalety ma oddzielny plik nagłówkowy?


Pytanie uzupełniające:

W jaki sposób kompilator znajduje plik .cpp z zawartym w nim kodem, skoro wszystko, co dołączam, to plik .h? Czy zakłada, że ​​plik .cpp ma taką samą nazwę jak plik .h, czy też faktycznie przegląda wszystkie pliki w drzewie katalogów?

Marius
źródło
2
Jeśli chcesz edytować pojedynczy plik, wyewidencjonuj tylko lzz (www.lazycplusplus.com).
Richard Corden
3
Dokładny duplikat: stackoverflow.com/questions/333889 . Prawie duplikat: stackoverflow.com/questions/752793
Peter Mortensen

Odpowiedzi:

105

Wydaje się, że pytasz o oddzielenie definicji od deklaracji, chociaż istnieją inne zastosowania plików nagłówkowych.

Odpowiedź jest taka, że ​​C ++ nie „potrzebuje” tego. Jeśli oznaczysz wszystko w linii (co i tak jest automatyczne dla funkcji składowych zdefiniowanych w definicji klasy), nie ma potrzeby separacji. Możesz po prostu zdefiniować wszystko w plikach nagłówkowych.

Powody, dla których warto się rozdzielić, to:

  1. Aby poprawić czas budowy.
  2. Łączenie z kodem bez posiadania źródła definicji.
  3. Aby uniknąć oznaczania wszystkiego „w tekście”.

Jeśli twoje bardziej ogólne pytanie brzmi „dlaczego C ++ nie jest identyczne z Javą?”, To muszę zapytać „dlaczego piszesz C ++ zamiast Javy?” ;-p

Ale poważniej, powodem jest to, że kompilator C ++ nie może po prostu sięgnąć do innej jednostki tłumaczeniowej i dowiedzieć się, jak używać jej symboli w sposób, w jaki może to robić i robi javac. Plik nagłówkowy jest potrzebny do zadeklarowania kompilatorowi tego, czego może się spodziewać w czasie łączenia.

Podobnie #includejest z prostą substytucją tekstową. Jeśli zdefiniujesz wszystko w plikach nagłówkowych, preprocesor w końcu utworzy ogromną kopię i wklej każdy plik źródłowy w twoim projekcie i przekaże to do kompilatora. Fakt, że standard C ++ został ratyfikowany w 1998 roku, nie ma z tym nic wspólnego, to fakt, że środowisko kompilacji dla C ++ jest tak blisko oparte na środowisku C.

Przekształcam moje komentarze, aby odpowiedzieć na pytanie uzupełniające:

W jaki sposób kompilator znajduje plik .cpp z zawartym w nim kodem

Tak nie jest, przynajmniej nie w momencie kompilacji kodu używającego pliku nagłówkowego. Funkcje, z którymi się łączysz, nie muszą być jeszcze napisane, nieważne, że kompilator wie, w jakim .cpppliku będą się znajdować. Wszystko, co kod wywołujący musi wiedzieć w czasie kompilacji, jest wyrażone w deklaracji funkcji. W momencie łączenia dostarczysz listę .oplików lub bibliotek statycznych lub dynamicznych, a nagłówek w efekcie jest obietnicą, że definicje funkcji będą gdzieś tam.

Steve Jessop
źródło
3
Aby dodać do "Powody, dla których możesz chcieć rozdzielić to:" Najważniejszą funkcją plików nagłówkowych jest: Oddzielenie projektu struktury kodu od implementacji, ponieważ: A. Kiedy wchodzisz w naprawdę skomplikowane struktury, które obejmują wiele obiektów, jest znacznie łatwiejsze do przeglądania plików nagłówkowych i zapamiętywania ich współpracy, uzupełnione komentarzami nagłówka. B. Jedna osoba musiała zająć się zdefiniowaniem całej struktury obiektu, a inna implementacją, co utrzymuje porządek. Wydaje mi się, że dzięki temu złożony kod jest bardziej czytelny.
Andres Canella,
W najprostszy sposób mogę sobie wyobrazić użyteczność separacji nagłówków i plików cpp polega na oddzieleniu interfejsu od implementacji, co naprawdę pomaga w średnich / dużych projektach.
Krishna Oza
Chciałem, żeby to było zawarte w (2), „link do kodu bez definicji”. OK, więc możesz nadal mieć dostęp do repozytorium git z definicjami zawartymi w programie, ale chodzi o to, że możesz skompilować swój kod oddzielnie od implementacji jego zależności, a następnie połączyć. Jeśli dosłownie wszystko, czego chciałeś, to rozdzielenie interfejsu / implementacji na różne pliki, bez obawy o osobne budowanie, możesz to zrobić, po prostu dodając foo_interface.h do foo_implementation.h.
Steve Jessop
4
@AndresCanella Nie, nie ma. To sprawia, że ​​czytanie i utrzymywanie kodu innego niż własny staje się koszmarem. Aby w pełni zrozumieć, co coś robi w kodzie, musisz przeskoczyć przez 2n plików zamiast n plików. To po prostu nie jest notacja Big-Oh, 2n robi dużą różnicę w porównaniu do samego n.
Błażej Michalik
1
Po drugie, jest to kłamstwo, które pomagają nagłówki. sprawdź na przykład źródło minixa, tak trudno jest śledzić, gdzie zaczyna się, gdzie przekazywana jest kontrola, gdzie rzeczy są deklarowane / definiowane .. jeśli zostało zbudowane za pomocą oddzielnych modułów dynamicznych, byłoby to strawne przez nadanie sensu jednej rzeczy, a następnie przejście do moduł zależności. zamiast tego musisz podążać za nagłówkami, a to sprawia, że ​​czytanie dowolnego kodu napisanego w ten sposób jest piekłem. W przeciwieństwie do tego, nodejs wyjaśnia, skąd pochodzi, bez żadnych ifdef, i możesz łatwo zidentyfikować, skąd pochodzi.
Dmitry
91

C ++ robi to w ten sposób, ponieważ C zrobił to w ten sposób, więc prawdziwe pytanie brzmi: dlaczego C zrobił to w ten sposób? Wikipedia trochę o tym mówi.

Nowsze języki kompilowane (takie jak Java, C #) nie używają deklaracji przekazywania; identyfikatory są rozpoznawane automatycznie z plików źródłowych i odczytywane bezpośrednio z symboli bibliotek dynamicznych. Oznacza to, że pliki nagłówkowe nie są potrzebne.

Donald Byrd
źródło
13
+1 Trafia gwóźdź w głowę. To naprawdę nie wymaga szczegółowego wyjaśnienia.
MSalters
6
Nie uderzyło mnie to w głowę :( Nadal muszę sprawdzić, dlaczego C ++ musi używać deklaracji do przodu i dlaczego nie może rozpoznać identyfikatorów z plików źródłowych i czytać bezpośrednio z symboli bibliotek dynamicznych i dlaczego C ++ to zrobił tylko dlatego, że C zrobił to w ten sposób: p
Alexander Taylor
3
A ty jesteś lepszym programistą, że to zrobiłeś @AlexanderTaylor :)
Donald Byrd
66

Niektórzy uważają pliki nagłówkowe za zaletę:

  • Twierdzi się, że umożliwia / wymusza / umożliwia rozdzielenie interfejsu i implementacji - ale zwykle tak nie jest. Pliki nagłówkowe są pełne szczegółów implementacji (na przykład zmienne składowe klasy muszą być określone w nagłówku, mimo że nie są częścią interfejsu publicznego), a funkcje mogą i często są zdefiniowane w deklaracji klasy w nagłówku, ponownie niszcząc tę ​​separację.
  • Czasami mówi się, że poprawia to czas kompilacji, ponieważ każda jednostka tłumaczeniowa może być przetwarzana niezależnie. A jednak C ++ jest prawdopodobnie najwolniejszym istniejącym językiem, jeśli chodzi o czasy kompilacji. Częściowo jest to spowodowane wieloma powtarzającymi się włączeniami tego samego nagłówka. Wiele jednostek tłumaczeniowych zawiera dużą liczbę nagłówków, co wymaga ich wielokrotnego analizowania.

Ostatecznie system nagłówka jest artefaktem z lat 70-tych, kiedy zaprojektowano C. W tamtych czasach komputery miały bardzo mało pamięci, a przechowywanie całego modułu w pamięci po prostu nie wchodziło w grę. Kompilator musiał zacząć czytać plik od góry, a następnie przejść liniowo przez kod źródłowy. Umożliwia to mechanizm nagłówka. Kompilator nie musi brać pod uwagę innych jednostek tłumaczeniowych, musi po prostu czytać kod od góry do dołu.

C ++ zachował ten system ze względu na kompatybilność wsteczną.

Dziś to nie ma sensu. Jest nieefektywny, podatny na błędy i nadmiernie skomplikowany. Jeśli taki był cel, istnieją znacznie lepsze sposoby na oddzielenie interfejsu i implementacji .

Jednak jedną z propozycji dla C ++ 0x było dodanie odpowiedniego systemu modułowego, pozwalającego na kompilację kodu podobnego do .NET czy Java do większych modułów, wszystko za jednym razem i bez nagłówków. Ta propozycja nie odniosła sukcesu w C ++ 0x, ale uważam, że nadal znajduje się w kategorii „chcielibyśmy zrobić to później”. Być może w TR2 lub podobnym.

jalf
źródło
TO jest najlepsza odpowiedź na stronie. Dziękuję Ci!
Chuck Le Butt
29

Według mojego (ograniczonego - normalnie nie jestem programistą C), jest to zakorzenione w C. Pamiętaj, że C nie wie, jakie są klasy lub przestrzenie nazw, to tylko jeden długi program. Ponadto funkcje muszą zostać zadeklarowane przed ich użyciem.

Na przykład poniższy kod powinien dać błąd kompilatora:

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}

Błąd powinien być taki, że „SomeOtherFunction nie jest zadeklarowana”, ponieważ wywołujesz ją przed deklaracją. Jednym ze sposobów rozwiązania tego problemu jest przeniesienie SomeOtherFunction nad SomeFunction. Innym podejściem jest zadeklarowanie najpierw podpisu funkcji:

void SomeOtherFunction();

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}

Dzięki temu kompilator wie: spójrz gdzieś w kodzie, jest funkcja o nazwie SomeOtherFunction, która zwraca wartość void i nie przyjmuje żadnych parametrów. Więc jeśli kodujesz kod, który próbuje wywołać SomeOtherFunction, nie panikuj i zamiast tego poszukaj go.

Teraz wyobraź sobie, że masz SomeFunction i SomeOtherFunction w dwóch różnych plikach .c. Następnie musisz #include „SomeOther.c” w Some.c. Teraz dodaj kilka funkcji „prywatnych” do SomeOther.c. Ponieważ C nie zna funkcji prywatnych, ta funkcja byłaby również dostępna w Some.c.

W tym miejscu pojawiają się pliki .h: Określają one wszystkie funkcje (i zmienne), które chcesz „wyeksportować” z pliku .c, do którego można uzyskać dostęp w innych plikach .c. W ten sposób zyskujesz coś w rodzaju zakresu publicznego / prywatnego. Możesz również przekazać ten plik .h innym osobom bez konieczności udostępniania swojego kodu źródłowego - pliki .h działają również na skompilowanych plikach .lib.

Tak więc głównym powodem jest wygoda, ochrona kodu źródłowego i trochę oddzielenia między częściami aplikacji.

To było jednak C. C ++ wprowadził klasy i modyfikatory prywatne / publiczne, więc chociaż nadal możesz zapytać, czy są potrzebne, C ++ AFAIK nadal wymaga deklaracji funkcji przed ich użyciem. Ponadto wielu programistów C ++ jest lub było również deweloperami C i przejęło swoje koncepcje i nawyki do C ++ - po co zmieniać to, co nie jest zepsute?

Michael Stum
źródło
5
Dlaczego kompilator nie może przejść przez kod i znaleźć wszystkich definicji funkcji? Wygląda na to, że byłoby to całkiem łatwe do zaprogramowania w kompilatorze.
Marius
3
Jeśli masz źródła, które często nie mają. Skompilowany C ++ to w rzeczywistości kod maszynowy z wystarczającą ilością dodatkowych informacji, aby załadować i połączyć kod. Następnie wskazujesz procesorowi punkt wejścia i pozwól mu działać. Różni się to zasadniczo od języka Java czy C #, w których kod jest kompilowany do pośredniego kodu bajtowego zawierającego metadane dotyczące jego zawartości.
DevSolar
Zgaduję, że w 1972 roku mogła to być dość kosztowna operacja dla kompilatora.
Michael Stum
Dokładnie to, co powiedział Michael Stum. Kiedy zdefiniowano to zachowanie, jedyną rzeczą, którą można było w praktyce zaimplementować, był skan liniowy przez pojedynczą jednostkę tłumaczeniową.
jalf
3
Tak - kompilacja na 16 gorzkiej masie z taśmą jest nietrywialna.
MSalters
11

Pierwsza zaleta: jeśli nie masz plików nagłówkowych, musiałbyś dołączyć pliki źródłowe do innych plików źródłowych. Spowodowałoby to ponowne skompilowanie plików włączających po zmianie dołączonego pliku.

Druga zaleta: umożliwia udostępnianie interfejsów bez udostępniania kodu między różnymi jednostkami (różnymi programistami, zespołami, firmami itp.)

erelender
źródło
1
Czy sugerujesz, że np. W C # „musiałbyś uwzględnić pliki źródłowe w innych plikach źródłowych”? Bo oczywiście nie. Jeśli chodzi o drugą zaletę, myślę, że jest to zbyt zależne od języka: nie będziesz używać plików .h np. W Delphi
Vlagged
I tak musisz przekompilować cały projekt, więc czy naprawdę liczy się pierwsza korzyść?
Marius
ok, ale nie sądzę, żeby była to funkcja językowa. Bardziej praktycznym rozwiązaniem jest zajęcie się deklaracją C przed zdefiniowaniem „problemu”. To tak, jakby ktoś sławny powiedział „to nie błąd, to cecha” :)
neuro
@Marius: Tak, to naprawdę się liczy. Łączenie całego projektu różni się od kompilowania i łączenia całego projektu. Wraz ze wzrostem liczby plików w projekcie kompilowanie ich wszystkich staje się naprawdę denerwujące. @Vlagged: Masz rację, ale nie porównałem C ++ z innym językiem. Porównałem używanie tylko plików źródłowych z użyciem plików źródłowych i nagłówkowych.
erelender
C # nie zawiera plików źródłowych w innych, ale nadal musisz odwoływać się do modułów - i to sprawia, że ​​kompilator pobiera pliki źródłowe (lub odzwierciedla je w pliku binarnym), aby przeanalizować symbole używane w kodzie.
gbjbaanb
5

Potrzeba plików nagłówkowych wynika z ograniczeń, jakie ma kompilator, jeśli chodzi o informacje o typie funkcji i / lub zmiennych w innych modułach. Skompilowany program lub biblioteka nie zawiera informacji o typie wymaganych przez kompilator do powiązania z jakimikolwiek obiektami zdefiniowanymi w innych jednostkach kompilacji.

Aby zrekompensować to ograniczenie, C i C ++ zezwalają na deklaracje i te deklaracje mogą być dołączane do modułów, które ich używają za pomocą dyrektywy #include preprocesora.

Z drugiej strony języki takie jak Java lub C # zawierają informacje niezbędne do powiązania w danych wyjściowych kompilatora (plik klasy lub zestaw). W związku z tym nie ma już potrzeby utrzymywania samodzielnych deklaracji, które będą dołączane przez klientów modułu.

Przyczyna, dla której informacje o powiązaniu nie są uwzględniane w danych wyjściowych kompilatora, jest prosta: nie jest to potrzebne w czasie wykonywania (jakiekolwiek sprawdzanie typu odbywa się w czasie kompilacji). Po prostu marnowałoby miejsce. Pamiętaj, że C / C ++ pochodzi z czasów, gdy rozmiar pliku wykonywalnego lub biblioteki miał duże znaczenie.

VoidPointer
źródło
Zgadzam się z Tobą. Podobny pomysł mam tutaj: stackoverflow.com/questions/3702132/…
smwikipedia
4

C ++ został zaprojektowany w celu dodania nowoczesnych funkcji języka programowania do infrastruktury C, bez niepotrzebnych zmian w C, co nie dotyczyło samego języka.

Tak, w tym momencie (10 lat po pierwszym standardzie C ++ i 20 lat po tym, jak zaczął poważnie rosnąć w użyciu) łatwo jest zapytać, dlaczego nie ma odpowiedniego systemu modułowego. Oczywiście jakikolwiek nowy projektowany dziś język nie działałby jak C ++. Ale nie o to chodzi w C ++.

Celem C ++ jest ewolucja, płynna kontynuacja istniejącej praktyki, dodawanie tylko nowych możliwości bez (zbyt często) niszczenia rzeczy, które działają odpowiednio dla społeczności użytkowników.

Oznacza to, że niektóre rzeczy są trudniejsze (szczególnie dla osób rozpoczynających nowy projekt), a niektóre są łatwiejsze (zwłaszcza dla tych, którzy utrzymują istniejący kod) niż inne języki.

Więc zamiast oczekiwać, że C ++ zamieni się w C # (co byłoby bezcelowe, ponieważ mamy już C #), dlaczego nie wybrać odpowiedniego narzędzia do tego zadania? Sam staram się pisać znaczące fragmenty nowej funkcjonalności w nowoczesnym języku (tak się składa, że ​​używam C #) i mam dużą ilość istniejącego C ++, które trzymam w C ++, ponieważ ponowne napisanie go nie miałoby żadnej wartości wszystko. Zresztą bardzo ładnie się integrują, więc jest to w dużej mierze bezbolesne.

Daniel Earwicker
źródło
Jak integrujesz C # i C ++? Poprzez COM?
Peter Mortensen
1
Istnieją trzy główne sposoby, z których „najlepszy” zależy od istniejącego kodu. Użyłem wszystkich trzech. Najczęściej używam COM, ponieważ mój istniejący kod został już zaprojektowany wokół niego, więc jest praktycznie bezproblemowy, działa bardzo dobrze dla mnie. W niektórych dziwnych miejscach używam C ++ / CLI, co zapewnia niewiarygodnie płynną integrację w każdej sytuacji, w której nie masz jeszcze interfejsów COM (i możesz preferować używanie istniejących interfejsów COM, nawet jeśli je masz). Wreszcie jest p / invoke, który w zasadzie pozwala wywołać dowolną funkcję podobną do C, która jest udostępniona z biblioteki DLL, więc umożliwia bezpośrednie wywołanie dowolnego interfejsu API Win32 z C #.
Daniel Earwicker,
4

Cóż, C ++ został ratyfikowany w 1998 roku, ale był używany znacznie dłużej, a ratyfikacja miała przede wszystkim na celu określenie bieżącego użycia, a nie narzucenie struktury. A ponieważ C ++ był oparty na C, a C ma pliki nagłówkowe, C ++ też je ma.

Głównym powodem tworzenia plików nagłówkowych jest umożliwienie oddzielnej kompilacji plików i zminimalizowanie zależności.

Powiedzmy, że mam foo.cpp i chcę użyć kodu z plików bar.h / bar.cpp.

Mogę #include "bar.h" w foo.cpp, a następnie zaprogramować i skompilować foo.cpp, nawet jeśli bar.cpp nie istnieje. Plik nagłówkowy stanowi obietnicę dla kompilatora, że ​​klasy / funkcje z bar.h będą istniały w czasie wykonywania i zawiera już wszystko, co musi wiedzieć.

Oczywiście, jeśli funkcje w bar.h nie mają treści, kiedy próbuję połączyć mój program, to nie będzie się on łączyć i pojawi się błąd.

Efektem ubocznym jest to, że możesz udostępnić użytkownikom plik nagłówkowy bez ujawniania kodu źródłowego.

Innym jest to, że jeśli zmienisz implementację swojego kodu w pliku * .cpp, ale w ogóle nie zmienisz nagłówka, wystarczy skompilować plik * .cpp zamiast wszystkiego, co go używa. Oczywiście, jeśli umieścisz dużo implementacji w pliku nagłówkowym, stanie się to mniej przydatne.

Mark Krenitsky
źródło
3

Nie potrzebuje oddzielnego pliku nagłówkowego z tymi samymi funkcjami, co w main. Potrzebuje go tylko wtedy, gdy tworzysz aplikację przy użyciu wielu plików kodu i używasz funkcji, która nie została wcześniej zadeklarowana.

To naprawdę problem z zakresem.

Alex Rodrigues
źródło
1

C ++ został ratyfikowany w 1998 roku, dlaczego więc został zaprojektowany w ten sposób? Jakie zalety ma oddzielny plik nagłówkowy?

W rzeczywistości pliki nagłówkowe stają się bardzo przydatne przy pierwszym badaniu programów, wyewidencjonowanie plików nagłówkowych (przy użyciu tylko edytora tekstu) daje przegląd architektury programu, w przeciwieństwie do innych języków, w których trzeba używać zaawansowanych narzędzi do przeglądania klas i ich członków.

Diaa Sami
źródło
1

Myślę, że prawdziwa (historyczna) Powodem plików nagłówkowych robił jak łatwiejsze dla twórców kompilatora ... ale potem, header pliki nie dają korzyści.
Sprawdź ten poprzedni post, aby uzyskać więcej dyskusji ...

Paolo Tedesco
źródło
1

Cóż, możesz doskonale rozwijać C ++ bez plików nagłówkowych. W rzeczywistości niektóre biblioteki, które intensywnie używają szablonów, nie używają paradygmatu plików nagłówka / kodu (patrz boost). Ale w C / C ++ nie można używać czegoś, co nie jest zadeklarowane. Jednym praktycznym sposobem radzenia sobie z tym jest użycie plików nagłówkowych. Ponadto zyskujesz przewagę współdzielenia interfejsu bez udostępniania kodu / implementacji. I myślę, że twórcy C nie przewidzieli tego: kiedy używasz współdzielonych plików nagłówkowych, musisz użyć słynnego:

#ifndef MY_HEADER_SWEET_GUARDIAN
#define MY_HEADER_SWEET_GUARDIAN

// [...]
// my header
// [...]

#endif // MY_HEADER_SWEET_GUARDIAN

nie jest to tak naprawdę funkcja języka, ale praktyczny sposób radzenia sobie z wielokrotnym włączaniem.

Tak więc myślę, że kiedy powstawało C, niedoceniane były problemy z deklaracją forward, a teraz, kiedy używamy języka wysokiego poziomu, takiego jak C ++, musimy sobie z tym radzić.

Kolejne obciążenie dla nas, biednych użytkowników C ++ ...

neuro-
źródło
1

Jeśli chcesz, aby kompilator automatycznie wyszukiwał symbole zdefiniowane w innych plikach, musisz zmusić programistę do umieszczenia tych plików w predefiniowanych lokalizacjach (tak jak struktura pakietów Java określa strukturę folderów projektu). Wolę pliki nagłówkowe. Potrzebowałbyś także źródeł bibliotek, których używasz, lub jakiegoś jednolitego sposobu umieszczania informacji potrzebnych kompilatorowi w plikach binarnych.

Tadeusz Kopeć
źródło