GRATUIT

Vos offres d'emploi informatique

Développeurs, chefs de projets, ingénieurs, informaticiens
Postez gratuitement vos offres d'emploi ici visibles par 4 000 000 de visiteurs uniques par mois

emploi.developpez.com

Initiation au langage C


précédentsommairesuivant

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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
int t[] = {0, 10, 20, 30};

est strictement identique à :

 
Sélectionnez
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 :

 
Sélectionnez
int x;

et

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
typedef int TABLEAU[10];

Désormais :

 
Sélectionnez
int t[10];

et :

 
Sélectionnez
TABLEAU t;

sont strictement équivalents.

IV-A-4. Les tableaux à plusieurs dimensions

On peut également créer un tableau à plusieurs dimensions. Par exemple :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
int t[3][4] = { {0, 1, 2, 3},
                {4, 5, 6, 7},
                {8, 9, 10, 11} };

Qu'on aurait également pu tout simplement écrire :

 
Sélectionnez
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 :

 
Sélectionnez
#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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
t[5] = '*';

est strictement équivalent à :

 
Sélectionnez
*(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 :

 
Sélectionnez
#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

 
Sélectionnez
int * const

Si on avait placé le mot-clé const avant int *, on obtiendrait :

 
Sélectionnez
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 :

 
Sélectionnez
int n, m;
int * const p = &n;
*p = 10;
/* p = &m : INTERDIT ! p est une constante ! */
 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
int t[10][3];

Définissons le type TRIPLET par :

 
Sélectionnez
typedef int TRIPLET[3];

De façon à avoir :

 
Sélectionnez
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 :

 
Sélectionnez
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. A près cet entier, on se retrouve au premier élément de t[3], en p + 9.

En conclusion, pour un tableau déclaré :

 
Sélectionnez
<type> t[N][M];

on a la formule :

 
Sélectionnez
t[i][j] = *(p + N*i + j) /* ou encore p[N*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,

 
Sélectionnez
<type> <identificateur>[]

est strictement équivalent à :

 
Sélectionnez
<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.

 
Sélectionnez
#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 :

 
Sélectionnez
char s[50] = "Bonjour";

Qui est strictement équivalente à :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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
 
Sélectionnez
#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
 
Sélectionnez
#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

 
Sélectionnez
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 :

 
Sélectionnez
#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 :

 
Sélectionnez
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 :

 
Sélectionnez
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

précédentsommairesuivant

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2008 Melem. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.