Często zdarza się tak, że chcemy wykorzystać ten sam kawałek kodu w dwu różnych programach (np. ten sam algorytm numeryczny w dwu zadaniach czy wspólny dla wszystkich programów interfejs). Duży program pisany jako jedna całość jest też nieczytelny (brak pośrednich form organizacji poza podziałem na funkcje), łatwiej w nim także popełnić błędy (zmodyfikowanie zmiennej globalnej, zależność działania jednej funkcji od ubocznych efektów działania drugiej). Dlatego programy dzieli się na części (moduły). Modułem będziemy nazywać fragment programu kompilowany jako jeden plik.
Dodatkową zaletą dzielenia programu na moduły jest fakt, że moduły mogą być kompilowane niezależnie. Jeśli zatem dokonamy zmian w jednym z modułów nie będziemy musieli rekompilować całości, a tylko ten jeden plik. Standardowym narzędziem w Unices do zarządzania które fragmenty programu wymagają rekompilacji jest program make. Program ten domyślnie poszukuje informacji o zależnościach w pliku o nazwie Makefile.
Załóżmy dla przykładu, że główny plik programu (zawierający funkcję main) nazywa się main.c. Niech wykorzystuje on funkcję f1, którą chcemy umieścić w module znajdującym się w pliku func.c. Kompilator przy przetwarzaniu pliku main.c musi wiedzieć jakie argumenty przyjmuje funkcja f1 i jakiego typu wartość zwraca. Musimy więc gdzieś umieścić deklarację tej funkcji: najlepiej zrobić to w pliku nagłówkowym func.h (o tej samej nazwie co plik zawierający definicję tej funkcji).
Zawartość pliku main.c będzie zatem następująca:
#include
<stdio.h>
#include
"func.h"
int
main(void)
{
float
x = 0.5;
printf("Wartość funkcji f1 w
punkcie %f wynosi: %f\n", x, f1(x));
return 0;
}
Zauważmy, że nazwa pliku nagłówkowego z biblioteki standardowej został umieszczony w nawiasach trójkątnych co oznacza, że ma być poszukiwany na początku w "miejscach standardowych", zaś nazwa naszego (prywatnego) pliku nagłówkowego w cudzysłowach, co oznacza, że ma być poszukiwany najpierw w bieżącym katalogu.
Zobaczmy teraz jak wygląda zawartość pliku nagłówkowego (definicyjnego) test.h. Każdy plik nagłówkowy powinien być napisany tak, by można go było kilkakrotnie włączać do dowolnego modułu i w dowolnym miejscu. Zrobimy to za pomocą dyrektyw preprocesora, definiując odpowiednią stałą (najlepiej składającą się z nazwy pliku i rozszerzenia, rozdzielonych podkreśleniem) i sprawdzając na początku pliku czy została już ona zdefiniowana (czyli czy plik ten został już włączony).
#ifndef
TEST_H
#define
TEST_H
float
f1(float
x);
#endif
/* !TEST_H */
Pozostaje przedstawić zawartość pliku func.c. Aby uniknąć sytuacji gdy deklaracja funkcji f1 w pliku nagłówkowym func.h jest niezgodna z jej definicją w pliku func.c do każdego modułu włączamy jego plik definicyjny.
#include
"func.h"
static
float
g(float
x);
float
f1(float
x)
{
return x*g(x);
}
static
float
g(float
x)
{
return 0.5*x;
}
W definicji funkcji f1 skorzystaliśmy z lokalnej, prywatnej funkcji g. Funkcja g dzięki kwalifikatorowi static nie jest widoczna w innych modułach (nie trzeba zatem dbać by nie zachodził konflikt nazw). Tak samo można utworzyć zmienne prywatne dla modułu (niewidoczne na zewnątrz).
Uwaga: dla skrócenia nie są wypisane nagłówki komentarzowe modułów, funkcji i programu; brak też komentarzy opisujących zmienne.
Kompilacja w języku C składa się z dwóch faz. Pierwszy z nich to wygenerowanie kodu wykonywalnego (zamiana zdań w języku C na język maszynowy) do tzw. pliku pośredniego (object file), zazwyczaj z rozszerzeniem .o. Następnie pliki modułów i głównego programu są łączone za pomocą konsolidatora (linker) w plik wykonywalny. W tym etapie także wstawiane są odpowiednie odwołania (odnośniki) do bibliotek dynamicznych wykorzystywanych w programie (zazwyczaj wszystkie biblioteki standardowe są linkowane (dowiązywane) dynamicznie, tzn. w programie są tylko odnośniki gdzie można znaleźć kod funkcji z biblioteki). Dla prostych programów te dwie operacje mogą być wykonane w jednym kroku.
Zbadajmy jakie mamy zależności w naszym prostym przykładzie. Jeśli zmienimy moduł func.c nie zmieniając jego interfejsu (tzn. deklaracje funkcji, typów i zmiennych eksportowanych, czyli nie prywatnych (bez kwalifikatora static)), to będziemy musieli rekompilować tylko ten plik (no i oczywiście utworzyć nowy plik wykonywalny programu). Plik main.c trzeba rekompilować zarówno przy zmianach w samym programie, jak i przy zmianach w pliku definicyjnym func.h; np. przy zmianie typu argumentu funkcji f1 z float na double kompilator musi wstawić odpowiednie rzutowanie.
Tak więc plik func.c trzeba rekompilować tylko przy zmianach w nim samym, plik main.c trzeba rekompilować przy zmianach w nim samym oraz zmianach w func.h. Program składa się z modułów main.o i func.o. Zapiszmy to teraz w postaci pliku sterującego dla make (zazwyczaj nazywa się go Makefile; jest to jedna z domyślnych nazw).
Makefile składa się z zestawu reguł. Reguły składają się zazwyczaj z celu (nazwy celu i zazwyczaj zarazem nazwy pliku jaki zostanie wygenerowany za pomocą danej reguły), zależności (mogą to być nazwy istniejących plików jak i nazwy innych reguł) oraz zestawu komend które mają być wykonane przy danej regule (zazwyczaj tworzą one plik celu). Ponadto w Makefile można deklarować zmienne i się nimi posługiwać. Do wartości zmiennych odwołujemy się poprzedzając je znakiem $ i biorąc nazwę w nawiasy okrągłe (nawiasy pomija się przy zmiennych jednoznakowych).
Przedstawmy teraz najprostszy Makefile dla naszego programu:
CC=gcc
CFLAGS=-Wall
LDLIBS=-lm
all: main
main: main.o func.o
$(CC)
$(CFLAGS)
-o main main.o func.o
$(LDLIBS)
main.o: main.c func.h
$(CC)
$(CFLAGS)
-c main.c
func.o: func.c
$(CC)
$(CFLAGS)
-c func.c
Na początku naszego pliku znajdują się definicje zmiennych. Wartości podane w pliku Makefile można zmienić w linii poleceń, w wywołaniu polecenia make. Jeśli chcielibyśmy np. skompilować nasz program z informacjami dla debuggera (czyli z opcją -g) to użylibyśmy polecenia make CFLAGS=-Wall -g. Potem następują reguły. Chcąc wykonać konkretne reguły podajemy je jako parametry wykonania make, np. make func.o. Jeśli nie podamy żadnej reguły, wykonywana jest pierwsza w kolejności (w naszym przypadku reguła all). Każda reguła składa się z linii zawierającej cel i oddzielone od niego dwukropkiem zależności. Linię możemy złamać na części stawiając jako ostatni znak w kontynuowanej linii backslash (\). Pod "nagłówkiem" reguły może znajdować się jedna lub więcej komend wykonywanych dla tej reguły; każda komenda musi być poprzedzona znakiem tabulacji. Uwaga: komendy podane w plikach Makefile są interpretowane przez /bin/sh. W Makefile można stosować komentarze: wszystko od znaku # do końca linii uważane jest za komentarz (nie dotyczy to komend).
Dla każdej reguły, make najpierw odświeża (jeśli to możliwe,
tzn. jeśli zależność nie jest np. plikiem źródłowym) jej zależności,
następnie jeśli którakolwiek z zależności jest nowsza od celu, albo
cel nie istnieje wykonywane są komendy zadane przez tą regułę (które
zazwyczaj "odświeżają" cel). Inaczej mówiąc, make
tworzy drzewo zależności i jeśli któryś z plików źródłowych jest
nowszy od wybranego celu (lub cel nie istnieje) to cel jest
uaktualniany wykonując tylko komendy niezbędne do odświerzenia celu.
Przykładowo, reguła
main.o: main.c func.h
$(CC)
$(CFLAGS)
-c main.c
mówi dwie rzeczy. Po pierwsze, kiedy cel reguły (w tym
przypadku plik main.o) wymaga odświerzenia (w tym przypadku:
rekompilacji): jest tak wtedy gdy albo plik main.o nie
istnieje, albo gdy którykolwiek z main.c lub func.h
jest nowszy od tego pliku. Po drugie, w jaki sposób odświerzyć
main.o: w naszym przypadku przez uruchomienie gcc
jak podano powyżej.
Przypomnienie: opcja -c mówi, że kompilator ma wykonac tylko fazę kompilacji (compile only), bez fazy konsolidacji, opcja -o podaje nazwę pliku wynikowego (output file) zaś opcja -Wall włącza wszystkie ostrzeżenia (all warnings). Opcja -lm powoduje dołączenie biblioteki matematycznej libm; biblioteka libc włączana jest domyślnie.
Nasz Makefile można zapisać o wiele prościej, korzystając z tego, że make zawiera wbudowane domyślne reguły (implicit rules) m.in. dla tworzenia plików pośrednich (obiektowych) ze źródeł w C. Korzysta on przy tym z odpowiednich zmiennych; w naszym przykładzie dobraliśmy nazwy zmiennych tak, aby były zgodne z domyślnymi regułami. Aby nie przepisywać naz zależności i celów mozemy skorzystać z tzw. zmiennych automatycznych (automatic variables). I tak $@ oznacza nazwę pliku celu, $< oznacza nazwę pierwszej zależności, zaś $^ listę wszystkich zależności (z usuniętymi powtórzeniami). Nasz Makefile z wykorzystaniem tych właściwości wygląda następująco (być może można go jescze uprościć):
CC=gcc
CFLAGS=-Wall
LDLIBS=-lm
all: main
main: main.o func.o
$(CC)
$(CFLAGS)
-o $@ $^
$(LDLIBS)
main.o: main.c func.h
$(CC)
$(CFLAGS)
-c $<
Więcej informacji n.t. programu make i pisania plików Makefile znaleźć można w dokumentacji do make (w języku angielskim), która dostępna jest przez wydanie komendy info make (man make ogranicza się do opisu opcji i może nie być aktualne). Informacje n.t. opcji kompilatora znaleźć można za pomocą man gcc. Więcej informacji (m.in. na temat rozszerzeń GNU) w info gcc. Skrócony opis opcji dostępny jest przez make --help oraz gcc --help.
Kolorowanie składni jak w Emacsie.
Dokumentacja dotycząca Makefile na WWW: