IV. Tableaux, pointeurs et chaînes de caractères▲
IV-A. Les tableaux▲
IV-A-1. Définition▲
Un tableau est un regroupement d'une ou plusieurs données de même type contigus en mémoire. L'accès à un élément du tableau se fait par un système d'indice, l'indice du premier élément étant 0. Par exemple :
int
t[10
];
déclare un tableau de 10 éléments (de type int) dont le nom est t. Les éléments du tableau vont donc de t[0], t[1], t[2] ... à t[9]. t est une variable de type tableau, plus précisément (dans notre cas), une variable de type tableau de 10 int (int [10]). Les éléments du tableau sont des int. Ils peuvent être utilisés comme n'importe quelle variable de type int.
IV-A-2. Initialisation▲
On peut initialiser un tableau à l'aide des accolades. Par exemple :
int
t[10
] =
{
0
, 10
, 20
, 30
, 40
, 50
, 60
, 70
, 80
, 90
}
;
Bien évidemment, on n'est pas obligé d'initialiser tous les éléments, on aurait donc pu par
exemple nous arrêter après le 5ème élément, et dans ce cas les autres éléments du tableau
seront automatiquement initialisés à 0. Attention ! une variable locale non initialisée contient
« n'importe quoi », pas 0 !
Lorsqu'on déclare un tableau avec initialisation, on peut ne pas spécifier le nombre
d'éléments car le compilateur le calculera automatiquement. Ainsi, la déclaration :
int
t[] =
{
0
, 10
, 20
, 30
}
;
est strictement identique à :
int
t[4
] =
{
0
, 10
, 20
, 30
}
;
IV-A-3. Création d'un type « tableau »▲
Tout d'abord, étudions un peu la logique du mot-clé typedef. Pour cela, supposons que l'on veuille pouvoir utiliser indifféremment les formules suivantes pour déclarer un entier :
int
x;
et
ENTIER x;
Dans la première forme, on remplace x par ENTIER et on la fait précéder du mot-clé typedef, ce qui nous donne :
typedef
int
ENTIER;
Concrètement, cela signifie : un ENTIER est tout ce qui suit le mot int dans une déclaration.
On peut également formuler une phrase similaire (genre : un TABLEAU est tout ce qui est
précédé du mot int et suivi de [10] dans sa déclaration) pour définir un type TABLEAU mais
il y a plus malin :
On remplace t par TABLEAU dans sa déclaration puis on ajoute devant le mot-clé typedef,
exactement comme ce qu'on a fait avec x et ENTIER. Ainsi, le type tableau de 10 int se
définit de la manière suivante :
typedef
int
TABLEAU[10
];
Désormais :
int
t[10
];
et :
TABLEAU t;
sont strictement équivalents.
IV-A-4. Les tableaux à plusieurs dimensions▲
On peut également créer un tableau à plusieurs dimensions. Par exemple :
int
t[10
][3
];
Un tableau à plusieurs dimensions n'est en fait rien d'autre qu'un tableau (tableau à une dimension) dont les éléments sont des tableaux. Comme dans le cas des tableaux à une dimension, le type des éléments du tableau doit être parfaitement connu. Ainsi dans notre exemple, t est un tableau de 10 tableaux de 3 int, ou pour vous aider à y voir plus clair :
2.
typedef
int
TRIPLET[3
];
TRIPLET t[10
];
Les éléments de t vont de t[0] à t[9], chacun étant un tableau de 3 int.
On peut bien entendu créer des tableaux à 3 dimensions, 4, 5, 6, ...
On peut également initialiser un tableau à plusieurs dimensions. Par exemple :
2.
3.
int
t[3
][4
] =
{
{
0
, 1
, 2
, 3
}
,
{
4
, 5
, 6
, 7
}
,
{
8
, 9
, 10
, 11
}
}
;
Qu'on aurait également pu tout simplement écrire :
2.
3.
int
t[][4
] =
{
{
0
, 1
, 2
, 3
}
,
{
4
, 5
, 6
, 7
}
,
{
8
, 9
, 10
, 11
}
}
;
IV-A-5. Calculer le nombre d'éléments d'un tableau▲
La taille d'un tableau est évidemment le nombre d'éléments du tableau multiplié par la taille de chaque élément. Ainsi, le nombre d'éléments dans un tableau est égal à sa taille divisée par la taille d'un élément. On utilise alors généralement la formule sizeof(t) / sizeof(t[0]) pour connaître le nombre d'éléments d'un tableau t. La macro définie ci-dessous permet de calculer la taille d'un tableau :
#define COUNT(t) (sizeof(t) / sizeof(t[0]))
IV-B. Les pointeurs▲
IV-B-1. Les tableaux et les pointeurs▲
Pour nous fixer les idées, considérons le tableau t suivant :
char
t[10
];
Mais les règles que nous allons établir ici s'appliquent à n'importe quel type de tableau,
y compris les tableaux à plusieurs dimensions.
Définissons ensuite le type TABLEAU par :
typedef
char
TABLEAU[10
];
Mais avant d'aller plus loin, j'aimerais déjà préciser que les tableaux et les pointeurs n'ont rien de commun, à part peut-être le fait qu'on peut pointer sur n'importe quel élément d'un tableau (ou sur un tableau ...), tout comme on peut pointer sur n'importe quoi. Mais il y a quand même une chose qui lie les deux notions, c'est que : lorsqu'elle n'est pas utilisée en unique argument de sizeof ou en unique argument de l'opérateur & ("adresse de"), une expression de type tableau est toujours convertie par le compilateur en l'adresse de son premier élément. Cela signifie, dans ces conditions, que si t est un tableau de disons 10 éléments, l'écriture t est strictement équivalente à &t[0], donc t + 1 (qui est strictement équivalent à &(t[0]) + 1) est strictement équivalent à &t[1] et ainsi de suite. Ainsi :
t[5
] =
'
*
'
;
est strictement équivalent à :
*(
t +
5
) =
'
*
'
;
Et ainsi de suite.
Dans la pratique, on utilise un pointeur sur un élément du tableau,
généralement le premier. Cela permet d'accéder à n'importe quel élément du tableau par
simple calcul d'adresse. En effet, comme nous l'avons dit plus haut :
t + 1 est équivalent à &(t[1]), t + 2 à &(t[2]), etc.
Voici un exemple qui montre une manière de parcourir un tableau :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
#include <stdio.h>
#define COUNT(t) (sizeof(t) / sizeof(t[0]))
void
Affiche
(
int
*
p, size_t nbElements);
int
main
(
)
{
int
t[10
] =
{
0
, 10
, 20
, 30
, 40
, 50
, 60
, 70
, 80
, 90
}
;
Affiche
(
t, COUNT
(
t));
return
0
;
}
void
Affiche
(
int
*
p, size_t nbElements)
{
size_t i;
for
(
i =
0
; i <
nbElements; i++
)
printf
(
"
%d
\n
"
, p[i]);
}
IV-B-2. L'arithmétique des pointeurs▲
L'arithmétique des pointeurs est née des faits que nous avons établis précédemment. En effet
si p pointe sur un élément d'un tableau, p + 1 doit pointer sur l'élément suivant. Donc si la
taille de chaque élément du tableau est par exemple de 4, p + 1 déplace le pointeur de 4 octets
(où se trouve l'élément suivant) et non de un.
De même, puisque l'on devrait avoir (p + 1) - p = 1 et non 4, la différence entre deux adresses
donne le nombre d'éléments entre ces adresses et non le nombre d'octets entre ces adresses.
Le type d'une telle expression est ptrdiff_t, qui est défini dans le fichier stddef.h.
Et enfin, l'écriture p[i] est strictement équivalente à *(p + i).
Cela montre à quel point le typage des pointeurs est important. Cependant, il existe des
pointeurs dits génériques capables de pointer sur n'importe quoi. Ainsi, la conversion d'un pointeur
générique en un pointeur d'un autre type par exemple ne requiert aucun cast et vice versa.
IV-B-3. Pointeurs constants et pointeurs sur constante▲
L'utilisation du mot-clé const avec les pointeurs est au début un peu délicate. En effet, en C on a ce qu'on appelle des pointeurs constants et des pointeurs sur constante (et donc aussi des pointeurs contants sur constante ...). Il faut bien savoir les différencier.
- Pointeur constant : Le pointeur, qui est une variable comme toute les autres, est déclaré const.
- Pointeur sur constante : C'est l'objet pointé qui est constant.
Dans le deuxième cas, l'objet pointé n'est pas nécessairement une constante.
Seulement, on ne pourra pas le modifier via le pointeur (qui considère que l'objet est une
constante). De même, on peut pointer sur une mémoire en lecture seule avec n'importe quel pointeur (et non nécessairement un pointeur sur constante)
mais cela ne signifie pas que la mémoire devient désormais accessible en écriture (puisque la mémoire pointée
est en lecture seule. Ce n'est pas en pointant là-dessus avec quoi que ce soit qu'on pourra changer cela.).
Si le pointeur n'est pas un pointeur sur contante alors qu'il pointe sur une constante, le compilateur acceptera
évidemment de modifier le contenu de la mémoire pointée (puisque l'écriture est syntaxiquement correcte),
mais le problème se manifestera à l'exécution, lorsque le programme tentera de modifier le contenu de la zone en lecture seule.
Comme int * est le type d'un pointeur sur int, un pointeur constant sur un int est de type
int
*
const
Si on avait placé le mot-clé const avant int *, on obtiendrait :
const
int
*
qui correspond plutôt, contrairement à ce qu'on attendait, au type pointeur sur const int (pointeur sur constante) ! Voici deux illustrations de l'utilisation de const avec les pointeurs :
2.
3.
4.
int
n, m;
int
*
const
p =
&
n;
*
p =
10
;
/* p = &m : INTERDIT ! p est une constante ! */
2.
3.
4.
int
n, m;
int
const
*
p =
&
n; /* Ou const int * p = &n; */
p =
&
m;
/* *p = 10 : INTERDIT ! *p est une constante ! */
IV-B-4. Pointeurs génériques▲
Le type des pointeurs génériques est void *. Comme ces pointeurs sont génériques, la taille des données pointées est inconnue et l'arithmétique des pointeurs ne s'applique donc pas à eux. De même, puisque la taille des données pointées est inconnue, l'opérateur d'indirection * ne peut être utilisé avec ces pointeurs, un cast est alors obligatoire. Par exemple :
2.
3.
4.
5.
int
n;
void
*
p;
p =
&
n;
*((
int
*
)p) =
10
; /* p etant desormais vu comme un int *, on peut alors lui appliquer l'operateur *. */
Etant donné que la taille de toute donnée est multiple de celle d'un char, le type char * peut
être également utilisé en tant que pointeur universel. En effet, une variable de type char * est
un pointeur sur octet autrement dit peut pointer n'importe quoi. Cela s'avère pratique des
fois (lorsqu'on veut lire le contenu d'une mémoire octet par octet par exemple) mais dans la
plupart des cas, il vaut mieux toujours utiliser les pointeurs génériques. Par exemple, la
conversion d'une adresse de type différent en char * et vice versa nécessite toujours un cast, ce
qui n'est pas le cas avec les pointeurs génériques.
Dans printf, le spécificateur de format %p permet d'imprimer une adresse (void *) dans le
format utilisé par le système.
Et pour terminer, il existe une macro à savoir NULL, définie dans stddef.h, permettant
d'indiquer q'un pointeur ne pointe nulle part. Son intérêt est donc de permettre de tester la
validité d'un pointeur et il est conseillé de toujours initialiser un pointeur à NULL.
IV-B-5. Exemple avec un tableau à plusieurs dimensions▲
Soit :
int
t[10
][3
];
Définissons le type TRIPLET par :
typedef
int
TRIPLET[3
];
De façon à avoir :
TRIPLET t[10
];
En dehors du cas sizeof et opérateur & ("adresse de"), t représente l'adresse de t[0] (qui est un TRIPLET) donc l'adresse d'un
TRIPLET. En faisant t + 1, on se déplace donc d'un TRIPLET soit de 3 int.
D'autre part, t peut être vu comme un tableau de 30 int (3 * 10 = 30) puisque les éléments d'un tableau sont toujours contigus en mémoire.
On peut donc accéder à n'importe quel élément de t à l'aide d'un pointeur sur int.
Soit p un pointeur sur int et faisons :
p =
(
int
*
)t;
On a alors, numériquement, les équivalences suivantes :
t | p |
t + 1 | p + 3 |
t + 2 | p + 6 |
... | |
t + 9 | p + 27 |
Prenons alors à présent, le 3ème TRIPLET de t soit t[2].
Puisque le premier élément de t[2] se trouve à l'adresse t + 2 soit p + 6, deuxième se trouve
en p + 6 +1 et le troisième et dernier en p + 6 + 2. Après cet entier, on se retrouve au premier
élément de t[3], en p + 9.
En conclusion, pour un tableau déclaré :
<
type>
t[N][M];
on a la formule :
t[i][j] =
*(
p +
M*
i +
j) /* ou encore p[M*i + j] */
Où évidemment : p = (int *)t.
Et on peut bien sur étendre cette formule pour n'importe quelle dimension.
IV-B-6. Passage d'un tableau en argument d'une fonction▲
Nous avons déjà vu que le passage d'un tableau en argument d'une fonction se fait tout simplement en passant l'adresse de son premier élément. Sachez également que, en argument d'une fonction,
<
type>
<
identificateur>
[]
est strictement équivalent à :
<
type>
*
<
identificateur>
Alors que dans une « simple » déclaration, la première déclare un tableau (qui doit être impérativement initialisé) et la deuxième un pointeur. On peut également préciser le nombre d'éléments du tableau mais cette information sera complètement ignorée par le compilateur. Elle n'a donc d'intérêt que pour la documentation.
IV-C. Les chaînes de caractères▲
IV-C-1. Chaîne de caractères▲
Par définition, une chaîne de caractères, ou tout simplement : chaîne, est une suite finie de caractères. Par exemple, "Bonjour", "3000", "Salut !", "EN 4", ... sont des chaînes de caractères. En langage C, une chaîne de caractères littérale s'écrit entre double quottes, exactement comme dans les exemples donnés ci-dessus.
IV-C-2. Longueur d'une chaîne▲
La longueur d'une chaîne est le nombre de caractères qu'elle comporte. Par exemple, la chaîne "Bonjour" comporte 7 caractères ('B', 'o', 'n', 'j', 'o', 'u' et 'r'). Sa longueur est donc 7. En langage C, la fonction strlen, déclarée dans le fichier string.h, permet d'obtenir la longueur d'une chaîne passée en argument. Ainsi, strlen("Bonjour") vaut 7.
IV-C-3. Représentation des chaînes de caractères en langage C▲
Comme nous l'avons déjà mentionné plus haut, les chaîne de caractères littérales
s'écrivent en langage C entre double quottes. En fait, le langage C ne dispose pas vraiment de
type chaîne de caractères. Une chaîne est tout simplement représentée à l'aide d'un tableau
de caractères.
Cependant, les fonctions manipulant des chaînes doivent être capables de détecter la fin d'une
chaîne donnée. Autrement dit, toute chaîne de caractères doit se terminer par un caractère
indiquant la fin de la chaîne. Ce caractère est le caractère '\0' et est appelé le caractère nul
ou encore caractère de fin de chaîne. Son code ASCII est 0. Ainsi la chaîne "Bonjour" est en
fait un tableau de caractères dont les éléments sont 'B', 'o', 'n', 'j', 'o', 'u', 'r',
'\0', autrement dit un tableau de 8 caractères et on a donc "Bonjour"[0] = 'B', "Bonjour"[1] =
'o', "Bonjour"[2] = 'n', ... "Bonjour"[7] = '\0'. Toutefois, la mémoire utilisée pour stocker la chaîne peut être accessible qu'en lecture, le
il n'est donc pas portable de tenter de la modifier.
Les fonctions de manipulation de chaîne de la bibliothèque standard du langage C sont
principalement déclarées dans le fichier string.h. Voici un exemple d'utilisation d'une de ces
fonctions.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
#include <stdio.h>
#include <string.h>
int
main
(
)
{
char
t[50
];
strcpy
(
t, "
Hello, world!
"
);
printf
(
"
%s
\n
"
, t);
return
0
;
}
Dans cet exemple, la chaîne t ne peut contenir tout au plus que 50 caractères, caractère de fin de chaîne inclus. Autrement dit t ne peut que contenir 49 caractères « normaux » car il faut toujours réserver une place pour le caractère de fin de chaîne : '\0'. On peut aussi bien sûr initialiser une chaîne au moment de sa déclaration, par exemple :
char
s[50
] =
"
Bonjour
"
;
Qui est strictement équivalente à :
char
s[50
] =
{
'
B
'
, '
o
'
, '
n
'
, '
j
'
, '
o
'
, '
u
'
, '
r
'
, '
\0
'
}
;
Puisque, vu d'un pointeur, la valeur d'une expression littérale de type chaîne n'est autre que l'adresse de son premier élément, on peut utiliser un simple pointeur pour manipuler une chaîne. Par exemple :
char
*
p =
"
Bonjour
"
;
Dans ce cas, p pointe sur le premier élément de la chaîne "Bonjour". Or, comme nous l'avons déjà dit plus haut, la mémoire allouée pour la chaîne "Bonjour" est en lecture seule donc on ne peut pas écrire par exemple :
p[2
] =
'
*
'
; /* Interdit */
Avec un tableau, ce n'est pas l'adresse en mémoire de la chaîne qui est stockée, mais les
caractères de la chaîne, copiés caractère par caractère. La mémoire utilisée par le tableau étant
indépendante de celle utilisée par la chaîne source, on peut faire ce qu'on veut de notre
tableau. La fonction strcpy permet de copier une chaîne vers un autre emplacement mémoire.
Le paragraphe suivant discute des fonctions de manipulation de chaînes en langage C.
IV-C-4. Les fonctions de manipulation de chaîne▲
strcpy, strncpy▲
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
#include <stdio.h>
#include <string.h>
int
main
(
)
{
char
t1[50
], t2[50
];
strcpy
(
t1, "
Hello, world!
"
);
strcpy
(
t2, "
*************
"
);
strncpy
(
t1, t2, 3
);
printf
(
"
%s
\n
"
, t1);
return
0
;
}
Attention ! Si t1 n'est pas assez grand pour pouvoir contenir la chaîne à copier, vous aurez un
débordement de tampon (buffer overflow). Un tampon (ou buffer) est tout simplement
une zone de la mémoire utilisée par un programme pour stocker temporairement des données.
Par exemple, t1 est un buffer de 50 octets. Il est donc de la responsabilité du programmeur de
ne pas lui passer n'importe quoi ! En effet en C, le compilateur suppose que le programmeur sait ce qu'il fait !
La fonction strncpy s'utilise de la même manière que strcpy. Le troisième argument indique le
nombre de caractères à copier. Aucun caractère de fin de chaîne n'est automatiquement
ajouté.
strcat, strncat▲
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
#include <stdio.h>
#include <string.h>
int
main
(
)
{
char
t[50
];
strcpy
(
t, "
Hello, world
"
);
strcat
(
t, "
from
"
);
strcat
(
t, "
strcpy
"
);
strcat
(
t, "
and strcat
"
);
printf
(
"
%s
\n
"
, t);
return
0
;
}
strlen▲
Retourne le nombre de caractères d'une chaîne.
strcmp, strncmp▲
On n'utilise pas l'opérateur == pour comparer des chaînes car ce n'est pas les adresses qu'on veut comparer mais le contenu mémoire. La fonction strcmp compare deux chaînes de caractères et retourne :
- zéro si les chaînes sont identiques
- un nombre négatif si la première est "inférieure" (du point de vue lexicographique) à la seconde
- et un nombre positif si la première est "supérieure" (du même point de vue ...) à la seconde
Ainsi, à titre d'exemple, dans l'expression
strcmp
(
"
clandestin
"
, "
clavier
"
)
La fonction retourne un nombre négatif car, 'n' étant plus petit que 'v' (dans le jeu de caractères ASCII, ça n'a rien à voir avec le langage C), "clandestin" est plus petit que "clavier".
IV-C-5. Fusion de chaînes littérales▲
Le C permet de fusionner deux chaînes littérales en les plaçant simplement côte à côte. Cela se révèle particulièrement utile lorsqu'on doit composer une chaîne et que cette dernière soit trop longue a écrire sur une seule ligne. Par exemple :
2.
3.
4.
5.
6.
7.
8.
#include <stdio.h>
int
main
(
)
{
printf
(
"
Voici une chaine de caracteres particulierement longue, tres longue,
"
"
tellement longue (oui, longue !) qu'il a fallu la decouper en deux !
"
);
return
0
;
}
IV-D. Exercices▲
IV-D-1. Recherche dans un tableau▲
Nous nous proposons d'écrire un programme qui permet de trouver toutes les occurrences d'un nombre entré par l'utilisateur parmi un un ensemble de nombres précédemment entrés. L'utilisateur entrera dix nombres (que vous placerez dans un tableau), ensuite il entrera le nombre à rechercher et le programme devra afficher toutes les occurrences de ce nombre s'il y en a ainsi que le nombre d'occurrences trouvées. Voici un exemple d'exécution :
Ce programme permet de trouver toutes les occurrences d'un nombre.
Entrez 10 nombres :
t[0] : 2
t[1] : -1
t[2] : 3
t[3] : 28
t[4] : 3
t[5] : -8
t[6] : 9
t[7] : 40
t[8] : -1
t[9] : 8
Entrez le nombre a rechercher : 3
t[2]
t[4]
2 occurrence(s) trouvee(s).
Merci d'avoir utilise ce programme. A bientot !
IV-D-2. Calcul de la moyenne▲
Nous nous proposons d'écrire un programme permettant de calculer la moyenne d'un élève. L'utilisateur entrera 5 "notes". Une note est constituée d'une note sur 20 (de type entier)
et d'un coefficient compris entre 1 et 5 (entier également). La note définitive pour une matière est la note sur 20 multipliée par le coefficient. La moyenne de l'étudiant est égale au total
des notes définitives divisé par la somme des coefficients.
On créera un tableau t de 5 tableaux de 3 entiers, c'est-à-dire int t[5][3]. Les 5 éléments de t sont destinés à accueillir les 5 notes. Pour chaque note t[i], t[i][0] contiendra la note sur 20,
t[i][1] le coefficient et t[i][2] la note définitive. Le programme demandera à l'utilisateur d'entrer les notes puis affichera la moyenne de l'élève avec 2 chiffres après la virgule et les calculs
intermédiaires.
Voici un exemple d'exécution :
Ce programme permet de calculer votre moyenne scolaire.
Note 1 (0 a 20) : 14
Coef 1 (1 a 5) : 5
Note 2 (0 a 20) : 10
Coef 2 (1 a 5) : 5
Note 3 (0 a 20) : 16
Coef 3 (1 a 5) : 3
Note 4 (0 a 20) : 8
Coef 4 (1 a 5) : 1
Note 5 (0 a 20) : 12
Coef 5 (1 a 5) : 1
+------+------+----------+
| Note | Coef | Note Def |
+------+------+----------+
| 14 | 5 | 70 |
+------+------+----------+
| 10 | 5 | 50 |
+------+------+----------+
| 16 | 3 | 48 |
+------+------+----------+
| 8 | 1 | 8 |
+------+------+----------+
| 12 | 1 | 12 |
+------+------+----------+
| Tot. | 15 | 188 |
+------+-----------------+
| Moy. | 12.53 |
+------+-----------------+
Merci d'avoir utilise ce programme. A bientot !
IV-D-3. Manipulation de chaînes▲
Ecrire les fonctions de manipulation de chaînes suivantes :
- str_cpy, de rôle et d'utilisation semblables à la fonction strcpy de la bibliothèque standard
- str_len, de rôle et d'utilisation semblalbles à la fonction strlen de la bibliothèque standard
- str_equals, qui reçoit en arguments de chaînes de caractères et qui retourne VRAI si elles sont égales et FAUX dans le cas contraire