Programmation Procédurale P2

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 24

III.

Tableaux, chaînes, et pointeurs


Les tableaux permettent de gérer un ensemble de variables de même type. Un tableau est une zone
mémoire constituée de cases contiguës où sont rangées des données de même type. Les cases ont donc
une taille identique. Un tableau possède un nombre fixé de cases qui doit être connu quand il est créé.
La zone mémoire possède donc un début et une fin. Pour accéder à une case, nous utilisons un indice
qui repère le numéro de la case à laquelle on fait référence.
L'indice d'un tableau est un index, il y a ainsi un lien étroit entre tableau et pointeur comme nous allons
le voir dans ce chapitre.
Comme toute variable, une case d'un tableau doit être initialisée avant d'être utilisée.
a. Les tableaux unidimensionnels
La syntaxe de déclaration d'une variable de type tableau est la suivante : type identificateur [N];
Où :
 « type » est le type de variable contenu dans de chaque case du tableau
 « identificateur » est le nom donné au tableau
 « N » est une valeur entière qui donne le nombre total de cases du tableau
La valeur de N doit être connue à la compilation, il n'est pas possible d'utiliser des tableaux de taille
variable.
Par contre, nous pouvons décider de n'utiliser qu'une partie des cases d'un tableau.
i. Exemple de déclaration
...
{
int tableau[3];
...
}
Est une déclaration qui réserve une zone mémoire qui peut être représentée comme suit :

"tableau" est l'identificateur de la variable tableau qui correspond à une zone mémoire où nous pouvons
stocker 3 entiers. Elle comporte 3 cases. Chaque case peut contenir une valeur entière de type int.
Il ne faut pas confondre le numéro d'un élément du tableau et l'indice de la case qu'il occupe. Vous
remarquerez qu'en langage C les indices de cases commencent à 0. Ainsi, la déclaration int t[3] crée 3
cases dont les indices sont 0, 1, 2. Nous avons bien 3 indices pour repérer les 3 cases.
Le compilateur alloue la zone mémoire, mais n'initialise pas le contenu des cases. C'est pourquoi nous
avons indiqué « ? » pour le contenu de chaque case.

Exemple d'accès à une case du tableau "tableau" :


tableau [0] La valeur 0 correspond à l'indice de la première case du tableau. Nous rencontrons les
délimiteurs '[' et ']' qui encadrent une dimension (pour la déclaration) ou l'indice (pour accéder à une
case) d'un tableau.
C'est la technique la plus simple pour accéder à une case du tableau. De façon générique l'accès à une
case du tableau respecte la syntaxe suivante :
identificateur_du_tableau [ expression entière ]
En effet, la valeur d'un indice d'une case peut être le résultat d'un calcul qui renvoie une valeur
entière.
Un tableau à une dimension est également appelé vecteur.

25
Attention :
Le nombre de cases d'un tableau doit impérativement être connu à la compilation. En effet, il faut
pouvoir réserver la mémoire nécessaire à la zone de données contiguës.
À l'issue de la déclaration, le tableau est créé mais le contenu de ses cases n'est pas initialisé. Par
exemple « int truc[3] », déclare un tableau de 3 entiers, mais les entiers truc[0], truc[1] et truc[2] ne
sont pas initialisés.

ii. Initialisation à la déclaration


...
{
int truc [ 3 ] = {999, 777, 222} ;
...
}
A droite de l'opérateur d'affectation nous avons une liste de valeurs d'initialisation. Nous retrouvons
les délimiteurs '{'et '}' qui servent ici à encadrer les valeurs de la liste. Les valeurs de la liste sont
séparées par le délimiteur ','. Les valeurs sont rangées dans l'ordre gauche vers droite dans les cases du
tableau 'truc' à partir de la case d'indice 0 (zéro). Suite à cette déclaration, truc[0] contient 999, truc [1]
contient 777 et truc [2] contient 222. Toutes les cases du tableau 'truc' sont initialisées.
Si on déclare :
...
{
int machin [ 10 ] = {9, 7, 2} ;
...
}
Nous constatons qu'il y a moins de valeurs dans la liste d'initialisation que de cases dans le tableau.
Dans ce cas, seules les premières cases seront initialisées. A partir de la case d'indice 3, les valeurs ne
sont pas initialisées, par exemple nous ne connaissons pas la valeur de machin[3].
iii. Initialisation par affectation
Nous pouvons simplement écrire :
...
{
truc [0] = -1;
truc [1] = 10;
truc [2] = truc[1]*truc[1]; // nous avons en ce point truc[2] = 100
...
}
Cependant, pour réaliser l'initialisation complète des cases d'un tableau, il est plus judicieux d'utiliser
une boucle à bornes définies puisque l'on connaît le nombre maximum de cases.
Initialisation des cases de "machin" avec la valeur qui correspond à son indice de case :
...
{
for (i=0;i<10;i++)
{
machin[i] = i;
}
}
Il faut évidemment que la variable i ait été préalablement déclarée.
La partie droite de l'opérateur d'affectation correspond à une expression qui retourne une valeur qui
doit être compatible avec le type d'une case du tableau.
Autre exemple :
...
{
for (i=0;i<10;i++)

26
{
machin[i] = 10-i;
}
}
Remplira les cases 0,1,2,3,4,5,6,7,8,9 du tableau avec dans l'ordre les valeurs 9,8,7,6,5,4,3,2,1.
iv. Initialisation par lecture
#include <stdio.h>
int main()
{
int i ;
float t_r [10] ;
printf("\nentrer 10 valeurs flottantes") ;
for (i = 0 ; i < 10 ; i ++)
{
scanf(" %f ", &t_r[ i ]);
}
}
« t_r[i] » est une variable de type flottante et &t_r[i] est bien son adresse mémoire. C'est ce dont a
besoin le « scanf » pour ranger l'information lue au clavier.
v. Techniques algorithmiques liées aux tableaux
L'usage de constantes pour coder le nombre de cases maximales du tableau est conseillé.
Considérons le code :
#include <stdio.h>
int main()
{
int machin[10];
for (i=0;i<10;i++)
{
machin[i] = 10-i;
}
}
Si nous souhaitions travailler avec un tableau de taille 50, nous aurions besoin de modifier 3 lignes.
Considérons maintenant le code :
#include <stdio.h>
#define MAX 10
int main()
{
int machin[MAX];
for (i=0;i<MAX;i++)
{
machin[i] = MAX-i;
}
}
Avec ce code, si l'on veut travailler avec un tableau de 50 cases, il suffit de modifier une ligne (le «
#define »). Le précompilateur fera le travail de modification à notre place. C'est une technique de
programmation qui évite bien des erreurs.
Nous rappelons que la taille d'un tableau doit être connue à la compilation et que la taille d'un tableau
ne peut être modifiée au cours de l'exécution d'un programme.
Soit le problème suivant posé : le nombre d'éléments que l'on veut ranger et utiliser dans un tableau
est inconnu, mais on sait qu'il n'y en aura pas plus que 1000.
Dans ce cas, il suffit d'utiliser la technique que nous illustrons dans le code suivant :
include <stdio.h>
#define MAX 1000
int main()
{

27
int machin[MAX];
int nb_machin; // le nombre d'entiers rangés dans le tableau machin
}
Nous déclarons le tableau de taille maximale.
Puis on ajoute la déclaration de la variable « nb_machin » qui indique le nombre d'éléments que l'on a
effectivement rangés dans le tableau « machin ».
Vous remarquerez le choix effectué pour construire l'identificateur de la variable qui gère le nombre
d'entiers rangés dans le tableau.
Bien évidemment, c'est au programmeur d'incrémenter « nb_machin» quand on ajoute un élément et
de le décrémenter quand on en retire un.

28
b. Les tableaux à deux dimensions
i. Problème de la linéarisation de la structure de donnée
Voici un tableau à deux dimensions tel qu'il est présenté en mathématique par exemple :
7 8 9

15 16 17
Nous remarquons immédiatement qu'il s'agit d'une donnée qui possède deux dimensions. Il faut un
indice de ligne et un indice de colonne pour accéder au contenu d'une case. Or, la mémoire d'une
machine est vue comme un très grand ruban d'octets, c'est une organisation linéaire avec une seule
dimension puisque l'on accède à un octet à l'aide de son adresse mémoire. Ce ruban peut être vu comme
un tableau à une dimension.
Il faut donc trouver une technique pour linéariser un tableau en deux dimensions afin qu'il puisse être
rangé et manipulé dans un tableau à une dimension.
Nous avons deux possibilités :

 Linéarisation colonne/colonne
 Linéarisation ligne/ligne
Dans le premier cas le tableau est découpé en colonnes, et les colonnes consécutives sont rangées les
unes derrière les autres. Dans le second cas le tableau est découpé en lignes, et les lignes sont rangées
les unes derrière les autres. Le langage C utilise la deuxième technique.
Le découpage et l'organisation en lignes donne :
7 8 9 15 16 17

Les valeurs 7, 8, 9, 15, 16, 17 peuvent se ranger les unes à la suite des autres dans la mémoire : nous
avons linéarisé le rangement du tableau.
Un tableau à deux dimension est aussi appelé matrice.

ii. Déclaration et initialisation à la déclaration


#define MAX_L 2
#define MAX_C 3
int t[MAX_L] [MAX_C];

Permet de définir la variable "t" comme un tableau à deux dimensions de 2 lignes et 3 colonnes.
Pour accéder à une case il suffit d'utiliser la syntaxe :
t[1][2]
Ce qui permet d'accéder à la case qui contient un entier qui se trouve en deuxième ligne, troisième
colonne du tableau. (les indices commencent à zéro).
Bien que le tableau soit "linéarisé", pour accéder aux cases du tableau « t », l'opération d'indexation
s'écrit tout simplement « t[i][j] ». Le compilateur se charge de retrouver la bonne case, cette syntaxe
est transparente pour le programmeur.
1. Exemple d'initialisation à la déclaration
#define MAX_L 2
#define MAX_C 3
double td[MAX_L] [MAX_C] = {{37.2,37.5},{38.4, 40.5, 43.2}};

L'initialisation s'effectue ligne par ligne avec les listes de valeurs d'initialisation. Comme pour les
tableaux à une dimension, si une liste n'est pas complète, seules les premières cases sont remplies dans
l'ordre de la liste.

29
Ainsi, sur l'exemple ci-dessus la case td[0][2] n'est pas initialisée car il n'y a pas de troisième valeur
dans la liste d'initialisation de la première ligne.
iii. Initialisation par affectation
En considérant les déclarations :
#include<stdio.h>
#include <math.h>
#define MAX_L 2
#define MAX_C 3
#define PI 3.14159265
int main(void)
{
double td[MAX_L] [MAX_C];
int i,j;
for(i=0;i<MAX_L;i++)
{
for (j=0;j<MAX_C;j++)
{

td[i][j] = sin(PI/4);
printf("Les valeurs sont:%d \n",td[i][j]);

}
}

Nous avons initialisé toutes les cases du tableau "td" par calcul. Le type d'une case du tableau doit être
compatible avec le type de valeur retournée par l'expression à droite de l'opérateur d'affectation.

iv. Initialisation par lecture


Considérons :
#define MAX_L 2
#define MAX_C 3
int t[MAX_L] [MAX_C];
int i,j;
Puisque « t[i][j] » correspond à la variable de type entier rangée aux indices (i,j) du tableau, l'expression
« &t[i][j] » retourne son adresse, ce dont a besoin la fonction « scanf » pour lire une valeur au clavier
et la ranger au bon format dans la case.
Par exemple, le bout de code :
for (i=0;i<MAX_L;i++)
{
for (j=0;j<MAX_C;j++)
{
scanf("%d",&t[i][j]);
}
}

Va effectuer la lecture et affecter la valeur de toutes les cases du tableau « t ». Par souci de lisibilité,
nous vous conseillons d'écrire « &(t[i][j]) ». Cette écriture met plus en évidence que l'opérateur unaire
« & » (adresse de) s'applique à la case « t[i][j] ».
v. Débordement par excès et par défaut

30
Le débordement par excès survient quand on cherche à accéder à une case en dehors du tableau en
utilisant un indice plus grand que l'indice maximal possible.
include <stdio.h>
#define MAX 1000
int main()
{
int machin[MAX];
machin[1500] = 10;
}
La case d'indice 1500 n'existe pas puisque l'indice maximal pour le tableau qui a été déclaré est 999.
Attention :
Le débordement par défaut survient quand on cherche à accéder à une case en dehors du tableau en
utilisant un indice plus petit que l'indice minimal possible.
machin[-2] = 17;
La case d'indice -2 n'existe pas puisque l'indice minimal est 0.
Cela peut sembler très surprenant que le compilateur accepte ces instructions, mais c'est le cas. Plus
surprenant encore, le programme compilé peut aussi bien fonctionner que ne pas fonctionner. Les
débordements constituent évidemment des erreurs graves de programmation.
Car il est très dangereux d'accéder ainsi à une donnée qui n'existe pas et de la modifier.
En effet, on accède ainsi à des octets dans la mémoire et on les modifie. Cela peut faire "planter" le
code, comme cela peut ne pas le faire "planter".
Et puisque le C ne vérifie pas ce genre de chose, ce sont des erreurs très difficiles à détecter quand on
les commet.

c. Les chaînes de caractères


i. Tableau de caractères
Nous pouvons définir des tableaux de caractères, par exemple :
char t[10];
Dans ce cas, chaque case peut contenir un caractère. Nous pouvons le déclarer et l'initialiser ainsi :
char t[10] = {'a','l','p','h','a'};
Dans ce cas seules les 5 premières cases seront initialisées. De même, nous pouvons écrire ensuite :
t[6]='b';
A ce stade, les cases d'indices 0,1,2,3,4 et 6 sont initialisées, pas la case d'indice 5.
Ce tableau de caractères ne constitue pas une chaîne de caractères, d'une part parce que le caractère
de la case 5 n'est pas initialisé, et d'autre part parce que l'on ne sait pas quelle est la case qui contient
le dernier caractère.

En résumé, un simple tableau de caractères n'est pas une chaîne de caractères.

ii. Convention de codage des chaînes de caractères en langage C


En langage C, une chaîne de caractères correspond à un tableau de caractères et à l'utilisation d'une
convention pour marquer la fin de la chaîne de caractères. C'est-à-dire la fin de la partie du tableau qui
contient effectivement les caractères qui constituent la chaîne.
Pour cela, le langage C utilise un caractère particulier de la table des codes ASCII, c'est le '\0' (back
slash zéro). Il sert de marqueur pour indiquer la fin d'une chaîne de caractères.

31
Un des caractères du tableau doit être le marqueur de fin de chaîne '\0'. Dans ce cas tous les caractères
qui précèdent font partie de la chaîne de caractères et on ne se préoccupe pas de ceux qui suivent le
marqueur.
1. Les littéraux constants
Nous rappelons que la syntaxe 'a' désigne un unique caractère tandis que la syntaxe "a" désigne une
chaîne de caractères constante.
Dans le premier cas nous avons un unique octet, dans le second la chaîne de caractère occupe deux
octets dans une zone mémoire qui est un tableau de caractères. En effet, le premier octet contient le
codage du caractère 'a', le second le codage du '\0' qui marque la fin du littéral constant de type chaîne
de caractères "a".

iii. Initialisation d'une chaîne à la déclaration


char s1[ 10 ] = {'b', 'o', 'n', ' ', '!', '\0'} ;
On obtient les 6 premières cases du tableau qui contiennent :
S1 b o n ! \0
Indice de t[6] 0 1 2 3 4 5

C'est une chaîne de caractères car nous utilisons la convention.


L'instruction :
printf("%s",s1);
Affichera, en séquence les uns derrières les autres tous les caractères du tableau s1 jusqu'à trouver le
caractère '\0'. C'est-à-dire que l'on obtiendra : « bon ! » à l'écran. Sur l'écran.
Ce qui est équivalent à :

i = 0;
while (s1[i]!='\0') putchar(s1[i++]);

Nous pouvons remarquer que la liste d'initialisation fait explicitement apparaître le caractère de
marquage '\0'. Cette initialisation par liste peut être fastidieuse, il existe une autre façon de faire dans
le cas des chaînes de caractères.

char s2[10] = "bonjour";


En effet comme pour le "a", le caractère '\0' est implicite après le caractère 'r' dans le littéral constant
de type chaîne de caractères "bonjour".
Pour s2, qui comporte 10 cases, seules les 8 premières cases seront utilisées.
Introduisons maintenant les syntaxes équivalentes suivantes :
char *s3 = "bonjour";
char s4[] = {'b','o','n','j','o','u','r','\0'};
A la compilation, le compilateur réserve automatiquement autant de cases que nécessaires pour ranger
les caractères de l'initialisation à la déclaration.
Ces syntaxes impliquent que s3 et s4 sont des tableaux d'exactement 8 caractères, 7 sont nécessaires
pour les lettres de "bonjour" et un huitième pour coder le '\0' qui marque la fin des chaînes.

Cette valeur est calculée par le compilateur à partir des nombres de caractères utilisés pour
l'initialisation à la déclaration.

iv. Initialisation d'une chaîne par saisie


Considérons l'exemple :
char chai[ 6 ] = {'a', '\0'} ; scanf("%s",chai);

32
Initialement la chaîne « chai » contient le littéral constant de type chaîne de caractères « a ». Puis
l'usage de « scanf » avec le format %s effectue la lecture au clavier d'une chaîne de caractères qui va
écraser cette ancienne valeur.
Vous remarquerez que nous n'utilisons pas l'opérateur & (adresse de) devant l'identificateur « chai »
du tableau de caractères, ce n'est pas une erreur (se référer aux explications dans la section "lien entre
tableau, indice et pointeur")

Si l'on saisit plus de caractères que ne peut en contenir le tableau, seuls les premières cases seront
remplies, la chaîne saisie est alors tronquée et les caractères en surnombre sont perdus.

v. Initialisation d'une chaîne par calcul


Le tableau peut évidemment se remplir case par case mais il ne faut pas oublier d'ajouter le '\0' à la
fin.
#define MAX 5 int i; char truc[MAX]; for (i=0;i<MAX-1;i++) truc[i]='A'+i;
truc[MAX-1] = '\0';
Les cases d'indices 0,1,2,3 sont remplies avec successivement les caractères 'A', 'B', 'C', 'D' (dans l'ordre
des codes de la table ASCII). Ensuite la case d'indice 4 est remplie avec '\0'.
Mais il est préférable d'utiliser les fonctions prédéfinies dans la bibliothèque <strings.h>.
#include <string.h>
#define MAX_LEN 8
char toto[MAX_LEN];
strcpy(toto,"bonjour");
Où la fonction strcpy copie le mot « bonjour » dans la chaîne de caractères toto.
Le caractère '\0' prend une place dans le tableau de caractères. Si vous avez besoin de manipuler des
chaînes de 8 caractères, il faudra déclarer des tableaux de caractères de taille 9.

vi. Bibliothèque <string.h>


Cette bibliothèque fournit un ensemble de fonctions qui permettent de manipuler des chaînes de
caractères. En voici la liste :
strcat, strchr, strcmp, strcpy, strcspn, strlen, strncat, strncmp, strncpy, strpbrk, strrchr, strspn, strtok.
Vous remarquerez que leurs noms commencent toujours par "str" pour string (chaîne en anglais).
Nous détaillons le fonctionnement de :

 char * strcpy (chaîne1, chaîne2) : copie le contenu de « chaîne2 » dans « chaîne1 ». L'ancienne
valeur de chaîne1 est perdue. Retourne un « pointeur sur » chaîne1.
 int strlen (chaîne) : renvoie le nombre de caractères de la chaîne. Cette fonction compte les
caractères jusqu'à trouver le ‘\0'. La valeur renvoyée est le nombre de caractères avant le ‘\0'
 char * strcat (chaîne1, chaîne2) : copie le contenu de chaîne2 à la fin de chaîne1. Retourne un
« pointeur sur » chaîne1.
 int strcmp ( chaîne1 , chaîne2 ) : compare les deux chaînes, retourne 0 en cas d'égalité, une
valeur négative si « chaîne1 » est "avant" « chaîne2 » dans l'ordre lexicographique, une valeur
strictement positive sinon.
 char * strncat ( chaine1 , chaine2 , n ) : concatène au plus les « n » premiers caractères de «
chaine2 » à « chaine1 ». Retourne un pointeur sur « chaine1 ». Si n est plus grand que la
longueur de « chaine2 », la longueur de « chaine2 » est utilisée à la place de n. Retourne un «
pointeur sur » chaîne1.
 char * strncpy ( chaine1 , chaine2 , n) : copie « n » caractères de « chaine2 » sur « chaine1 ».
Retourne « un pointeur sur chaine1 ». Si « n » est inférieur à longueur de « chaine2 » « \0 »
n'est pas ajouté en fin de « chaine1 ». Si « n » est supérieur à longueur de « chaine2 », « chaine1
» est complétée par des « \0 ». Retourne un « pointeur sur » chaîne1.

33
Pour les opérations de copies, si la chaîne de caractères cible « chaîne1 » ne contient pas assez de place
pour contenir « chaîne2 » alors seuls les caractères que l'on peut copier le sont. On dit que « chaîne2 »
est tronquée.

Nous ne les décrirons pas toutes dans le cadre de ce cours introductif. Connaissant leur nom, il vous
sera facile de retrouver ce qu'elles effectuent comme traitement en consultant les documentations
adhoc.

Attention :

Si vous ne leur fournissez pas de tableaux de caractères qui comportent quelque part le caractère de
marquage '\0', vous pouvez avoir des problèmes en les utilisant.

Attention :

« chaine1 » et « chaine2 » sont des chaînes de caractères c'est-à-dire qu'il faut transmettre aux fonctions
des « pointeurs sur char »
En C, on ne peut pas comparer deux chaînes de caractères avec les opérateurs >, >=, <, <=, ==. Il faut
utiliser la fonction « strcmp ».

d. Lien entre tableau, indice et pointeur


Soit la déclaration de tableau suivante :
int t[3];
Nous avons déclaré et réservé une zone mémoire contiguë. L'identificateur « t » est en fait
l'identificateur d'une CONSTANTE qui est de type 'pointeur vers int'.

La valeur de cette constante est l'adresse du premier octet du premier élément du tableau. Comme le
montre le schéma ci-dessous :

En résumé, « t » est l'identificateur d'un pointeur constant vers un entier et dont la valeur est l'adresse
du premier octet de la zone réservée. Puisque « t » est une constante, on ne peut pas modifier sa valeur.
int t[3];
int * point_sur_int;
int i = 5;
point_sur_int = &i;
t = point_sur_int;
/* t est l'identificateur d'un pointeur constant : cette instruction est INTERDITE !*/
t = t + 1;
/* t est l'identificateur d'un pointeur constant : cette instruction est INTERDITE */

L'opérateur d'affectation n'est pas directement utilisable entre les identificateurs de tableaux. Mais on
peut manipuler les tableaux case par case.

Par exemple, si on veut faire la somme de 2 vecteurs « v1 » et « v2 » dans « v3 », on ne peut pas écrire
« v3=v1+v2 ». Il faut ajouter les cases de « v1 » et « v2 » une à une à l'aide d'une boucle selon une
instruction du type « v3[i] = v1[i] + v2[i] ».
i. Cas des tableaux unidimensionnels

34
Puisque dans la déclaration :
int t[10];
« t » est l'identificateur d'un pointeur constant, nous pouvons introduire une autre manière d'accéder à
une case du tableau en utilisant l'arithmétique sur les pointeurs. En effet, les instructions suivantes sont
strictement identiques :
t[i] = 0;
*(t+i) = 0;
De la même façon : &t[i] et (t+i) fournissent la même valeur. C'est l'adresse du premier octet de la case
d'indice i du tableau t.
En fait, quand nous écrivons :
t[i] = 0;
Le compilateur génère des instructions machine que l'on peut écrire comme :
*(t+ i*sizeof(int))= 0;
Ce qui permet de remplir la case i à partir de son premier octet avec la valeur 0. Les 4 octets consécutifs
que l'on trouve à partir de l'adresse t+ i*sizeof(int)) sont donc initialisés.
C'est donc un décalage de i entiers à partir de l'adresse du début du tableau. Ceci est transparent pour
le programmeur.
float t[3] = {1.0, 2.0 ,3.0};
/* t est l'identificateur d'un pointeur constant, dont la valeur est l'adresse du
premier octet du tableau */
float * pt_sur_flottant;
/* « pt_sur_flottant » est l'identificateur d'une variable qui contient une
adresse vers un flottant */
/* les deux lignes suivantes sont equivalentes */
pointeur_sur_flottant = t; // on peut affecter la valeur d'une constante à une
variable
pointeur_sur_flottant = &t[0]; // c'est la même chose que l'instruction ci-dessus
/* car &*(t+0) équivanut à (t+0) donc t */
/* et les deux lignes suivantes sont aussi équivalentes */
*pointeur_sur_flottant = -273.15;
/* car c'est *t donc *(t+0) donc t[0] */
t[0] = -273.15; // c'est la façon la plus simple de procéder.
bidule[i] et *(bidule +i)
Nous avons donc 2 façons de réaliser l'accès à des cases d'un tableau "bidule" :
bidule[i] et *(bidule +i)

Mais la notation bidule[i] est beaucoup plus facile à comprendre, à lire et à écrire. C'est donc celle
qu'il faut privilégier.
ii. Cas des tableaux bi-dimensionnels ou à 2 dimensions

Considérons l'exemple :
#define MAX_L 2
#define MAX_C 3
int t[MAX_L] [MAX_C];
int i,j;
Cela correspond au dessin linéarisé suivant :
t
indices 0 1 2 3 4 5
t[0][0] t[0][1] t[0][2] t[1][0] t[1][1] t[1][2]
Tableau 1: Linéarisation d'un tableau à deux dimension

En effet, le tableau est linéarisé selon le schéma ligne/ligne. Dans ce cas, t est l'identificateur d'un
pointeur constant aussi. Mais ici c'est l'adresse d'une adresse.

35
C'est l'adresse de l'adresse de la première ligne. Et *t est donc l'adresse de la première ligne c'est en fait
*(t+0).
Donc :
(t+i) est l'adresse de l'adresse de la ligne d'indice i
*(t+i) est l'adresse de la ligne i
*(t+i) + j est l'adresse du premier octet de la case j de la ligne i car on a un décalage de j entiers
dans la ligne i
*(*(t+i)+j) est donc la valeur de l'entier qui se trouve là,
et c'est la même chose que t[i][j]
donc, si vous avez bien compris le cours sur les tableaux :
t[1][2] est la même chose que :
*(t[1]+2)
*(*(t + 1) + 2)
*((int *)t+5)
Qui sont des façons d'accéder à la case qui se trouve en ligne 1 colonne 2.
Explications particulière pour la notation « *((int *) t+5) » : il y a 3 colonnes et ((1*3)+2)=5 est bien
le nombre d'entiers depuis le début du tableau qui correspond à la case d'indices (1,2) avec la
linéarisation.
Mais il faut un « cast » car « t » est vu comme « un pointeur de pointeur vers entier » : il faut que le
compilateur le "voit" comme un « pointeur vers entier » pour ne pas avoir de « warning » à la
compilation.
Si on écrit directement l'expression (t+5) pour calculer l'adresse d'un entier (pointeur vers entier), le
compilateur va l'interpréter comme l'accès à la ligne d'indice 5, ce qui peut être gênant.

Attention
Quand on écrit dans le programme :
&(t[i][j])
La machine calcule en fait l'expression mathématique suivante pour avoir l'adresse :
t + ( i x MAX_C + j) x Taille
avec Taille = sizeof(int)
C'est-à-dire la taille en nombre d'octets d'une case du tableau.
Et forcément quand on écrit :
t[i][j] = 0;
Le compilateur effectue quelque chose comme :
*(t + (i x MAX_C + j) x Taille) = 0
Un tableau à deux dimensions peut donc être vu comme un tableau de tableau à une dimension.
Considérons la déclaration suivante :
float machin[2][4] = {{10.0,9.0,8.0,7.0},{51.0,52.0,53.0,54.0}};
Le compilateur le « voit » comme dans la figure suivante :
pointeur de ligne :
machin+0 -> 10.0 9.0 8.0 7.0
machin+1 -> 51.0 52.0 53.0 54.0
La valeur « machin[0] » est une valeur qui est un pointeur vers le premier octer de la ligne 0, ce qui
correspond à *(machin +0).
Ce qu'il faut retenir :
 Le compilateur à absolument besoin de connaître le nombre de colonnes car il a besoin de la
valeur MAX_C pour effectuer ces calculs d'adresses
 L’usage des pointeurs pour accéder aux tableaux à deux dimensions est compliqué.
1. Usage des tableaux multidimensionnels
N'utilisez pas les pointeurs avec des tableaux à 2 dimensions
Utilisez les notations :
36
 tab[i][j] pour les opérations manipulant la valeur de la case (i,j)
 &(tab[i][j]) pour les opérations qui ont besoin de l'adresse de la case comme le « scanf » par
exemple
En effet :
scanf("%d",&t[i][j]); sera toujours plus clair que scanf(%d",&*((int *)*(t+i)+j));
Les tableaux à « n » dimension (n>2) correspond à un tableau de tableaux de dimension n-1. Nous ne
présenterons pas en détail les tableaux à plus de 2 dimensions, sachez seulement que :
#define MAX_L 2
#define MAX_C 3
#define MAX_P 7
int t_3[MAX_L] [MAX_C] [MAX_P];
int i,j,k;
Déclare un tableau à 3 dimensions, et que :
t_3[i][j][k]= 7;
Pour mettre la valeur 7 dans la case d'indices (i,j,k) ainsi que :
scanf("%d",&(t_3[i][j][k]));
Pour saisir la case d'indices (i,j,k), fonctionnent parfaitement.
Et « t_3 » est l'identificateur d'une constante qui est vue comme une adresse d'adresse d'adresse d'entier
par le compilateur ...

Comme nous l'avons vu il y a un lien étroit entre pointeurs et indices de tableau.


Considérons l'exemple suivant :
#include <stdio.h>
#define MAX 30
int main()
{
float t_reels[MAX];
int i,j;
float * p1, *p2;
p1 = &t_reels[3];
p2 = &t_reels[7];
printf("\n7-3 = %d",7-3);
printf("\n(p2-p1) = %d ",(p2-p1));
printf("\n(p2-p1) avec cast void = %d\n",(void *)p2-(void *)p1);
printf("\n\n");
}
Nous aurons à l'exécution l'affichage :
7-3 = 4
(p2-p1) = 4 (p2-p1) avec cast void = 16
En effet, "p1" et "p2" sont deux variables de type "pointeur vers flottant".
Elles sont initialisées avec deux adresses de flottant qui correspondent aux cases 3 et 7.
L'expression "(p2-p1)" effectue la soustraction de deux pointeurs.
Le compilateur sait que ce sont deux pointeurs vers des flottants. Il calcule donc le nombre de flottant
qui séparent les deux adresses en utilisant l'information sur la taille d'un flottant.
Si l'on transforme les "pointeurs sur float" en "pointeur sur void" avec des cast, la différence donne 16
car dans ce cas on soustrait deux adresses d'octets. Sur cette machine, nous en déduisons qu'un flottant
est codé sur 4 octets.

37
IV. Structure des données
a. Définition

38
V. Allocation dynamique
a. Rappel

La mémoire centrale d'un ordinateur peut être vue comme un long ruban d'octets. Chaque octet possède
une adresse unique. Dans les chapitres précédents, les variables ont été présentées comme un couple
(identificateur, réceptacle), le réceptacle étant constitué de plusieurs octets contigus en mémoire
centrale.
Nous avons vu que, connaissant la taille (le nombre d'octets) d'un réceptacle et l'adresse de son premier
octet, il était possible de lire et d'écrire de l'information comme si c'était une variable : c'est une des
utilisations possibles des pointeurs. Cependant, nous n'avons pas précisé où se trouvaient rangées les
variables en mémoire centrale. Nous dirons simplement qu'il existe deux zones de la mémoire où on
peut ranger les variables :
 Une zone 1 qui correspond à ce que nous avons vu jusqu'à présent
 Une zone 2 qui peut être utilisée dynamiquement par le programmeur
La première zone correspond par exemple à la déclaration ci-dessous :
void main(void)
{
int i;
float t[50];
}
Note importante :
Nous avons utilisée ce type de déclaration dans tous nos exemples, depuis le début de ce cours
introductif. Nous allons vous présenter succinctement comment utiliser la zone 2 que l'on appelle le
"tas" (heap en anglais).
Important :
Nous avons volontairement simplifié la présentation en scindant la mémoire en deux zones, la réalité
est plus complexe. Les explications fournies dans ce cours introductif sont suffisantes pour
l'appréhender. Néanmoins, la « carte » d'un programme qui s'exécute en mémoire centrale est plus
complexe.
Ce cours introductif n'a pas pour objectif de prendre en considération des concepts trop complexes. A
ce stade, ce qu'il faut retenir, c'est que l'on peut dynamiquement demander la création de nouveaux
réceptacles pour des données. Ceci signifie que l'on peut allouer de l'espace pour les données au cours
de l'exécution du programme.
Cette création s'effectuera dans la zone de tas. De même, on peut récupérer dynamiquement les octets
qui ont été alloués. Pour cela, nous allons utiliser deux fonctions :
 malloc
 free
Ces fonctions sont définies dans le fichier "en-tête" « stdlib.h » qui fait partie de la bibliothèque
standard définie par la norme ANSI. Ce fichier contient aussi les déclarations de fonctions traitant de
conversion de chaînes de caractères en types numériques ("itoa" par exemple), de tirages aléatoires («
srand » et « rand ») et d'autres fonctions d'allocation mémoire.

b. La fonction « malloc »
Nous vous rappelons qu'il est impossible de déclarer une variable de type « void » et qu'une fonction
de type « void » ne retourne aucune valeur.
Par contre, « void * » correspond à une donnée de type pointeur vers octet. Nous pouvons donc déclarer
une variable de type « void * ».
Si une fonction est de type « void * » elle retournera une valeur de type « adresse d'un octet ».

L'en-tête de la fonction « malloc » est le suivant : void *malloc(size_t taille) ;


39
La fonction malloc sert à faire une allocation dynamique de mémoire dans la zone du « tas ». Le
paramètre « taille » est de type « size_t » (type entier), c'est la taille de l'espace-mémoire que l'on veut
obtenir en nombre d'octets.
Elle retourne une adresse de type pointeur vers « void » (pointeur vers octet), qui nécessite l'utilisation
de la conversion explicite de type.
Prenons l'exemple suivant :
void main()
{
char * s;
// une variable de type pointeur sur caractères
int * t;
// une variable de type pointeur sur entier
float * td;
// une variable de type pointeur sur flottant
int i;
s = (char *) malloc(10 * sizeof(char));
t = (int *) malloc(12 * sizeof(int));
td = (float *) malloc(51 * sizeof(float));
scanf("%s",s);
for (i = 0;i<10;i++)
scanf("%d",&(t[i]));
for (i = 0;i<10;i++)
scanf("%f",&(td[i]));
}
L'utilisation de la fonction « sizeof » permet de retourner la taille du réceptacle (en octets) associé à un
type. Si on multiplie ensuite par une valeur positive entière, nous avons une valeur entière qui
correspond à un nombre d'octets. La fonction « malloc » demande alors au gestionnaire (fictif) de la
mémoire centrale de lui réserver le nombre d'octets consécutifs demandés en mémoire.
Si cette allocation est impossible par manque de place, « malloc » retourne la valeur de pointeur «
NULL » (Voir chapitre 4), sinon elle retourne l'adresse du premier octet de la zone allouée dans le tas.
Toutes les valeurs retournées par la fonction « malloc » sont de type « void * ».
Pour que le compilateur puisse considérer autre chose qu'une zone d'octets, il faut effectuer une
conversion explicite de type (un cast). La conversion est réalisée ici grâce aux (char *), (int *), (float
*) qui transforment la valeur retournée par « malloc » en « une adresse vers » respectivement : un char,
un int et un float.
Après les trois premières instructions qui utilisent "malloc", "s" est une variable qui contient l'adresse
d'une zone de caractères, "t" est une variable qui contient l'adresse d'une zone d'entiers et enfin "td" est
une variable qui contient l'adresse d'une zone de float.
En conséquence, nous pouvons accéder aux éléments comme avec des tableaux.
En effet, nous vous rappelons que :
 s[2] est syntaxiquement la même chose que *(s+2)
 t[4] est syntaxiquement la même chose que *(t+4)
 td[11] est syntaxiquement la même chose que *(td+11)
Sauf que cette fois-ci "s", "t" et "td" sont des variables de type "pointeur vers".
Nous pouvons donc les manipuler comme un tableau, la seule différence étant que "s", "t", et "td" sont
des variables et non des constantes (voir chapitre 6). Nous les utilisons ensuite comme des tableaux
dans les instructions qui suivent.
L'allocation dynamique est très utile en programmation pour créer des réceptacles temporaires pour
des données.
Cet exemple permet de réserver en mémoire la place pour un tableau d'élèves dont on demande la taille
à l'utilisateur.

40
#define MAX 50
struct eleve
{
char nom[MAX],

prenom[MAX],

float note_moyenne;
};
typedef struct eleve T_eleve;
int * allouer_tableau_eleve(int nb)
{
T_eleve * inter;
// variable locale de la fonction allouer_tableau_eleve
// qui contient une valeur qui est l'adresse d'un tableau alloué dynamiquement
inter = (T_eleve *)malloc(nb*sizeof(T_eleve));
//allocation de « nb » cases, chaque case étant de
// type « T_eleve ».
// La fonction « malloc » retourne un « void* », il ne faut
// donc pas oublier l'opération de « cast ».
return inter;
// on retourne la valeur du pointeur : elle n'est pas perdue
// bien que la variable locale soit détruite après l'appel de la fonction.
}
int main(void)
{
T_eleve * tab_eleve;
int nombre;
prinf("\n entrer le nombre d'eleves : ");
scanf("%d",&nombre);
tab_eleve = allouer_tableau_eleve(nombre);
// appel de la fonction « allouer_tableau_eleve » qui va
//dynamiquement créer
// un tableau de « nombre » cases. Chaque case étant
//une variable de type « T_eleve »
if (tab_eleve == NULL)
prinf("\n probleme d'allocation dynamique");

else
{

// la suite de votre code

}
}

c. La fonction "free"
La fonction "free" sert à restituer l'espace que l'on avait alloué avec "malloc".
Nous vous conseillons d'utiliser cette fonction autant de fois que la fonction "malloc" pour libérer la
place allouée dynamiquement. Si "p" pointe sur le début d'une zone mémoire allouée dynamiquement
alors l'appel de la fonction « free(p) » libère cette zone mémoire.
Le gestionnaire (fictif) de mémoire centrale récupère cette place et peut éventuellement l'allouer à
nouveau.
Pour comprendre le fonctionnement de la fonction « free », il peut être utile de savoir que, au moment
d'une allocation dynamique, le gestionnaire (fictif) de mémoire réserve en fait une petite place
supplémentaire juste avant la zone allouée au programme pour y noter la longueur de la zone allouée.
Lorsque l'instruction « free(p) » est effectuée, il suffit alors au gestionnaire d'aller relire la longueur de
la zone allouée pour connaître la taille de la zone à "désallouer".

41
Si la mémoire allouée avec « malloc » n'est pas libérée avec « free », elle est quand même libérée à la
fin du programme, mais ce n'est pas de la programmation « propre ». Si l'on cherche à libérer une zone
mémoire non allouée avec « malloc », cela peut provoquer une erreur à l'exécution du code.

d. Un exemple de gestion de listes chaînées


Le code complet qui suit résume ce cours. Une liste chaînée est constituée de cellules. Une cellule est
une donnée structurée qui comporte une information que l'on stocke et une information sur l'adresse
de la cellule qui suit.

Nous avons, dans l'exemple qui suit, une structure avec deux champs : un entier et un pointeur vers
une structure de type cellule qui suit la cellule courante.

Nous utilisons les techniques d'allocation dynamique pour créer dynamiquement les zones mémoires
(réceptacles) qui correspondent aux cellules.
La liste est repérée simplement par un pointeur vers la tête de la liste. La tête de liste correspond à
l'adresse de la première cellule de la liste. Quand la liste est vide, ce pointeur vaut NULL. Sur le
dessin qui précède, la cellule de tête est celle qui contient la valeur 12.
Et la dernière cellule est celle qui contient la valeur 37. Le pointeur qui suit la dernière cellule pointe
sur une X, ce qui symbolise graphiquement la valeur particulière NULL. NULL est une valeur
particulière qui correspond à une adresse mémoire qui n'existe pas.
Il est donc facile de tester si la liste est vide. Nous comparons la valeur du pointeur de la liste avec
cette valeur particulière (opérateurs & et *).
Dans le code ci-dessous nous utilisons le prototypage de fonction.
Ce code permet :
 D’ajouter des cellules en tête de la liste,
 D’afficher les cellules de la liste,
 De rechercher une valeur particulière dans la liste et
 De récupérer tous les réceptacles alloués dynamiquement quand on désire détruire toute la
liste.
C'est une base, qui peut vous servir pour vos propres applications (voir code en annexe 1).

42
VI. Les entrées/sorties

43
Références
 Cours et Exercices Pratiques — S3-PRC École Nationale d'Ingénieurs de Brest (ENIB) ;
 https://www.w3schools.com/c/index.php;
 http://ressources.unit.eu/cours/Cfacile/co;
 Le langage C, IUT de Ngaoundére, 2010-2011.

44
Annexe 1
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
struct cellule
{
int valeur;
struct cellule * succ;
// pointeur vers la cellule suivante
};
typedef struct cellule T_cellule;
// definition de type, permet d'eviter
// d'avoir a ecrire a chaque fois "struct cellule"
// les prototypes des fonctions
char bon_choix(char *s);
int affiche_menu_et_saisie_choix(void);
void afficher_cellule(T_cellule * c);
void affiche_liste(T_cellule * c);
T_cellule * detruire_liste(T_cellule * c);
T_cellule * ajouter(T_cellule * c);
void rechercher(T_cellule * c);
on principale
int main(void)
{
char choix;
T_cellule * tete;
tete = NULL;
do {
switch (choix = affiche_menu_et_saisie_choix())
{
case 'A' : tete = ajouter(tete); break;
case 'V' : affiche_liste(tete); break;
case 'S' : tete = detruire_liste(tete); break;
case 'R' : rechercher(tete); break;
case 'Q' : { tete = detruire_liste(tete);
// desallouer toujours proprement avant de
sortir
printf("\n\nAu revoir\n");
}

} while (choix !='Q');

printf("\n");

}
// les declarations completes des fonctions
char bon_choix(char *s) {
/* cette fonction teste si le premier caractère d'une chaîne correspond à un
caractère parmi ‘A','a', ‘V', ‘v', ‘S', ‘s', ‘R', ‘r', ‘Q', ‘q', elle retourne un
unique caractère majuscule dans ce cas et le caractère ‘\0' sinon. */
char r;
switch (s[0])
{
case 'A' :;
case 'a' : r = 'A';break;
case 'V' :;
case 'v' :r = 'V';break;

i
case 'S' :;
case 's' :r = 'S';break;
case 'R' :;
case 'r' :r = 'R';break;
case 'Q' :;
case 'q' : r = 'Q';break;
default: r='\0';
}
return r;
}
int affiche_menu_et_saisie_choix(void)
{
/* Permet d'afficher un menu, de lire une chaîne et de vérifier si le premier
caractère de cette chaîne correspond à un des choix possibles du menu.*/
// Elle affiche un message d'erreur si le choix n'est pas bon et ré-affiche
ensuite le menu.
char s_choix[2];
do {
printf("\n ajouter -> A");
printf("\n voir liste -> V");
printf("\n supprimer liste -> S");
printf("\n rechercher -> R");
printf("\n quitter -> Q");
printf("\n");
printf("\n votre choix : ");
scanf("%s",s_choix);
printf("\n");
if (!bon_choix(s_choix))
{
printf("\n\nVotre choix n'est pas bon ,recommencez SVP\n");
}
} while (!bon_choix(s_choix));
return (bon_choix(s_choix));
}
void afficher_cellule(T_cellule * c) {
// Fonction qui affiche le contenu d'une cellule
if (c==NULL)
{
printf("\rien a afficher, cellule vide\n");
}
else
{
printf("\nla cellule rangee a l'adresse %p contient la valeur %d ",c,c-
>valeur);
printf("\nla cellule qui suit est rangee a l'adresse %p ",c->succ);
printf("\n");
}

}
void affiche_liste(T_cellule * c) {
// Fonction qui affiche le contenu d'une liste cellule par cellule
if (c==NULL)
{
printf("\nrien a afficher, liste vide\n");
}
else {
do {
afficher_cellule(c);
c = c -> succ;
} while (c!=NULL);
}

ii
}
T_cellule * detruire_liste(T_cellule * c) {
// Fonction qui libère toute la place mémoire occupée par une liste que l'on veut
détruire
//initialement c contient la valeur du pointeur vers la cellule de tête de liste
à détruire
T_cellule * inter;
if (c==NULL)
{
printf("\nrien a detruire, liste vide\n");
}
else
{
do {
inter = c -> succ; //inter pointe sur la prochaine cellule a détruire
printf("\nrecuperation de la memoire de la cellule a l'adresse %p",c);
free(c);
c = inter;
} while (c!=NULL);
}
return NULL; //puisque toute la liste a été détruite on renvoie NULL
}
T_cellule * ajouter(T_cellule * c) {
// Fonction qui crée une cellule (allocation dynamique avec « malloc »).
T_cellule * nouvelle;
nouvelle = ( T_cellule *)malloc(sizeof( T_cellule));
printf("\nun entier SVP ");
scanf("%d",&(nouvelle->valeur));
nouvelle -> succ = c; return nouvelle;
}
void rechercher(T_cellule * c) {
// Fonction qui recherche une valeur particulière dans une liste.
// Affiche l'adresse de la cellule qui contient la valeur si cette dernière est
dans la liste et un message
// disant que la valeur n'est pas dans la liste sinon.
int val,trouve;
trouve = 0;
if (c==NULL)
{
printf("\nrien a rechercher, liste vide\n");
}
else {
printf("\nl'entier recherche SVP ");
scanf("%d",&val);
do {
if (c->valeur == val)
{
printf("\n\nl'entier recherche %d",val);
printf("\nse trouve dans la cellule a l'adresse %p",c);
c = NULL;
// pour stopper la boucle do while
trouve = 1;
// pour memorise le fait que l'on a trouve
}
else
{
printf("\npanparcours cellule a l'adresse %p, valeur %d",c,c->valeur);
c = c-> succ;
}
} while ((c!=NULL)&& (trouve !=1));
if (trouve ==0)

iii
{
printf("\n\nrecherche entier %d ",val);
printf("ne se trouve pas dans la liste");
printf("\n");
}
}
}

iv

Vous aimerez peut-être aussi