- Po co są w ogóle pliki nagłówkowe?
Pozwalają na współdzielenie danych między jednostkami kompilacji. Ogólnie temat jest dość złożony, ale postaram się nieco przybliżyć działanie kompilacji i linkowania (w sporym uproszczeniu).
Na początek, czym różni się deklaracja od definicji.
- Deklaracja to określenie, że gdzieś w programie znajduje się taka zmienna bądź funkcja. Jej wartość jest nieznana, po prostu wiemy, że gdzieś jest.
- Definicja to zdefiniowanie w danym miejscu danej funkcji bądź zmiennej. Wszelkie odwołania do zmiennej piszą do tego konkretnego miejsca w pamięci. Wszelkie odwołania do funkcji będą wykonywały waśnie ten kod.
int foo(int x); //< to jest deklaracja funkcji – gdzies oczekujemy definicji
int foo(int x) { return x*x; } //< to jest definicja – ten kod bedzie wywolany przy kazdym wywolaniu tej funkcji gdzies w programie
Teraz fazy kompilacji:
Kompilacja
Kompilowane są osobno pliki .cpp jako tzw. jednostki kompilacji. GCC tworzy pliki .o, VC++ .obj. Pierwszą fazą kompilacji jest wykonanie dyrektyw preprocesora, m.in. #include
. Ta dyrektywa to nic innego jak wklejenie tekstu z nagłówka w miejsce dyrektywy.
Następnie odbywa się właściwa kompilacja. Definicje funkcji i zmiennych globalnych są „wystawione” na zewnątrz jednostki kompilacji dla fazy linkowania całego programu. Jeżeli dana jednostka korzysta z deklaracji funkcji, to w miejsce jej wywołania wstawiany jest odwołanie do odpowiedniej definicji obiektu (funkcji lub zmiennej – od tej pory będę pisał obiekt mając na myśli funkcję bądź zmienną globalną). Odwołanie jest po prostu nieco „upiększoną” nazwą tego obiektu (tzw. name mangling).
Linkowanie
Kolejnym krokiem jest linkowanie, czyli składanie programu do kupy. Linker skanuje wszystkie jednostki kompilacji w poszukiwaniu znaczników (odwołań). Jeśli taki znajdzie, to skanuje wszystkie jednostki w poszukiwaniu definicji takiego obiektu i wstawia go w miejsce znacznika.
I tutaj właśnie napotkałeś swój problem, bo w dwóch jednostkach kompilacji miałeś definicję obiektu o takim samym identyfikatorze. W takim wypadku linker sygnalizuje błąd redefinicji, bo nie wie, której definicji ma użyć. Może się też zdarzyć błąd braku definicji, jeśli odwołujesz się do obiektu, który jest tylko zadeklarowany, ale nie ma nigdzie definicji.
Po przydługim wstępie właściwa odpowiedź na postawione pytanie: pliki nagłówkowe zawierają deklaracje funkcji i zmiennych globalnych. Bez deklaracji niemożliwe jest skompilowanie jednostki kompilacji, bo muszą zostać powstawiane odwołania do definicji. Takie odwołania są zamieniane na prawdziwe wywołania funkcji i zmiennych na etapie linkowania.
- Jak się dzieli kod, programując tego typu aplikacje? Na wykładzie nakazali mi jakoś tak to dzielić, ale czy nie byłoby w zgodzie ze "sztuką" załączenie wszystkiego w jednym pliku main.cpp?
Dzieli się tak, że w każdej jednostce kompilacji definiuje się zmienne globalne i funkcje odpowiadające jakiemuś logicznemu podziałowi programu. Na przykład funkcje operujące na tekście – taka mini-biblioteka – może zostać wydzielona do osobnego pliku .cpp. Natomiast w odpowiadającym mu pliku nagłówkowym wystawia się deklaracje zdefiniowanych w nim funkcji i zmiennych tak, aby inne jednostki kompilacji mogły powstawiać odpowiednie odwołania dla linkera.
Łączenie wszystkiego w jeden plik .cpp jest czymś zupełnie łamiącym „sztukę”. Wyobrażasz sobie jakiś większy program w jednym pliku? Ostatni projekt w C++ nad jakim pracowałem miał około 500000 (pół miliona) linii kodu. Taki plik miałby ~40 MB czystego tekstu. Niemożliwym jest pracować nad taką kobyłą.
Poza tym podział na jednostki kompilacji pozwala przyspieszyć proces kompilacji. Jeśli nie było żadnych zmian w pliku .cpp to nie ma potrzeby ponowna kompilacja, można wykorzystać istniejący już plik obiektowy (.o albo .obj) do samego linkowania. Zmiana tylko w jednym pliku nie powoduje pełnej rekompilacji a tylko kompilację tego zmienionego pliku, reszta już jest gotowa i czeka na zlinkowanie.