Προγράμματα Πολλαπλών Αρχείων


  Η ενότητα αυτή ασχολείται με προβλήματα που αντιμετωπίζουμε όταν γράφουμε σχετικά μεγάλα προγράμματα. Συνήθως τα μεγάλα προγράμματα είναι χωρισμένα σε αρθρώματα (modules), τα οποία βρίσκονται σε ξεχωριστά αρχεία πηγαίου κώδικα. Η συνάρτηση  main() θα βρίσκεται σε ένα αρχείο, έστω main.c, πιθανώς με κάποιες άλλες συναρτήσεις, ενώ τα υπόλοιπα αρχεία θα περιέχουν άλλες συναρτήσεις, αλλά όχι συνάρτηση main().

Επίσης μπορούμε να δημιουργήσουμε μια δική μας βιβλιοθήκη, γράφοντας μια ομάδα (suite) από συναρτήσεις (χωρίς να υπάρχει συνάρτηση main() σε αυτές), οι οποίες θα βρίσκονται σε ένα (ή περισσότερα) αρθρώματα. Τέτοιες βιβλιοθήκες συναρτήσεων μπορούν να ενσωνατωθούν σε διάφορα προγράμματα, με τον ίδιο περίπου τρόπο που ενσωματώνουμε τις συναρτήσεις των πρότυπων βιβλιοθηκών της C.

Τα πλεονεκτήματα της χρήσης πολλαπλών αρχείων (αρθρωμάτων) είναι τα εξής:

Αρχεία κεφαλής

Σε περίπτωση που έχουμε πολλαπλά αρχεία, το κάθε ένα από αυτά θα περιέχει τους ορισμούς των σταθερών, μεταβλητών και συναρτήσεών του, αλλά θα πρέπει (πιθανά) να γνωρίζει και τους αντίστοιχους ορισμούς από τα υπόλοιπα αρχεία. Πώς επιτυγχάνεται η κοινοποίηση αυτών των ορισμών;

Η καλύτερη λύση είναι να συλλέξουμε αυτές τις οδηγίες σε ένα αρχείο, το οποίο και να ενσωματώνουμε (include) σε καθένα από τα επιμέρους αρχεία. Ένα τέτοιο αρχείο λέγεται αρχείο κεφαλής (header file).

Συνήθως τα αρχεία αυτά έχουν την κατάληξη .h.  Έχουμε ήδη συναντήσει αρχεία κεφαλής, όπως το stdio.h, η δε ενσωμάτωση επιτυγχάνεται με την οδηγία προεπεξεργαστή #include, π.χ.

   #include <stdio.h>

Τα σύμβολα < > σημαίνουν οτι τo αρχείο κεφαλής βρίσκεται στον προκαθορισμένο κατάλογο των αρχείων κεφαλής (συνήθως στο UNIX/Linux είναι /usr/include). 

Έστω οτι ορίζουμε το δικό μας αρχείο κεφαλής, για παράδειγμα με όνομα my_head.h. Τότε μπορούμε να το ενσωματώσουμε στο πρόγραμμά μας με την οδηγία:

   #include "my_head.h"

Τα σύμβολα " " σημαίνουν οτι το αρχείο κεφαλής βρίσκεται στον κατάλογο όπου εκτελείται η μεταγλώττιση.

Σημείωση: Τα αρχεία κεφαλής συνήθως περιέχουν ΜΟΝΟ ορισμούς τύπων δεδομένων, συναρτήσεων και οδηγίες προεπεξεργαστή της C.

Το παράδειγμα που ακολουθεί επεξηγεί τα προηγούμενα.

 

Σχήμα 1. Αρθρωτή δομή προγράμματος C.

Ο κώδικας των προγραμμάτων main.c, WriteMyString.c και header.h θα είναι κάπως έτσι:

main.c:

/*
* main.c
*/
#include <stdio.h>
#include "header.h"

char *AnotherString = "Hello Everyone";

main()
{
printf("Running...\n");

/*
* Call WriteMyString() - defined in another file
*/
WriteMyString(MY_STRING);

printf("Finished.\n");
}

WriteMyString.c:

/*
* WriteMyString.c
*/
#include <stdio.h>

extern char *AnotherString;

void WriteMyString(ThisString)
char *ThisString;
{
printf("%s\n", ThisString);
printf("Global Variable = %s\n", AnotherString);
}

header.h:

/*
* header.h
*/
#define MY_STRING "Hello World"

void WriteMyString();

Τα αρθρώματα θα πρέπει να μεταλωττιστούν ξεχωριστά (θα το δούμε αργότερα). 

Το αρχείο main.c ενσωματώνει το αρχείο κεφαλής stdio.h, από το προκαθορισμένο κατάλογο, και το αρχείο κεφαλής header.h από τον κατάλογο μεταγλώττισης. Η συνάρτηση main() καλεί τη συνάρτηση WriteMyString() που ορίζεται στο αρχείο WriteMyString.c αλλά το πρότυπό της δηλώνεται και στο αρχείο κεφαλής header.h.

Το αρχείο WriteMyString.c δεν χρησιμοποιεί άμεσα ορισμούς του header.h , αλλά μόνο μέσω του main.c. όπως θα δούμε παρακάτω. 

Σημείωση: Είναι σαφές οτι υπάρχει μια ισορροπία μεταξύ της πολυπλοκότητας και της ευκολίας διαχέιρισης των πολλαπλών αρχείων, κάτι που δεν είναι εμφανές σε μικρά προγράμματα, όπως σε αυτό το παράδειγμα που είναι υπερβολικά σύνθετο απλά για να επιδειχθούν διάφορα θέματα. Τα ουσιαστικά κέρδη εμφανίζονται σε πραγματικά μεγάλα προγράμματα, ειδικά αν χρησιμοποιήσουμε τις ευκολίες του UNIX/Linux.

Ένα πρόβλημα με την αρθρωτή προσέγγιση : ΜΟΙΡΑΣΜΑ ΜΕΤΑΒΛΗΤΩΝ.

Έστω οτι έχουμε καθολικές μεταβλητές δηλωμένες και ορισμένες σε ένα άρθρωμα. Με ποιό τρόπο μπορούμε να περάσουμε αυτές τις μεταβλητές στα άλλα αρθρώματα;

Θα μπορούσαμε να τις περάσουμε ως παραμέτρους σε συναρτήσεις, ΑΛΛΑ:

Εξωτερικές Μεταβλητές και Συναρτήσεις, Κλάσεις Μεταβλητών

Εσωτερικές μεταβλητές ονομάζουμε τις μεταβλητές που δηλώνονται και ορίζονται σε ένα αρχείο. Αυτές οι μεταβλητές μπορεί να είναι: είτε Τοπικές (Local) -- δηλαδή δηλώνονται και ορίζονται σε μια μόνο συνάρτηση: εϊναι οι τυπικές μεταβλητές της C, ή Καθολικές (Global) -- δηλαδή δηλώνονται έξω από τις συναρτήσεις του αρχείου, ορίζονται μέσα σε κάποια συνάρτηση. 

Εξωτερικές (External) λέγονται οι Καθολικές μεταβλητές που δήλώνονται μεν σε ένα αρχείο αλλά γίνονται γνωστές και μπορούν να οριστούν και να χρησιμοποιηθούν και από συναρτήσεις άλλων αρχείων. Όλες οι Εξωτερικές μεταβλητές είναι Καθολικές αλλά το αντίστροφο δεν ισχύει. Στα αρχεία που οι Εξωτερικές μεταβλητές δεν δηλώνονται αλλά ορίζονται και  χρησιμοποιούνται  δηλώνονται ως extern , π.χ.

 /* file1.c */				/* file2.c */
extern int count; int count=5;

write() main()
{ {
printf("count is %d\n", count); write();
} }

Στο παράδειγμα της προηγούμενης ενότητας η μεταβλητή char *AnotherString είναι Καθολική στο αρχείο main.c και Eξωτερική στο αρχείο WriteMyString.c.

Οι Εξωτερικές μεταβλητές, όπως και οι Καθολικές, είναι Μόνιμες (static) -- δηλαδή έχουν διάρκεια ζωής ίση με τη διάρκεια ζωής της εκτέλεσης του προγράμματος. Αντίθετα, οι Τοπικές μεταβλητές είναι Προσωρινές (auto) -- δηλαδή έχουν διάρκεια ζωής ίση με τη διάρκεια κλήσης της συνάρτησης όπου ορίζονται.

Στη πράξη, βέβαια,  οι μεταβλητές οι οποίες είναι Τοπικές στη συνάρτηση main() είναι στην ουσία Μόνιμες αφού η διάρκεια ζωής της συνάρτησης main() είναι ίση με διάρκεια ζωής της εκτέλεσης του προγράμματος. Υπάρχει η δυνατότητα να μετατρέψουμε μια Τοπική μεταβλητή σε Μόνιμη αν την δηλώσουμε ως static. Αυτό σημαίνει οτι σε διαδοχικές κλήσεις της συνάρτησης η μεταβλητή διατηρεί την τιμή της.

Επίσης υπάρχει η δυνατότητα να δηλώσουμε μια μεταβλητή ως register ώστε να επιβάλλουμε την αποθήκευσή της στον επεξεργαστή, για λόγους ταχύτητας (π.χ. σε κάποιο μετρητή).

Οι συναρτήσεις στη C είναι ΠΑΝΤΟΤΕ Καθολικές, άρα και και εν δυνάμει Εξωτερικές, με τη χρήση των αρχείων κεφαλής. ΔΕΝ μπορούμε να δηλώσουμε μια συνάρτηση μέσα σε μια άλλη όπως στη PASCAL. Μερικές φορές, αντί για τη χρήση αρχείου κεφαλής, όταν πρόκειται για λίγες συναρτήσεις, μπορεί να συναντήσουμε τη δήλωση extern και σε συναρτήσεις, όπως στο παράδειγμα που ακολουθεί. Εννοείται οτι τα δύο αρχεία πρέπει να συνδεθούν πριν την εκτέλεση, όπως και στη περίπτωση των αρχείων κεφαλής.

/* main.c */ 			/* print.c */
extern void foo ( void ); #include <stdio.h>

int main ( void ) void foo ( void )
{
foo(); {
return 0; puts ( "foo!" );
} }

Εμβέλεια Εξωτερικών Δηλώσεων

Μια εξωτερική μεταβλητή (ή συνάρτηση) δεν είναι πάντα εντελώς καθολική. Η C εφαρμόζει τον ακόλουθο κανόνα:

Η εμβέλεια μια εξωτερικής δήλωσης αρχίζει από το σημείο της δήλωσής της και έχει εμβέλεια μέχρι το τέλος του αρχείου (αρθρώματος) όπου έγινε η δήλωση.

Έστω η παρακάτω εκοδχή:

main()
{ .... }
 
int what_scope;
float end_of_scope[10]
 
void what_global()
{ .... }
 
char alone;
 
float fn()
{ .... }

Η συνάρτηση main() δεν γνωρίζει τις μεταβλητές what_scope ή end_of_scope αλλά οι συναρτήσεις what_global() και  fn() τις γνωρίζουν. ΜΟΝΟ η συνάρτηση fn() βλέπει τη μεταβλητή alone.

Γι' αυτό το λόγο, τόσο οι καθολικές μεταβλητές όσο και οι δηλώσεις των συναρτήσεων (prototypes) πρέπει να βρίσκονται στην αρχή του κώδικα σε κάθε αρχείο.

Ο άλλος λόγος που δηλώνουμε τις συναρτήσεις στην αρχή είναι για να μπορεί ο μεταγλωττιστής να κάνει έλεγχο αριθμού και τύπου παραμέτρων.

Αν χρειαστεί να αναφερθούμε σε μεταβλητή ή συνάρτηση πριν τη δήλωσή της (ή όταν δηλώνεται σε άλλο αρχείο, όπως έχουμε ήδη πεί) μπορούμε να τη δηλώσουμε ως εξωτερική, π.χ. η δήλωση

   extern int what_scope

πριν από τη συνάρτηση main() επιτρέπει τη χρήση της σε αυτή τη συνάρτηση.

ΠΡΟΣΟΧΗ: η παραπάνω δήλωση extern int what_scope είναι απλά δήλωση (declaration) ΌΧΙ  ορισμός (definition), δηλαδή ΔΕΝ δεσμεύεται μνήμη eγια την εξωτερική μεταβλητή, απλά ανακοινώνουμε στο μεταγλωττιστή οτι αργότερα μέσα στο ίδιο ή σε άλλο αρχείο υπάρχει ο κανονικός ορισμός της μεταβλητής και η εξωτερική αναφορά θα λυθεί (πιθανότατα) στη σύνδεση. Ο κανοικός ορισμός πρέπει να υπάρχει μόνο σε ένα σημείο σε όλο τον κώδικα (σε όλα τα αρχεία). 

Εφ' όσον πρόκειται για απλή δήλωση, δεν είναι απαραίτητο να οριστεί μέγεθος. π.χ.:

   main.c:    int arr[100]:

   file.c:    extern int arr[];

Διαίρεση Προγράμματος σε Πολλαπλά Αρχεία

Οι προγραμματιστές συνήθως προσεγγίζουν ένα πρόβλημα με δύο (εν πολλοίς συμπληρωματικές) μεθόδους. Η μια μέθοδος είναι αυτή της σταδιακής ανάλυσης (stepwise refinement) όπου το πρόβλημα αναλύεται σταδιακά σε επιμέρους προβλήματα, αυτά με τη σειρά τους σε νέα επιμέρους προβλήματα κ.ο.κ. μέχρι να καταλήξουμε σε βήματα που είναι εύκολο να υλοποιηθούν. Η δεύτερη μέθοδος είναι αυτή της αντικειμενοστραφούς ανάλυσης (object oriented analyshs) όπου το πρόβλημα ανάγεται στον ορισμό ορισμένων βασικών δομών δεδομένων και των λειτουργιών επ' αυτών των δομών 

Οι δύο προσεγγίσεις μπορεί να χρησιμοποιηθούν συμπληρωματικά. Στην πρώτη περίπτωση, ένα αρχείο μπορεί να περιέχει τις συναρτήσεις που ασχολούνται με την επίλυση ενός υπο-προβλήματος, για παράδειγμα οι συναρτήσεις που ασχολούνται με την υλοποίηση μιας εντολής του φλοιού UNIX/Linux, και στη συνέχεια όλες οι σχετικές εντολές  συνθέτουν μια βιβλιοθήκη. Στη δεύτερη περίπτωση, ένα σύνολο αρχείων μπορεί να  ασχολείται με μια δομή δεδομένων, για παράδειγμα με τους σηματοφορείς ή τα σήματα και τις λειτουργίες τους, και να αποτελούν μια βιβλιοθήκη.

Γενικά η τεχνική της επιτυχημένης διαίρεσης απαιτεί τον προσεκτικό καθορισμό της διεπαφής  του αρχείου (ή αρθρώματος ή βιβλιοθήκης) με τα υπόλοιπα προγράμματα έτσι ώστε

Για κάθε άρθρωμα δημιουργούμε δύο αρχεία. Αυτό με κατάληξη .c που περιλαμβάνει την υλοποίηση και αυτό με κατάληξη .h που περιλαμβάνει τις δηλώσεις των συναρτήσεων (prototypes) και άλλων σταθερών.

Όποτε κάποιο πρόγραμμα πρέπει να χρησιμοποιήσει μια συνάρτηση από το άρθρωμά μας, τότε το αρχείο με κατάληξη .h θα πρέπει να συμπεριληφθεί στο τμήμα με τα  #include του προγράμματος. Το άρθρωμα είτε θα διατηρείται σε μορφή πηγαίου κώδικα (soure code) και θα μεταγλωττίζεται κάθε φορά ή θα βρίσκεται σε μορφή αντικείμενου κώδικα (object code) και θα συνδέεται μετά τη μεταγλώττιση του προγράμματος. Σε αυτή τη δεύτερη περίπτωση ο αντικείμενος κώδικας μπορεί να φυλάσσεται σε ειδικό κατάλογο αρθρωμάτων ή να βρίσκεται στον τρέχοντα κατάλογο μεταγλώττισης. 

Σε κάθε αρχείο υλοποίησης αρθρώματος τα δεδομένα είναι οργανωμένα με συγκεκριμένο τρόπο. Τυπικά έχουμε:

Η σειρά είναι σημαντική με την έννοια οτι όποια μεταβλητή ή συνάρτηση χρησιμοποιούμε πρέπει να δηλώνεται πρώτα. Για τις συναρτήσεις ισχύουν τα παρακάτω:

Μια συνάρτηση που ορίζεται ως

 float find_max(float a, float b, float c)
{ /* etc ... ... */ }

δηλώνεται (prototyped) ως

 float find_max(float a, float b, float c);


Η εφαρμογή make

Η εφαρμογή make είναι ένα πρόγραμμα που διχειρίζεται προγράμματα πολλαπλών αρχείων, ή γενικά ένα ενιαίο σύνολο αρχείων. Η κύρια χρήση της εφαρμογής είναι η υποβοήθηση των προγραμματιστών στην ανάπτυξη συστημάτων λογισμικού. Η εφαρμογή δημιουργήθηκε αρκετά στο UNIX αλλά τώρα διατίθεται σε πολλά συστήματα.

Έστω οτι έχουμε ένα σύνολο αρχείων πηγαίου κώδικα:

   main.c f1.c ......... fn.c

Η μεταγλώττιση του συνόλου αυτού θα έχει τη μορφή:

   gcc -o main main.c f1.c f2.c......... fn.c

Στη περίπτωση του παραδείγματος στο Σχήμα 1., η εντολή θα είχε τη μορφή

  gcc -o main main.c WriteMyString.c

Όμως, αν μερικά αρχεία έχουν ήδη μεταγλωττιστεί σε αντικείμενο κώδικα, και δεν έχουν μεταβληθεί, μπορούμε να γλιτώσουμε χρόνο μεταγλωττίζοντας μόνο μόνο αυτά που έχουν τροποποιηθεί. Έστω για παράδειγμα οτι έχουμε αλλάξει μόνο τα αρχεία main.c και f1.c. Τότε η εντολή μεταγλώττισης, σύνδεσης θα μπορύσε να είναι:

   gcc -o main main.c f1.c f2.o.......... fn.c

Ημεταγλώττιση μέχρι το στάδιο του αντικείμενου κώδικα επιτυγχάνεται στη gcc με την επιλογή -c. Τότε παράγεται ένα αρχείο με ιδιο όνομα με το αρχικό και κατάληξη.o. For παράδειγμα, επιστρέφοντας στο Σχήμα 1, η εντολές:

  gcc -c main.c
  gcc -c
 WriteMyString.c

παράγουν τα αρχεία main.o και WriteMyString.ο. Σε αυτή τη φάση δεν απαιτούνται επιλογές τύπου -l (σύνδεση με βιβλιοθήκες) αφού αυτό θα γίνει στη φάση της σύνδεσης. 

Η τελική σύνδεσή τους γίνεται με την εντολή

  gcc -o main main.ο WriteMyString.o

Εδώ θα έπρεπε να προστεθούν επιλογές σύνδεσης βιβλιοθηκών, αν αυτό απαιτείται.

Σε περίπτωση, πχ που τροποποιούμε μόνο το main.c και το WriteMyString.ο παραμείνει στη θέση του τότε μπορούμε να εκτελέσουμε μόνο:

  gcc -c main.c
  gcc -o main main.ο WriteMyString.o

Σε περίπτωση όμως που έχουμε ένα μεγάλο σύνολο αρχείων με πιθανά σύνθετες εξαρτήσεις μεταγλώττισης, η παραπάνω διαδικασία μπορεί να αποδειχτεί επίπονη.:

Η εφαρμογή GNU make αναλαμβάνει τον έλεγχο της διαδικασίας μεταγλώττισης και ενημέρωσης προγραμμάτων πολλαπλών αρχείων.

Η εφαρμογή είναι διαθέσιμη για διάφορες πλατφόρμες στη διεύθυνση

   http://www.gnu.org/software/make/

όπου υπάρχει και όλη η σχετική τεκμηρίωση στη διεύθυνση

  http://www.gnu.org/software/make/manual/

Προγραμματισμός με make 

Ο προγραμματισμός με make είναι απλός. Βασικά, γράφουμε μια ακολουθία από εντολές που περιγράφουν τον τρόπο που οικοδομείται (builded), δηλαδή μεταγλωττίζεται, το πρόγραμμά μας από τα επιμέρους αρχεία του.

Η ακολουθία οικοδόμησης περιγράφεται και αποθηκεύεται σε ένα makefile. Η ακολουθία οικοδόμησης αποτελείται από δύο ειδών κανόνες: κανόνες εξάρτησης (dependency rules) και κανόνες οικοδόμησης (construction rules).

Ένας κανόνας εξάρτησης έχει δύο μέρη -- μια αριστερή και μια δεξιά πλευρά που χωρίζονται με ένα ":"

   left side: right side

Η αριστερή πλευρά (left side) δίνει το όνομα του στόχου (target), δηλαδή το όνομα του αρχείου προς οικοδόμηση. Η δεξιά πλευρά (right side) δίνει τα ονόματα των εξαρτήσεων (dependencies), δηλαδή τα ονόματα των αρχείων από τα οποία εξαρτάται ο στόχος, π.χ. αρχεία πηγαίου κώδικα, αρχεία κεφαλής, αρχεία δεδομένων.

Αν ο στόχος είναι μη-ενημερωμένος (out of date), δηλαδή κάποια από τις εξαρτήσεις του έχει ενημερωθεί, τότε εφαρμόζονται οι κανόνες οικοδόμησης που ακολουθούν τον κανόνα εξάρτησης.

Έτσι για ένα τυπικό πρόγραμμα C, όταν η εφαρμογή make διατρέχει ένα makefile, εκτελούνται οι ακόλουθες εργασίες:

  1. Η make διαβάζει το makefile. To makefile εξηγεί ποιά αρχεία αντικείμενου κώδικα και βιβλιοθήκες πρέπει να συνδεθούν, ή ποιά αρχεία πηγαίου κώδικα ή αρχεία κεφαλής πρέπει να μεταγλωττιστούν πριν από τη σύνδεση.
  2. Για κάθε στόχο ελέγχονται η ημέρα και ώρα τροποποίησης, και συγκρίνονται με τις ημέρες και ώρες τροποποίησης των εξαρτήσεων. Αν κάποια εξάρτηση έχει μεταγενέστερη ημέρα και ώρα τροποποίησης τότε ο στόχος θεωρείται μη-ενημερωμένος και εφαρμόζονται οι αντίστοιχοι κανόνες οικοδόμησης.
  3. Στη συνέχεια συγκρίνεται η ημέρα και ώρα τροποποίησης των εκτελεσίμων αρχείων σε σχέση με την ημέρα και ώρα τροποποίησης των στόχων. Αν υπάρχουν στόχοι με μεταγενέστερη ημέρα και ώρα, οιστόχοι μεταγλωττίζονται ή και συνδέονται για τη παραγωγή νέων εκτελεσίμων.

Σημείωση: Η εφαρμογή make υπακούει οποιαδήποτε εντολή που βρίσκεται στο makefile και αντιστοιχεί σε νόμιμη εντολή φλοιού UNIX/Linux, όχι μόνον σε αυτές που συνδέονται με τη μεταγλώττιση προγραμμάτων.  Επομένως μπορούμε να χρησιμοποιήσουμε την εφαρμογή make για τη δημιουργία αρχειοθηκών, δημιουργία εφεδρικών αντιγράφων, συγχρονισμό καταλόγων, καθαρισμό καταλόγων κλπ.

Δημιουργία makefile

Η δημιουργία είναι μάλλον απλή: αρκεί ένας απλός συντάκτης κειμένου. Το makefile περιέχει απλά τους κανόνες εξάρτησης και οικοδόμησης. Ας δούμε ένα παράδειγμα, που βασίζεται στο Σχήμα 1:

main: main.o WriteMyString.o
gcc -o main main.o
WriteMyString.o
 
main.o: header.h main.c
gcc -c main.c
 
WriteMyString.o: WriteMyString.c
gcc -c WriteMyString.c 

Σημείωση: οι κανόνες οικοδόμησης σε νέα σειρά ξεκινούν με ΤΑΒ (στηλοθέτηση) και χωρίς άλλα κενά.

Η εφαρμογή make διερμηνεύει το makefile ως εξής:

  1. Ο στόχος main εξαρτάται από δύο αρχεία: τα main.o WriteMyString.ο. Αν κάποιο από αυτά τα αρχεία αντικείμενου κώδικα έχουν τροποποιηθεί μετά τη σύνδεση του main, τότε απαιτείται επανασύνδεση.
  2. Ο στόχος main.o εξαρτάται από δύο αρχεία: τα header.h main.c. Αν κάποιο από αυτά τα αρχεία έχει τροποποιηθεί μετά τη μεταγλώττιση του main.o τότε απαιτείται επαναμεταγλώττιση.
  3. Ο στόχο WriteMyString.ο εξαρτάται από ένα αρχείο: το WriteMyString.c. Αν  αυτό το αρχείο έχει τροποποιηθεί μετά τη μεταγλώττιση του  WriteMyString.ο τότε απαιτείται επαναμεταγλώττιση.

Οι παραπάνω κανόνες λέγονται άμεσους κανόνες (explicit rules) -- επειδή τα αρχεία αναφέρονται με το πλήρες τους όνομα.

Θα μπορούσαμε να έχουμε έμμεσους κανόνες (ιmplicit rules) που γενικεύουν όμοιους κανόνες και μας γλιτώνουν πληκτρολογήσεις.

Αν για παράδειγμα είχαμε κανόνες της μορφής

f1.o: f1.c
gcc -c f1.c
 
f2.o: f2.c
cc -c f2.c

θα μπορούσαμε να γενικεύσουμε ως:

.c.o: gcc -c $<

Αυτό διαβάζεται ως: 

.επέκταση_εξάρτησης.επέκταση_στόχου: εντολή όρισμα

Η έκφραση $< είναι συντομογραφία για το όνομα αρχείου με επέκταση .c.

Μπορούμε επίσης να βάλουμε σχόλια σε ένα makefile αρχίζοντας μια γραμμή με το σύμβολο #.

Η εφαρμογή make περιλαμβάνει πολλές ενσωματωμένες εντολές, εντολές του φλοιού UNIX/Linux καθώς και δυνατότητες συντμήσεων, κανονικών εκφράσεων, δομών ελέγχου κλπ. Μερικά παραδείγματα :

   break     date        mkdir
   type      chdir       mv (move or rename)
   cd        rm (remove) ls
   cp (copy) path  

Στην ουσία η εφαρμογή make παρέχει ένα ευέλικτο περιβάλλον προγραμματισμού με πολλές δυνατότητες. Για περισσότερες πληροφορίες δείτε στο σχετικό εγχειρίδιο.

Μακροεντολές make

Τι περιβάλλον προγραμματισμού  της εφαρμογής make επιτρέπει και τον ορισμό μακροεντολών (ή μεταβλητών). Τυπικά χρησιμοποιούνται για την αποθήκευση ονομάτων αρχείων πηγαίου ή αντικείμενου κώδικα, διαδρομών καταλόγων βιβλιοθηκών, αρχείων κεφαλής, επιλογών μεταγλώττισης κλπ.

Ο ορισμός είναι απλός, π.χ.:

SOURCES = main.c f1.c f2.c
CFLAGS = -g -C
LIBS = -lm
PROGRAM = main
OBJECTS = (SOURCES: .c = .o)

where (SOURCES: .c = .o) αλλάζει τη κατάληξη των αρχείων της λίστας SOURCES από .c  σε  .o.

Η χρήση μιας μακροεντολής γίνεται με την εντολή $(όνομα_μακροεντολής), π.χ:


$(PROGRAM) : $(OBJECTS)
$(LINK.c) -o $@ $(OBJECTS) $(LIBS)

Σημείωση:

Υπάρχουν πολλές εσωτερικές μακροεντολές (δείτε το εγχειρίδιο χρήσης). Ακολουθούν μερικές συνηθισμένες:

$* -- τό ονομα του αρχείου της τρέχουσας εξάρτησης, χωρίς τη κατάληξη (.c, .h, .o κλπ).
$@ -- το πλήρες όνομα αρχείου του τρέχοντος στόχου.
$< -- αρχείο στόχου με κατάληξη .c.

Ακολoυθεί ένα αρχείο makefile με χρήση μακροεντολών για το παράδειγμα του Σχήματος 1. Εδώ τα αρχεία .o χρησιμοποιούνται και διαγράφονται. Επίσης φαίνεται η χρήση επιλογών κανόνων όπως η all, debug και clean. Η clean διαγράφει τα αρχικά αρχεία και αφήνει μόνο το δημιουργηθέν εκτελέσιμο. Η χρήση τους εξηγείται στην αμέσως επόμενη παράγραφο.

#
# Makefile
#
SOURCES.c = main.c WriteMyString.c
INCLUDES = header.h
CFLAGS =
SLIBS =
PROGRAM = main

OBJECTS = $(SOURCES.c: .c = .o)

.KEEP_STATE:

debug := CFLAGS = -g

all debug: $(PROGRAM)

$(PROGRAM): $(INCLUDES) $(OBJECTS)
$(LINK.c) -o $@ $(OBJECTS) $(SLIBS)

clean:
rm -f $(SOURCES.c) $(INCLUDES)

Εκτέλεση και χρήση make

Η εκτέλεση απαιτεί απλά την εισαγωγή της εντολής make στη γραμμή εντολών. Αν δεν υπάχρχει κάποιο όρισμα το UNIX/Linux υποθέτει οτι ο τρέχων κατάλογος είναι ο κατάλογος εργασίας και αναζητά ένα makefile με προεπιλεγμένο όνομα Makefile (προσοχή: το M κεφαλαίο, τα υπόλοιπα γράμματα πεζά).

Σε περίπτωση που βρισκόμαστε σε άλλο κατάλογο ή το makefile έχει διαφορετικό όνομα εισάγουμε την εντολή στη μορφή make -f make_filename

π.χ..   make -f my_make

Υπάρχουν αρκετές επιλογές, ελέγξτε τις σελίδες του εγχειριδίου.

Η πιο συνηθισμένη χρήση της εφαρμογής make είναι κατά την εγκατάσταση μιας εφαρμογής UNIX/Linux σε μορφή tarball (*.tar.gz) δηλαδή αρχείων  πηγαίου κώδικα τα οποία συνήθως μεταφορτώνουμε από το διαδίκτυο. Μετά την αποσυμπίεση και απο-αρχειοθήτηση της εφαρμογής, συνήθως καταλήγουμε σε ένα αρχικό κατάλογο ο οποίος, μεταξύ των άλλων, περιέχει και ένα αρχείο με όνομα Makefile.in και ένα σενάριο φλοιού με όνομα configure. Η εκτέλεση του σεναρίου configure προκαλεί την επεξεργασία του αρχείου Makefile.in ώστε να παραχθεί το τελικό αρχείο Makefile. Το σενάριο configure συνήθως ελέγχει στοιχεία όπως η σεχτική θέση του τρέχοντος καταλόγου στο σύστημα αρχείων, τα ονόματα διαδρομής των αρχείων κεφαλής και βιβλιοθήκης, οι εκδόσεις του λειτουργικού συστήματος UNIX/Linux και του μεταγλωττιστή gcc, τις βιβλιοθήκες γραφικών κλπ. Έτσι παράγει ένα αρχείο Makefile που θα επιτρέψει την ορθή λειτουργία της εντολής make που εκτελείται αμέσως μετά. 

Συνήθως τα αρχεία Makefile.in και configure δημιουργούνται αυτόματα μέσω των ειδικών GNU εφαρμογών automake και autoconf. Για περισσότερες πληροφορορίες δείτε το εγχειρίδιο της εφαρμογής make και τις σχετικές ιστοσελίδες του GNU. 

Σε αυτή τη περίπτωση το παραγόμενο Makefile είναι αρκετά πιο σύνθετο και περιέχει κανόνες για περισσότερες λειτουργίες που εκετελούνται με τη μορφή

make <rule option>

όπου μερικές επιλογές κανόνων είναι οι εξής (χωρίς απαραίτητα να υλοποιούνται όλες, ενώ μπορεί να υπάρχουν και άλλες):

all --  Προεπιλογή, αντιστοιχεί με την απλή make, και μεταγλωττίζει την εφαρμογή.
install --
Εγκαθιστά τη μεταγλωττισμένη εφαρμογή, τις σελίδες εγχειριδίου κλπ.
uninstall -- Απεγκαθιστά την εφαρμογή.
clean -- Διαγράφει τα προσωρινά αρχεία που δημιουργήθηκαν κατά την εγκατάσταση.
distclean -- Διαγράφει τα αρχεία που δημιουργήθηκαν ώστε να ξεκινήσουμε νέα εγκατάσταση.
realclean -- Διαγράφει τα πάντα, όπως αρχεία πληροφόρησης κλπ.
debug -- Μεταγλώττιση ή εγκατάσταση με επιλογή εκσφαλμάτωσης.


Ακολουθεί ένα παράδειγμα σύνθετου αρχείου Makefile που αναφέρεται στην εγκατάσταση της εφαρμογής GNU tar.

 # Generated automatically from Makefile.in by configure.
# Un*x Makefile for GNU tar program.
# Copyright (C) 1991 Free Software Foundation, Inc.

# This program is free software; you can redistribute
# it and/or modify it under the terms of the GNU
# General Public License ...
...
...

SHELL = /bin/sh

#### Start of system configuration section. ####

srcdir = .

# If you use gcc, you should either run the
# fixincludes script that comes with it or else use
# gcc with the -traditional option. Otherwise ioctl
# calls will be compiled incorrectly on some systems.
CC = gcc -O
YACC = bison -y
INSTALL = /usr/local/bin/install -c
INSTALLDATA = /usr/local/bin/install -c -m 644

# Things you might add to DEFS:
# -DSTDC_HEADERS If you have ANSI C headers and
...
...

DEFS = -DSIGTYPE=int -DDIRENT -DSTRSTR_MISSING \
-DVPRINTF_MISSING -DBSD42
# Set this to rtapelib.o unless you defined NO_REMOTE,
# in which case make it empty.
RTAPELIB = rtapelib.o
LIBS =
DEF_AR_FILE = /dev/rmt8
DEFBLOCKING = 20

CDEBUG = -g
CFLAGS = $(CDEBUG) -I. -I$(srcdir) $(DEFS) \
-DDEF_AR_FILE=\"$(DEF_AR_FILE)\" \
-DDEFBLOCKING=$(DEFBLOCKING)
LDFLAGS = -g

prefix = /usr/local
# Prefix for each installed program,
# normally empty or `g'.
binprefix =

# The directory to install tar in.
bindir = $(prefix)/bin

# The directory to install the info files in.
infodir = $(prefix)/info

#### End of system configuration section. ####

SRC1 = tar.c create.c extract.c buffer.c \
getoldopt.c update.c gnu.c mangle.c
SRC2 = version.c list.c names.c diffarch.c \
port.c wildmat.c getopt.c
SRC3 = getopt1.c regex.c getdate.y
SRCS = $(SRC1) $(SRC2) $(SRC3).
OBJ1 = tar.o create.o extract.o buffer.o \
getoldopt.o update.o gnu.o mangle.o
OBJ2 = version.o list.o names.o diffarch.o \
port.o wildmat.o getopt.o
OBJ3 = getopt1.o regex.o getdate.o $(RTAPELIB)
OBJS = $(OBJ1) $(OBJ2) $(OBJ3)
AUX = README COPYING ChangeLog Makefile.in \
makefile.pc configure configure.in \
tar.texinfo tar.info* texinfo.tex \
tar.h port.h open3.h getopt.h regex.h \
rmt.h rmt.c rtapelib.c alloca.c \
msd_dir.h msd_dir.c tcexparg.c \
level-0 level-1 backup-specs testpad.c

.PHONY: all
all: tar rmt tar.info

.PHONY: tar
tar: $(OBJS)
$(CC) $(LDFLAGS) -o $@ $(OBJS) $(LIBS)

rmt: rmt.c
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ rmt.c

tar.info: tar.texinfo
makeinfo tar.texinfo

.PHONY: install
install: all
$(INSTALL) tar $(bindir)/$(binprefix)tar
-test ! -f rmt || $(INSTALL) rmt /etc/rmt
$(INSTALLDATA) $(srcdir)/tar.info* $(infodir)

$(OBJS): tar.h port.h testpad.h
regex.o buffer.o tar.o: regex.h
# getdate.y has 8 shift/reduce conflicts.

testpad.h: testpad
./testpad

testpad: testpad.o
$(CC) -o $@ testpad.o

TAGS: $(SRCS)
etags $(SRCS)

.PHONY: clean
clean:
rm -f *.o tar rmt testpad testpad.h core

.PHONY: distclean
distclean: clean
rm -f TAGS Makefile config.status

.PHONY: realclean
realclean: distclean
rm -f tar.info*

.PHONY: shar
shar: $(SRCS) $(AUX)
shar $(SRCS) $(AUX) | compress \
> tar-`sed -e '/version_string/!d' \
-e 's/[^0-9.]*\([0-9.]*\).*/\1/' \
-e q
version.c`.shar.Z

.PHONY: dist
dist: $(SRCS) $(AUX)
echo tar-`sed \
-e '/version_string/!d' \
-e 's/[^0-9.]*\([0-9.]*\).*/\1/' \
-e q
version.c` > .fname
-rm -rf `cat .fname`
mkdir `cat .fname`
ln $(SRCS) $(AUX) `cat .fname`
tar chZf `cat .fname`.tar.Z `cat .fname`
-rm -rf `cat .fname` .fname

tar.zoo: $(SRCS) $(AUX)
-rm -rf tmp.dir
-mkdir tmp.dir
-rm tar.zoo
for X in $(SRCS) $(AUX) ; do \
echo $$X ; \
sed 's/$$/^M/' $$X \
> tmp.dir/$$X ; done
cd tmp.dir ; zoo aM ../tar.zoo *
-rm -rf tmp.dir

Ασκήσεις

Άσκηση 1

Έστω οτι έχετε ένα πρόγραμμα C με κύρια συνάρτηση τη main.c και ακόμη τις συναρτήσεις input.c και output.c σε ξεχωριστά αρχεία

Άσκηση 2

Έστω οτι έχετε ένα πρόγραμμα C που αποτελείται από πολλαπλά αρχεία, που εξαρτώνται το ένα  από το άλλο όπως φαίνεται στον πίνακα που ακολουθεί:

Σχήμα 2: Ενδεικτικός πίνακας εξαρτήσεων αρχείων
Αρχείο        include      
main.c stdio.h, process1.h
input.c stdio.h, list.h
output.c stdio.h
process1.c stdio.h, process1.h
process2.c stdio.h, list.h
Άσκηση 3

Δημιουργείστε ένα πρόγραμμα πολλαπλών αρχείων ως εξής: Το κύριο πρόγραμμα διαβάζει δύο ακεραίους από το πληκτρολόγιο, καλεί δύο συναρτήσεις που υπολογίζουν το μέγιστο και το ελάχτιστο και στη συνέχεια εμφανίζουν τα αποτελέσματα στην οθόνη.  Οι συναρτήσεις βρίσκονται σε δύο ξεχωριστά αρχεία. Επίσης τα μηνύματα πριν την εισαγωγή των αριθμών και κατά την εμφάνιση των αποτελεσμάτων δίνονται με #define σε ξεχωριστό αρχείο.

Άσκηση 4

Δοκιμάστε να μεταφορτώσετε και να εγκαταστήσετε την εφαρμογή tar ή κάποια ανάλογη. Ελέγξτε τα αρχεία Makefile.in, Makefile, configure κλπ. Διερευνήστε διαφορετικές επιλογές της make.
Dave Marshall
1/5/1999
μετάφραση και προσαρμογή στα Ελληνικά Κ.Γ. Μαργαρίτης
15/4/2008