Ceci sont des notes, et elles sont remplies d'oublis.

Compléments de C

Sommaire

0. La création de programmes

  1. La syntaxe d'un programme
  2. La syntaxe du C
    1. La sytaxe de printf
    2. Les variables
    3. La syntaxe de scanf
    4. Les tests et les booléens
    5. Les boucles
    6. Les fonctions
  3. Types Avancés
    1. Les tableaux
    2. Les pointeurs
    3. Les structures
    4. Les énumérations
    5. Les allocations
  4. Librairies
    1. Introduction
    2. libc
  5. Meta
    1. Organisation du Code
    2. Makefiles

0 - La création de programmes

Le C, langage préhistorique, est toujours d'actualité grâce à ses deux avantages: la vitesse et la vitesse. Pour ce faire, un programme en C ne se lit pas ligne par ligne comme un certain langage vu en prépa: il est compilé, soit traduit en langage machine, avant d'être exécuté.

Pour ce faire, on utilise la commande:

gcc <nom du fichier à compiler>

Ainsi, un fichier a.out sera créé dans le répertoire où gcc fut utilisé: en l'exécutant (en lançant ./a.out), on accède au programme. GCC (GNU Compiler Collection) est ainsi un traducteur, mais peut aussi effectuer d'autres tâches, via des options:

Beaucoup, BEAUCOUP d'autres options existent. Elles permettent de customiser les actions de GCC, mais ceci sont des notes et ne cherchent pas à perdre les lecteurs, au contraire des cours de quelqu'un; les options possibles sont visibles en éxécutant man gcc dans un terminal ou sur google.

1 - La syntaxe d'un programme

Historiquement, la structure d'un programme en C est fortement orientée vers la créations de programmes UNIX; ainsi, on attends de ces derniers de renvoyer un code indiquant le statut du programme lorsqu'il se termine. (cf cours d'OSS, 0 = OK, 1 ou plus = erreur)

C'est la raison pour laquelle un programme en C comporte nécessairement un bloc int main(void){}, souvent terminé par un return 0: cela permet à la machine de comprendre que tout s'est bien passé.

Seul le code à l'intérieur de ce bloc sera éxécuté. On peut cependant définir des variables, fonctions ou passer des messages au compilateur en les écrivant en dehors de ce bloc: #include<stdlib.h> se traduit en:

hey mec j'ai besoin de ce qui se trouve dans la librairie stdlib.h

Dans le cadre de gros projets, il est parfois utile de séparer son code en plusieurs fichiers: dans ce cas, on utilisera #include pour importer des fichiers externes. Je détaille ceci dans la section Meta.

2 - La syntaxe du C

Bien ! Une fois un que le bloc main a été ouvert, il est temps de passer aux choses sérieuses.

#include <stdio.h>
    
int main(void) {
    printf("Hello, World!\n");
    return 0;
}

Le code de base, qui renvoie Hello, World!, utilisant la fonction printf de stdio.h.

On remarque que chaque instruction est terminée par un ;: cela permet au compilateur de bien délimiter les instructions. En effet, la syntaxe du C n'est pas affectée par les espaces, et le programme précédent est équivalent à:

#include <stdio.h>
int main(void){printf("Hello, World!\n");return 0;}

qui assure un zéro aux partiels lorsque personne ne voudra vous relire.

2.1 - La syntaxe de printf

printf prend des arguments peu communs: une string de 'formatage' tout d'abord, montrant ce qui va apparaitre à l'écran, puis le reste des variables à afficher, aux endroits spécifiés dans la string initiale.

printf("Tu as eu %d aux partiels\n", variable_note);

Affiche:

Tu as eu <le contenu de variable_note> aux partiels

Ici, \n n'est pas une variable mais un code pour indiquer un retour à la ligne, comme \t indique une tabulation.

Les 'emplacements' indiqués dans la string peuvent être parmi cette liste non exhaustive:

Mettre un nombre entre % et un identifieur indique le nombre d'espaces à utiliser:

printf("%5d %5d %5d",10,100,1000);
>>   10   100  1000

2.2 - Les variables

Définition

En C, le développeur gère l'espace mémoire de son programme. Pour éviter les surplus, il doit définir toute variable utilisée ultérieurement:

<type de variable> <nom>

ou

<type de variable> <nom> = <valeur>

La première méthode définit sans pour autant assigner une valeur à la variable: celle ci aura donc une valeur dépendant de l'état de la mémoire où elle est créée, presque aléatoire.

La variable a un temps de vie. Elle n'existe que dans le 'scope' où elle a été créée. Définir une variable dans une boucle for ne la fera exister que dans cette boucle. Cela peut être évité en définissant des variables statiques:

static int a

générera une variable s'étendant à tout le programme. Cependant, c'est brouillon, et il est commode de définir les variables en tête de programme.

On peut aussi définir des variables pour l'entièreté de programme en passant un message au compilateur:

#define PI 3.14159265358979

indique à ce dernier de remplacer toute allusion à PI par 3.14159265358979, ce qui peut se révéler très utile lors d'une éventuelle fluctuation de pi (lol), pour éviter de modifier chaque équation du programme.

Types

Les différents types de variables de base sont:

Les conversions

Un caractère est stocké en tant que code ASCII. Mais comment accéder à cette valeur ? On utilise un cast, du british pour un moule.

char carac = 'a';
printf("%c -> %d", carac, (int) carac);

renvoie le code ASCII de a, soit 97:

a -> 97

L'opérateur (dest) valeur va renvoyer l'interprétation de type dest de valeur. Cela marche pour plusieurs types, mais il est important de faire attention à ne pas perdre d'information en changeant de type:

float f = 3.1416;
printf("%f -> %d", f, (int) f);

revoie:

3.1416 -> 3

2.3 - La syntaxe de scanf

Plutot que de recompiler le programme à chaque changement de valeur, il vaut mieux faire des programmes interactifs, demandant à l'utilisateur des valeurs. Pour cela, on utilise scanf, qui comme printf, prend en premier argument une string de formatage, mais ensuite prend les adresses des variables à modifier. La string de formatage peut être une 'Regular Expression', mais dans le cas où vous l'utiliserez, il suffit de faire:

scanf("%d", &a);

Ce qui va lire un 'd -> digit' et le stocker dans la variable du bon type a. Toute information autre que ce qui est précisé sera détruite. Les identifieurs possibles sont:

Ici on utilise l'opérateur & pour accéder à l'adresse de la variable. Il suffit de l'ajouter avant le nom de la variable à changer !

2.4 - Les test et les booléens

Des variables, c'est bien, mais les utiliser, c'est mieux. Pour faire un bloc conditionnel, on écrit:

if (<test booleen ici>) {<code à executer là>}

où le test booléen est un abus de langage, les booléens n'étant pas un type existant en C, on utilise dans ces tests des opérateurs, pouvant être:

qui sont en fait des fonctions renvoyant un booléen, bien que ce type n'existe pas. En effet, true et false n'ont pas de sens en C, mais sont en fait remplacés par 1 et 0.

if (1) {printf("Ceci est toujours vrai");}
if (0) {printf("Ceci est toujours faux");}

Plus précisément, false équivaut à 0 et true équivaut à tout le reste, mais il est plus propre d'écrire 1 à la place de true.

if (-420) {printf("Ceci est toujours vrai");}
if (1478) {printf("Ceci est toujours vrai");}

Pour en revenir aux blocs conditionnels, il est possible de les compléter pour couvrir l'entièreté des cas possibles:

if (<test booleen ici>) {
    code si vrai
} else { 
    code si faux
}

Ou pour éviter les suites infectes de if else, on peut utiliser une structure switch:

switch(<variable>) {
    case 0: /* La variable vaut 0 */
        code ici, termine par un
        break;
    case 1: /* La variable vaut 1 */
        code ici, encore un
        break;
}

2.5 - Les boucles

Des blocs de contrôle du flux du programme: le while:

while (<test booleen ici>) {
    code ici
}

Le do..while:

do {
    code ici
} while (<test booleen ici>)

Qui permet d'exécuter la boucle au moins une fois, si l'expression contenue dans le test est fausse;

Le for:

for (<initialisation>; <condition d arret>; <iteration>) {
    code ici
}

Ainsi,

for (int i = 0; i < n; i = i+1) {
    printf("%d\n",i);
}

va afficher tous les nombres de 0 à n-1, i commençant à 0, continuant tant que i est strictement inférieur à n, et incrémentant i de 1 à chaque passage.

2.6 - Les fonctions

Définition

Ecrire du code c'est long. Surtout si l'on effectue des tâches répétitives: le fichier risque de devenir un affreux bloc de texte. C'est pour cela que l'on crée des fonctions, groupant certaines instructions ensemble; le code est ainsi comme un livre, où le texte est organisé en paragraphes. La syntaxe de base est:

<type renvoyé> <nom>(<arguments>) {
    code ici
    return <type renvoyé>:
}

Le type pouvant être void, si votre fonction n'a rien à renvoyer. (plutot que de mettre return 0 partout)

Elles peuvent être définies un peu partout dans le code, mais avant d'être appelées, sinon le compilateur va renvoyer une erreur, en se plaignant que la fonction n'est pas définie. Et il a bien raison. Un peu de rigueur.
Il existe cependant un moyen de définir une fonction après l'avoir utilisée. Il faut pour cela donner un prototype au compilateur, avant l'appel de la fonction. En soi, cela consiste à écrire la même chose que lors de la définition, sauf la partie entre accolades: int somme(int, int); se traduit par:

hey mec fais comme si j'avais défini somme qui prend en arguments deux int et qui renvoie un int

Bien sur il ne faut pas oublier de la définir plus tard dans le programme !

Comportement

Les fonctions en C ne se comportent pas comme en python. Par exemple:

#include<stdio.h>

void increment(int n) {
    n = n + 1;
}

int main(void) {
    int a = 5;
    increment(a);
    printf("a = %d\n",a);
}

va renvoyer une fois compilé et exécuté:

jb@arc2:~ $ gcc test.c
jb@arc2:~ $ ./a.out
a = 5

Pourquoi ? Parce qu'en C, tout ce qui est passé à une fonction est copié sur la 'pile d'éxécution', ou le stack. (qui est juste une zone de la mémoire ou le programme balance ses variables) En gros, la valeur touchée par la fonction increment est une copie de celle passée en argument: ce qui implique que toute modification dans la fonction ne sera pas répercutée sur la variable d'origine.

3 - Types avancés

3.1 - Les tableaux

Définition

Des tableaux, ou array, sont des collections linéaires de données en relation. Et en Français, ils permettent de stocker un nombre fini d'élément, accessibles par la suite en utilisant des crochets [ ]. En C, ils sont rudimentaires: pas de changements de taille, ni d'information sur la taille en elle-même.

Il existe plusieurs façon de les définir:

int tableau[5];

Crée un tableau de taille 5, pouvant contenir des ints, et non initialisé. Pour éviter de devoir remplir une à une les cases du tableau, on peut écrire:

int tableau[] = {1,2,3,4,5};

Qui crée un tableau suivant le tableau écrit ensuite. Ceci n'est possible qu'une fois, lors de la définition du tableau. Pour modifier les valeurs contenues après défintion, il faut le faire une par une.

Un tableau peut être de n'importe quel type, même des structs customisées !

Tableaux et fonctions

Voici un prototype de fonction prenant un tableau en argument:

int egalite(int tableau1[], int tableau2[]);

Cependant, lors de l'appel de cette fonction, on écrit juste le nom du tableau comme argument:

int t1[] = {1,2,3,4};
int t2[] = {3,1,4,2};
int resultat = egalite(t1, t2);

On passe en fait des pointeurs vers les premiers éléments de ces tableaux. (cf arithmétique sur pointeurs) Cela veut dire que des modifications faites à un tableau sont effectuées sur le tableau d'origine et non sur une copie. Dans la fonction, on utilise les crochets normalement, seul le passage en argument omet les crochets.

Chaînes de caractères

Une chaîne de caractères, en C, est en fait un tableau de caractères dont le dernier élément est '\0'. On accède donc aux éléments de str en utilisant des crochets.

3.2 - Les pointeurs

Définition

Les pointeurs sont une particularité du C et ont pour fonction de pointer.

J'ai déjà dit que tout développeur gérait sa mémoire, et les pointeurs permettent d'y accéder directement. L'opérateur & permet de donner l'adresse mémoire d'une variable:

&a devient 0x7ffdcc5dc624

Ce terme barbare peut ensuite être utilisé pour accéder à une variable. Pour ce faire nous allons créer un pointeur:

int a;
int* pointeur;
pointeur = &a;

pointeur pointe maintenant vers a; pour accéder à a via pointeur on utilise:

*pointeur = 4;

Ceci a pour effet de donner 4 comme valeur à la variable a.

Il est important de distinguer création et assignement d'un pointeur. int* représente ici un type, et non le contenu de pointeur !

int* pointeur = &a; // est légal,
*pointeur = a; // aussi

Cependant,

*pointeur = &a

implique le changement du contenu de la variable vers laquelle pointe pointeur vers l'adresse de a, donc pointeur pointe vers un pointeur, ce qui en fait un int** .. Il est donc conseillé de définir un type pointeur <type>* <nom> pour éviter les confusions.

Finalement, il est pratique de retenir *P = a <=> P = &a.

Si votre pointeur pointe vers une structure, l'opérateur -> permet d'accéder au contenu de cette structure. J'y reviendrai dans le paragraphe destiné aux structures.

Pointeurs et fonctions

Vous souvenez vous de l'incapacité des fonctions à changer la valeur d'une variable ?

#include<stdio.h>

void increment(int* n) {
    *n = *n + 1;
}

int main(void) {
    int a = 5;
    increment(&a);
    printf("a = %d\n",a);
}

va renvoyer une fois compilé et exécuté:

jb@arc2:~ $ gcc test.c
jb@arc2:~ $ ./a.out
a = 6

Ca marche ! Qu'est ce qui a changé ? void increment(int n) est devenu void increment(int* n). Ce qui veut dire qu'au lieu de passer une variable, on passe un pointeur vers la variable à modifier. Le pointeur est copié, mais pointe toujours au même endroit, donc on accède à la variable d'origine.

Intérêt

Mais c'est stupide, me direz vous. Autant ne pas s'embêter avec des pointeurs et faire comme en python. C'était vraiment des néandertaux les créateurs du C !
Eh bien oui. Leur grand age fait que leurs machines, suivant la loi de Moore, ressemblaient plus à des cailloux qu'aux ordinateurs d'aujourd'hui. (Dites vous qu'une disquette stockait jusqu'à 3MB, donc il en faudrait environ 22 000 pour contenir GTA V) Le C fut donc créé dans le but d'être rapide et léger, tout en permettant beaucoup de choses.

Imaginez une variable de poids énorme, où est stockée énormément d'informations. (une structure par exemple) La passer à une simple fonction implique une copie entière de cette variable à chaque appel de la fonction. Lourd, et donc lent, à force. On passe alors un pointeur vers cette variable et bim ! Plus de copies intempestives.

Arithmétique sur pointeurs

Rien que le titre de cette section donne mal à la tête. Qu'est ce que c'est ? Eh bien c'est très simple: on peut ajouter et soustraire à des pointeurs. L'addresse ne varie de ce qu'on ajoute cependant, mais plutôt saute le nombre ajouté de cases mémoires. Qu'est ce que ça veut dire ? Cela veut dire que si p est un int*, p + 1 va pointer vers le prochain entier en mémoire. Le changement varie suivant le type de pointeur, (la taille d'un char est plus petit que celle d'un int, donc l'addresse va avancer d'un nombre de bytes différent) mais il n'y a pas de soucis à se faire, tout est calculé automatiquement.
À quoi ça sert ? Eh bien

int main(void) {
    int a[4] = { 50, 99, 3490, 0 };
    int* p;
    p = a;		/* p pointe vers le premier élément de a */
    while(*p > 0) {	/* Tant que p n'est pas au dernier element, 0 */
        printf("%i\n", *p);
        p++;		/* avancer p vers l'entier suivant stocké en mémoire */
    }
    return 0;
}

Va afficher:

50
99
3490

Pourquoi ? Parce que tous les élements d'un tableau sont 'adjacents' en mémoire: p++ va donc passer à l'élément suivant. L'arithmétique sur les pointeur est peu utilisée, mais elle mérite une mention dans ces notes.

3.3 - Les structures

Des types de base c'est bien, mais des structures, c'est mieux. Pourquoi ? Et bien disons que pour modéliser une balle en 2D, il faut un couple de flottants. Deux balles demandent deux couples. Modéliser quatre balles implique de définir 8 flottants. Ensuite, pour chaque balle, on demande la vitesse, la direction, le poids, etc. Clairement, définir tout, c'est lourd.

struct point {
    float x, y, vitesse, direction;
} rouge;

crée une variable de type struct point nommée rouge et ayant quatre propriétés:

On peut alors définir ce type sous un autre nom, à l'aide de typedef:

typedef <ancien type> <nouveau type>;
typedef struct point{float x, y, vitesse, direction;} balle;

définit un type balle, et en écrivant:

balle rouge, vert, bleu, jaune;

on définit ainsi quatres balles comme demandé.

On peut modéliser des types relativement complexes à l'aide de structures et de pointeurs, par exemple des piles. Une pile est un 'empilement' de valeurs, dont seule la première est accessible. La taille d'une pile est variable: elle ne doit jamais être fixée. Pour modéliser un tel objet, on définit un type 'élément' qui contient une des valeurs de la pile, et un pointeur vers l'élément suivant:

struct element{
    int n;
    struct element* suivant;
    int fin;
}

L'entier fin est un booléen qui permet de savoir quand la pile se termine: un élément dont la propriété fin est non nulle indique le dernier élément. Un code utilisant cette définition est disponible ici.

Déférence

Considérons:

struct element{
    int n;
    struct element* suivant;
    int fin;
}

struct element* debut;

Pour accéder au contenu de début, on doit écrire:

(*debut).n

Pour accéder au contenu du second élément:

(*(*debut).suivant).n

Ce qui est très moche. On peut donc utiliser l'opérateur de déférence -> pour accéder au contenu de la stuct pointée:

debut->n
debut->suivant->n

3.4 - Les énumérations

Définition

Une énumération permet de traduire des noms en entiers. A chaque contenu de l'énumération est associé un int, par défault son ordre dans l'enum:

enum <nom> { contenu_1, contenu_2 ... };

ou en assignant des valeurs définies:

 enum <nom> { contenu_1 = x, contenu_2 = y ... }

Le type ainsi créé est un type pouvant prendre comme valeur un des contenus définis.

enum professeurs {
    SZAFRANSKI,
    FOREST,
    AHMED = 6,
    VITERA
}
    
int prof_de_C = AHMED;
 
if (prof_de_C == AHMED) {
    printf("Plus maintenant mdr\n");
    prof_de_C = VITERA;
}

Ici, comme pour un tableau, l'indexation commence à zéro: SZAFRANSKI est un lien symbolique vers 0, FOREST vers 1, AHMED vers 6 comme défini, et VITERA vers 7, la dernière valeurs prise, incrémentée de 1.

Intérêt

Il peut rendre un code bien plus lisible dans certains cas.

3.5 - Les allocations

Introduction

En C, le développeur gère sa mémoire. Mais pour l'instant, on ne peut rien gérer du tout ! Chaque variable en mémoire est effacée en fin de fonction, indépendemment des choix effectués. Comment faire pour créer des données de façon dynamique ? Il faut allouer de la mémoire, non sur la pile déxecution, mais statique. Pour cela, on utilise les fonctions définies dans la librairie stdlib.h:

ainsi que l'opérateur sizeof, qui renvoie une valeur de type size_t en lui donnant un type comme argument.

Des déclarations aux types complexes. Comment lire tout ça ?

malloc

Le fonctionnement de malloc est plutôt simple. On lui donne une taille, et il renvoie un pointeur vers une case mémoire de cette taille. Facile, non ? Le pointeur renvoyé n'est pas typé: il est juste une addresse vers un endroit de taille adéquate, d'où le void*. Si ce n'est pas possible, malloc ne plante pas: il renvoie NULL, un pointeur sans addresse.

Dans la pratique, une déclaration utilisant malloc est un peu difficile à lire.

int* p = (int*) malloc(sizeof(int)*10);

p contient maintenant un pointeur vers une zone mémoire de la taille de 10 int. Mais p est de type int*: pour lui assigner le résultat de malloc, on va convertir le pointeur en utilisant un cast, soit (int*)

calloc

calloc ressemble fortement à malloc, à l'exception de deux éléments:

free

Le problème de la vie d'étudiant, c'est faire le ménage. Vider la poubelle, c'est lourd, mais si tu le fais pas, elle déborde. En C, c'est pareil. Si tu nettoies pas tes conneries, le programme va prendre beaucoup trop de place, bouffer de la RAM, être lent, tuer birdperson, en bref: c'est pas cool.

Le remède à cela, c'est free: il fait 'disparaître' la mémoire allouée au moyen de malloc ou de calloc. Il ne faut donc pas oublier de l'appeler dans le cas où vous créez des objets de façon dynamique !

4 - Librairies

4.1 - Introduction

Le C est un language sans énormément de fonctionnalités introduites. Ce choix de design assure un poids minimal de programme: seules les fonctionnalités utiles sont chargées. Des librairies sont alors des sortes de modules où sont définies des types et fonctions utiles au but recherché. On #include alors les librairies nécessaires en début de programme:

#include <stdio.h> //utilise les fonctions de la librairie standard in/output

Les librairies les plus employées sont généralement:

La notation <nom_de_la_lib.h> dit au programme de chercher nom_de_la_lib.h dans le répertoire de librairies: généralement /usr/include/ sous un système UNIX.

4.2 - libc

A venir -

5 - Meta

5.1 - Organisation du code

Vous avez lu le paragraphe sur les fonctions, mais ne comprenez pas à quoi servent les prototypes. On est pas des tanches, on va tout définir d'un coup.
Eh bien figurez vous qu'en avançant, il devient de plus en plus long de compiler. Un programme de dix millions de lignes compile un peu plus lentement que 'hello world !'. Puis lorsqu'on se rend compte que l'on a oublié un s dans un printf, il faut aller le changer et tout recompiler.
Pour éviter ce calvaire, les astucieux développeurs ont décidé de fragmenter leur code. Cela consiste a mettre toutes les fonctions servant un but commun dans le même fichier .c, et d'utiliser un #include pour y accéder ultérieurement. Le problème, c'est que #include ne fait que copier le fichier référencé et le coller avant le code: le problème reste le même. Pour éviter cela, on crée un fichier .h pour chaque fichier .c du programme, où l'on déclare toutes les fonctions du fichier .c à l'aide de prototypes, puis #include le fichier .h là où les fonctions qu'il définit sont utilisées.
Le seul fichier sans .h est le fichier où est écrit la boucle main, généralement nommé main.c, mais qui peut être nommé n'importe comment.

En clair, un projet comporte:

Pour compiler, on peut utiliser:

gcc main.c <fichier.c n1> <fichier.c n2> ...

Mais on peut aussi utiliser make et profiter des avantages qu'apporte la fragmentation. Le procédé nécessite la rédaction d'une Makefile qui est une suite d'intructions, généralement la compilation de fichiers .c annexes avant le main.c.

5.2 - Makefiles

Imaginons un projet ressemblant à ceci:

-rw-r--r--  1 jb jb  13K Jan 10 21:01 arbre.c
-rw-r--r--  1 jb jb 2.2K Jan 10 21:01 arbre.h
-rw-r--r--  1 jb jb 2.5K Jan  6 13:30 donnees.c
-rw-r--r--  1 jb jb 1.3K Jan 10 21:01 donnees.h
-rw-r--r--  1 jb jb  472 Jan 10 21:13 main.c
-rw-r--r--  1 jb jb 5.1K Jan 10 21:19 mediane.c
-rw-r--r--  1 jb jb  250 Jan 10 21:06 mediane.h
-rw-r--r--  1 jb jb 2.4K Jan 10 21:01 menu.c
-rw-r--r--  1 jb jb  179 Jan 10 21:01 menu.h

Avec main.c important menu.h et donnees.h, arbre.h important mediane.h, menu.h important arbre.h. Pour compiler tout ce beau monde, il faut:

Clairement, on a pas le temps pour ces conneries. Pour remédier à cela, on fait des Makefiles: des fichiers de recettes de compilation. En voila un exemple:

CC=gcc -std=c99 -Wall -Wextra

decision: main.c donnees.o arbre.o mediane.o menu.o
        $(CC) -o decision main.c arbre.o donnees.o mediane.o menu.o

donnees.o: donnees.c
        $(CC) -c donnees.c

arbre.o: arbre.c donnees.o
        $(CC) -c arbre.c

mediane.o: mediane.c donnees.o arbre.o
        $(CC) -c mediane.c

menu.o: menu.c
        $(CC) -c menu.c

clean:
        rm -rf *.o

Comment lire ça ? C'est très simple. Une Makefile se lit comme une recette de cuisine:

fichier_a_creer: ingredient1 ingredient2 ingredient3 
    commande à effectuer

Le <tab> à la deuxième ligne est important. On peut déclarer des variables (genre le CC au début) et les appeler avec un $( .. ). Les 'ingrédients' peuvent être des fichiers, ou d'autres actions de la Makefile ! Ainsi pour faire decision, make va voir que donnees.o est nécessaire, donc le compiler, puis arbre.o, et ainsi de suite.

Pour utiliser la Makefile, rien de plus simple ! Lancer make dans un terminal, dans le même dossier que la Makefile, va lancer make sur la première directive. Par exemple:

jb@arc1:~/Projects/TD8-9_Struct $ make
gcc -std=c99 -Wall -Wextra -c donnees.c
gcc -std=c99 -Wall -Wextra -c arbre.c
gcc -std=c99 -Wall -Wextra -c mediane.c
gcc -std=c99 -Wall -Wextra -c menu.c
gcc -std=c99 -Wall -Wextra -o decision main.c arbre.o donnees.o mediane.o menu.o

Et voilà ! En rentrant 4 lettres, le programme est compilé !

On peut aussi lancer une directive précise en donnant le nom:

jb@arc1:~/Projects/TD8-9_Struct $ make clean
rm -rf *.o