C++ bez choletsterolu

Moduły, czyli praca z wieloma plikami

Jak powstaje aplikacja

Poza samym jako-takim językiem, C++ definiuje również sposób dzielenia programu na kawałki, które będą się potem składały na gotowy program. Standard do tego celu używa dość niezależnego od środowiska pojęcia "jednostka kompilacji", ale tak naprawdę to owo określenie oznacza plik z końcówką .c* (tzw. pliki źródłowe - napisałem w ten sposób, bo pliki źródłowe w C++ mogą mieć różne końcówki; najbardziej znane są .cc, .cpp, .cxx oraz wprowadzająca niekiedy straszne zamieszanie .C - w odróżnieniu od .c przeznaczonych dla języka C).

C++ odziedziczył po języku C podział na tzw. moduły (choć oficjalnie nikt tak tego nie nazywa). Moduły istnieją na poziomie systemu i są w pewnym sensie punktem stycznym systemu i języka. Każdy plik będący jednostką kompilacji jest mielony przez kompilator, w wyniku czego powstaje plik obiektowy (czasem jeszcze pośrednio przez asembler). Plik ten zawiera kod źródłowy skompilowany do kodu maszynowego, ale jeszcze nie gotowy do użycia. Znajdują się w nim bowiem miejsca, w których następują odwołania do różnych istnień fizycznych (nazwijmy to tak), które się w tym pliku nie znajdują (tzn. albo do obiektów, albo funkcji).

Mając grupę takich plików oraz pliki z bibliotekami, do roboty bierze się linker, który przeprowadza proces wiązania (ang. linking). Polega to na tym, że zbiera wszystkie te "istnienia fizyczne", znajduje w nich odwołania do "symboli" (tak się to oficjalnie określa) i każde z nich próbuje "rozwiązać" (ang. resolve). Jeśli mu się to uda, to modyfikuje kod maszynowy z danego pliku, żeby odwołanie do nieznanego symbolu zamienić na odwołanie do rzeczywistego istnienia. Symbole takie są znajdowane we wszystkich plikach, które linkerowi nakazano brać pod uwagę, a także w branych pod uwagę plikach bibliotek. W wyniku tego procesu tworzy on dopiero plik wykonywalny.

Jednostki kompilacji

Źródło programu w C++, jak wiemy, składa się z plików źródłowych (*.c*) i plików nagłówkowych (*.h*). Pliki źródłowe to są te, które są bezpośrednio podawane kompilatorowi w celu stworzenia pliku obiektowego. Pliki nagłówkowe zaś służą kilku celom. Najważniejszym z nich jest dostarczenie definicji, z których elementy programu zawarte w pliku źródłowym mają skorzystać. Standard, żeby było śmieszniej, w ogóle nie określa osobnych pojęć dla plików źródłowych i plików nagłówkowych - może to i dobrze, bo standard opisuje wymagania dla kompilatora, a nie dla programisty, który pisze program.

Ponieważ plik nagłówkowy "włącza" się (ang. include) do pliku źródłowego, zatem w efekcie działania preprocesora wszystkie definicje są tam "na żywca" wstawiane (no, z tym żywcem to też nie do końca, bo jeszcze jest #define). Dopiero to, co preprocesor wyprodukuje, brane jest do kompilacji. Jeden plik nagłówkowy może być oczywiście wczytywany przez różne pliki źródłowe, w związku z czym dostarcza on wspólne deklaracje, dzielone między sobą przez pliki źródłowe. Widać z tego wyraźnie zatem, że wszystkie deklaracje, które muszą być dzielone między plikami, powinny być wprowadzane wyłącznie w plikach nagłówkowych. Jeśli deklaracje dotyczące tej samej rzeczy będą się różnić w różnych plikach źródłowych, narazimy się na mnóstwo błędów, których żadne z narzędzi (ani kompilator, ani linker) nie wykrywa.

Przykład? A choćby zapomnijmy nakazanie przekompilowania któregoś z plików źródłowych w wyniku zmiany w pliku nagłówkowym. Do linkera pójdzie wtedy jeden plik ze starą wersją pliku nagłowkowego i drugi z nową. Powiedzmy, że mamy w nim strukturę z polami "bolek" i "lolek". Teraz wprowadzimy sobie pole "tosia" pomiędzy te dwa. Jeden plik skompilowaliśmy gdy za bolkiem byl lolek, a drugi gdy za bolkiem byla tosia. Co wtedy? Wtedy będziemy mieli do czynienia z taką sytuacją, że funkcja z tego pierwszego pliku może zmienić pole "lolek", ale funkcja z drugiego pliku zobaczy, że zmieniło się pole "tosia". Taka sytuacja zresztą będzie miała miejsce i tak tylko w przypadku, gdy te pola są jednakowego typu. No, powiedzmy jednakowego rozmiaru. Odwołania do pól struktur są bowiem realizowane w postaci tzw. delty, która jest całkowitym przesunięciem względem początku struktury. To nic, że w obu plikach mamy odwołanie do "lolek"; jeśli jeden plik był skompilowany z innym układem pól, niż drugi, to polu "lolek" będzie odpowiadało zupełnie inne przesunięcie w skompilowanym kodzie.

Widzimy zatem, że w C++ istnieją definicje, które muszą być dostarczone wspólnie i najczęściej dotyczy to definicji oznaczanych po prostu tym samym symbolem. W niektórych sytuacjach można dla nich nie korzystać z plików nagłówkowych, ale są to wyłącznie szczególne sytuacje. W większości przypadków powinniśmy korzystać z plików nagłówkowych.

Każda jednostka kompilacji dostarcza zatem różne "fizyczne definicje", które następnie zostaną odtworzone odpowiednio w pliku obiektowym. Zaś definicje dostarczane przez plik nagłówkowy to definicje "ulotne", gdyż w plikach obiektowych nie zostaną (w praktyce) odtworzone w żaden sposób. Mylenie definicji fizycznych i ulotnych jest jednym z częściej popełnianych błędów przez początkujących. A zarazem uczciwie trzeba stwierdzić, że te różnice nie są takie proste.

Definicje fizyczne

Definicje fizyczne stanowią obiekty globalne oraz "regularne" funkcje (regularne, czyli nie inline). Definicjom tym będą odpowiadały konkretne rzeczy w pliku obiektowym. Definicje te będą się zawierały w konkretnych plikach obiektowych. Jeśli odwołania do nich będą zawarte w innych plikach obiektowych, linker zamieni je na odwołania do konkretnych "fizycznych istnień".

Rozróżniamy dwa rodzaje definicji fizycznych: dzielone i lokalne. Do obiektów i funkcji dzielonych mogą istnieć odwołania z innych plików obiektowych. Aby te odwołania mogły istnieć, konieczne jest dostarczenie w pliku źródłowym odpowiednich definicji ulotnych, które będą się do nich odwoływały. Należy wyróżnić tutaj właściwie trzy typy fizycznych istnień (w C akurat wystarczyły dwa): obiekty zmienne, obiekty stałe i funkcje.

Duże podobieństwa istnieją pomiędzy obiektami zmiennymi i funkcjami. Aby zadeklarować je jako istnienia dzielone między plikami obiektowymi, nie potrzebują żadnych dodatkowych deklaracji (są takie domyślnie). Aby były one lokalne, należy umieścić je wewnątrz anonimowych przestrzeni nazw (alternatywną metodą jest poprzedzenie ich deklaracji słowem static, ale ta metoda nie jest polecana). Możliwe jest używanie opcjonalne słowa extern, ale w przypadku "pełnych definicji" nie ma ono dla tych rzeczy znaczenia.

Wspomniałem o extern, bo jest tutaj pewien drobny kruczek. Otóż obiekty zmienne możemy dostarczyć wraz z inicjalizacją, albo bez (bez tego obiekty te są na starcie wypełniane zerami - UWAGA! Zerami, a nie konstruktorem domyślnym!). Wiemy już, że extern służy głównie do importowania definicji, ale dzieje się tak tylko w przypadku, gdy mamy do czynienia z "zapowiedzią", a nie z pełną definicją. W przypadku funkcji ich pełną definicję stanowi nagłowek funkcji wraz z jej ciałem. I akurat w przypadku funkcji, w zapowiedzi wystarczy zakończyć nagłówek średnikiem; extern już nie jest potrzebne. W przypadku zmiennych jest niestety drobne zamieszanie. Otóż deklaracja zmiennej globalnej domyślnie deklaruje zmienną dzieloną. Możemy ją również zainicjalizować. Jednak obie te postacie stanowią deklarację fizyczną (tu za wyjątkowość należy uznać to, że "int i" i "int i = 0" stanowią deklarację fizyczną). Różnice istnieją dopiero w momencie, gdy do tej deklaracji dodamy słowo extern. Wtedy to pierwsze stanowi tylko "zapowiedź" owej zmiennej, podczas gdy druga (extern int i = 0) jest deklaracją fizyczną, identyczną z "int i = 0". Dlatego ostrzegam uczciwie, żeby tej konstrukcji NIGDY nie używać (gcc rzuca do niej ostrzeżenie, nawet bez -Wall).

Natomiast stałe są obarczone trochę innymi regułami. Domyślnie deklaracje bez dodatkowych oznaczeń są lokalne na daną jednostkę kompilacji (ma to swoje dobre strony, bo najczęściej const używa się dla stałych całkowitych, więc byłoby bez sensu, gdyby je trzeba było dodatkowo oznaczać, jeśli się chce, by były one zrealizowane przez kompilator). Aby uczynić je dzielonymi, należy ich deklarację poprzedzić słowem extern. Zapowiedź zatem różni się od definicji fizycznej tym, że definicja fizyczna ma dostarczoną inicjalizację.

Definicje ulotne

Definicje ulotne to te, którym w pliku obiektowym nie będzie odpowiadało żadne konkretne istnienie fizyczne. Przykładem tej definicji są już wspomniane zapowiedzi zmiennych i funkcji dzielonych. Należą do nich również deklaracje struktur i klas oraz funkcje inline.

Definicje ulotne powinny zawierać się wyłącznie w plikach nagłówkowych. Wynika to z faktu, jak wspomniałem, że takie definicje, jeśli są używane w ten sam sposób przez pliki źródłowe, to muszą mieć identyczną deklarację. Sam się zresztą jakiś czas temu na to paskudnie nadziałem: zrobiłem zmienną globalną typu "short int", ale w drugim pliku dostarczyłem jej definicję "extern int". Ponieważ akurat na tych platformach (i takie najczęściej są teraz w użyciu) rozmiary tych typów się różnią, więc powodowało to, że w tym drugim pliku próbowano zapisywać w niewłaściwym miejscu pamięci. Błąd ten jest niewykrywalny przez kompilator ani linker w takiej postaci!

Co więc należy zrobić, żeby taki potencjalny błąd został wykryty? Oczywiście należy umieścić taką deklarację w pliku nagłówkowym. Ale to nie wszystko! Ten plik nagłówkowy musi być wczytany również w tym pliku źródłowym, w którym dostarczono daną deklarację fizyczną. Wszystko jest w porządku jeśli dostarczymy zapowiedź i w tym samym pliku definicję fizyczną. Jeśli jednak definicja fizyczna danego symbolu byłaby niezgodna z zapowiedzią, kompilator to natychmiast wykryje. W ten sposób zapewniamy, że zapowiedź ma deklarację zgodną z definicją fizyczną.

Do deklaracji ulotnych zalicza się też funkcje inline. Dlatego taka funkcja musi mieć pełną definicję dostarczoną w pliku nagłówkowym. Co ciekawe, może być ona opcjonalnie lokalna. W ogólności nie powinno to niczego zmienić, ale cały myk w tym, że owo "inline" dla kompilatora jest jedynie podpowiedzią; kompilator nie musi dokonywać rozwijania takiej funkcji (można to też po prostu włączyć odpowiednimi opcjami kompilatora, np. -fno-default-inline w gcc). W takim przypadku kompilator zrobi "wersje outline" funkcji inline. Dlatego właśnie kwestia tego, czy funkcja inline jest lokalna, czy dzielona, ma również znaczenie - skoro z dużym prawdopodobieństwem kompilator wygeneruje "wersję outline", to taka funkcja też może być lokalna albo dzielona.

Słabe i silne wiązanie

Definicje w C++ w zależności od rodzaju podlegają też dwóm rodzajom wiązania, które jest odpowiednio uwzględniane przez linker. Sprawa sprowadza się do kwestii napotkania przez linker dwóch symboli o tej samej nazwie (czyli dwóch osobnych fizycznych istnień znalezionych w składnikach do wiązania, mających tą samą nazwę). Jeśli definicja, do której odnosi się dany symbol, podlega słabemu wiązaniu, to linker wybierze sobie z nich "pierwszy lepszy". Jeśli podlega silnemu wiązaniu, to dany symbol może wystąpić w składnikach dokładnie raz; jeśli wystąpi więcej niż raz, linker zgłasza błąd.

Tu zresztą istnieje istotna różnica pomiędzy C a C++. W C obowiązuje wyłacznie słabe wiązanie. Spróbuj sobie np. skompilować (do pliku obiektowego, czyli w przypadku gcc będzie to "gcc -c <plik>") plik w C, który zawiera takie coś: "int i; int i;". Skompiluje się? Oczywiście. Wszystkie 'i' będą się odnosiły do tego samego obiektu. To samo, jeśli w dwóch osobnych plikach zostaną zamieszczone dwie deklaracje zmiennych o tej samej nazwie.

W C++ natomiast istnieją dwa rodzaje obiektów: jawne i niejawne. Obiekty jawne podlegają silnemu wiązaniu. Jeśli wspomniany plik spróbujesz skompilować jako plik C++, kompilator zgłosi błąd. Wszelkie obiekty (i funkcje) występujące w języku C to obiekty jawne, dopiero w C++ występują obiekty niejawne (mówię o ANSI C - nie wiem, jak w C99). Co to takiego zatem obiekty niejawne?

Obiekty niejawne to są takie obiekty, które zostaną wygenerowane dla każdego pliku obiektowego osobno, ale w zamierzeniu mają oznaczać ten sam obiekt. W C++ są to (przynajmniej jak na razie): obiekty charakterystyczne klas polimorficznych (będzie przy programowaniu obiektowym), "wersje outline" funkcji inline, no i najprawdopodobniej też obiekty RTTI. Obiekty niejawne to są obiekty, które nie pochodzą z żadnych definicji fizycznych, tylko zostały "na rzecz" niektórych definicji ulotnych wygenerowane przez kompilator.

Oczywiście słabe wiązanie (ang. vague linkage, przynajmniej takie określenie spotkałem w dokumentacji do gcc) ma swoje poważne konsekwencje, co najbardziej widać w takich definicjach, które mogłyby się różnić w różnych plikach źródłowych. Najbardziej jaskrawym (ale na szczęście chyba jedynym) przykładem są funkcje inline. Jeśli kompilator akurat nie będzie rozwijał funkcji, to musi do nich dostarczyć wersje outline. Ale definicje funkcji inline są dostarczone osobno w każdym pliku źródłowym, zatem w każdym pliku źródłowym zostanie zamieszczona taka wersja tej funkcji. Na szczęście jest to obiekt niejawny, więc podlega on słabemu wiązaniu. Problem w tym, że - jak wspomniałem - słabe wiązanie polega na tym, że linker wybierze "pierwszy lepszy". Dlatego właśnie w standardzie jest określone, że funkcja inline musi być identycznie zdefiniowana w każdej jednostce kompilacji wchodzącej do składników wiązania. Niejawnie jest to oczywiście wymuszenie, że funkcja inline musi być zdefiniowana w pliku nagłówkowym.

Poeksperymentowałem swego czasu z tą kwestią w gcc, dostarczając różne definicje funkcji inline w osobnych plikach źródłowych. To, jaką funkcję faktycznie linker wybierze do wiązania zależało od kolejności plików obiektowych podanych do polecenia linkera. Jak więc widzimy, jest to poważne zastrzeżenie, którego nie wolno lekceważyć.

Oczywiście istnieją też inne ciekawe właściwości kompilatorów. Np. Visual C++ 6.0 (nie testowałem, jak to jest na 7) nie dostarczał wersji outline funkcji inline, jeśli nie zażądano nierozwijania funkcji. To powodowało, że jeśli się nie skompilowało wszystkich składników konsekwentnie z żądaniem rozwijania lub nierozwijania funkcji, to następował błąd podczas wiązania. Nie ma tego problemu w gcc; ten dostarcza zawsze do każdej funkcji inline wersję outline.

Biblioteki

Grupy plików obiektowych można łączyć w jedno wspólne archiwum zwane biblioteką. Początkowo istniały wyłącznie biblioteki statyczne (co powodowało paskudne powiększanie się plików wykonywalnych), ale od dłuższego czasu są w użyciu biblioteki dynamiczne.

Najprostsze do wyjaśnienia są biblioteki statyczne. Biblioteka statyczna to nic innego jak archiwum z plikami obiektowymi. Wiązanie z użyciem tej biblioteki wygląda zatem tak samo, jakby te pliki były jawnie podane jako składniki wiązania. Biblioteki jednak otrzymują odpowiednie wsparcie od systemu i są inaczej traktowane przez kompilator. Pod uniksami przykładowo, jeśli chcemy dodać bibliotekę do składników wiązania, nie dodajemy jawnie pliku biblioteki. Jeśli plik biblioteki ma nazwę np. "libuser.a", to do kompilatora dodajemy opcję "-luser". Istnieją też często zmienne środowiskowe, wskazujące na ścieżki, w jakich tych bibliotek należy szukać (pod unixami LD_LIBRARY_PATH oraz dodatkowo można je też dodać w opcjach kompilatora). Nie mamy jednak jasno określone, czy chcemy używać biblioteki statycznej, czy dynamicznej. Przykładowo w przypadku gcc (i pewnie wszystkich kompilatorów uniksowych) jest tak, że wiązanie jest z biblioteką dynamiczną, chyba że kompilator znalazł tylko bibliotekę statyczną (ewentualnie można wymusić wiązanie statyczne opcją -static).

Z bibliotekami dynamicznymi sprawa jest troszkę bardziej skomplikowana. Jeśli linker zdecyduje się zastosować bibliotekę dynamiczną, to w pliku wykonywalnym zapamiętywana jest jej nazwa (wraz z wersją), a gdy się ten plik odpali, następuje proces wiązania dynamicznego (wykonywany przez system). Cały prawdziwy proces wiązania rozpoczyna się dopiero teraz, w tym wszelkie błędy wiązania, które mogłyby podczas tego procesu wystąpić, są zgłaszane dopiero wtedy. Proces wiązania przebiega oczywiście w ten sam sposób, jak proces wiązania statycznego. W przypadku bibliotek dynamicznych proces wiązania sprowadza się jedynie do sprawdzenia zgodności symboli ze znalezioną podczas tego procesu biblioteką. Zwracam na to uwagę, bo nie jest niczym nienormalnym, żeby wyprodukowana binarka próbowała się dynamicznie wiązać z biblioteką dynamiczną znajdującą się w zupełnie innym pliku, niż ta, do której referował linker. Tak sprawa wygląda oczywiście na unixie, gdzie biblioteki dynamiczne to pliki z końcówką .so (plus ewentualnie jakieś .1.2 jako numer wersji).

Sprawa ta jest znacznie bardziej skomplikowana pod systemem Windows, jako że ten system nie posiada naturalnego wsparcia dla bibliotek dynamicznych (w Cygwinie zrobiono naprawdę kawał niezłej gimnastyki, żeby to uprościć i uczynić w miare bliskim uniksa). Tam niestety linker nie odwołuje się do bibliotek dynamicznych. Każda biblioteka dynamiczna (plik z końcówką .dll) musi mieć dostarczony odpowiadający jej plik biblioteki statycznej (zwykle z końcówką .lib; pod Cygwinem te biblioteki mają końcówki .dll.a). Linker wtedy wiąże się z tą biblioteką statyczną, która zawiera tylko odpowiednie odwołania do biblioteki dynamicznej (.dll). Takoż w odróżnieniu od unixa, w którym biblioteki dynamiczne są poszukiwane w zmiennej LD_LIBRARY_PATH, pod Windows pliki .dll muszą się znajdować na liście zmiennej PATH (i tak samo pod Cygwinem główne biblioteki dynamiczne zawierają się w /usr/bin, a nie w /usr/lib).

Podsumowanie

Zatem, z czego dokładnie składa się nasz program?

Na kod źródłowy programu składają się pliki źródłowe (*.c*) oraz pliki nagłówkowe (*.h). Pliki nagłówkowe z założenia powinny zawierać deklaracje współdzielone pomiędzy plikami źródłowymi. Każdy z plików źródłowych jest kompilowany do postaci pliku obiektowego (pośredniego), który zawiera skompilowaną postać każdej z fizycznych deklaracji i każda taka deklaracja (jeśli jest eksportowana) posiada swój symbol (deklaracje, które są lokalne na jednostkę kompilacji - przypominam, że jednostkę kompilacji stanowi plik źródłowy wraz ze wszystkimi wczytywanymi przezeń plikami nagłówkowymi - istnieją tylko w tej jednostce kompilacji i na zewnątrz nie są widoczne). Symbole następnie posłużą do związania deklaracji fizycznych znajdujących się w różnych plikach obiektowych.

Pliki źródłowe powinny zawierać wyłącznie deklaracje fizyczne, a pliki nagłówkowe - deklaracje ulotne. Każda deklaracja ulotna o tym samym symbolu powinna być w każdej jednostce kompilacji zdefiniowana w sposób identyczny. Co prowadzi "na skróty" do prostej reguły: każda z deklaracji ulotnych może być zdefiniowana albo w pliku nagłówkowym, albo może być zdefiniowana w pliku źródłowym, ale wtedy żaden inny plik źródłowy z tego samego programu nie może dostarczać deklaracji o takim samym symbolu. Inaczej mówiąc - deklaracja ulotna o określonej nazwie symbolu może być dostarczona tylko i wyłącznie dokładnie raz: albo w pliku źródłowym, albo (częściej) w pliku nagłówkowym.

Zbiór plików obiektowych może się teraz składać na program lub bibliotekę. Biblioteka jest po prostu zbiorem plików obiektowych. Obiekty eksportowane z tej biblioteki można następnie łączyć z odwołaniami do nich z plików źródłowych programu.

Aha, zapomniałem o najważniejszym! Oczywiście, cały zbiór plików .o można też związać do postaci pliku wykonywalnego, pod warunkiem, że DOKŁADNIE JEDEN z tych plików .o eksportuje symbol o nazwie "main" (mówię w przybliżeniu; na niektórych systemach nazwy zaczynają się od podkreślenia, albo mają jakieś jeszcze dziwne manglowanie - poza manglowaniem C++-owym). Staje się on funkcją wywoływaną w momencie odpalania takiego programu przez powłokę. Oczywiście to wcale nie znaczy, że jest to funkcja main() (jest tak w C, ale nie w C++). W C++ istnieje jeszcze tzw. globalny konstruktor i globalny destruktor. Zatem to, co zostało zdefiniowane jako funkcja main, w istocie będzie miało odpowiednio przekręconą nazwę, powiedzmy że cpp_main. Natomiast to, co będzie eksportowane na zewnątrz jako "main" to będzie sztucznie wygenerowana przez kompilator funkcja, która wywoła globalny konstruktor, potem cpp_main, a potem globalny destruktor. Globalny konstruktor np. zawiera instrukcje, które są potrzebne do zainicjalizowania zmiennych globalnych, jeśli inicjalizowano je wywołaniem funkcji.

Spróbujmy więc podać mały, abstrakcyny przykład: mamy pliki p1.cc, p2.cc i p.h. Pliki p1.cc i p2.cc składają się na program, którego plik wykonywalny będzie się nazywał p. Jednym z popularniejszych narzędzi do zorganizowania takiego programu jest make, który używa pliku konfiguracyjnego o domyślnej nazwie Makefile. Służy on również do tego, żeby oszczędzić niepotrzebnych rekompilacji, czyli żeby po zmianie w dowolnym z tych trzech plików przekompilowały się tylko te, które od tych zmian zależą. Nasz Makefile będzie zatem wyglądał w ten sposób:

p1.o: p1.cc p.h
p2.o: p2.cc p.h
  g++ -c $<

p: p1.o p2.o
  g++ p1.o p2.o -o p

Składnia pliku Makefile jest bardzo zależna od odpowiednich białych znaków. Reguły zaczynają się zawsze od początku linijki, a potem ostatnią linijkę, która zaczyna się tabulatorem, stanowi polecenie mające wyprodukować dany obiekt. W pierwszych trzech linijkach zatem definiujemy, że np. na plik p1.o składają się pliki p1.cc i p.h i podobnie na p2.o składają się p2.cc i p.h. Aby je wyprodukować, należy użyć polecenia "g++ -c" z plikiem odpowiednio p1.cc i p2.cc jako argumentem (składnia "$<" oznacza w tym wypadku pierwszy plik znajdujący się na liście składników). Oczywiście moglibyśmy to napisać również w ten sposób:

p1.o: p1.cc p.h
  g++ -c p1.cc

p2.o: p2.cc p.h
  g++ -c p2.cc

Ale wtedy używalibyśmy tego samego polecenia dwa razy i musielibyśmy używać za każdym razem wprost nazwę pliku, który chcemy kompilować. Nazwy p1.o i p2.o nie są w tym poleceniu użyte jawnie; gcc z opcją -c domyślnie tworzy plik z końcówką .o (ale oczywiście można też jawnie podać nazwę tego pliku po opcji -o).

Te reguły służą nam do zdefiniowania samego procesu kompilacji, czyli utworzenia plików obiektowych. Zaś ostatnia z reguł w podanym pliku Makefile definiuje proces wiązania (link), który utworzy plik wynikowy. Aby zatem utworzyć nasz plik wynikowy, należy wpisać "make p".

Możemy też oczywiście tak zorganizować nasz Makefile, żeby tworzył plik p samym wywołaniem "make" - w tym celu należy na samym początku pliku dodać takie reguły:

all: p

.PHONY: all

Pierwsza reguła określa nam, że aby zrobić "all", należy najpierw zrobić p. Ponieważ all jest pierwszą regułą w Makefile, zostanie wybrana domyślnie, jeśli make nie ma argumentu. Natomiast druga linijka wskazuje, że cel "all" jest "oszukany". Nie jest to konieczne, ale gdyby tej reguły nie było, to make niczego by nie wykonał, jeśli w bieżącym katalogu znajdowałby się plik o nazwie "all", który byłby nowszy, niż p.

Zajmijmy się teraz poszczególnymi plikami. Tak wygląda p1.cc:

#include <iostream>
#include "p.h"

int main() {
  Klocek k;
  std::cout << k.number() << g_number << std::endl;
  return 0;
}

int g_number;
namespace {
  int s_number;
}

Natomiast p2.cc

#include "p.h"
int Klocek::number() {
  g_number = 3;
  return 2;
}

Oraz p.h

struct Klocek {
  int number();
};
extern int g_number;

Proszę tutaj zwrócić uwagę na następujące rzeczy: