To prawdopodobnie bardziej szczegółowa odpowiedź, niż chciałeś, ale myślę, że przyzwoite wyjaśnienie jest uzasadnione.
W językach C i C ++ jeden plik źródłowy jest definiowany jako jedna jednostka tłumaczenia . Zgodnie z konwencją, pliki nagłówkowe zawierają deklaracje funkcji, definicje typów i definicje klas. Rzeczywiste implementacje funkcji znajdują się w jednostkach tłumaczeniowych, tj. Plikach .cpp.
Pomysł polega na tym, że funkcje i funkcje składowe klasy / struktury są kompilowane i składane raz, a następnie inne funkcje mogą wywoływać ten kod z jednego miejsca bez tworzenia duplikatów. Twoje funkcje są domyślnie deklarowane jako „extern”.
/* Function declaration, usually found in headers. */
/* Implicitly 'extern', i.e the symbol is visible everywhere, not just locally.*/
int add(int, int);
/* function body, or function definition. */
int add(int a, int b)
{
return a + b;
}
Jeśli chcesz, aby funkcja była lokalna dla jednostki tłumaczeniowej, definiujesz ją jako „statyczną”. Co to znaczy? Oznacza to, że jeśli włączysz pliki źródłowe z funkcjami extern, otrzymasz błędy redefinicji, ponieważ kompilator napotka tę samą implementację więcej niż raz. Dlatego chcesz, aby wszystkie jednostki tłumaczeniowe widziały deklarację funkcji, ale nie jej treść .
Jak więc to wszystko się na końcu zlewa? To jest praca linkera. Linker czyta wszystkie pliki obiektowe, które są generowane przez etap asemblera i rozwiązuje symbole. Jak powiedziałem wcześniej, symbol to tylko nazwa. Na przykład nazwa zmiennej lub funkcji. Gdy jednostki tłumaczeniowe, które wywołują funkcje lub deklarują typy, nie znają implementacji tych funkcji lub typów, mówi się, że te symbole są nierozwiązane. Linker rozwiązuje nierozwiązany symbol, łącząc jednostkę translacyjną, która zawiera niezdefiniowany symbol, z tym, który zawiera implementację. Uff. Dotyczy to wszystkich symboli widocznych na zewnątrz, niezależnie od tego, czy są one zaimplementowane w kodzie, czy dostarczane przez dodatkową bibliotekę. Biblioteka to tak naprawdę tylko archiwum z kodem wielokrotnego użytku.
Istnieją dwa godne uwagi wyjątki. Po pierwsze, jeśli masz małą funkcję, możesz ją wstawić. Oznacza to, że wygenerowany kod maszynowy nie generuje wywołania funkcji extern, ale jest dosłownie konkatenowany w miejscu. Ponieważ zwykle są małe, rozmiar narzutu nie ma znaczenia. Możesz sobie wyobrazić, że działają statycznie. Dlatego bezpieczne jest implementowanie funkcji inline w nagłówkach. Implementacje funkcji wewnątrz definicji klasy lub struktury są również często wstawiane automatycznie przez kompilator.
Innym wyjątkiem są szablony. Ponieważ kompilator musi widzieć całą definicję typu szablonu podczas ich tworzenia, nie jest możliwe oddzielenie implementacji od definicji, tak jak w przypadku funkcji autonomicznych lub normalnych klas. Cóż, być może jest to teraz możliwe, ale uzyskanie szerokiego wsparcia kompilatora dla słowa kluczowego „eksport” zajęło dużo czasu. Tak więc bez obsługi „eksportu” jednostki tłumaczeniowe otrzymują swoje własne lokalne kopie typów i funkcji z szablonami, na których działają instancje, podobnie jak działają funkcje wbudowane. W przypadku obsługi „eksportu” tak nie jest.
Z dwóch wyjątków niektórzy uważają, że „przyjemniej” jest umieścić implementacje funkcji wbudowanych, funkcji opartych na szablonach i typów opartych na szablonach w plikach .cpp, a następnie # uwzględniać plik .cpp. Nie ma znaczenia, czy jest to nagłówek, czy plik źródłowy; preprocesor nie dba o to i jest tylko konwencją.
Krótkie podsumowanie całego procesu od kodu C ++ (kilka plików) do końcowego pliku wykonywalnego:
- Uruchomiony zostaje preprocesor , który analizuje wszystkie dyrektywy zaczynające się od znaku „#”. Dyrektywa #include łączy dołączony plik z gorszym, na przykład. Wykonuje również zastępowanie makr i wklejanie tokenów.
- Rzeczywisty kompilator działa na pośrednim pliku tekstowym po etapie preprocesora i emituje kod asemblera.
- W asemblera działa na pliku montaż i emituje kodu maszynowego, jest to zwykle nazywa się plik obiektu i następuje binarny format wykonywalny systemu operacyjnego, o którym mowa. Na przykład Windows używa PE (przenośny format wykonywalny), podczas gdy Linux używa formatu ELF Unix System V z rozszerzeniami GNU. Na tym etapie symbole są nadal oznaczone jako niezdefiniowane.
- Na koniec uruchamiany jest konsolidator . Wszystkie poprzednie etapy zostały przeprowadzone na każdej jednostce tłumaczeniowej po kolei. Jednak etap konsolidatora działa na wszystkich wygenerowanych plikach obiektów, które zostały wygenerowane przez asembler. Linker rozwiązuje symbole i wykonuje wiele magicznych czynności, takich jak tworzenie sekcji i segmentów, co jest zależne od platformy docelowej i formatu binarnego. Generalnie programiści nie muszą o tym wiedzieć, ale w niektórych przypadkach z pewnością pomaga.
Ponownie, było to zdecydowanie więcej, niż prosiłeś, ale mam nadzieję, że drobiazgowe szczegóły pomogą ci zobaczyć większy obraz.