Les tableaux
Dernière mise à jour : 16 février 2010

Définitions et déclarations de tableaux

Un tableau est un ensemble indexé de grandeurs du même type.

Il peut s'agir d'une grandeur d'un type scalaire de base (par exemple char, int ou double), d'une structure ou une union, de pointeurs ou de tableaux.

Exemples de définitions de tableaux

▶ Commençons par quelques exemples.

  • int tab1[5];

    définit un tableau de nom tab1 de 5 entiers de type int.

    Les cinq éléments du tableau sont identifiables individuellement par

             tab1[0], tab1[1], tab1[2], tab1[3] et tab1[4].

  • struct point tabPoint[3];

    définit un tableau de trois éléments dont chacun a la structure point, supposée définie préalablement par exemple par

           struct point {float abs; float ord;};;

    Les trois éléments du tableau sont identifiables individuellement par tabPoint[0], tabPoint[1] et tabPoint[2].
    Ainsi, pour une valeur correcte de i (0, 1 ou 2), « tabPoint[i].abs » et « tabPoint[i].ord » identifieront les deux champs du point en position i dans le tableau.

  • char tab2[4][3];

    définit un tableau de nom tab2 de 4 tableaux de 3 caractères :

    • tab2[0], tab2[1], tab1[2] et tab2[3] sont des tableaux de 3 caractères;
    • tab2[0][0], tab2[0][1] et tab2[0][2] désignent les 3 éléments de type char du tableau tab2[0].

▶ La définition d'un tableau de nom t de grandeurs d'un certain type T est donc réalisée sous la forme
       « T t[entier_positif]; ».

Comme toute définition de variable, la définition d'un tableau vise à associer de la mémoire à une variable (et donc d'allouer un espace en mémoire).
Cela impose donc d'en fixer le nombre d'éléments (d'où se déduira la taille à allouer).

Remarque : à ce niveau, on voit une différence avec ce qui se passe en Java où la déclaration d'une variable du type « tableau d'entiers » et l'allocation du tableau sont disjointes. Cette séparation réapparaitra en C lorsque nous « regarderons » les tableaux comme des pointeurs.

Une définition de tableau telle que :

       int tt[ ];

provoque lors de la compilation un message d'avertissement (warning) qui n'empêche cependant pas la construction d'un binaire exécutable :

       warning: array 'tt' assumed to have one element

Le type spécifié pour tt est ce qu'on appelle un type incomplet : il ne contient pas suffisamment d'information pour en déterminer la taille (un autre exemple, de nature un peu différente, de type incomplet est void).

Par contre, une tentative d'accès à la taille du tableau dans le programme avec l'opérateur sizeof provoquerait une véritable erreur de compilation sans construction d'exécutable :

        error: invalid application of 'sizeof' to incomplete type 'int[ ]'

Remarque : si on reprend la définition du tableau tt avec le type incomplet, un programme dans lequel on écrirait ensuite :

       tt[10] = 10;

se compile sans aucun problème et s'éxécute le cas échéant (cela peut dépendre des systèmes et des machines). Dans ce cas, inutile de préciser que des données ont du être malencontreusement écrasées et que les résultats obtenus au bout du compte sont peu fiables!

▶ La définition d'un tableau peut être accompagnée de son initialisation comme dans les définitions suivantes :

    int t[ ] = {1, 2, 3};
    char c1[10] = {'a', 'b', 'c', 'd'};
    /* assez warning: excess elements in array initializer */
    char c2[2] = {'a', 'b', 'c', 'd'};
    int tt[ ][4] = {{1,2},{1,3,5},{5},{1,5,7,8}};

Définir un type tableau

Il est possible de définir un type de « tableau de quelque chose » en utilisant typedef.

Par exemple :

       typedef int tab10int[10];

définit un type de nom tab10int correspondant à des tableaux de 10 entiers.

La définition « tab10int t; » définit alors un tableau t de 10 entiers.

Taille d'un tableau

Pour un identificateur déclaré en tant que tableau via un type complet, l'opérateur sizeof donne la taille totale de l'espace occupé par l'ensemble des éléments du tableau et donc pour nos exemples :

  • sizeof(tab1) est égal à « 5 * sizeof(int) » : pour une taille 4 de int, on obtient 20;
  • sizeof(tabPoint) est égal à « 3 * sizeof(struct point) » : pour la structure point que nous avons proposée dont la taille est 8, on obtient 24 comme taille globale;
  • sizeof(tab2) est égal à « 4 * 3 * sizeof(char) », et donc 12 comme taille globale.

Indexation et implantation d'un tableau en mémoire

▶ Les différents éléments d'un tableau sont implantés de manière contigue en mémoire.

▶ Si t est un tableau d'éléments de type T, le premier élément, désignable par l'expression t[0], occupe les sizeof(T) premiers octets alloués au tableau, le deuxième élément désignable par t[1] occupe les sizeof(T) octets suivants, ...

▶ L'indexation d'un tableau permettant d'accéder aux éléments d'un tableau est réalisée entre crochets [ et ];

▶ L'indexation commence à 0.

Insistons sur le fait qu'en C aucun contrôle n'est réalisé lors de l'accès à un tableau par indexation, contrairement à ce qui se passe en Java par exemple.

Pour l'expliquer il faut savoir (et cela sera repris en détail plus loin) que

  • un nom de tableau est de fait un pointeur (constant) donnant l'adresse du premier élément du tableau;
  • les indexations de tableaux sont converties en des manipulations de pointeurs et donc en des déplacements en mémoire par rapport au début du tableau. Il n'y a aucun contrôle à ce niveau et tant que l'adresse obtenue est légale au regard des adresses du programme (et non du tableau), l'exécution sera possible, une écriture espérée dans le tableau risquant de fait de se faire en dehors du tableau avec écrasement possible de données.
    Une erreur se produira à l'exécution seulement si le programme essaie d'écrire en dehors de l'espace mémoire qui lui appartient.
L'exemple suivant illustre cette observation :
    --> cat adresse_et_tableau.c
    #include <stdio.h>
    #include <stdlib.h>

    int main() {
       int j;
       int i = 100;
       int tab[3] = {10, 20, 30};
       printf("%p %p\n", tab+3, &i);
       for(j = 0; j <= 3; j++)
          printf("%d ", tab[j]);
       putchar('\n');
       return EXIT_SUCCESS;
    }
    --> ./adresse_et_tableau
    0xbffffa38 0xbffffa38
    10 20 30 100
    -->
▶ Si t est un tableau à deux dimensions d'éléments de type T (c'est-à-dire un tableau de tableaux d'éléments de type T), déclaré sous la forme :
            T t[n1][n2];

le tableau est rangé en mémoire dans l'ordre :
   t[0][0], t[0][1], ..., t[0][n2-1],t[1][0],t[1][1],..., t[1][n2-1], t[2][0]..., t[n1-1][n2-1]

Si on assimile le premier indice à un indice de ligne et le second à un indice de colonne, on peut dire que les tableaux à deux dimensions (tableaux de tableaux) sont rangés lignes par lignes.

Déclarer un tableau

Il y a des circonstances où on est amené à utiliser des tableaux dont on ne connaît pas a priori la taille et donc déclarés (et non définis) localement au travers de types incomplets.

▶ On peut citer comme exemples :

  • les tableaux manipulés dans un programme et qui sont définis ailleurs. Ces tableaux doivent être déclarés localement avec le qualificatif extern comme par exemple :

           extern int t[ ];

    Cette déclaration indique qu'on souhaite utiliser un tableau de nom t d'entiers supposé défini ailleurs.

  • les tableaux manipulés dans une fonction et qui sont supposés définis dans le module appelant et transmis en paramètres lors de l'appel de la fonction.
    Cela conduit par exemple à une définition de fonction dont l'en-tête est  alors:

           int fonc(int t1[ ], int t2[ ])

    Le prototype d'une telle fonction (typiquement défini dans un fichier d'en-tête) est :

           int fonc(int [ ], int [ ]);

Il est important de noter que, si le nombre d'éléments du tableau n'est pas fourni, la taille du tableau ne pourra pas être calculée par le compilateur : sizeof appliqué au tableau renverra la taille d'un pointeur.

▶ Cependant tout type incomplet n'est pas acceptable dans ces deux cas.

Il faut que l'information fournie par le type permette au compilateur de calculer, à partir d'indices dans le tableau, le déplacement correspond en mémoire par rapport au début du tableau.

  • dans le cas où le paramètre est un tableau à une dimension, il faut simplement que le type des éléments du tableau soit connu. Cela permet en effet de déterminer la taille d'un élément du tableau et par suite un indice en un déplacement par rapport au début du tableau. Cela interdit donc d'utiliser le type void. La taille du tableau n'est pas nécessaire. Une déclaration d'un paramère tableau d'éléments de type T sous la forme « T t[ ] » est donc possible;
  • pour les tableaux à plusieurs dimensions, la réponse n'est qu'un cas particulier de celle donnée pour les tableaux à une dimension.
    Ainsi par exemple, si on considère un tableau t de n1 tableaux de n2 entiers dont la définition est « int t[n1][n2]; »
    • pour tout entier i, t[i] est un tableau de n2 entiers. Pour l'atteindre en mémoire il faut se déplacer depuis le début du tableau (c'est-à-dire la constante t ou &t[0][0]) de i fois la taille d'un tableau de n2 entiers;
    • pour localiser un élément quelconque t[i][j] du tableau, il faut d'abord se déplacer sur t[i] (c'est-à-dire sur t[i][0]), puis dans ce tableau se déplacer de j positions.
    Ce qui importe donc est donc la connaissance du type et le nombre n2 d'élements de chacune des lignes.
    La valeur n1 de la première composante n'intervient pas dans le calcul du déplacement.

    Donc, une déclaration telle que

           « int t[ ][n2] »

    est possible.

  • Cela s'étend à un tableau à p dimensions pour lequel on peut écrire

           « int t[ ][n2]...[np] »


Tableaux et pointeurs

Introduction

▶ On dit souvent qu'en C, tableaux et pointeurs sont la même chose et peuvent être interchangés.
Nous nous proposons ici de faire le point de ces deux notions qui ont certes des liens mais ne sont pas complètement interchangeables.

▶ Les règles suivantes qui sont appliquées en langage C peuvent effectivement prêter à confusion et nous les reprendrons en détail dans la suite :

  • la définition d'un identificateur de tableau correspond à celle d'un pointeur constant : cela signifie que sa valeur ne peut pas être modifié (donc qu'il ne peut constituer la partie gauche d'une affectation);
  • utilisé en paramètre de fonction, un identificateur de tableau est assimilé à un pointeur;
  • l'opérateur d'indexation est traduit en un déréférencement de pointeur. Ainsi « t[i] » est traduit en « *(t + i) »
De fait, en dehors d'un contexte « paramètre formel » où des écritures telles que « int t[] » et « int *t » sont équivalentes, dans tous les autres contextes tableau et pointeur sont « quelque part » différents.

▶ Les principales différences qu'on peut voir entre ces deux concepts se résument à ce que :

En définissant un tableau, on définit un pointeur constant

▶ La définition

d'un tableau tab de N (expression constante) éléments de type T correspond à ▶ La définition d'un tableau nécessite la connaissance de sa taille dès la compilation : elle ne peut pas être calculée par le programme.

▶ Dans le cas d'un tableau bidimensionnel, toutes les lignes doivent avoir le même nombre d'élements. Les tableaux ainsi définis sont donc toujours rectangulaires.

Tableaux, chaînes et pointeurs

La différence entre tableau et pointeur peut encore être illustrée tout d'abord par ce qui se passe avec les deux déclarations suivantes :

    char tab1[ ] = "Bonjour";
    char *tab2 = "Bonjour";
On utilise ici une chaîne de caractères littérale pour initialiser un tableau de caractères et un pointeur sur caractère.
    --> cat tableau_chaine.c
    #include <stdio.h>
    #include <stdlib.h>
    char tab1[ ] = "Bonjour";
    char *p1 = "Bonjour";
    int main( ) {
       char tab2[ ] = "Bonjour";;
       char *p2 = "Bonjour";
       printf("tab1 :\n");
       printf("   taille : %ld\n", sizeof(tab1));
       printf("    adresse : %p\n", &tab1);
       printf("    valeur : %p\n", tab1);
       printf("p1 :\n");
       printf("    taille : %ld\n", sizeof(p1));
       printf("    adresse : %p\n", &p1);
       printf("    valeur : %p\n", p1);
       printf("tab2 :\n");
       printf("    taille : %ld\n", sizeof(tab2));
       printf("    adresse : %p\n", &tab2);
       printf("    valeur : %p\n", tab2);
       printf("p2 :\n");
       printf("    taille : %ld\n", sizeof(p2));
       printf("    adresse : %p\n", &p2);
       printf("    valeur : %p\n", p2);
       return EXIT_SUCCESS; // return 0;
    }
    --> ./tableau_chaine
    tab1 :
        taille : 8
        adresse : 0x2014
        valeur : 0x2014
    p1 :
        taille : 4
        adresse : 0x201c
        valeur : 0x1fa6
    tab2 :
        taille : 8
        adresse : 0xbffffad8
        valeur : 0xbffffad8
    p2 :
        taille : 4
        adresse : 0xbffffad4
        valeur : 0x1fa6
    -->

    Les résultats fournis par ce programme correspondent au schéma suivant en mémoire dans lequel on a distingué

    • les données en zone statique qui existent dès le lancement du programme et ne disparaissent qu'à sa terminaison
    • les données sur la pile dont l'existence est liée aux appels de fonctions (ou aux blocs) au cours de l'exécution du programme et dont la durée de vie n'est que celle d'un appel de la fonction associée (ou de l'ex&eacutre;cution de bloc).

Tableaux et pointeurs dans des structures

Une autre illustration de cette différence concerne les tableaux ou pointeurs membres d'une structure et ce qui se passe quand une telle structure est passée en paramètre :

    --> cat struct_tab_ptr.c
    #include <stdio.h>
    #include <stdlib.h>

    int tab[2] = {1, 2};
    int tab2[2] = {7, 8};

    struct st1 {int a; int p[2];} s1 = {3, {1, 2}};;
    struct st2 {int a; int *p;} s2 = {3, tab};

    void f1(struct st1 s) {
       s.a++; s.p[0]++; s.p[1]++;
    }

    void f2(struct st2 s) {
       s.a++; s.p[0]++; s.p[1]++;
    }
    void f3(struct st2 s) {
       s.a++; s.p = tab2;
       s.p[0]++; s.p[1]++;
    }

    int main() {
       printf("s1 avant f1 : %d %d %d\n", s1.a, s1.p[0], s1.p[1]);
       f1(s1);
       printf("s1 apres f1 : %d %d %d\n", s1.a, s1.p[0], s1.p[1]);
       printf("------\n");
       printf("s2 avant f2 : %d %d %d\n", s2.a, s2.p[0], s2.p[1]);
       f2(s2);
       printf("s2 apres f2 : %d %d %d\n", s2.a, s2.p[0], s2.p[1]);
       printf("tab2 apres f2 : %d %d\n", tab2[0], tab2[1]);
       printf("------\n");
       printf("s2 avant f3 : %d %d %d\n", s2.a, s2.p[0], s2.p[1]);
       f3(s2);
       printf("s2 apres f3 : %d %d %d\n", s2.a, s2.p[0], s2.p[1]);
       printf("tab2 apres f3 : %d %d\n", tab2[0], tab2[1]);
       return EXIT_SUCCESS; // return 0;
    }
    --> ./struct_tab_ptr.c
    s1 avant f1 : 3 1 2
    s1 apres f1 : 3 1 2
    ------
    s2 avant f2 : 3 1 2
    s2 apres f2 : 3 2 3
    tab2 apres f2 : 7 8
    ------
    s2 avant f3 : 3 2 3
    s2 apres f3 : 3 2 3
    tab2 apres f3 : 8 9
    -->
La figure suivante résume ce qui se passe à l'exécution de ce programme.

   

Pointeurs et tableaux à une dimension

▶ Ce que nous venons de voir avec les chaînes de caractères est un cas très particulier de ce qui passe de manière générale avec les tableaux à une dimension.
Ainsi, s'il est possible de déclarer par exemple des tableaux d'entiers d'une manière assez semblable à ce que l'on fait avec les tableaux de caractères

    char t_char[ ] = "ABC";
    int t1_int[ ] = {65, 66, 67, 0};

il n'est pas possible de définir un pointeur vers un tableau d'entiers d'une manière analogue à ce qu'on peut faire vers un tableau de caractères :

    char *p_char = "ABC"; // correct
    int *p1_int = {65, 66, 67, 0}; // incorrect ou dénué de sens
▶ Le passage par un pointeur pour définir un tableau permet d'adapter sa taille à l'espace effectivement nécessaire en réalisant dynamiquement l'allocation pendant l'exécution du programme comme dans la séquence suivante :
    int nb_int, *p_int;
      ...
    nb_int = fonc(...); // nombre d'entiers résultant d'un calcul
    p_int = malloc(nb_int * sizeof(int));
    /* p_int sera utilisable comme
       tableau : p_int[i]
       pointeur : *(p_int + i) */
Rappelons que l'espace alloué avec malloc ne fait l'objet d'aucune initialisation particulière.
Dans le cas où une initialisation à zéro de l'espace est souhaitée, il faut soit l'effectuer par une boucle explicite, soit réaliser l'allocation de la mémoire non pas avec malloc mais avec calloc comme dans  :
    p_int = calloc(nb_int, sizeof(int));

▶ Autre illustration avec le parcours de la liste des paramètres transmis à la fonction main au lancement d'un programme. Les arguments de la commande sont transmis sous la forme d'un tableau de chaînes de caractères (donc un tableau de pointeurs sur char).
Le tableau est parcouru de deux manières : tout d'abord classiquement par indexation puis, en « jouant avec un pointeur ».

Pointeurs et tableaux à plusieurs dimensions

▶ En langage C, comme en Java, quand on parle d'un tableau à plusieurs dimensions, on parle de tableaux de tableaux.

Ainsi, un tableau à deux dimensions dont les éléments sont de type T est un tableau de tableaux d'éléments de type T.

Après une définition d'un tableau tab telle que

    T tab[4][10];
  • sizeof(tab) est égal à « 4 * 10 * sizeof(T) »
  • le tableau est un tableau de 4 tableaux de 10 entiers (4 lignes de 10 colonnes);
  • tab est une constante de type « pointeur sur tableau de 10 éléments de type T »
  • c'est-à-dire
    • de type incomplet « T (*)[10] » utilisable dans le prototypage d'une fonction ayant un paramètre de ce type;
    • de type ptr_tab10T défini par « typedef int (*ptr_tab10T)[10]; »
    • Sa valeur permet d'accéder au début de la première ligne du tableau.
  • de manière générale, pour toute valeur i (normalement comprise entre 0 et 3), tab[i] est un pointeur constant sur un tableau de 10 éléments de type T.
    C'est l'adresse du premier élément de la ligne i du tableau. Une expression qui lui est équivalente est « tab[0] + i »
▶ L'exemple suivant illustre ces différentes propriétés :
    --> cat tableau2dim.c
    #include <stdio.h>
    #include <stdlib.h>

    /* ptr_tab10T : pointeur sur tableau de 10 éléments de type T */
    typedef int (*ptr_tab10int)[10];
    ptr_tab10int t1;
    void init(int (*)[10]); // prototype de init avec type incomplet pour le paramètre
    int t2[4][10];

    void init(int (*t)[10]) {
       int i;
       for(i = 0; i < 10; i++)
         (*t)[i] = i + 1;
    }

    int main ( ) {
       int i;
       t1 = malloc(10 * sizeof(int));
       printf("taille de t1 : %ld\n", sizeof(t1));
       printf("taille de *t1 : %ld\n", sizeof(*t1));
       init(t1);
       for(i = 0; i < 10; i++)
         printf("%d ", (*t1)[i]);
       putchar('\n');
       printf("taille de t2 : %ld\n", sizeof(t2));
       printf("taille de t2[i] : %ld [0x%lx]\n",
               sizeof(t2[0]), sizeof(t2[0]));
       printf("valeur de t2 : %p\n", t2);
       printf("valeur de t2 + 1 : %p\n", t2 + 1);
       t1 = t2 +1;
       if(t2 == &t2[0])
         printf("oui\n");
       else
         printf("non\n");
       if(t2 + 2 == &t2[2])
         printf("oui\n");
       return EXIT_SUCCESS; // return 0;
    }
    --> ./tableau2dim
    1 2 3 4 5 6 7 8 9 10
    taille de t1 : 4
    <-- t1 est un pointeur
    taille de *t1 : 40
    <-- l'objet pointé par t est de taille 40 (10 int)
    taille de t2 : 160
    <-- t2 est un tableau de 40 int
    taille de t2[i] : 40 [0x28]
    <-- une ligne de t2 est un tableau de 10 int
    valeur de t2 : 0x2060
    valeur de t2 + 1 : 0x2088
    <-- +1 signifie augmenter de 40 (0x28)
    oui
    oui
    -->
ce qui correspond au schéma suivant :

▶ Dans ce second exemple, on souhaite construire une matrice rectangulaire d'entiers de lig lignes et col colonnes, ces deux nombres n'étant pas connus àla compilation.
La définition sous forme de tableau classique n'étant de ce fait pas possible, on utilise un pointeur de pointeur sur int pour construire le tableau.
Une fois le tableau construit, il est possible d'y accéder par double indexation (ou au travers de pointeurs).

    --> cat matrice_pointeur.c
    #include <stdio.h>
    #include <stdlib.h>
    int main() {
       int lig, col, i, j;
       int **mat;
       printf("nombre de lignes ? ");
       scanf("%d", &lig);
       printf("nombre de colonnes ? ");
       scanf("%d", &col);
       mat = (int**) malloc(lig * sizeof(int*));
       for (i = 0; i < lig; i++){
         mat[i] = (int*)malloc(col * sizeof(int));
         for(j = 0; j < col; j++)
           mat[i][j] = lig * i + j;
       }
       for (i = 0; i < lig; i++){
         for(j = 0; j < col; j++)
           printf("%4d ", mat[i][j]);
         putchar('\n');
       }
       return EXIT_SUCCESS; // return 0;
    }
    --> ./matrice_pointeur
    nombre de lignes ? 3
    nombre de colonnes ? 4
       0    1    2    3
       3    4    5    6
       6    7    8    9
    -->
Remarque : si on souhaite définir une fonction de nom creer_tableau permettant de construire un tableau de lig lignes et col colonnes et récuperer l'adresse du tableau construit dans un pointeur transmis en paramètre, il faut avoir transmis l'adresse de ce paramètre, ce qui conduit à la définition suivante de la fonction  :
    #include <stdlib.h>
    void creer_matrice (int lig, int col, int ***mat) {
       int i, j;
       *mat = (int**) malloc(lig * sizeof(int*));
       for (i = 0; i < lig; i++){
         (*mat)[i] = (int*)malloc(col * sizeof(int));
         for(j = 0; j < col; j++)
           (*mat)[i][j] = lig * i + j;
       }
    }
L'appel de la fonction prend alors la forme suivante :
      int lig, col, **mat;
         ...
      creer_matrice(lig, col, &mat);
Une solution alternative est de ne pas renvoyer l'adresse du tableau construit via un paramètre mais comme valeur de retour de la fonction :
    #include <stdlib.h>
    int **creer_matrice (int lig, int col) {
       int i, j;
       int **mat = (int**) malloc(lig * sizeof(int*));
       for (i = 0; i < lig; i++){
         mat[i] = (int*)malloc(col * sizeof(int));
         for(j = 0; j < col; j++)
           mat[i][j] = lig * i + j; // une initialisation comme une autre
       }
       return mat;
    }
L'appel de la fonction prend alors la forme suivante :
      int lig, col, **mat;
         ...
      mat = creer_matrice(lig, col);
▶ Enfin, utiliser des pointeurs de pointeurs plutôt que des tableaux bidimensionnels permet de définir des tableaux non rectangulaires.

C'est ce que fait la fonction creer_tableau suivante :

    int ** creer_tableau(int lig[], int taille){
       int i, j, **mat;
       mat = (int**) malloc(taille/sizeof(int) * sizeof(int*));
       for(i = 0; i < taille/sizeof(int); i++) {
         mat[i] = (int*)malloc(lig[i] * sizeof(int));
         for(j = 0; j < lig[i]; j++)
           mat[i][j] = taille * i + j; // une initialisation comme une autre
       }
       return mat;
    }
Un exemple d'application l'utilisant :
    --> cat tableau_pointeur.c
    #include <stdio.h>
    #include <stdlib.h>
    int main() {
       int i, j;
       int lig[3] = {5, 8, 3};
       int **mat;
       mat = creer_tableau(lig, sizeof(lig));
       for (i = 0; i < sizeof(lig)/sizeof(int); i++){
         for(j = 0; j < lig[i]; j++)
           printf("%4d ", mat[i][j]);
         putchar('\n');
       }
       // ..... suite du programme
    }
    --> ./tableau_pointeur
       0 1 2 3 4
      12 13 14 15 16 17 18 19
      24 25 26