Dlaczego pliki nagłówkowe i .cpp? [Zamknięte]

484

Dlaczego C ++ ma pliki nagłówkowe i .cpp?

Peter Mortensen
źródło
3
Powiązane pytanie: stackoverflow.com/questions/1945846/...
Spoike
jest to powszechny paradygmat OOP, .h jest deklaracją klasy, a cpp jest definicją. Nie trzeba wiedzieć, w jaki sposób jest on implementowany, powinien znać tylko interfejs.
Manish Kakati,
To najlepsza część interfejsu oddzielającego c ++ od implementacji. To zawsze dobrze, zamiast trzymać cały kod w jednym pliku, mamy interfejs oddzielony. Pewna ilość kodu jest zawsze taka jak funkcja wstawiana, która jest częścią plików nagłówkowych. Wygląda dobrze, gdy plik nagłówkowy jest widoczny, wyświetla listę zadeklarowanych funkcji i zmiennych klas.
Miank,
Są chwile, w których pliki nagłówkowe są niezbędne do kompilacji - nie tylko preferencje organizacji lub sposób dystrybucji wstępnie skompilowanych bibliotek. Załóżmy, że masz strukturę, w której game.c zależy od OBA fizyki.c i matematyki.c; physics.c zależy również od matematyki. c. Jeśli dołączysz pliki .c i na zawsze zapomnisz o plikach .h, będziesz mieć zduplikowane deklaracje z matematyki.c i nie ma nadziei na kompilację. To właśnie ma dla mnie sens, dlaczego pliki nagłówkowe są ważne. Mam nadzieję, że pomoże to komuś innemu.
Samy Bencherif,
Myślę, że ma to związek z tym, że w rozszerzeniach dozwolone są tylko znaki alfanumeryczne. Nie wiem nawet, czy to prawda, tylko zgaduję
użytkownik12211554

Odpowiedzi:

201

Głównym powodem byłoby oddzielenie interfejsu od implementacji. Nagłówek deklaruje „co” zrobi klasa (lub cokolwiek, co jest implementowane), podczas gdy plik cpp określa „jak” będzie wykonywać te funkcje.

Zmniejsza to zależności, dzięki czemu kod wykorzystujący nagłówek niekoniecznie musi znać wszystkie szczegóły implementacji i inne klasy / nagłówki potrzebne tylko do tego. Skróci to czas kompilacji, a także ilość ponownej kompilacji potrzebnej, gdy coś w implementacji zmieni się.

To nie jest idealne i zwykle stosujesz takie techniki, jak Pimpl Idiom, aby odpowiednio oddzielić interfejs i implementację, ale to dobry początek.

MadKeithV
źródło
177
Niezupełnie prawda. Nagłówek nadal zawiera większą część implementacji. Od kiedy zmienne instancji prywatnych były częścią interfejsu klasy? Prywatne funkcje członka? Co do diabła robią w publicznie widocznym nagłówku? I rozpada się dalej za pomocą szablonów.
czerwiec
13
Dlatego powiedziałem, że nie jest idealny, a idiom Pimpl jest potrzebny do większej separacji. Szablony to zupełnie inna puszka robaków - nawet jeśli słowo kluczowe „export” było w pełni obsługiwane w większości kompilatorów, nadal byłby to cukier syntaktyczny, a nie rzeczywista separacja.
Joris Timmermans,
4
Jak radzą sobie z tym inne języki? na przykład - Java? W Javie nie ma koncepcji pliku nagłówkowego.
Lazer,
8
@Lazer: Java jest łatwiejsze do analizy. Kompilator Java może parsować plik bez znajomości wszystkich klas w innych plikach, a później sprawdzić typy. W C ++ wiele konstrukcji jest niejednoznacznych bez informacji o typie, więc kompilator C ++ potrzebuje informacji o typach, do których istnieją odwołania, aby parsować plik. Dlatego potrzebuje nagłówków.
Niki
15
@nikie: Co ma z tym wspólnego „łatwość” analizowania? Jeśli Java miałaby gramatykę co najmniej tak złożoną jak C ++, nadal mogłaby używać plików java. W obu przypadkach co z C? C jest łatwy do przeanalizowania, ale używa zarówno plików nagłówkowych, jak i c.
Thomas Eding,
609

Kompilacja w C ++

Kompilacja w C ++ odbywa się w 2 głównych fazach:

  1. Pierwszą jest kompilacja „źródłowych” plików tekstowych w binarne pliki „obiektowe”: plik CPP jest plikiem skompilowanym i jest kompilowany bez wiedzy o innych plikach CPP (lub nawet bibliotekach), chyba że zostanie do niego dostarczony poprzez surową deklarację lub włączenie nagłówka. Plik CPP jest zwykle kompilowany do pliku „OB ”lub .OBJ.

  2. Drugim jest połączenie wszystkich plików „obiektowych”, a tym samym utworzenie końcowego pliku binarnego (biblioteki lub pliku wykonywalnego).

Gdzie HPP pasuje do tego procesu?

Słaby samotny plik CPP ...

Kompilacja każdego pliku CPP jest niezależna od wszystkich innych plików CPP, co oznacza, że ​​jeśli A.CPP potrzebuje symbolu zdefiniowanego w B.CPP, takiego jak:

// A.CPP
void doSomething()
{
   doSomethingElse(); // Defined in B.CPP
}

// B.CPP
void doSomethingElse()
{
   // Etc.
}

Nie skompiluje się, ponieważ A.CPP nie ma sposobu, aby wiedzieć, że istnieje „doSomethingElse”… Chyba że w A.CPP istnieje deklaracja, taka jak:

// A.CPP
void doSomethingElse() ; // From B.CPP

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

Następnie, jeśli masz C.CPP, który używa tego samego symbolu, to skopiuj / wklej deklarację ...

KOPIUJ / WKLEJ ALERT!

Tak, jest problem. Kopiowanie / wklejanie jest niebezpieczne i trudne w utrzymaniu. Co oznacza, że ​​byłoby fajnie, gdybyśmy mieli sposób NIE kopiować / wklejać i nadal deklarować symbol ... Jak możemy to zrobić? Przez dołączenie jakiegoś pliku tekstowego, który jest zwykle dodawany przez .h, .hxx, .h ++ lub, mój preferowany dla plików C ++, .hpp:

// B.HPP (here, we decided to declare every symbol defined in B.CPP)
void doSomethingElse() ;

// A.CPP
#include "B.HPP"

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

// B.CPP
#include "B.HPP"

void doSomethingElse()
{
   // Etc.
}

// C.CPP
#include "B.HPP"

void doSomethingAgain()
{
   doSomethingElse() ; // Defined in B.CPP
}

Jak includedziała

Dołączenie pliku w istocie parsuje, a następnie kopiuje i wkleja jego zawartość do pliku CPP.

Na przykład w następującym kodzie z nagłówkiem A.HPP:

// A.HPP
void someFunction();
void someOtherFunction();

... źródło B.CPP:

// B.CPP
#include "A.HPP"

void doSomething()
{
   // Etc.
}

... stanie się po włączeniu:

// B.CPP
void someFunction();
void someOtherFunction();

void doSomething()
{
   // Etc.
}

Jedna mała rzecz - dlaczego warto włączyć B.HPP w B.CPP?

W obecnym przypadku nie jest to konieczne, a B.HPP ma doSomethingElsedeklarację funkcji, a B.CPP ma doSomethingElsedefinicję funkcji (która sama w sobie jest deklaracją). Ale w bardziej ogólnym przypadku, gdy B.HPP jest używany do deklaracji (i kodu wbudowanego), może nie istnieć odpowiednia definicja (na przykład wyliczenia, zwykłe struktury itp.), Więc włączenie może być potrzebne, jeśli B.CPP korzysta z deklaracji B.HPP. Podsumowując, „dobrym gustem” jest, aby źródło domyślnie zawierało nagłówek.

Wniosek

Plik nagłówkowy jest zatem konieczny, ponieważ kompilator C ++ nie jest w stanie samodzielnie szukać deklaracji symboli, dlatego należy pomóc, włączając te deklaracje.

Ostatnie słowo: powinieneś umieścić osłony nagłówków wokół zawartości plików HPP, aby mieć pewność, że wiele inkluzji niczego nie zepsuje, ale w sumie uważam, że główny powód istnienia plików HPP został wyjaśniony powyżej.

#ifndef B_HPP_
#define B_HPP_

// The declarations in the B.hpp file

#endif // B_HPP_

lub nawet prościej

#pragma once

// The declarations in the B.hpp file
paercebal
źródło
2
@nimcap:: You still have to copy paste the signature from header file to cpp file, don't you?Nie ma potrzeby. Tak długo, jak CPP „zawiera” HPP, prekompilator będzie automatycznie kopiował i wklejał zawartość pliku HPP do pliku CPP. Zaktualizowałem odpowiedź, aby to wyjaśnić.
paercebal,
7
@Bob: While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself. Nie, nie ma. Zna tylko typy podane przez użytkownika, co w połowie czasu nie będzie nawet przeszkadzało w odczytaniu zwracanej wartości. Następnie zachodzą niejawne konwersje. A kiedy masz kod: foo(bar)nie możesz nawet być pewien, że foojest funkcją. Zatem kompilator musi mieć dostęp do informacji w nagłówkach, aby zdecydować, czy źródło kompiluje się poprawnie, czy nie ... Następnie, po skompilowaniu kodu, linker po prostu połączy połączenia funkcji.
paercebal
3
@Bob: [kontynuuje] ... Teraz linker może wykonać pracę kompilatora, tak sądzę, co umożliwiłoby ci wybór. (Myślę, że jest to temat propozycji „modułów” do następnego standardu). Seems, they're just a pretty ugly arbitrary design.: Gdyby C ++ został stworzony w 2012 roku. Ale pamiętaj, że C ++ został zbudowany na C w latach 80., a wówczas ograniczenia były w tym czasie zupełnie inne (IIRC, dla celów adopcji, postanowiono zachować te same linkery niż C).
paercebal
1
@paercebal Dzięki za wyjaśnienia i notatki, paercebal! Dlaczego nie mogę być pewien, że foo(bar)jest to funkcja - jeśli jest uzyskiwana jako wskaźnik? Mówiąc o złym projekcie, obwiniam C, a nie C ++. Naprawdę nie lubię niektórych ograniczeń czystego C, takich jak pliki nagłówkowe lub funkcje zwracające jedną i tylko jedną wartość, jednocześnie przyjmując wiele argumentów na wejściu (nie wydaje się naturalne, aby wejścia i wyjścia zachowywały się w podobny sposób ; dlaczego wiele argumentów, ale pojedyncze wyjście?) :)
Boris Burkov
1
@ Bobo:: Why can't I be sure, that foo(bar) is a functionfoo może być typem, więc można by wywołać konstruktora klasy. In fact, speaking of bad design, I blame C, not C++: Mogę obwiniać C za wiele rzeczy, ale bycie zaprojektowanym w latach 70. nie będzie jedną z nich. Ponownie, ograniczenia tego czasu ... such as having header files or having functions return one and only one value: Krotki mogą pomóc to złagodzić, a także przekazywać argumenty przez referencję. Jaka byłaby składnia, aby odzyskać wiele wartości i czy warto byłoby zmienić język?
paercebal
93

Ponieważ C, skąd narodziła się koncepcja, ma 30 lat, a wtedy był to jedyny możliwy sposób na połączenie kodu z wielu plików.

Dzisiaj jest to okropny hack, który całkowicie niszczy czas kompilacji w C ++, powoduje niezliczone niepotrzebne zależności (ponieważ definicje klas w pliku nagłówkowym ujawniają zbyt wiele informacji o implementacji) i tak dalej.

jalf
źródło
3
Zastanawiam się, dlaczego pliki nagłówkowe (lub cokolwiek tak naprawdę potrzebne do kompilacji / łączenia) nie były po prostu „automatycznie generowane”?
Mateen Ulhaq
54

Ponieważ w C ++ końcowy kod wykonywalny nie zawiera żadnych informacji o symbolach, jest to mniej więcej czysty kod maszynowy.

Dlatego potrzebujesz sposobu na opisanie interfejsu fragmentu kodu, który jest niezależny od samego kodu. Ten opis znajduje się w pliku nagłówkowym.

rozwijać
źródło
16

Ponieważ C ++ odziedziczył je po C. Niestety.

andref
źródło
4
Dlaczego dziedziczenie C ++ od C jest niefortunne?
Lokesh
3
@Lokesh Ze względu na swój bagaż :(
陳 力
1
Jak to może być odpowiedź?
Shuvo Sarker
14
@ShuvoSarker, ponieważ jak pokazały tysiące języków, nie ma technicznego wyjaśnienia dla C ++ zmuszającego programistów do dwukrotnego pisania podpisów funkcji. Odpowiedź na „dlaczego?” to „historia”.
Boris
15

Ponieważ ludzie, którzy zaprojektowali format biblioteki, nie chcieli „marnować” miejsca na rzadko używane informacje, takie jak makra preprocesora C i deklaracje funkcji.

Ponieważ potrzebujesz tych informacji, aby poinformować kompilator „ta funkcja jest dostępna później, gdy linker wykonuje swoją pracę”, musieli wymyślić drugi plik, w którym mogłyby być przechowywane te wspólne informacje.

Większość języków po C / C ++ zapisuje te informacje w danych wyjściowych (na przykład kod bajtowy Java) lub w ogóle nie używają formatu prekompilowanego, zawsze są dystrybuowane w formie źródłowej i kompilowane w locie (Python, Perl).

Aaron Digulla
źródło
Nie działałoby, cykliczne odwołania. Nie możesz zbudować a.lib z a.cpp przed zbudowaniem b.lib z b.cpp, ale nie możesz też zbudować b.lib przed a.lib.
MSalters
20
Java rozwiązała to, Python może to zrobić, każdy współczesny język może to zrobić. Ale w momencie wynalezienia C pamięć RAM była tak droga i skąpa, że ​​po prostu nie było takiej opcji.
Aaron Digulla,
6

Jest to preprocesorowy sposób deklarowania interfejsów. Interfejs (deklaracje metod) umieszczasz w pliku nagłówkowym, a implementację w cpp. Aplikacje korzystające z Twojej biblioteki muszą znać interfejs, do którego mogą uzyskać dostęp poprzez #include.

Martin v. Löwis
źródło
4

Często będziesz chciał mieć definicję interfejsu bez konieczności wysyłania całego kodu. Na przykład, jeśli masz bibliotekę współdzieloną, dostarczasz z nią plik nagłówka, który definiuje wszystkie funkcje i symbole używane w bibliotece współdzielonej. Bez plików nagłówkowych należy wysłać źródło.

W ramach jednego projektu wykorzystywane są pliki nagłówkowe IMHO do co najmniej dwóch celów:

  • Przejrzystość, tzn. Oddzielając interfejsy od implementacji, łatwiej jest odczytać kod
  • Czas kompilacji. Używając tylko interfejsu tam, gdzie to możliwe, zamiast pełnej implementacji, czas kompilacji można skrócić, ponieważ kompilator może po prostu odwoływać się do interfejsu zamiast parsować rzeczywisty kod (co idealnie byłoby tylko zrobić jednorazowo).
user21037
źródło
3
Dlaczego dostawcy bibliotek nie mogli po prostu wysłać wygenerowanego pliku „nagłówka”? Plik „nagłówka” wolnego od procesora powinien zapewniać znacznie lepszą wydajność (chyba że implementacja została naprawdę zepsuta).
Tom Hawtin - tackline
Wydaje mi się, że nie ma znaczenia, czy plik nagłówkowy jest generowany czy pisany ręcznie, pytanie nie brzmiało „dlaczego ludzie sami piszą pliki nagłówkowe?”, Ale „dlaczego mamy pliki nagłówkowe”. To samo dotyczy nagłówków wolnych od preprocesora. Jasne, byłoby to szybsze.
-5

Odpowiadając na odpowiedź MadKeithV ,

Zmniejsza to zależności, dzięki czemu kod wykorzystujący nagłówek niekoniecznie musi znać wszystkie szczegóły implementacji i inne klasy / nagłówki potrzebne tylko do tego. Skróci to czas kompilacji, a także ilość ponownej kompilacji potrzebnej, gdy coś w implementacji ulegnie zmianie.

Innym powodem jest to, że nagłówek nadaje unikalny identyfikator każdej klasie.

Więc jeśli mamy coś takiego

class A {..};
class B : public A {...};

class C {
    include A.cpp;
    include B.cpp;
    .....
};

Podczas próby zbudowania projektu wystąpią błędy, ponieważ A jest częścią B, dzięki nagłówkom uniknęlibyśmy tego rodzaju bólu głowy ...

Alex przeciwko
źródło