Dlaczego C ++ ma pliki nagłówkowe i .cpp?
Dlaczego C ++ ma pliki nagłówkowe i .cpp?
Odpowiedzi:
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.
Kompilacja w C ++ odbywa się w 2 głównych fazach:
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.
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?
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ę ...
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
}
include
działaDołą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.
}
W obecnym przypadku nie jest to konieczne, a B.HPP ma doSomethingElse
deklarację funkcji, a B.CPP ma doSomethingElse
definicję 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.
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
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ć.
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 foo
jest 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.
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).
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?) :)
Why can't I be sure, that foo(bar) is a function
foo 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?
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.
Ponieważ C ++ odziedziczył je po C. Niestety.
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).
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.
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:
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 ...