Pisanje obsežnih programov

Zaglavne datoteke (header files)
Zunanje spremenljivke in funkcije
Prednosti uporabe več datotek
Kako razbijemo program na več datotek
Organizacija podatkov v vsaki datoteki
Orodje Make
Napravimo datoteko makefile
Makroji Make
Kompleten primer datoteke Makefile

Pisanje bolj obsežnih programov terja delitev programov na module oziroma ločene datoteke. Zato bo funkcija main( ) v eni datoteki, druge funkcije pa verjetno v kateri drugi.

Tvorimo lahko knjižnico funkcij, ki so lahko skupinsko pomnjene v takih modulih. To omogoča uporabo pripravljenih modulov v različnih programih, v katere jih dodamo v času samega prevajanja.
 

Zaglavne datoteke (Header files)

Pri uvedbi takega, modularnega programiranja želimo obdržati definicije spremenljivk, prototipe funkcij ipd. znotraj posameznega modula. Kaj pa,če take definicije souporablja več modulov?

Bolje je, če centraliziramo take definicije v eni datoteki in souporabljamo to datoteko v drugih modulih. Taki datoteki pravimo zaglavna datoteka (header file) in ima podaljšek .h

Standardne zaglavne datoteke upoštevamo v našem programu na naslednji znan način:

   #include <stdio.h>

Lahko pa napišemo tudi lastne zaglavne datoteke, ki jih v našem programu upoštevamo tako:

   #include ``mojaDatoteka.h''

Primer
 
V programu main.c imamo funkcijo main( ), ki kliče funkcijo WriteMyString( ), ta pa se nahaja v modulu WriteMyString.c

Prototip te funkcije pa se nahaja v zaglavni datoteki  Header.h

main.c:

#include "header.h"
#include <stdio.h>

char *drugNiz = "Pozdravljeni";

main(){
    WriteMyString(MOJ_NIZ); /* klic funkcije iz drugega modula */
}
WriteMyString.c:
extern char *drugNiz;

void WriteMyString(char *taNiz){
        printf("%s\n", taNiz);
        printf("Globalna spremenljivka = %s\n", drugNiz);
}
header.h:
#define MOJ_NIZ "Dober dan"

void WriteMyString();

Zunanje spremenljivke in funkcije

Zunanje (external) spremenljivke so vedno definirane izven funkcij. Le tako (ne pa nujno) so lahko vidne celotnemu programu. Vse definicije funkcij so v Cju zunanje (nimamo vgrajenih funkcij).

Območje (scope) zunanje spremenljivke ali funkcije nastopi na mestu njene definicije in velja do zaključka datoteke (modula), v katerem je definirana.
 

Primer

main(){ .... }
 
int a;
float b[10]
 
void f1(){ .... }
 
char c;
 
float f2(){ .... }
Funkcija main( ) ne vidi spremenljivk a in b ter funkcij f1 ( ) in f2( ). Tudi zato  moramo prototipe funkcij navesti pred kodo, ki te funkcije uporablja.

Če potrebujemo naslavljanje neke zunanje spremenljivke, še preden je bila definirana ozirama, če je definirana v nekem drugem modulu, moramo povedati, da je zunanja:
 

   extern int aaa;

Tako imamo v prejšnjem primeru niz drugNiz, ki je deklariran v  main.c in souporabljen v WriteMyString.c , kjer smo ga deklarirali kot zunanjo spremenljivko.

Opozorilo: prefiks extern le napove spremenljivko in je ne definira.S tem še ne zasedemo nič pomnilnika.

Imeti moramo le eno  definicijo takih spremenljivk, imamo pa lahko poljubno število njihovih zunanjih navedb.

Primer z zunanjo navedbo polja

   main.c:    int polje[100]:

   file.c:    extern int polje[];


Prednosti uporabe več datotek


Kako razdelimo program na več datotek


Programerji običajno začnejo snovanje programa tako, da razbijejo problem na več sekcij. Vsako od teh sekcij lahko realiziramo z eno ali več funkcijami. Vse funkcije iste sekcije pomnimo v isti datoteki.

Podobno velja, če implementiramo objekte kot podatkovne strukture. Vse funkcije, ki delajo s tako strukturo, pomnimo v isti datoteki. Kasnejša sprememba "objekta" terja spremembo le v njemu ustrezni datoteki.

Najbolje je,če za vsako od teh izvornih datotek (s podaljškom .c) napišemo še ustrazno zaglavno datoteko (z enakim imenom, toda s podaljškom .h). Zaglavna datoteka vsebuje definicije vseh funkcij, ki jih "njena" izvorna datoteka uporablja. Druge datoteke,ki uporabljajo te funkcije morajo na začetku imeti ustrezen stavek #include z navedbo datoteke .h


Organizacija podatkov v datotekah

Tipičen vrstni red je naslednji:



Orodje Make

Orodje make zagotavlja konsistenco zbirke programskih modulov, programov ali kakšnega drugega celostnega sistema datotek.

Vzemimo problem vzdrževanja naslednje večje zbirke izvornih datotek :

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

Običajno bi jih prevedli na naslednji način:

   cc -o main main.c f1.c ......... fn.c

Če vemo, da nekaterih datotek nismo popravljali in ni potrebno ponovno prevajanje, zadošča le povezovanje njihove objektne kode:

   cc -o main main.c f1.c ... fi.o .. fj.o ... fn.c

Uporaba orodja make omogoča, da nam za te podrobnosti ni potrebno skrbeti. "Sprogramirati moramo le zaporedje ukazov, ki opisuje, kako lahko naš program (ali sistem več programov) zgradimo iz naših izvornih datotek)

Pri tem moramo upoštevati pravila odvisnosti in pravila izgradnje.

Pravilo odvisnosti ima levo in desno stran, ločeni z dvopičjem :

   levaStran : desnaStran

levaStran podaja imena ciljev (na primer imena programov ali datotek), ki jih želimo graditi.  desnaStran navaja imena datotek, od katerih je cilj odvisen (na primer imena izvornih datotek, zaglavnih datotek, podatkovnih datotek) .

Sestaviti moramo datoteko "make", ki pomni tako zaporedje ukazov. Pogosto damo tej datoteki ime
Makefile.
 

Orodje sprožimo tako

make         (UNIX poskuša izvesti ukaze v datoteki Makefile)

oziroma tako

make -f mojmake (UNIX bo izvedel ukaze v datoteki mojmake)

Če je nek cilj starejši od datotek, ki nanj vplivajo, uporabi orodje make ustrezna pravila izgradnje (prevajanje, povezovanje, lahko tudi kaj drugega).

Opomba: datoteke "make" ubogajo tudi druge ukaze UNIX, ki jih običajno tipkamo v ukaznih vrsticah. Zato jih lahko uporabimo tudi za tvorbo rezervnih kopij (backup), poganjanje programov, čiščenje direktorijev ipd.


Napravimo datoteko makefile

Postopek je preprost. Napisati moramo tekstovno datoteko s seznamom ukazov odvisnosti

Primer take datoteke:

prog: prog.o f1.o f2.o
  c89 prog.o f1.o f2.o -lm etc.
 
prog.o: header.h prog.c
                 c89 -c prog.c
 
f1.o: header.h f1.c
                 c89 -c f1.c
f2.o: --- ----

Kar lahko razumemo tako:

prog je odvisen od datotek: prog.o, f1.o and f2.o. Če je katera od teh bila spremenjena, bo povezovanje ponovljeno.

prog.o  je odvisen od  dveh datotek. Če je katera od teh spremenjena, je potrebno ponoviti prevajanje.  Podobno velja za  f1.o in f2.o.

Zadnji trije ukazi predstavljajo eksplicitna pravila, ker smo datoteke v ukazih navedli z imeni.

Lahko pa imamo implicitna pravila, ki so bolj splošna.

Vzemimo na primer

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


in to posplošimo v:

.c.o:   cc -c $<

To lahko beremo kot ukaz "podaljšekIzvora"  " podaljšekCilja"

$< je skrajšava za ime datoteke s podaljškom .c

V datoteki imamo lahko tudi komentarje, ki sledijo znaku #.


Makroji make

Definicija makrojev za make je preprosta. Poglejmo nekaj primerov:
SOURCES    = main.c f1.c f2.c
CFLAGS     = -g -C
LIBS       = -lm
PROGRAM    = main
OBJECTS    = (SOURCES: .c = .o)
Makro uporabimo na naslednji način

$(macro_name)

Primer:

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


Opomba:

Poznamo tudi več internih makrojev, na primer:
 
$* ime trenutne odvisne datoteke (brez suffiksa)
$@ polno ime trenutnega cilja
$< ciljna .c datoteka 

Kompleten primer datoteke Makefile

#
# Makefile
#
SOURCES.c= main.c WriteMyString.c
INCLUDES= 
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 $(PROGRAM) $(OBJECTS)