Vous êtes sur la page 1sur 161

Convention sur les acétates

• Concernant la présentation des acétates :


– malgré le fait qu’elles soient relativement bien remplies, les
acétates tentent de présenter un contenu :
• cohérent
• facile à suivre
• avec un contenu pouvant servir de référence
• Concernant les exemples :
– plusieurs exemples sont inclus afin d’illustrer les différents
concepts enseignés
– certains exemples contiennent quelques notions plus
avancés à titre de référence
– afin d’illustrer davantage les notions du langage C, peu
d’exemple sont en pseudo code

1
Convention sur les acétates

• Coloration du texte en fonction des éléments du


langage C
Mots réservés en bleu Constantes en rouge
Instructions en mauve Commentaires en vert

• Format des exemples


Stéréotypes de :
déclarations, fonctions, éléments de langage, ...

Exemples de :
déclarations, fonctions, éléments de langage, portions de code, programmes, ...

Exemple de pseudo code pour représenter des :


algorithmes, éléments logiques, ...

2
Généralité du langage C

• Importance de la programmation
– Que permet la programmation?
– Outil technologique incomparable
– Donne une longueur d’avance à ceux qui la connaisse
– Exemples concrets d’application
• Informatique « utilitaire »
– Automatisation de tâches routinières ()
• Informatique de gestion
– Gestion de projet
gestion des dépassements de coût, gestion des feuilles de temps, ...
– Gestion des ressources – gestion du matériel, gestion du personnel, ...
– Gestion de l’information – sauvegarde d’information, base de connaissance, ...
• Informatique industriel
– Contrôle de procédé
contrôle industriel, optimisation de systèmes de production, ...
– Manipulation de données
acquisition, interprétation, prise de décision, représentation, ...
robotique industriel, représentation graphique, vision, réseaux de neurones, ***

3
Généralité du langage C

• Historique
– 1960 – Algol60
très abstrait, donne le Pascal, PL/I et CPL
– 1967 – BCPL par Martin Richards
Basic Combined Programming Language
– 1970 – Langage B par Ken Thompson
afin d’assurer l’évolution de Unix écrit en assembleur, son créateur crée ce
langage inspiré du BCPL
– 1972 – Langage C par Dennis Ritchie et Ken Thompson
après modification du langage B

4
Généralité du langage C

• Près du langage machine


– initialement destiné à la programmation de système
– assembleur évolué
• Langage typé faible
– permet l’affectation de types différents
– contrairement au langage C++

5
Généralité du langage C

• Constituants d’un programme


– Essentiel
• Fonction main
• Utilisation d’instructions de contrôle, de branchement et
d’itération
• Utilisation d’instructions d’accès mémoire, d’arithmétique et de
logique
– Possible
• Plusieurs fichiers sources référencés par #include
• Déclaration, définition et utilisation de fonctions personnalisées
• Appel de fonctions provenant des
– déclarations personnalisées
– bibliothèques standard

6
Généralité du langage C

• Exemple d’un programme très simple


#include "stdio.h"

/* début du programme */
void main(void)
{
printf("Ceci est un programme simple!");

return;
}

Entrée
- aucune entrée -

Sortie
Ceci est un programme simple!

7
Généralité du langage C

• Exemple d’un programme


#include "stdio.h"
Entrée
/* début du programme */ GPA665, J’adore
void main(void)
{ Sortie
char C;
G - 71 - 47
P - 80 - 50
do {
A - 65 - 41
C = getch();
6 - 54 - 36
printf("%c - %d - %x\n", C, C, C);
6 - 54 - 36
} while (C != EOF);
5 - 53 - 35
, - 44 - 2c
return;
- 32 - 20
}
J - 74 - 4a
' - 39 - 27
a - 97 - 61
d - 100 - 64
o - 111 - 6f
r - 114 - 72
e - 101 - 65

8
Généralité du langage C

• Exemple d’un programme


#include "stdio.h" Entrée
- aucune entrée -
/* début du programme */
void main(void)
{ Sortie
int i, n, Sum; Somme cumulative a l'element 00 : 000
float Mean; Somme cumulative a l'element 01 : 001
Somme cumulative a l'element 02 : 003
n = 15; Somme cumulative a l'element 03 : 006
Sum = 0; Somme cumulative a l'element 04 : 010
Mean = 0.0f; Somme cumulative a l'element 05 : 015
for (i = 0; i < n; i++) { Somme cumulative a l'element 06 : 021
Sum += i; Somme cumulative a l'element 07 : 028
printf("Somme cumulative a l'element Somme cumulative a l'element 08 : 036
%02d : %03d\n", i, Sum); Somme cumulative a l'element 09 : 045
} Somme cumulative a l'element 10 : 055
Somme cumulative a l'element 11 : 066
Mean = (float) Sum / n; Somme cumulative a l'element 12 : 078
printf("\nMoyenne des %d element(s) : Somme cumulative a l'element 13 : 091
%0.2f", n, Mean); Somme cumulative a l'element 14 : 105

return; Moyenne des 15 element(s) : 7.00


}

9
Généralité du langage C

• Compilateur et éditeur de liens

10
Types de base
• 5 types de base
– char, int, float, double et void

• 2 modificateurs de type
– Signe (pour les entiers seulement char et int : où b représente le nombre de bit)
• signed (par défaut) : variable  [ -2b/2, 2b/2 – 1 ]
• unsigned : variable  [ 0, 2b – 1]
– Taille mémoire
• short : réduit le domaine des valeurs possibles
– uniquement pour int
• long : augmente l’intervalle [min, max]
– uniquement pour int et float selon la norme ANSI
– certains compilateurs supportent aussi le type double

• Attention au type bool

11
Types de base

• Le type char
– Un seul type, deux interprétations :
• caractère lorsque utilisé en conjonction avec les fonctions de
gestion de chaîne de caractères et la table Ascii
• entier autrement

12
Types de base

• Le type int (16 bits) ou short int

13
Types de base

• Le type int (32 bits) ou long int

unsigned
0 X 4 294 967 295

32 bits de données

32 bits - 4 octets

14
Types de base

• Le type float

3.4 10-38 |X| 3.4 1038

1 bit de signe

8 bits de données pour l'exposant


23 bits de données pour la mantisse

32 bits - 4 octets

15
Types de base

• Le type long float ou double

16
Types de base

• Le type long double


Ce n’est pas un type standard du C ou du C++, il est seulement disponible sur
certains compilateurs (par exemple : VisualC++ et C++Builder)

17
Types de base

• Le type void
– Ne permet pas de déclarer une variable
– Permet d’indiquer au compilateur que le type est indéfini
pour les cas suivants :
• retour de fonction
(aucune valeur de retour)
• paramètres d’une fonction
(aucun paramètre passé à la fonction)
• pointeur de type indéfini

18
Éléments du langage et règles d’écriture

• Les identificateurs
– Sert à nommer les :
• variables
• étiquettes
• fonctions
– Constitués d’une suite de lettres et de chiffres commençant
par une lettre
• le seul autre caractère permis : '_'
– Il faut faire attention à :
• le langage C distingue les majuscules des minuscules
• la liste des mots réservés
• Les types externes peuvent être restreint à un maximum de 8
caractères pour certains compilateurs

19
Éléments du langage et règles d’écriture

• Les mots réservés du langage C


auto break case char const continue

default do double else enum extern

float for goto if int long

register return short signed sizeof static

struct switch typedef union unsigned void

volatile while

20
Éléments du langage et règles d’écriture

• Les commentaires
– Pour déterminer une zone de commentaires, on utilise :
• /* pour le début
• */ pour la fin

– Attention aux :
• imbrications de commentaires
• commentaires commençant par //

21
Éléments du langage et règles d’écriture

• Les instructions
– Appel d’un opérateur ou d’une fonction
– Plusieurs instructions peuvent être regroupées par un bloc

• Les blocs d’instructions


– Défini par des accolades
• { marque le début d’un bloc
• } marque la fin d’un bloc
{ /* début d’un bloc d’instructions */

/* instruction 1 */
/* ... */
/* instruction n */

} /* fin d’un bloc d’instructions */

22
Éléments du langage et règles d’écriture

• Les expressions
– Correspondent à une combinaison d’éléments tel que des :
• identificateurs
• constantes
• variables
• tableaux
• pointeurs
• structures
• unions
• appels de fonctions
• opérateurs unaires ou binaires
• ...
• chaque ligne de code excluant celles ayant uniquement des
commentaires

23
Éléments du langage et règles d’écriture

• Les constantes
– Entier :
• octal : commençant par un zéro 0145376
• décimal : tel quel 51966
• hexadécimal : commençant par un zéro et un x 0xCAFE
les majuscules et minuscules fonctionnent 0Xcafe
– Nombre réel
• de type float : terminant par f 3.141593f
• de type double : tel quel 3.141593

24
Éléments du langage et règles d’écriture

• Les constantes
– Caractère(s)
• un seul caractère selon la table
de caractères ASCII : entre apostrophes '0'
le caractère zéro est équivalent à l’entier 48
• une chaîne de caractères respectant
les normes établies : entre guillemet "GPA665"
la chaîne de caractères "GPA665" correspond au
tableau de caractères : 71-80-65-54-54-53-0
• il existe plusieurs caractères spéciaux :
\a - beep - \\ barre oblique inverse
\b retour arrière \' apostrophe
\f chargement de page \" guillemet
\n saut de ligne \? point d’interrogation
\r retour en début ligne \nnn valeur ASCII en octal
\t tabulation horizontale \xnnn valeur ASCII en hexadécimal
\v tabulation verticale \0 fin de la chaîne de caractère

25
Éléments du langage et règles d’écriture

• Les opérateurs
– Unaires – Sont évalués de droite à gauche
• ++ var effectue une pré incrémentation
• -- var effectue une pré décrémentation
• var ++ effectue une post incrémentation (de gauche à droite)
• var -- effectue une post décrémentation (de gauche à droite)
• (type) exp spécifie une modification de type (« type cast »)
• Sizeof() ou sizeof(var ou type) retourne la taille en octet d’une variable ou d’un type
• & var retourne l’adresse de la variable
• * exp retourne le contenu de l’adresse spécifiée (déréférence)
• + exp retourne la valeur d’une variable (type numérique)
• - exp retourne la valeur négative d’une variable (type numérique)
• ~ exp retourne le complément bit à bit d’une variable
• ! exp retourne le complément logique d’une variable

26
Éléments du langage et règles d’écriture

• Les opérateurs
– Binaires – Sont évalués de gauche à droite
• Opérateurs multiplicatifs
– exp1 * exp2 retourne le résultat de la multiplication
– exp1 / exp2 retourne le résultat de la division
– exp1 % exp2 retourne le résultat du modulo (reste de la division entière)

• Opérateurs additifs
– exp1 + exp2 retourne le résultat de l’addition
– exp1 - exp2 retourne le résultat de la soustraction

• Opérateurs de décalage de bits


– exp1 << exp2 retourne le résultat du décalage à gauche *
– exp1 >> exp2 retourne le résultat du décalage à droite *

• Opérateurs relationnels
– exp1 < exp2 retourne le résultat de la comparaison est plus petit que
– exp1 > exp2 retourne le résultat de la comparaison est plus grand que
– exp1 <= exp2 retourne le résultat de la comparaison plus petit ou égal
– exp1 >= exp2 retourne le résultat de la comparaison plus grand ou égal
– exp1 == exp2 retourne le résultat de la comparaison est égal à
– exp1 != exp2 retourne le résultat de la comparaison est différents de

27
Éléments du langage et règles d’écriture

• Les opérateurs
– Binaires – Sont évalués de gauche à droite (suite)
• Opérateurs sur les bits
– exp1 & exp2 retourne le résultat du ET logique bit à bit
– exp1 | exp2 retourne le résultat du OU logique bit à bit
– exp1 ^ exp2 retourne le résultat du OU EXCLUSIF logique bit à bit

• Opérateurs logiques
– exp1 && exp2 retourne le résultat du ET logique
– exp1 || exp2 retourne le résultat de OU logique

• Opérateur d’évaluation séquentielle


– exp1 , exp2 effectue les expressions séquentiellement

– Ternaire – Est évalué de gauche à droite


– exp ? val1 : val2 retourne val1 si exp est vraie sinon val2

28
Éléments du langage et règles d’écriture

• Les opérateurs
– D’assignation – Sont évalués de droite à gauche
• var = exp assignation directe

• var *= exp assignation suite à la multiplication


• var /= exp assignation suite à la division
• var %= exp assignation suite au modulo
• var += exp assignation suite à l’addition
• var -= exp assignation suite à la soustraction
• var <<= exp assignation suite au décalage de bits à gauche
• var >>= exp assignation suite au décalage de bits à droite
• var &= exp assignation suite au ET logique bit à bit
• var |= exp assignation suite au OU logique bit à bit
• var ^= exp assignation suite au OU EXCLUSIF logique bit à bit

• Ils sont tous équivalent à : var = var opérateur exp

29
Éléments du langage et règles d’écriture

• Les opérateurs
– Autres opérateurs
• ( exp ) - appel de fonction
- spécification de priorité dans une expression
• [ exp ] - retourne un élément spécifique d’un tableau
(décalage plus déréférence)
• var . élément - retourne l’élément spécifié d’une structure (indirection) ou
d’un type union
• var -> élément - retourne l’élément spécifié par le pointeur d’une structure
(indirection)

30
Éléments du langage et règles d’écriture

• Priorité des opérateurs


Ordre Opérateurs
1 () [] . ->
2 ++ -- +(signe) -(signe) *(déréférence) &(adresse) ! ~ (type)(« type cast ») sizeof
3 *(multiplication) / %
4 +(addition) -(soustraction)
5 >> <<
6 == !=
7 &
8 ^
9 |
10 &&
11 ||
12 ?:
13 = += -= *= /= %= &= ^= |= <<= >>=
14 ,

Si deux opérateurs possèdent la même priorité, ils seront exécuté de gauche à droite selon
l’ordre d’apparition sur la ligne d’instruction
31
Éléments du langage et règles d’écriture

• Les instructions de contrôle


– L’instruction conditionnelle if
Attention aux if imbriqués. Utilisez les définitions de bloc.
if ( exp ) inst1 [ else inst2 ]

if (n != 0) {
Mean = Sum / n;
} else {
printf("Erreur : division par 0");
}

vrai exp faux

inst1 inst2

32
Éléments du langage et règles d’écriture

• Les instructions de contrôle


– L’instruction conditionnelle switch
switch ( integer-exp ) {
[ case constexp1 : inst1 [ break ;] ]
...
[ case constexpn : instn [ break ;] ]
[ default : instd ]
}
sans break

sans break

sans break

sans break
33
Éléments du langage et règles d’écriture
#include <stdio.h>
#include <ctype.h>
#include <conio.h>
#include "Customfunctions.h"

void main(void)
{
while (1) {
printf("Quelle operation voulez-vous faire ?\n");
printf("\t1 (Q)uitter\n\t2 (S)auvegarder\n\t 3 (P)oursuivre\n\t4 (M)ettre en veille\n");
char Reponse = toupper(getch()); /* Attention */

switch (Reponse) {
case 1 :
case 'Q' : return;
case 2 :
case 'S' : SaveData();

case 3 :
case 'P' : GetNewData();
AnalyseNewData();
break;
case 4 :
case 'M' : GotoSleep();
}
}
}

34
Éléments du langage et règles d’écriture

• Les instructions de contrôle


– L’instruction itérative while
while ( exp ) inst

while (Answer == 'Y' || Answer == 'y') {


DoSomething();

printf("Voulez-vous poursuivre?");
Answer = getch();
}

exp faux

vrai

inst

35
Éléments du langage et règles d’écriture

• Les instructions de contrôle


– L’instruction itérative do
do inst while ( exp );

do {
DoSomething();

printf("Voulez-vous poursuivre?");
Answer = getch();
} while (Answer != 'N' && Answer != 'n');

36
Éléments du langage et règles d’écriture

• Les instructions de contrôle


– L’instruction itérative for
for ( inst1; exp; inst2 ) inst3
inst1
#define N 65535
int i, Sum, Data[N];

for ( i = 0, Sum = 0; i < N; i++) {


Sum += Data[i]; exp faux
}
vrai

inst3

inst2

37
Éléments du langage et règles d’écriture

• Les instructions de contrôle


– Les commandes de
terminaison sont : inst1
• break termine
l’instruction do, for,
switch ou while le
plus près exp faux

• continue passe à la vrai


prochaine itération des
goto .
instructions do, for ou inst3
while
• goto passe directement
inst2
à l’étiquette spécifiée

38
Éléments du langage et règles d’écriture

• La déclaration d’étiquettes
– La fonction goto doit indiquer une étiquette existante dans
le programme.
– Les étiquettes sont définies par le deux points
identificateur :

Erreur007:

– Les étiquettes doivent obligatoirement être mises devant


une instruction pour être valide

– Malgré le fait qu’elle est très puissante, l’instruction goto


n’est pratiquement jamais recommandée.

39
Éléments du langage et règles d’écriture

• La déclaration de nouveaux types


– Types synonymes avec typedef
En fait, ceci ne permet pas la déclaration d’un nouveau type mais plutôt
d’un synonyme pratique ou pertinent pour l’usager
typedef declaration-de-type synonyme;

typedef unsigned int uint;


typedef unsigned char uchar;
typedef uchar Pixel;

– Types énumérés avec enum


Type consistant à contraindre l’affectation de variable à un ensemble de
constantes appelées énumérateurs
enum [ type ] { liste-enumerations } [ variable ] ;
typedef enum [ type ] { liste-enumerations } [ type ] ;

enum JourSemaine { Lundi, Mardi, Mercredi, Jeudi, Vendredi } AujourdHui;


typedef enum { Rouge, Vert, Bleu } CouleursPrimairesAdditif;
typedef enum CouleursPrimaireSoustractif { Jaune, Magenta, Cyan };

40
Éléments du langage et règles d’écriture

• La déclaration de nouveaux types


– Types structurés avec struct
• Nouveau type permettant de contenir plusieurs types existants
(sous-types) simultanément pour une même variable.
• Tous les sous-types sont disponibles simultanément.
• La taille du nouveau type correspond au minimum à la somme
des tailles de chacun des sous-types.
struct [ type ] { liste-membres } [ variable ] ;
typedef struct [ tag ] { liste-membres } type ;

struct Individu { char Nom[64]; int Age; float Taille; } Nicolas, Yan, Philippe;
typedef struct refNode { float Data; refNode *Next; } LinkedListNode;

41
Éléments du langage et règles d’écriture

• La déclaration de nouveaux types


– Types unis avec union
• Nouveau type permettant de contenir différents types existants
(sous-types) pour une même zone mémoire.
• Puisque tous les sous-types partagent la même zone mémoire,
un seul de ces sous-types est disponible à la fois.
• La taille du nouveau type correspond au minimum à la taille du
plus grand sous-type.
union [ type ] { liste-membres } [ variable ] ;
typedef union [ tag ] { liste-membres } type ;

union Data { float LowPrecision; double HighPrecision; } Temperature;


typedef union { char Answer; float Value; } QuestionResult;

42
Éléments du langage et règles d’écriture

• La déclaration de variables
– La déclaration d’une seule variable :
[ classe_de_mémorisation ] type identificateur ;

static int Sum;

– La déclaration d’un tableau unidimensionnel :


type identificateur[ taille du tableau ] ;

float Vector[256];

– La déclaration d’un tableau multidimensionnel :


type identificateur[dim1][dim2][dim_n]... ;

unsigned char ColorImage[640][480][3];

on reviendra plus tard sur les classes de mémorisation

43
Éléments du langage et règles d’écriture

• La déclaration de variables
– La déclaration d’un pointeur :
[ classe_de_mémorisation ] type * identificateur ;

int *PtrData;

– On peut déclarer plusieurs variables simultanément :


type ident1, ident2, ident3;

float n, Sum, Mean, StdDev, Data[256];

Attention à la déclaration simultanée de pointeurs!

44
Éléments du langage et règles d’écriture

• La déclaration de variables
– La déclaration d’une variable constante :
[ classe_de_mémorisation ] const type identificateur = valeur;

const float PI = 3.141592654;

Une variable constante doit être initialisée à même sa déclaration


et ne peut jamais être modifiée.

45
Éléments du langage et règles d’écriture

• L’initialisation de variables
– Selon la norme ANSI, le langage C n’initialisent aucun type
automatiquement lors de l’instanciation de variables
– Malgré tout, les compilateurs modernes comme VisualC++
initialisent à 0 uniquement les variables provenant des
types de base
• char, int, float et double
• les pointeurs, tableaux et types personnalisés ne sont pas
initialisés

• malgré tout, faites attention aux optimisateurs de code

46
Éléments du langage et règles d’écriture

• L’initialisation de variables
– Les variables provenant des types de base peuvent être
explicitement initialisées lors de leur déclaration
• Pour les variables standard :
[ classe_de_mémorisation ] type identificateur = exp;

int IPos = 10, INeg = -10;


void *RefPtr = NULL;

• Pour les variables de type union


[ classe_de_mémorisation ] type identificateur = { exp };

typedef union { float NbrReel; int NbrEntier; } Nombre;


Nombre N1 = { 3.1416f }, N2 = { 101 }; // les bons types ont été assignés

• Pour les variables de type struct


[ classe_de_mémorisation ] type identificateur = { exp1, exp2, ... };

typedef struct { char Nom[256]; int Age; float Taille; } Individu;


Individu Pianiste = { "Julius Katchen", 43, 1.80 };

47
Éléments du langage et règles d’écriture

• L’initialisation de variables
• Pour un tableau unidimensionnel :
type identificateur[ [taille du tableau] ] = { exp1, exp2, exp3, ... };

float ResultatsLabGPA665[3] = { 89.0f, 97.0f, 100.0f };


float ResultatsExamGPA665[] = { 78.0f, 84.5f }; /* intéressant */

• Pour un tableau multidimensionnel :


type ident[dim 1][dim n]... = { ... { exp1, ... }, { expn, ... }, ... };

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

48
Éléments du langage et règles d’écriture

• Éléments de langage particuliers aux pointeurs


– Opérateurs d’adressage et de déréférence
• On utilise & pour obtenir l’adresse d’une variable ou d’une
fonction préalablement allouée
• On utiliser * (la déréférence) pour accéder au contenu
« pointé » par une adresse

int Nombre1, Nombre2;


int *PtrNbr;

Nombre1 = 666;
PtrNbr = &Nombre1;

Nombre2 = *PtrNbr;

49
Éléments du langage et règles d’écriture

• Éléments de langage particuliers aux types


composés
– Accès aux membres des types composés union ou struct
• On utilise l’opérateur d’indirection . (point) pour accéder à un
membre particulier
• Pour les accès via un pointeur, on peut utiliser :
– une combinaison du point et de la déréférence
– l’opérateur d’indirection ->
typedef struct { char Nom[256]; int Age; float Taille; } Individu;

Individu Employe1, Employe2, *RefEmp1, *RefEmp2;

Ref1Emp = &Employe1;
Ref2Emp = &Employe2

RefEmp1->Age = 2 * (*RefEmp2).Age;

50
Éléments du langage et règles d’écriture

• Éléments de langage particuliers aux tableaux


– Accès aux éléments d’un tableau
• On utilise l’opérateur de déréférence avec décalage [ ] pour
accéder à un élément spécifique d’un tableau
• On peut aussi utiliser l’opérateur de déréférence avec
l’arithmétique des pointeurs

float NotesExam[] = { 84.0f, 68.0f, 90.5f };


float PonderationsExam[] = { 0.30f, 0.30f, 0.40f };

float ResultatFinal = 0.0f;

for (int i = 0; i < 3; i++) { /* Attention a la declaration de i */


ResultatFinal += NotesExam[i] * PonderationsExam[i];
}

51
Éléments du langage et règles d’écriture

• Éléments de langage particuliers


typedef char String[256];

typedef union {
String Nom;
int NoRef;
} IDRef;

struct Individu {
IDRef ID;
int Age;
String SignesDistinctifs;
} Professeur, ChargeDeCours, ChargeDeLab;
Individu *Ref[3] = { &Professeur, &ChargeDeCours, &ChargeDeLab };

strcpy(Professeur.ID.Nom, "Mohamed Cheriet");


ChargeDeCours.ID.NoRef = 12345;
strcpy(ChargeDeLab.ID.Nom, "Yan Levasseur");
ChargeDeLab.Age = 25;
strcpy(ChargeDeLab.SignesDistinctifs, "Intellectuellement superieur");

(*Ref[0]).Age = Ref[2]->Age << 1;

52
Éléments du langage et règles d’écriture

• Les fonctions
– Les fonctions sont des entités permettant de regrouper
plusieurs instructions et expressions en un sous-ensemble
disjoint, autonome et fonctionnel.
– Entre autre, elles aident à la lisibilité, la modularité et la
réutilisabilité du code
/* Syntaxe moderne */
[ classe-mémorisation ] type-retour identificateur( [ type1 param1, ... ] )
{
exp1
exp2
...

[ return [ ( ] [ exp-retour ] [ ) ] ; ]
}

float Square(float Data)


{
return Data * Data;
}

53
Éléments du langage et règles d’écriture

• Les instructions au préprocesseur


– Ces instructions servent à donner des instructions
préalables au compilateur.
– Elles sont faciles à reconnaître car elles sont tous précédés
de #
• Inclusion de fichiers avec #include. Permet de faire référence
au contenu d’un ou plusieurs autres fichiers externes.
#include "nom_de_fichier" /* recherche locale + catalogue système */
#include <nom_de_fichier> /* recherche catalogue système uniquement */

#include "mes_propres_fonctions.h"
#include <math.h>

void main(void)
{
printf("Le racine carre de 9 est : %lf", sqrt(9.0));
printf("La racine cubique de 27 est : %lf", cubicroot(27.0));
}

54
Éléments du langage et règles d’écriture

• Les instructions au préprocesseur


• Les commandes de compilation conditionnelle sont :
– #if équivalent du if
– #elif équivalent à un else if
– #else équivalent au else
– #endif indique la terminaison du bloc if
– #ifdef si une étiquette est définie
– #ifndef si une étiquette n’est pas définie
#if exp1
inst1
#elif exp2
inst2
#else
inst3
#endif

55
Éléments du langage et règles d’écriture

• Les instructions au préprocesseur


– #define et #undef permettent de déclarer de nouvelles
définitions ou d’annuler des définitions existantes. Ces définitions
permettent au programmeur de nommer des éléments du
programme comme des constantes et des étiquettes de référence.
#define identificateur expression
#undef identificateur

#ifndef _CONSTANTES_H
Constantes.h

#define _CONSTANTES_H

#define PI 3.141592654

#endif

#include "Constantes.h"
Programme.c

void main(void)
{
printf("Le nombre pi est : %0.6lf", PI);
}

56
Éléments du langage et règles d’écriture

• Les instructions au préprocesseur


• #define permet aussi de définir des macro. Les macros
ressemblent à des fonctions sans définition de type.
#define nom_macro(param1, param2, ...) expression

#define Max(Val1, Val2) ((Val1) > (Val2) ? (Val1) : (Val2))

void main(void)
{
int N1 = 100, N2 = 500;
printf("Le maximum entre les nombres %d et %d est : %d",
N1, N2, Max(N1, N2));
}

57
Éléments du langage et règles d’écriture

• Les instructions au préprocesseur


• Pourquoi éviter les macros ?
#define Max(Val1, Val2) ((Val1) > (Val2) ? (Val1) : (Val2))
#define Square(Val) a*a

void main(void)
{
int A, *B;
A = Max('a', B); /* Compile malgré les types incompatibles */

int i = 1, j = 2;
A = Max(++i,++j); /* On s’attend à 3 alors qu’on obtient 4 */

double X = 3, Y;
Y = Square(X + 1); /* On s’attend à 16 alors qu’on obtient 7 */
}

• En fait, l’utilisation du #define correspond à un copier/coller


avant la compilation.

58
Principales fonctions provenant des bibliothèques
standard

• stdio.h
– fopen ouverture d’un fichier
– fclosefermeture d’un fichier
– ferrorvérifie si il y a eu une erreur avec un fichier
– feof vérifie si un fichier est arrivé à la fin
– fgetpos
détermine un indicateur pour la position courante d’un
fichier
– fsetpos positionne un fichier selon un indicateur de position
– ftell retourne la position d’un fichier
– fseek positionne un fichier selon un décalage spécifié
– fread lit des données provenant d’un fichier à partir de la
position courante
– fwrite écrit des données dans un fichier

59
Principales fonctions provenant des bibliothèques
standard

• stdio.h
– fgets lit une chaîne de caractères provenant d’un fichier
– gets lit une chaîne de caractères provenant de l’entrée
standard
– fputs écrit une chaîne de caractères dans un fichier
– puts écrit une chaîne de caractères à la sortie standard
– fgetc lit un caractère provenant d’un fichier
– getc lit un caractère provenant de l’entrée standard
– fputc écrit un caractère dans un fichier
– putc écrit un caractère à la sortie standard
– stdin constante de redirection vers l’entrée standard
– stdout constante de redirection vers la sortie standard
– stderr constante de redirection vers la sortie d’erreur standard

60
Principales fonctions provenant des bibliothèques
standard

• stdio.h
– printf* écrit une chaîne de caractères mise en forme vers la
sortie standard
– sprintf écrit une chaîne de caractères mise en forme dans une
variable de type chaîne de caractères
– fprintf écrit une chaîne de caractères mise en forme dans un
fichier
– scanf* lit une chaîne de caractères mise en forme provenant de
l’entrée standard
– sscanf lit une chaîne de caractères mise en forme provenant
d’une variable de type chaîne de caractères
– fscanf lit une chaîne de caractères mise en forme provenant
d’un fichier

61
Principales fonctions provenant des bibliothèques
standard

• stdlib.h
– NULL déclaration du pointeur NULL
– malloc allocation d’un bloc de mémoire
– calloc allocation d’un bloc de mémoire initialisé à zéro
– realloc réallocation d’un bloc de mémoire
– free libération d’un bloc de mémoire alloué
– system exécute une commande du système d’exploitation
– exit termine l’exécution du programme en cours
– atoi conversion d’une chaîne de caractères en valeur
numérique entière
– atof conversion d’une chaîne de caractères en valeur
numérique réelle

62
Principales fonctions provenant des bibliothèques
standard

• memory.h
– memcpy copie un bloc mémoire d’un endroit à un autre
– memcmp compare deux blocs mémoire caractère par caractère
– memset initialise chaque octet d’un bloc mémoire par la valeur
d’un octet spécifié

63
Principales fonctions provenant des bibliothèques
standard

• math.h
– cos retourne le cosinus d’un angle en radian
– acos retourne, en radian, l’arcosinus d’un nombre
– cosh retourne le cosinus hyperbolique d’un angle en radian
– sin retourne le sinus d’un angle en radian
– asin retourne, en radian, l’arcsinus d’un nombre
– sinh retourne le sinus hyperbolique d’un angle en radian
– tan retourne, la tangente d’un angle en radian
– atan retourne, en radian, la tangente d’un nombre
– atan2 retourne, en radian, la tangente d’un nombre
– tanh retourne la tangente hyperbolique d’un angle en radian

64
Principales fonctions provenant des bibliothèques
standard

• math.h
– pow calcule un nombre à la puissance d’un autre
– exp calcule l’exponentiel d’un nombre (en)
– log calcule le logarithme naturelle d’un nombre
– log10 calcule le logarithme en base 10 d’un nombre
– sqrt calcule la racine carrée d’un nombre
– abs calcule la valeur absolue d’un nombre entier
– fabs calcule la valeur absolue d’un nombre réel
– div effectue une division entière
– floor retourne l’arrondi inférieur d’un nombre
– ceil retourne l’arrondi supérieur d’un nombre
– srand détermine un point de départ pour la génération d’un
nombre pseudo aléatoire
– rand génère un nombre pseudo aléatoire

65
Principales fonctions provenant des bibliothèques
standard

• string.h
– strlen retourne la longueur d’une chaîne de caractères
– strset initialise une chaîne de caractères à un caractère
spécifique
– strcpy copie une chaîne de caractères
– strncpy copie n caractères provenant d’une chaîne de caractères
– strcmp compare deux chaînes de caractères
– strncmp compare n caractères provenant de deux chaînes de
caractères
– strcat concatène deux chaînes de caractères
– strncat concatène n caractères d’une chaîne de caractères dans
une autre

66
Principales fonctions provenant des bibliothèques
standard

• string.h
– strchr recherche un caractère spécifique dans une chaîne de
caractères
– strstr recherche une chaîne de caractères spécifique à
l’intérieur d’une chaîne de caractères
– strtok recherche la prochaine occurrence d’un « token » dans
une chaîne de caractères

67
Principales fonctions provenant des bibliothèques
standard

• ctype.h
– isdigit identifie si un caractère est numérique
– isalpha identifie si un caractère est alphabétique
– islower identifie si un caractère est en minuscule
– isupper identifie si un caractère est en majuscule
– tolower convertie un caractère à un caractère minuscule
– toupper convertie un caractère à un caractère majuscule

68
Portée et durée de vie des variables

• Gestion d’une variable par le compilateur C


– Les 2 points essentiels suivants :
• le domaine de validité d’une variable
(couramment appelé le « scope », la portée ou la visibilité)
• la durée de vie de la variable
(c’est-à-dire la durée pendant laquelle la variable existe vraiment)
sont déterminés par :
• l’emplacement de la définition de la variable
• la classe de mémorisation utilisée à sa déclaration
(« storage class »)

70
Portée et durée de vie des variables

• La notion de variable globale et locale est


directement liée à l’emplacement de la déclaration
– Par définition, une variable est visible par toutes les
fonctions et les expressions de code qui suit sa déclaration
si ces dernières sont à un niveau de bloc égal ou inférieur.
– Les variables globales :
• Elles sont déclarées hors de toute fonction
• Elles sont donc connues, valides et utilisables dans tout le
fichier où elle est définie; cela à partir de l’endroit de sa
déclaration. Selon sa classe de mémorisation, elle peut même
s’étendre à d’autres fichiers.
– Les variables locales :
• Elles sont définies à l’intérieur d’un bloc ou d’une fonction. Par
définition, elles sont donc visibles, valides et utilisables
uniquement pour ces blocs ou fonctions.

71
Portée et durée de vie des variables

• Description des classes de mémorisation


– Chaque variable possède une seule classe de mémorisation
parmi les suivantes :
• auto
• static
• extern
• register

– Les variables globales ne peuvent être que des classes


extern ou static

72
Portée et durée de vie des variables

• Description des classes de mémorisation pour les


variables locales :
– auto (pour « automatic ») est la classe de mémorisation
par défaut. Le bloc dans laquelle elles sont définie
détermine leur visibilité et leur durée de vie.
[ auto ] type identificateur ;

auto int Sum;


float Average;

– static Cette classe de mémorisation indique que la portée


d’une variable est la même que pour une variable auto
mais que sa durée de vie correspond à celle du programme.
static type identificateur ;

static int Flag;

73
Portée et durée de vie des variables

• Description des classes de mémorisation pour les


variables locales (suite) :
– extern Cette classe de mémorisation est particulière car
elle n’entraîne pas d’allocation mémoire. Elle fait plutôt
référence à une autre variable existante du même nom
ailleurs dans le programme.
extern type identificateur ;

extern int VersionNoSofware;

– register Ces variables obéissent aux même règles que


celles de type auto. La différence vient du fait que le
compilateur tente de la placé à même les registres du CPU
plutôt que dans la mémoire de travail.
register type identificateur ;

register int i, j, k, l;

74
Portée et durée de vie des variables

• Description des classes de mémorisation pour les


variables globales :
– extern Est la classe de mémorisation par défaut. Leur
portée est au moins le fichier où elle sont déclarées et peut
être étendue à plusieurs fichiers. Leur durée de vie sont
celle du programme.
extern type identificateur ;

extern int GlobalInformation;

– static Leur durée de vie est la même que pour les


variables globales extern. Leur portée inclue le fichier de
leur déclaration mais ne peut être étendue à d’autres
fichiers.
static type identificateur ;

static int RegionalInformation;

75
Portée et durée de vie des variables

• Résumé sur la portée et la durée de vie des


variables :

76
Portée et durée de vie des variables

#include <stdio.h>

int VarGlobale1 = 1; /* Variable globale */

void main(void)
{
int VarLocaleMain = 2; /* Variable locale au main */

if (VarLocaleMain >= 0) {
int VarLocalBlocIf = 321; /* Variable locale */
} /* visible dans le bloc if */

printf("%d, %d, %d\n", VarGlobale1, VarLocaleMain, Fonction(54321));


}

int VarGlobale2 = 123; /* Variable globale invisible par le main */

int Fonction(int Val)


{
int VarLocaleFonction = Val; /* Variable locale à la fonction */

return VarLocaleFonction * VarGlobale1 * VarGlobale2;


}

77
Rappel général sur les pointeurs

• Un pointeur est une variable dont le contenu a pour valeur une


adresse en mémoire.
• Le type du pointeur correspond au type de la variable pointée.
En fait, ce typage ne sert au compilateur qu’à souligner au
programmeur les erreurs potentielles lorsqu’elles surviennent.
• Tous les pointeurs sans exception ont 32 bits (en considérant un
environnement 32 bits tel que Windows)
• Attention à l’opérateur * puisqu’il est le seul élément
syntaxique du langage C à avoir 3 sens différents selon le
contexte :
– La multiplication dans une expression mathématique
– La déclaration d’un pointeur dans une déclaration de variable
– La déréférence d’un pointeur pour accéder au contenu de l’adresse
mémoire référencée

79
Représentation schématique de la mémoire
• Afin de bien comprendre l’état des pointeurs il est pratique de
schématiser l’état de la mémoire.
• Une astuce efficace consiste à observer l’évolution d’un programme
ligne par ligne en faisant le suivi de toutes les variables par la
représentation par case.
• Pour chaque allocation ou libération d’une variable, on crée ou détruit
une case représentant la variable à suivre.
• Ensuite, pour chaque affectation, comparaison ou manipulation de ces
variables, on s’assure que les types sont compatibles et que chacune
des instructions correspondent bien à ce qui est souhaité.
Variable standard Variable de type constant

• On assigne les adresses arbitrairement à titre de référence.

80
Représentation schématique des pointeurs

• Que pensez-vous de ce petit programme?


void main(void)
{
int V1, *P1; /* 01 */
int *P2, V2; /* 02 */
/* 03 */
P1 = &V1; /* 04 */
P2 = &V2; /* 05 */
/* 06 */
V1 = 100; /* 07 */
V2 = *P1; /* 08 */
*P2 = 2.0 * *P1; /* 09 */
/* 10 */
int V3 = P1; /* 11 */ /* Attention! Est-ce que ça compile? */
int *P3 = V1; /* 12 */ /* Attention! Est-ce que ça compile? */
}

81
Pointeurs multiples

• Partant du fait qu’un pointeur est une variable dont le contenu


indique l’endroit en mémoire du contenu d’une autre variable
de n’importe quel type. Qu’arrive-t-il si nous désirons utiliser
un pointeur indiquant un autre pointeur?
• On déclare les pointeurs multiples par l’utilisation de plusieurs
*. En fait, on utilise autant d’astérisque que d’indirection
nécessaire.
[ Classe_de_memorisation ] type ***...* identificateur ;

int Value;
int *PtrLevel1, **PtrLevel2, ***PtrLevel3;

PtrLevel1 = &Value;
PtrLevel2 = &PtrLevel1;
PtrLevel3 = &PtrLevel2
Value = 10;
*PtrLevel1 = 100;
**PtrLevel2 = 1000;
***PtrLevel3 = 10000;

82
Pointeurs et structures
• On revient sur les opérateurs d’indirection pour s’assurer de bien les maîtriser.
#define STRING_MAX 256
typedef struct {
int NoCivique; /* No civique */
char Rue[STRING_MAX]; /* Nom de la rue */
char Ville[STRING_MAX]; /* Nom de la ville */
} Adresse;
typedef struct {
char Nom[STRING_MAX]; /* Nom de l’individu */
int Age; /* Age en annee de l’individu */
int *RefNoCoursGPA; /* Numero du cours suivi en GPA */
Adresse *RefAd; /* Reference vers une adresse */
} Individu;

/* Declaration et initialisation */
Adresse Residence1 = { 1100, "Notre-Dame Ouest", "Montreal" };
Adresse Residence2 = { 666, "Rue de la fin du monde", "St-Isidor de l’Apocalypse" };
int CoursInteressantDeGPA = 665;
Individu Justin = { "Justin B. Sil", 24, &CoursInteressantDeGPA, &Residence1 };
Individu Jay = { "Jay Latrouille", 23, &CoursInteressantDeGPA, &Residence2 };
Individu *Personne = &Jay;
/* Quelques instructions */
int NoCiviqueQuelconque = Jay.RefAd->NoCivique; /* ou (*Jay.RefAd).NoCivique */
int NoCoursQuelconque = *Justin.RefNoCoursGPA; /* est-ce correct sans parathèse ? */
char NomVille[STRING_MAX], *PtrCible = NomVille;
char *PtrSource = Personne->RefAd->Ville; /* ou (*(*Personne).RefAd).Ville */
int i = 0; while ((*PtrCible++ = *PtrSource++) != '\0' && i++ < STRING_MAX);

83
Rappel général sur les tableaux

• Un tableau est un ensemble de variable de même type.


• Un tableau est toujours constitué de deux parties :
– l’espace des données qui est toujours contiguë en mémoire (que
ce soit pour un tableau unidimensionnel ou multidimensionnel)
– une référence de type pointeur indiquant toujours le début du
tableau
int Tableau[5] = { 10, 100, 1000, 10000, 1000000 };

84
Rappel général sur les tableaux

• Contrairement aux allocations dynamiques que nous


verrons plus loin, les allocations statiques de
tableaux ne génèrent pas d’allocation mémoire pour
le pointeur de référence.
• Ce pointeur est constant et ne peut être modifié.
• Les tableaux réservent le nombre d’octets suivant :
taille tableau = sizeof(type) * nombre d’éléments
• Quels sont les tailles des tableaux suivants?
unsigned short int Data1[5];
int Data2[] = { 0, 1, 2, 3, 4, 5, 6, 7 };
double Data3[10][10];

typedef struct { char Nom[256]; char Age; float Poids; int NoReference; } Individu;
Individu Pierre[1], Jean[100], Jacques[5][10][2];

85
Relation étroite entre pointeurs et tableaux

• Puisque tous les tableaux sont référencés par un


pointeur, il est souvent pratique et pertinent d’affecter
l’adresse de départ d’un tableau à un pointeur.
• Les tableaux sont toujours passés aux fonctions par leur
adresse de départ. Il est impossible de passer un
tableau par son contenu à une fonction.
• Il est aussi impossible qu’une fonction retourne le
contenu d’un tableau, elle ne peut que retourner
l’adresse du tableau
• Dans le but d’accélérer certains processus, il est parfois
efficace d’exécuter certaines opérations sur un tableau à
l’aide des pointeurs et de ne pas utiliser les opérateurs
[ ] pour chaque accès au tableau.
86
Arithmétique sur les pointeurs
• Les opérateurs d’addition, de soustraction, d’incrémentation ou de
comparaison sont permis sur les pointeurs. Ils permettent le calcul de
décalage entre une adresse de base et une adresse souhaitée.
• Tous ces calculs sont automatiquement gérés par le compilateur en
fonction de la taille du type de référence du pointeur.
#define TABLE_SIZE 5
const float CND_TO_USD = 1.2f
float CNDCost[TABLE_SIZE] = { 10.25f, 2.12f, 399.99f, 32.77f, 0.12f };
short int Quantity[TABLE_SIZE] = { 3, 12, 2, 8, 193 };
float GlobalUSDCost;
unsigned short int i;

/* Cout global en dollard americain */


GlobalUSDCost = 0.0f;
for (i = 0; i < TABLE_SIZE; i++) {
GlobalUSDCost += CNDCost[i] * CND_TO_USD * Quantity[i];
}

i = 0;
GlobalUSDCost = 0.0f;
short int *CurrentQuantity = Quantity, *PostLastQuantity = Quantity + TABLE_SIZE;
while (CurrentQuantity < PostLastQuantity) {
GlobalUSDCost += *(CNDCost + i++) * CND_TO_USD * *CurrentQuantity++;
}

87
Arithmétique sur les pointeurs

• Que se passe-t-il exactement avec ces lignes de code?


pour faire le suivi, on retire les lignes qui ne compilent pas et celles qui causent un
débordement de mémoire
int A[] = { 1, 2, 3, 4, 5 };
int a = 0x100;
int *pA = A, *pa = &a, **ppA = &pA;

a = A; /* 01 */
a = *A; /* 02 */
a = A[0] * 0; /* 03 */
a = pA; /* 04 */
a = *pA; /* 05 */
a = pA[0] + 0; /* 06 */
a = pa; /* 07 */
a = *pa * 0; /* 08 */
a = pa[0]; /* 09 */
*pa = *pA; /* 10 */
*(pA + 3) = *(pa + 3); /* 11 */
A[3] = a[3]; /* 12 */
a = A[5]; /* 13 */
A[1] = A[2]; /* 14 */
*(pA – 1) = a; /* 15 */
A = pA[2]; /* 16 */
a *= *A * *(A + *(pA + 1)); /* 17 */
a = --*pA++; /* 18 */
(A + 2)[2] = (*pa + 2) * *(pA – 1); /* 19 */
*(ppA[0] + 3) = (*ppA + 2)[0]; /* 20 */
88
Particularité des chaînes de caractères

• Le langage C et même le langage C++ n’offrent aucun type de


base correspondant à une chaîne de caractères (string).
(par contre, il existe plusieurs implémentations de classes performantes pour en faire la gestion
en C++ – voir la bibliothèque STL à titre d’exemple)

• La façon de gérer les chaînes de caractères est simplement par


l’usage d’un tableau de type char.
• Par convention, on utilise le caractère '\0' (correspondant à la
valeur 0) pour identifier la fin d’une chaîne de caractères.
• Ainsi, la longueur d’une chaîne de caractères ne correspond pas
nécessairement à la place qu’elle prend en mémoire.
• La déclaration d’une chaîne de caractères constante et toutes
les fonctions des bibliothèques standard respectent cette
convention. C’est pourquoi il est important de la suivre.
char Discipline[] = "Gymnastique";
char Prenom[256] = "Kassandra";

89
Gestion statique et dynamique de la mémoire

• Les compilateurs basés sur le langage C gèrent


automatiquement toutes les allocations mémoire
provenant des déclarations standard.
• Ils gèrent à la fois les allocations, les initialisations
(lorsque pratiquées) et les libérations de la mémoire
au moment opportun.
• Puisque toutes ces opérations sur la mémoire sont
connues au moment de la compilation, le
programme, lorsqu’il est compilé et généré, contient
déjà toutes les instruction de gestion de la mémoire
(gestion fixe et immuable)
• C’est ce mécanisme qu’on appel la gestion statique
de la mémoire.

90
Gestion statique et dynamique de la mémoire

• Avec les fonctions malloc et free, il est possible de faire soi


même la gestion de la mémoire au moment opportun – c’est ce
qu’on appel la gestion dynamique de la mémoire.
void * malloc(int taille-en-octet);

void free(void * bloc-memoire);

• Il est essentiel de libérer soi même la mémoire réservée. Sans


quoi votre programme présentera des fuites de mémoire
(« memory leak »).
int *VariableQuelconque = (int*) malloc(sizeof(int));
...
*VariableQuelconque = 10;
...
free(VariableQuelconque);

• Aucune allocation dynamique ne fait l’initialisation du contenu


des variables créées.

91
Gestion statique et dynamique de la mémoire

• Alors pourquoi utiliser la gestion dynamique de la


mémoire?
• Cette liberté supplémentaire, malgré le fait qu’elle
complexifie la lecture du code, permet de créer des
programmes :
– plus flexibles;
– plus performants;
– plus modulaires;
– mieux structurés.

92
Gestion statique et dynamique de la mémoire
• L’un des exemples les plus flagrant est l’instanciation d’un tableau :
– Les allocations statiques de tableaux nécessitent de connaître la
taille du tableau au moment de la compilation (« compile time »).
Puisqu’il arrive souvent qu’on ne connaisse pas la taille réelle
requise pour un tableau au moment de la compilation, il est
courrant de déterminer la taille de ce dernier égal au pire des cas
possibles. Ainsi, cette pratique engendre souvent des contraintes
importantes :
• mémoire réservée inutilement qui peut ne jamais servir (ou très
rarement)
• le tableau alloué peut ne pas pouvoir accueillir toutes les données
requises pour une application => le programme est limité et ne peut
réaliser toutes les tâches auxquelles il est destinées
– Les allocations dynamiques permettent d’allouer un tableau de la
taille voulue à l’instant voulu (au « run time »). Les possibilités
sont vastes et permettent d’allouer exactement la mémoire
requise et de gérer celle-ci de façon optimale.

93
Gestion statique et dynamique de la mémoire

• On se sert souvent des allocations dynamiques pour allouer des


tableaux dont la taille est indéfinie au moment de la
compilation.
• Il existe une différence notable entre un tableau alloué
statiquement et dynamiquement : la référence du tableau
nécessaire à l’exécution du programme.
int TableauStatique[3] = { 0, 1, 2 };
int *TableauDynamique = (int*) malloc(sizeof(int) * 3);

94
Gestion statique et dynamique de la mémoire

• Puisque le contenu de tout les tableaux statique est contiguë


en mémoire (qu’il soit unidimensionnel ou multidimensionnel),
c’est le compilateur qui gère les décalages selon l’adresse de
départ pour chaque accès mémoire.
• Pour les allocations dynamiques de tableaux
multidimensionnels, le compilateur ne peut calculer les
décalages souhaités étant donné qu’il ne connaît pas la taille
nominale de chacune des dimensions au moment de la
compilation. Il existe alors deux solutions qui se présentent :
– la gestion explicite des décalages par le programmeur;
– la déclaration
de plusieurs tableaux
en cascade.

95
Gestion statique et dynamique de la mémoire
const int TABLE_SIZE_X = 2, TABLE_SIZE_Y = 3;
float StaticMatrix[TABLE_SIZE_X][TABLE_SIZE_Y];
float *DynMatrixByOffset;
float **DynMatrixByCascade;
int i, x, y;

/* Allocation dynamique par gestion de décalage */


DynMatrixByOffset = (float*) malloc(sizeof(float) * TABLE_SIZE_X * TABLE_SIZE_Y);
/* Allocation dynamique par cascade */
DynMatrixByCascade = (float**) malloc(sizeof(float*) * TABLE_SIZE_X);
for (i = 0; i < TABLE_SIZE_X; i++)
DynMatrixByCascade[i] = (float*) malloc(sizeof(float) * TABLE_SIZE_Y);

/* Initialisation de tous les elements de chaque matrices a 0, 1 ou 2 */


for (x = 0; x < TABLE_SIZE_X; x++) {
for (y = 0; y < TABLE_SIZE_Y; y++) {
StaticMatrix[x][y] = 0;
DynMatrixByOffset[x + y * TABLE_SIZE_X] = 1; /* gestion explicite des décalages */
DynMatrixByCascade[x][y] = 2;
}
}

/* Liberation de la matrice dynamique par gestion de décalage */


free(DynMatrixByOffset);
/* Liberation de la matrice dynamique par cascade */
for (i = 0; i < TABLE_SIZE_X; i++)
free(DynMatrixByCascade[i]);
free(DynMatrixByCascade);

96
Gestion statique et dynamique de la mémoire

97
En général

• Paramètres d’une fonction :


– on appel la signature la liste des types qui sont passés à
une fonction
– tous les types peuvent être passés à une fonction à titre
d’argument mais :
• les tableaux doivent être passés par leur adresse
• attention aux structures qui sont copiés bit à bit
– il est possible de spécifier l’absence de paramètre par :
• l’usage du mot clé void
• en laissant l’intérieure des parenthèses vide
– on appel fonctions ellipsis celles dont le nombre d’argument
est variable (comme printf et scanf – nécessite ... à la fin de
la liste d’arguments au moment de la déclaration)
Fonction1(); /* retourne : int - parametres : aucun */
void Fonction2(void); /* retourne : rien - parametres : aucun */
void* Fonction3(void*); /* retourne : pointeur void – parametres : pointeur void */
float Fonction4(int, int); /* retourne : float - parametres : 2 int */

99
En général

• Type de retour :
– une fonction peut retourner n’importe quel type mais :
• les tableaux sont toujours retournés par leur adresse
• attention aux types structurés qui sont copié bit-à-bit
– si aucun type de retour n’est spécifié, le type de retour par
défaut est l’entier – int
– l’usage du mot clé void comme type de retour indique que
la fonction ne retourne aucune valeur

100
En général

• Terminer une fonction


– l’instruction return permet la terminaison d’une fonction à
n’importe quel endroit
– return peut retourner une valeur ou non dépendamment du
type de retour spécifié lors de la déclaration
– l’instruction return est facultative pour terminer une
fonction ne retournant aucune valeur.
return; /* l’utilisation du return a la fin d’une fonction est facultatif */
return Resultat; /* lorsqu’on retourne une valeur, on peut la mettre ou non entre ... */
return(Result); /* parenthèse */

101
Portée des fonctions
• De façon semblable à la déclaration des variables,
l’accessibilité ou la visibilité des fonctions est
déterminée par :
– l’emplacement de la déclaration
– la classe de mémorisation utilisée à sa déclaration
(« storage class »)
• L’emplacement de la déclaration d’une fonction
détermine le point à partir duquel celle-ci peut être
utilisée.
• Comme certains types de données, une fonction
peut être déclarée avant d’être définie. On utilise
souvent les « header files » (*.h) pour déclarer les
fonctions.

102
Portée des fonctions
#include <stdio.h>

struct Individu; /* Individu est declare avant sa definition */


int Age(Individu *I, int AnneeCourante); /* Prototype de fonction déclaré bien avant */

struct Individu { char Prenom[32]; char Nom[32]; int AnneeNaissance; };

void main(void)
{
char* Nom(Individu*, char*); /* Prototype de fonction déclaré à l’interne */
/* Le nom des variables est facultatif */
Individu Scientifique = { "Albert", "Einstein", 1879};
char Temp[256];

printf("Monsieur %s a %d an(s)", Nom(&Scientifique, Temp), Age(&Scientifique, 2006));


}

int Age(Individu *I, int AnneeCourante)


{
Entrée
return AnneeCourante - I->AnneeNaissance;
} - aucune entrée -

char* Nom(Individu *I, char *CC) Sortie


{
Monsieur Albert Einstein a 127 an(s)
sprintf(CC, "%s %s", I->Prenom, I->Nom);
return CC;
}

103
Portée des fonctions

• Description des classes de mémorisation des


fonctions :
– extern (par défaut) La fonction est une entité globale et
peut être utilisée partout où elle est référencée.
[ extern ] type-retour identificateur( [ type1 param1, ... ] ) ...

extern int NombreDeCaractere(char *ChaineCaracteres);

– static L’utilisation de la fonction est limitée dans le module


où elle a été définie.
[ static ] type-retour identificateur( [ type1 param1, ... ] ) ...

static void InverserChaineCaracteres(char *ChaineCaracteres);

104
Mécanismes d’appel d’une fonction

• Lorsqu’une fonction est appelée, un mécanisme est mis en


action afin de sauvegarder momentanément plusieurs
informations nécessaires au déroulement du programme.
• Ces informations servent au microprocesseur à faire le suivi
adéquat du programme.
• On utilise la pile du système pour y enregistrer temporairement
les informations nécessaires.
• Tous les appels de fonction gérés par le compilateur
fonctionnent de la même façon, peu importe leur nature.
• On nomme :
– fonction appelante celle qui appel
– fonction appelée celle qui est appelée

105
Mécanismes d’appel d’une fonction

• Pour chaque appel de fonction :


– Un ensemble d’information sur l’état courant du
programme, nommé enregistrement d’activation, est mis
sur la pile. On y retrouve principalement :
• L’adresse de retour de la fonction appelante
• Le résultat de la fonction appelée
• Tous les paramètres de la fonction appelée
• Toutes les variables locales de la fonction appelée
• L’état courant des registres de la fonction appelante qui sont
utilisés par la fonction appelée
– Trois registres sont systématiquement utilisés :
• ESP – stack pointer
• EBP – Base pointer
• EIP – Instruction Pointer

106
Mécanismes d’appel d’une fonction

• Les étapes du mécanisme d’appel d’une fonction sont :


1 Empile tous les paramètres de la fonction appelée
(de droite à gauche)

2 Empile et met à jour le registre EIP


77
EIP

XYZ
(l’adresse de retour de la fonction appelante est empilée et Dépile et met à jour l’état des registres de la fonction
l’adresse de la fonction appelée par le registre EIP est ajustée ) appelante

3 Empile et met à jour le registre EBP


88
EBP

(l’adresse de référence -BP- de la fonction appelante est empilé et Relâche la mémoire réservée pour les variables locales
celui de la fonction appelée par le registre EBP est ajusté) (il suffit de décrémenter ESP de la taille prise par les variables)

4 Alloue les variables locales de la fonction appelée à


99

EBP
même la pile
Dépile et met à jour le registre EBP
(il suffit d’incrémenter ESP de la taille nécessaire aux variables)

5 10
XYZ

Empile l’état des registres de la fonction appelante 10

EIP
Dépile et met à jour l’adresse de retour dans le registre
(seulement ceux qui sont utilisés par la fonction appelée)
EIP

6 11
Exécute la fonction appelée 11
Dépile tous les paramètres de la fonction appelée

12
12
Exécute la suite de la fonction appelante

• Que manque-t-il?
107
Mécanismes d’appel d’une fonction

• La valeur de retour est passée à titre de premier


paramètre de la fonction.
c’est-à-dire, comme dernier élément empilé sur la pile
• Par convention, ce paramètre possède toujours 4
octets (32 bits). Qu’arrive-t-il si la valeur de retour
spécifiée ne correspond pas aux 4 octets utilisés?
– Si la valeur de retour est plus petite (comme un char), on
utilise tout de même les 4 octets.
– Si la valeur de retour est plus grande (comme un double),
on porte en mémoire haute la valeur de la variable et on
utilise les 4 octets réservés comme pointeurs pour indiquer
cette l’adresse de cette position.

108
Le passage des paramètres à une fonction

• Il existe trois méthodes de passage de paramètres à


une fonction :
– Passage par valeur (« call by value »)
• les valeurs des paramètres d'appel sont recopiés dans la
fonction appelée
• ainsi la fonction appelée travaille avec une copie des
paramètres
• en fait, c'est le seul mode de passage de paramètres
réellement existant en C

111
Le passage des paramètres à une fonction

• … trois méthodes de passage de paramètres (2ième):


– Passage par référence (« call by reference »)
• dans ce mode de passage de paramètres, la fonction appelée
travail avec les références des paramètres d'appel
• il existe deux avantages importants de ce mode de
transmission
– il évite des copies coûteuses des paramètres de tailles importantes
– il permet la modification de variables externes à la fonction
• fondamentalement, ce type de passage est inexistant en C
(contrairement au C++ avec ses références)
• c’est l’utilisation des pointeurs qui permet, indirectement,
d’obtenir un comportement qui s’approche du passage par
référence

112
Le passage des paramètres à une fonction

• … trois méthodes de passage de paramètres (3ième):


– Passage par nom (« call by name »)
• c'est le mode de transmission utilisé par le préprocesseur avec
les arguments des macros
• dans ce mode, les paramètres de l'appel seront textuellement
substitués aux diverses occurrences des paramètres formels
(sans validation aucune – ni même la vérification de
compatibilité des types)

113
Le passage des paramètres à une fonction
#include <stdio.h>

void IncrementAndShow(int V1, int V2) Entrée


{ - aucune entrée -
V1++; V2++;
printf("Valeur des variables dans IncAndShow : \t%d\t%d\n", V1, V2);
} Sortie
void Swap(int *V1, int *V2) Valeur des variables dans main (1) : 5 10
{ Valeur des variables dans IncAndShow : 6 11
int VTemp = *V2; Valeur des variables dans main (2) : 5 10
*V2 = *V1; Valeur des variables dans main (3) : 10 5
*V1 = VTemp; Valeur des variables dans Show : 9 5
} Valeur des variables dans main (4) : 9 6
#define Show(V1, V2) printf("Valeur des variables dans Show : \t%d\t%d\n", V1, V2)

void main(void)
{
int V1 = 5, V2 = 10;

printf("Valeur des variables dans main (1) : \t%d\t%d\n", V1, V2);


IncrementAndShow(V1, V2);
printf("Valeur des variables dans main (2) : \t%d\t%d\n", V1, V2);
Swap(&V1, &V2);
printf("Valeur des variables dans main (3) : \t%d\t%d\n", V1, V2);
Show(--V1, V2++);
printf("Valeur des variables dans main (4) : \t%d\t%d\n", V1, V2);
}

114
Fonctions récursives

• Il existe deux types de solution pour résoudre des


problèmes nécessitant plusieurs calculs similaires et
répétitifs : les solutions itératives et récursives.
• Dans les deux cas, un mécanisme différent est mis
en place afin d’exécuter plusieurs fois la même
séquence d’instruction. Ces mécanismes se
traduisent par :
– des boucles de contrôle pour les solutions itératives
 implique des solutions linéaires
(des solutions non linéaires sont possibles avec des artifices)
– des fonctions qui s’appellent elles même
 implique des solutions linéaires et non linéaires

115
Fonctions récursives

• D’un point de vue théorique, lorsqu’une solution


récursive est bien structurée, elle est un moyen
puissant et unique de résoudre des problèmes.
• Par définition, la récursivité est une technique de
traitement d’une tâche T par le traitement d’une
tâche T’
– semblable à l’approche descendante
– la différence réside dans le fait que la tâche T’ est de même
nature que la tâche T
• D’un point de vue applicatif, il est important de se
rappeler que :
T’  T mais que T’ < T
• Est-ce qu’une fonction récursive est meilleur qu’une
solution itérative ?
116
Fonctions récursives

• La question importante : Comment arrêter une


fonction récursive?
• Les cas limites sont des éléments essentiels de
toutes fonctions récursives :
– ce sont les conditions qui cessent les appels récursifs et
permettent de terminer la fonction
– ce sont des tests d’arrêt qui sont liés aux parties non
récursives de la fonction
• Il existe deux types de récursivité :
– la récursivité directe correspond au cas où la fonction F
appel directement la fonction F
– la récursivité indirecte correspond au cas où une fonction F
appel F1, qui appel F2, qui appel F3, ..., qui appel Fn, qui
enfin appel la fonction F

117
Fonctions récursives

• Un premier exemple :
La recherche d’un mot dans le dictionnaire
– la solution itérative, simple mais peu efficace, correspond à
parcourir dans l’ordre, du premier vers le dernier, tous les
éléments du dictionnaire jusqu’à ce que le mot recherché
soit identifié
– une solution plus efficace et plus élégante consiste à faire ce
que nous faisons intuitivement : une recherche binaire
(« binary search »)

Comment procède-t-on pour résoudre ce problème?

118
Fonctions récursives

• Pseudo code d’un algorithme de recherche binaire :


Fonction-Recherche-binaire (Dictionnaire, Mot)
SI Dictionnaire est réduit à une seule page
ALORS parcourir la page pour localiser Mot
SINON
Trouver la partie du Dictionnaire séparé par le milieu où se trouve Mot
SI Mot se trouve dans la première partie du Dictionnaire
Fonction-Recherche-Binaire( première partie de Dictionnaire, Mot)
Sinon
Fonction-Recherche-Binaire( deuxième partie de Dictionnaire, Mot)

• Le tableau de données doit être trié sinon la


recherche dichotomique n’est pas applicable.
A[0]  A[1]  A[2]  A[3] ...  A[n]

119
Fonctions récursives

• Comment trouver une solution récursive?


• Réponses clés à trouver pour construire une
solution récursive :
– Est-ce que le problème peut être définie en terme
de sous problème de même nature?
– Est-ce que chaque appel récursif diminue la taille
du problème?
– Quel est le cas limite du problème?
– Puisque la taille du problème diminue, est on
assuré d’atteindre le cas limite?

120
Fonctions récursives

• Aussi, plusieurs algorithmes itératifs peuvent


facilement être modifier en algorithmes récursifs
• Un exemple simple de transformation
algorithmique : le parcours d’une boucle
void AfficheTousLesNombres2Chiffres_Iteratif(int APartirDuNombre)
{
for (int i = APartirDuNombre; i < 100; i++) {
printf("%d\n", i);
}
}

void AfficheTousLesNombres2Chiffres_Recursif(int APartirDuNombre)


{
if (APartirDuNombre < 100) {
printf("%d\n", APartirDuNombre);
AfficheTousLesNombres_Recursif(APartirDuNombre + 1);
}
}

121
Fonctions récursives

• Un exemple un peu plus étayé de transformation


algorithmique : le parcours d’une double boucle
void InitDynamicArrayByCascade_Iterative(int **Array, int SizeX, int SizeY, int Value)
{
for (int iY = 0; iY < SizeY; iY++) {
for (int iX = 0; iX < SizeX; iX++) {
Array[iX][iY] = Value;
}
}
}

void InitDynamicArrayByCascade_Recursive(int **Array, int SizeX, int SizeY, int Value)


{
if (SizeY > 0) {
if (SizeX > 0) {
Array[SizeY - 1][SizeX - 1] = Value;
InitDynamicArrayByCascade_Recursive(Array, SizeX - 1, SizeY, Value);
}
InitDynamicArrayByCascade_Recursive(Array, SizeX, SizeY – 1, Value);
}
}

122
Fonctions récursives

• La méthode de représentation suivante permet de


décrire simplement un problème récursif par
l’identification systématique de toutes les tâches
possibles à chaque « itération » :
– l’idée consiste à décrire tous les cas possibles du problème
– il est important d’exprimer les tâches à faire et la nature
récursive des cas concernés
– il est intéressant de voir que les cas limites apparaissent
clairement avec cette méthode

 résultat tâche 1 si cas 1


 résultat F(x' ) si cas 2

F(x)  
 ... ...
résultat tâche n si cas n

123
Fonctions récursives

• Un autre exemple : le calcul de la factorielle.


– La définition mathématique de la factorielle est connue et
simple :

est indéfini si n  0
n! 1 si n  0
n

 i  1 2  3  ...  (n  1)  n
i 1
si n  0

 erreur si n  0

Fact(n)   1 si n  0
n  Fact(n  1) si n  0

124
Fonctions récursives

• Suite de l’exemple : le calcul de la factorielle


 erreur si n  0

Fact(n)   1 si n  0
n  Fact(n  1) si n  0

Fonction-Factoriel (Nombre)
SI Nombre < 0
retourne erreur
Si Nombre = 0
retourne 1
SINON
retourne Nombre * Fonction-Factoriel(Nombre – 1)

int Factoriel(int N)
{
if (N < 0) { /* Parametre d’entree invalide */
return -1; /* -1 represente erreur */
} else if (N == 0) {
return 1; /* Cas limite */
} else {
return N * Factoriel(N – 1); /* Appel recursif */
}
}

125
Fonctions récursives

• Puisque chaque appel de fonction possède


son propre espace de variable, chaque appel
récursif possède le sien.
• Ceci implique un parcours descendant
jusqu’au dernier appel récursif pour ensuite
revenir au premier appel.
• La technique de représentation par boîtes est
très efficace pour illustrer le comportement
d’un programme récursif. Elle aide à :
– déverminer les programmes récursifs (parfois
difficiles à suivre)
– illustrer le comportement d’un algorithme
126
Fonctions récursives

• Les étapes à suivre pour réaliser la technique de


représentation par boîtes :
– pour chaque appel récursif (parcours descendant), on crée
une boîte qui contient
• l’appel de fonction incluant les valeurs de chaque paramètre
• l’environnement local du sous programme associé
• les instructions faites avant l’appel récursif
• une flèche initiant le nouvel appel récursif
– pour chaque dernier appel récursif, on revient à l’appel de
fonction appelante en créant une boîte contenant :
• l’environnement local du sous programme associé
• les instructions faites après l’appel récursif
• un flèche indiquant le retour de fonction avec la valeur
retournée s’il y a lieu
127
Fonctions récursives

• Exemple de représentation par boîte :


Entrée
int main(void) - aucune entrée -
{
printf("%d! = %d\n", 3, Factoriel(3));
} Sortie
3! = 6

128
Fonctions récursives

• Un exemple : écrire une chaîne de caractères à l’envers (1ier)


void Writebackward1(char *String, int Length)
{
if (Length > 0) {
printf("%c", String[Length - 1]);
Writebackward1(String, Length – 1); Entrée
} - aucune entrée -
}

Writebackward1("allo", 4); Sortie


olla

129
Fonctions récursives

• Un exemple : écrire une chaîne de caractères à l’envers (2ième)


void Writebackward2(char *String)
{
if (String[0] != '\0') {
Writebackward2(String + 1); /* équivalent à WB2(&String[0] */
printf("%c", String[0]); Entrée
} - aucune entrée -
}

Writebackward2("allo"); Sortie
olla

130
Fonctions récursives

• Un exemple : impression selon un ordre particulier


void ImpressionEtrange(int *Data, int N)
{
if (N > 0) {
ImpressionEtrange(Data + 1, N – 1);
printf("%d \n", *Data);
ImpressionEtrange(Data + 1, N – 1);
)
}

int Data[3] = { 1, 2, 3 };
ImpressionEtrange(Data, 3);

Entrée
- aucune entrée -

Sortie
3
2
3
1
3
2
3

131
Fonctions récursives

suite de l’exemple à la page suivante ...

132
Fonctions récursives
... fin de l’exemple de la page précédente

133
Fonctions récursives

• Comme le montre indirectement - aucune entrée -


Entrée

l’exemple précédent, il peut être très


pratique d’utiliser l’affichage de l’état 1 : 3 1
Sortie

courant du programme avant et après 1 : 2 2

chaque appel récursif.


1 : 1 3
2 : 1 3
3 : 1 3
• Cette pratique est performante pour aider 2 : 2 2

à la compréhension d’un algorithme et


1 : 1 3
2 : 1 3

surtout au déverminage.
3 : 1 3
3 : 2 2
2 : 3 1
void FonctionQuelconque(int *Data, int N)
1 : 2 2
{
1 : 1 3
if (N > 0) {
2 : 1 3
printf("1 : %4d %4d \n", N, *Data);
3 : 1 3
FonctionQuelconque(Data + 1, N – 1);
2 : 2 2
printf("2 : %4d %4d \n", N, *Data);
1 : 1 3
FonctionQuelconque(Data + 1, N – 1);
2 : 1 3
printf("3 : %4d %4d \n", N, *Data);
3 : 1 3
}
3 : 2 2
}
3 : 3 1
int Data[3] = { 1, 2, 3 };
FonctionQuelconque(Data, 3);

134
Fonctions récursives

• Comme il arrive souvent en algorithmie, il est


intéressant de faire certaines optimisations qui
augmentent significativement le rendement des
algorithmes réveloppés.
• Les fonctions récursives permettent certaines
optimisations intéressantes.

135
Fonctions récursives

• Prenons pour exemple la fonction de puissance.


 1 si y  0

 y
 x
 si y  0
x   i 1
y


 1 si y  0
 y
 x
 i 1
 1 si y  0


 x  Power  x, y  1 si y  0
Power  x, y   

 1
 si y  0
 Power  x, y 
136
Fonctions récursives

• La fonction de puissance (suite) : version récursive


float PowerRec(float X, int Y)
{
if (Y == 0) {
return 1.0f;
} else if (Y < 0) {
return 1.0f / PowerRec(X, -Y);
} else {
return X * PowerRec(X, Y – 1);
}
}

• Version itérative
float PowerIter(float X, int Y)
{
float Result = 1.0f;

for (int i = abs(Y); i > 0; i--) {


Result *= X;
}

return (Y > 0) ? (Result) : (1.0f / Result);


}

137
Fonctions récursives
• La fonction de puissance (suite) : version récursive optimisée
Une simple manipulation mathématique nous permet de démontrer :
  y 2
 x2  si y est paire
  

xy  
  y 1  2
x   x 2  si y est impaire
  

float PowerRecOptimized(float X, int Y)
{
if (Y == 0) {
return 1.0f;
} else if (Y < 0) {
return 1.0f / PowerRecOptimized(X, -Y);
} else {
if (Odd(Y)) {
return X * Sqr(PowerRecOptimized(X, Y / 2));
} else {
return Sqr(PowerRecOptimized(X, Y / 2));
}
}
}

138
Fonctions récursives

• Laquelle de ces trois fonctions est la plus performante et


pourquoi?
• L’exemple des fonctions de puissance montre de façon
convaincante l’amélioration que certaines optimisations
peuvent faire.
• Ce tableau montre un indice de performance pour l’appel des
trois fonctions montrées avec 10000 comme valeur d’exposant

Fonction Nombre de parcours de boucle

PowerRec 10000 appels récursifs

PowerIter 10000 itérations

PowerRecOptimized 14 appels récursifs


(répartit en 5 appels impairs et 9 pairs)

139
Fonctions récursives

• On revient sur l’exemple de recherche dichotomique.


Fonction-Recherche-binaire (Dictionnaire, Mot)
SI Dictionnaire est réduit à une seule page
ALORS parcourir la page pour localiser Mot
SINON
Trouver la partie du Dictionnaire séparé par le milieu où se trouve Mot
SI Mot se trouve dans la première partie du Dictionnaire
Fonction-Recherche-Binaire( première partie de Dictionnaire, Mot)
Sinon
Fonction-Recherche-Binaire( deuxième partie de Dictionnaire, Mot)

Individu* RechercheIndividu(Individu Individus[], char Nom[], int Premier, int Dernier)


{
if (Premier > Dernier) {
return NULL;
} else {
int Milieu = (Premier + Dernier) / 2;
int ResultatComparaison = strcmp(Individus[Milieu].Nom, Nom);
if (ResultatComparaison == 0) {
return &Individus[Milieu];
} else if (ResultatComparaison > 0) {
return RechercheIndividu(Individus, Nom, Premier, Milieu – 1);
} else {
return RechercheIndividu(Individus, Nom, Milieu + 1, Dernier);
}
}
}

140
Fonctions récursives

void main(void)
{
Individu ListePersonnes[9], *PersonneCible;

strcpy(ListePersonnes[0].Nom, "Aleksia"); ListePersonnes[0].Age = 31;


strcpy(ListePersonnes[1].Nom, "Frédérick"); ListePersonnes[1].Age = 2;
strcpy(ListePersonnes[2].Nom, "Karolanne"); ListePersonnes[2].Age = 14;
strcpy(ListePersonnes[3].Nom, "Kassandra"); ListePersonnes[3].Age = 10;
strcpy(ListePersonnes[4].Nom, "Kristian"); ListePersonnes[4].Age = 34;
strcpy(ListePersonnes[5].Nom, "Kzavier"); ListePersonnes[5].Age = 49;
strcpy(ListePersonnes[6].Nom, "Michaël"); ListePersonnes[6].Age = 12;
strcpy(ListePersonnes[7].Nom, "Nikolas"); ListePersonnes[7].Age = 30;
strcpy(ListePersonnes[8].Nom, "Patrick"); ListePersonnes[8].Age = 32;

PersonneCible = RechercheIndividu(ListePersonnes, "Frédérick", 0, 8);


PersonneCible = RechercheIndividu(ListePersonnes, "Kroutchev", 0, 8);
}
ListePersonnes
Nom
Âge

141
Fonctions récursives

• Encore un exemple : la suite de Fonacci


• Le rythme de reproduction des lapins est un
phénomènes pouvant être représenté
approximativement par cette suite.
• Quelle est l’évolution du nombre de lapin dans le
temps en faisant les suppositions suivantes :
– les lapins ne meurent jamais
– un lapin atteint sa maturité sexuelle 2 mois après sa
naissance (c’est-à-dire au début du 3ième mois de vie)
– au début de chaque mois, chaque paire mâle/femelle
sexuellement mûre donne naissance exactement à une
paire mâle/femelle
– on commence par une paire mâle/femelle naissants
142
Fonctions récursives

• Suite de Fibonacci (suite)


Non mûre Non mûre
Mois Mûre Total
1ière semaine 2ième semaine

0 0 0 0 0

1 1 0 0 1

2 0 1 0 1

3 1 0 1 2

4 1 1 1 3

5 2 1 2 5

6 3 2 3 8

7 5 3 5 13

143
Fonctions récursives

• Suite de Fibonacci (suite)


 0 si n  0


Fn    1 si n  1


Fn  1  Fn  2 si n  1

unsigned int FibonacciRec(unsigned int N)


{
if (N == 0) {
return 0;
} else if (N == 1) {
return 1;
} else {
return FibonacciRec(N - 1) + FibonacciRec(N – 2);
}
}

144
Fonctions récursives
• Suite de Fibonacci (suite)
– Cette première version récursive, quoique mathématiquement
élégante et juste, est si peu performante qu’elle devient
pratiquement non fonctionnelle.
void main(void)
{
FibonacciRec(0); /* 40
FibonacciRec(1);
FibonacciRec(2);
FibonacciRec(3);
FibonacciRec(4);
FibonacciRec(5);
FibonacciRec(6);
FibonacciRec(7); 0 appels
2
4
8
14
24appel
appelsrécursif
récursifs
récursifs
*/*/
*/
} F(3)
F(4)

F(3) F(2) F(1) F(2)

F(2) F(1) F(1) F(0) F(1) F(0)

F(1) F(0)

• Pour Fib(N), cette solution donne : Fib(N+1) – 1 additions


2 Fib(N+1) – 2 appels récursifs

145
Fonctions récursives

• Une simple optimisation est assez facile :


unsigned int FRO(unsigned int N, unsigned int Previous, unsigned int Current)
{
if (N <= 1) {
return Current;
} else {
return FRO(N – 1, Current, Current + Previous);
}
}

unsigned int FibonacciRecOptimized(unsigned int N)


{
return (N == 0) ? (N) : (FRO(N, 0, 1));
}

unsigned int FibonacciIter(unsigned int N)


{
unsigned int R1 = 0, R2 = 1, R = 0, i;
for (i = 1; i <= N; i++) {
R = R1 + R2; R2 = R1; R1 = R;
}
return R;
}

• Ces solutions donnent : N – 1 appels récursifs pour la version réc. optimisée


N itération pour la version itérative

146
Fonctions récursives

• Plusieurs fonction graphique utilisent la récursivité :


– Remplissage
d’une région
bornée
– Manipulations
de polygones
– Génération
de fractals
– Affichage de
géométries
particulières
– ...

147
Fonctions récursives

• Que fait la fonction suivante :


void CoolDrawing1(int X, int Y, int R)
{
DrawCircle(X, Y, R); /* fonction dessinant un cercle centré à (X, Y) et de rayon R */

if (R > 2) {
CoolDrawing1(X + R / 2, Y, R / 2);
CoolDrawing1(X – R / 2, Y, R / 2);
}
}

148
Fonctions récursives

• Comment tracer cette figure (lignes et flèches) ?


8 5

5
8
1 1
1 1

2 3

2 3
void CoolDrawing2(int PreviousLength, int Length, int MaxLength)
{ 21
if (Length <= MaxLength) {
DrawLineFromCurrentPositionAndDirection(Length);
TurnCursorToLeft(90);
13
DrawArrowFromCurrentPositionAndDirection(Length);

CoolDrawing2(Length, PreviousLength + Length, Maxlength);


}
}

CoolDrawing2(0, 1, 21);

13 21

149
Impact du mécanisme d’appel des fonctions
sur les fonctions récursives
• Puisque le mécanisme d’appel des fonctions s’applique
également sur les fonctions récursives ou non, regardons quel
est son impact sur l’efficacité d’une fonction.
• Prenons pour exemple la fonction suivante
(servant à afficher un nombre en format binaire) :
void WriteBinary(int N)
{
if (N <= 1) {
printf("%d", N);
} else {
WriteBinary(N / 2);
printf("%d", N % 2);
}
}

avec l’appel de fonction suivant :


...
WriteBinary(6);
...

150
Impact du mécanisme d’appel des fonctions
sur les fonctions récursives

151
Impact du mécanisme d’appel des fonctions
sur les fonctions récursives

• Le mécanisme d’appel des fonctions génère deux


impacts importants sur les fonctions récursives. En
effet, lors de chaque appel :
– une série d’instructions supplémentaires est générées. Ces
instructions rendent l’exécution de la fonction moins
performante qu’une solution itérative équivalente.
– un enregistrement d’activation est empilé sur la pile. Cet
empilement requiert une quantité de mémoire non
négligeable pour les fonctions récursives ayant un nombre
d’appel élevé. Il est relativement fréquent de voir une
fonction mal conçue à ce niveau générer une erreur fatal du
programme.

152
Pointeurs de fonctions

• En plus d’avoir des pointeurs faisant référence à des


variables de divers types, le langage C permet la
définition de pointeurs de fonction.
• Ces pointeurs servent à identifier l’adresse d’entrée
de la première instruction d’une fonction (point
d’entrée).
• On utilise souvent les pointeurs de fonction pour
passer une fonction en argument à une autre
fonction.
• La déclaration d’un pointeur de fonction ressemble à
la déclaration d’un pointeur de type et à la
déclaration d’une fonction à la fois.
153
Pointeurs de fonctions
• La déclaration d’un pointeur de fonction se fait par les parenthèses qui
permettent de déterminer la variable pointeur:
[ classe-mémorisation ] type-retour (*identificateur)( [ type1 param1, ... ] );

void (*pFonction1)(void);
int (*pFonction2)(int, int);
void* (*pFonction3)(void*)
(void (*)(void)) (*pFonction4)((int (*)(int, int)), int);

• On vient de déclarer quatre pointeurs de fonction :


– pFonction1 fait référence à une fonction ne retournant rien et ayant aucun
paramètre d’entrée
– pFonction2 fait référence à une fonction retournant un entier et ayant deux
entiers comme signature
– pFonction3 fait référence à une fonction retournant un pointeur
indéterminé et un pointeur indéterminé comme signature
– pFonction4 fait référence à une fonction retournant un pointeur de fonction
(ne retournant rien et n’ayant aucun paramètre d’entrée) et ayant un
pointeur de fonction (de même nature que pFonction2) et un entier comme
signature.
154
Pointeurs de fonctions
• Il faut faire attention à l’ambiguïté des déclarations d’un pointeur de
fonction et d’une fonction retournant un pointeur.
int (*pFonction)(int);
int *Fonction(int);

• On vient de déclarer deux types complètement différents :


– pFonction fait référence à un pointeur de fonction retournant un entier et
ayant un entier comme paramètre d’entrée
– Fonction fait référence à une fonction retournant un pointeur d’entier et
ayant un entier comme paramètre d’entrée

• On assigne la valeur d’un pointeur de fonction en spécifiant l’adresse


d’une fonction par :
int Fonction(int N);
...
int (*pFonction)(int) = &Fonction; /* l’opérateur d’adressage (&) est facultatif */

• On l’appel simplement comme une fonction standard par le pointeur :


int A = pFonction(N);

155
Pointeurs de fonctions
• Un premier exemple :
int Resultat(int V1, int V2, int (*Compare)(int, int))
{
return Compare(V1, V2);
}
Entrée
int Max(int V1, int V2) -
{
return (V1 >= V2) ? (V1) : (V2);
} Sortie
Min de 1 et 2 = 1
int Min(int V1, int V2) Max de 1 et 2 = 2
{
return (V1 <= V2) ? (V1) : (V2);
}

void main(void)
{
printf("Min de 1 et 2 = %d\n", Resultat(1, 2, &Min));
printf("Max de 1 et 2 = %d\n", Resultat(1, 2, &Max));
}

156
Pointeurs de fonctions
• Un deuxième exemple :
int BrowseData(int *Data, int N, int (*ToDo)(int, int))
{
int Resultat = *Data;
for (int i = 0; i < N; i++) Resultat = ToDo(Resultat, Data[i]);
return Resultat;
}
int Max(int V1, int V2) Entrée
{ -
return (V1 >= V2) ? (V1) : (V2);
}
int Min(int V1, int V2) Sortie
{ Min de toutes les donnÚes = -9
return (V1 <= V2) ? (V1) : (V2); Max de toutes les donnÚes = 9
} Affichage de toutes les données :
int Display(int V1, int V2) 0 1 2 3 4 5 6 7 8 9 -9
{ -8 -7 -6 -5 -4 -3 -2 -1
return printf(" %d ", V2);
}
void main(void)
{
int Data[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -9, -8, -7, -6, -5, -4, -3, -2, -1 };
printf("Min de toutes les données = %d\n", BrowseData(Data, 19, &Min));
printf("Max de toutes les données = %d\n", BrowseData(Data, 19, &Max));
printf("Affichage de toutes les données :\n");
BrowseData(Data, 19, &Display);
}

157
Fonctions ellipsis

• Le C permet de déclarer des fonctions dont le


nombre de paramètres est variable (les fonctions printf et
scanf en sont de parfait exemple).
• Cette pratique est habituellement à proscrire en
langage C++. L’un des points forts du langage C++
est de faire la vérification systématique de la
compatibilité de tous les types, or les fonctions
ellipsis ne permettent pas au compilateur de faire
cette vérification de type.
• Malgré tout, cette technique est puissante et peut
être très utile en langage C.

158
Fonctions ellipsis

• Cette technique est rendue possible grâce au fait que


toutes les variables passées à une fonction se retrouvent
tous empilées sur la pile lors de l’appel de fonction.
• Avec la position initiale des paramètres en mémoire, le
type et le nombre, il est assez aisé de les récupérer pour
les utiliser.
• Toutes les fonctions ellipsis possède au moins un
paramètre avant la déclaration ellipsis à proprement dite
(...).
• Ce paramètre permet de connaître l’adresse de départ de
tout les paramètres de la fonction. Il sert souvent aussi
pour déterminer le type et/ou le nombre de paramètre.

159
Fonctions ellipsis

• L’implémentation correcte de ce type de fonction


requiert une grande précaution car plusieurs règles
doivent être respectées :
– il existe au moins un argument définit avant la déclaration
ellipsis (...)
– il faut faire attention aux « type promotions » pratiqués par
les compilateurs
– le corps d’une fonction ellipsis doit être cohérent entre sa
déclarations (incluant son comportement – extraction des
données) et son utilisation
– les macros va_arg, va_start, va_end servent à l’extraction
des paramètres

160
Fonctions ellipsis

• Un premier exemple (sans les macros) :


void AfficheIntData(int N, ...) Entrée
{ -
for (int i = 0; i < N; i++) {
printf("%d ", *(&N + (1 + i)));
} Sortie
printf("\n"); 1 2 3 4 5 6 7 8 9 10
} 1.9 2.8 3.7 4.6 5.5
void AfficheDoubleData(int N, ...)
{
for (int i = 0; i < N; i++) {
printf("%0.1lf ", *(double*)((char*)&N + sizeof(int) + sizeof(double) * i));
}
printf("\n");
}

void main(void)
{
AfficheIntData(10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
AfficheDoubleData(5, 1.9, 2.8, 3.7, 4.6, 5.5);
}

161
Fonctions ellipsis

• Un deuxième exemple (avec les macros) :


void AfficheIntData(int N, ...)
{
va_list ParametreCourant;
va_start(ParametreCourant, N);
for (int i = 0; i < N; i++) printf("%d ", va_arg(ParametreCourant, int));
va_end(ParametreCourant);
printf("\n");
}
void AfficheDoubleData(int N, ...)
{
va_list ParametreCourant;
va_start(ParametreCourant, N);
for (int i = 0; i < N; i++) printf("%0.1lf ", va_arg(ParametreCourant, double));
va_end(ParametreCourant);
printf("\n");
}
Entrée
void main(void) -
{
AfficheIntData(10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
AfficheDoubleData(5, 1.9, 2.8, 3.7, 4.6, 5.5); Sortie
} 1 2 3 4 5 6 7 8 9 10
1.9 2.8 3.7 4.6 5.5

162

Vous aimerez peut-être aussi