Version en ligne

Tutoriel : Tableaux, pointeurs et allocation dynamique

Table des matières

Tableaux, pointeurs et allocation dynamique
Les pointeurs (rappel)
Les tableaux unidimensionnels
Les tableaux de tableaux ("tableaux à plusieurs dimensions")
Exercices

Tableaux, pointeurs et allocation dynamique

Les pointeurs (rappel)

Bien que les tableaux et les pointeurs soient souvent confondus, il s'agit de deux choses différentes.

Nous allons aborder ce sujet dans le but d'utiliser correctement chacun de ces types (pointeur et tableau) et de comprendre la différence entre ces deux notions.
Nous allons également voir certaines notions approfondies dans les manipulations des tableaux (les initialisations, les déclarations, les passages en paramètre pour les fonctions).

Sans plus tarder attaquons le vif du sujet en vous souhaitant une bonne lecture.

Les pointeurs (rappel)

Les tableaux unidimensionnels

Définition

A la déclaration d'une variable, un emplacement lui est accordé dans la mémoire. Cet emplacement possède une adresse. Cette adresse peut être stockée dans une variable de type pointeur.

Pour résumer un pointeur est une variable qui contient l'adresse mémoire d'une autre variable.

Déclaration

L'opérateur de déclaration de pointeur est l'astérisque '*', et il est caractérisé par le type de variable sur laquelle il va pointer.
Ainsi pour déclarer un pointeur on doit respecter la syntaxe suivante : type *nom_du_pointeur;

Si par exemple on souhaite déclarer un pointeur sur une variable de type int, on ferait comme ceci :

int * ptrint;

Utilisations

La règle à retenir est la suivante :

Initialisation

Pourquoi il faut toujours initialiser les pointeurs ?

A la déclaration d'une variable quelconque, sa valeur ne peut pas être déterminée. Elle peut valoir "n'importe quoi". Les pointeurs étant des variables aussi, alors à la déclaration ils valent n'importe quoi :) . Cette valeur se réfère donc à un emplacement mémoire dont on ignore sa signification. Et qui ne nous est pas alloué, donc inutilisable.

Et comment initialiser un pointeur ?

L'initialisation peut avoir trois formes :

Donc si vous déclarez un pointeur, ayez à l'esprit qu'il faudra l'initialiser tôt ou tard :) avec l'une des trois méthodes présentées ci-dessus, en fonction de ce que vous souhaitez en faire.
Autrement il y a de fortes chances pour que votre programme plante avec l'erreur "SEGFAULT".

Accéder à l'adresse pointée

L'astérisque '*', vous l'avez reconnue :) . C'est ce même opérateur qui est utilisé pour déclarer une variable de type pointeur et pour accéder à l'emplacement indiqué par notre pointeur.
Dans l'apprentissage de cette partie, les débutants confondent toujours quand est-ce qu'il faut utiliser l'étoile et quand est-ce qu'il ne faut pas l'utiliser.
Gardez à l'esprit que l'étoile, à la déclaration d'une variable de type pointeur, ne signifie pas qu'on accède à l'emplacement qu'il pointe, mais sert juste à dire au compilateur qu'il s'agit d'une déclaration de pointeur.
Mis à part ce cas là, toutes les autres utilisations de l'opérateur '*' suivi du nom d'un pointeur, signifient que c'est de l'emplacement pointé qu'il s'agit.

Exemple :

int * ptrint;        //Déclaration du pointeur
int variable;        //Déclaration d'une variable

ptrint = &variable;  //Initialisation de notre pointeur

*ptrint = 10;        //On inscrit 10 à l'espace pointé par notre pointeur (en l'occurrence la variable).

Argument de fonctions : passage par valeur

Lors de l'appel d'une fonction, on lui donne des valeurs sous forme de paramètres. Il existe donc le type de passages qu'on appelle par valeur, qui consiste à passer une copie de notre valeur à la fonction. Ainsi la fonction ne manipulera que cette copie de notre valeur, et tous les changements qui y seront apportés, ne seront pas pris en compte ailleurs.

Exemple :

void ma_fonction(int n)  //Fonction appelée
{
      n = 10;
}

int main (void)                         //Fonction appelante
{
      int variable = 123;

      ma_fonction(variable);
      printf("La valeur de variable est %d\n",variable);

      return 0;
}

Le résultat de ce code sera donc :

La valeur de variable est 123

Pourtant on a bien modifié la valeur envoyée à la fonction 'ma_fonction'. C'est ce qu'on appelle un passage par valeur :) .

Argument de fonctions : passage par adresse

Ce type de passage donnera un résultat différent de celui présenté précédemment. Il consiste à envoyer l'adresse mémoire de notre variable et non pas sa valeur. Ainsi on a accès à l'emplacement même de cette variable en mémoire. Donc les changements qu'on apportera à cet espace mémoire, seront des changements qu'on aura apportés à notre variable directement. Si je reprends le même exemple, ceci donnerait :

void ma_fonction(int * ptrn)
{
      *ptrn = 10;    //On inscrit la valeur 10 à l'emplacement mémoire indiqué par ptrn
}

int main (void)
{
      int variable = 123;

      ma_fonction( &variable );                            //On appelle la fonction
      printf("La valeur de variable est %d\n",variable);

      return 0;
}

Et le résultat est :

La valeur de variable est 10

C'est ce qu'on appel un passage par adresse ;) .

Argument de fonctions : passage par référence

Le passage par référence tel que nous pouvons le voir dans d'autres langages (C++ par exemple) ne peut pas être réalisé en C; mais il peut être implémenté par utilisation du "passage par adresse".
Ce passage par adresse est un passage par valeur quelque part, car on transmet une copie de l'adresse de la variable à la fonction. Donc ne soyez pas choqué si on vous dit quelque part qu'il n'y a pas de passage par référence en C.


Les tableaux unidimensionnels

Les tableaux unidimensionnels

Les pointeurs (rappel) Les tableaux de tableaux ("tableaux à plusieurs dimensions")

Définition

"Un tableau est une suite contigüe de données de même type dans la mémoire."

Beaucoup de gros mots dans cette phrase n'est-ce pas :) ?

Si nous les regardons de plus près :

Une suite contigüe :

Une suite contigüe signifie un ensemble d'éléments disposés les uns à la suite des autres sans être intercalés.

-Donnée1-

-Donnée2-

-Donnée3-

........

-DonnéeN-

Données de même type :

Ceci signifie que toutes les données qu'on va retrouver dans notre tableau vont être du même type :) .

Si nous prenons l'exemple ci-dessus, donnée1, donnée2 jusqu'à la donnéeN vont être exactement du même type.

Déclaration (syntaxe)

La déclaration d'un type tableau en C s'obtient par utilisation des crochets '[' ']', et en précisant le type de données qu'il y aura dedans ainsi que le nombre.

Le nombre de ces éléments sera appelé la taille de notre tableau, et leur type le type de notre tableau.

La déclaration sera donc sous la forme : typenom_du_tableau [ taille ];

Voici un exemple de déclaration d'un tableau de 10 entiers :

int tableau[10];

Utilisation

Initialisation

Il existe certaines expressions réservées à l'initialisation lors de la déclaration d'un tableau.

Ces expressions sont :

Parcourir un tableau

Le parcours d'un tableau s'effectue par l'utilisation des mêmes crochets '[]' utilisés à la déclaration de notre tableau, et en indiquant le rang de la case à laquelle on souhaite accéder. Par exemple pour écrire le nombre 12 dans la première case on ferait :

tableau[0] = 12;

Le '0' ici est appelé indice de la première case.

Maintenant qu'on sait que les indices d'un tableau commencent à 0 et vont jusqu'à taille - 1 (taille étant le nombre de cases de notre tableau), on va voir maintenant comment parcourir ce tableau à l'aide d'une boucle.

Pour ceci, il nous faudrait une variable qui jouera le rôle de l'indice, qu'on déclarera comme ceci :

int i;

Ensuite, par utilisation d'une boucle, on va parcourir les cases du tableau pour y mettre des zéros :

int i;

for( i=0 ; i < taille ; i++)  //Attention la condition est : i < taille !
{
     tableau[i] = 0;
}

'typedef' et le type tableau

Comme vous le savez peut être, le mot-clef typedef permet de définir (ou redéfinir) des types. C'est-à-dire, que l'on peut changer l'identificateur d'un objet par utilisation de typedef.

Je m'explique :) :

typedef int my_int;

Me permettrait d'utiliser my_int comme étant un type (en l'occurrence int ). C'est très utile pour alléger le code. L'exemple d'application classique sont les structures.

La syntaxe afin de définir un type tableau à l'aide du mot-clef typedef est :

typedef type_du_tableaunom_du_tableau [ taille_du_tableau ];

Exemple : Si l'on veut définir un type de tableau de 4 entiers, on ferait ainsi :

typedef int tab4 [4];

int main(void){
     tab4 tableauDe4 = {1,2,3,4};

     return 0;
}

Il est possible que l'on ait besoin de déclarer un type tableau sans taille, on ferait donc ainsi :

typedef int tab [];

int main(void){
     tab tableauDe4 = {1,2,3,4};

     return 0;
}

Qui est strictement équivalent à faire comme nous l'avons vu dans la partie "initialisation" à savoir :

int tableauDe4[] = {1,2,3,4};

Passer un tableau en argument à une fonction

Il existe quatre façons peu différentes de le faire, et ceci en respect à ce qu'on a vu dans les parties déclaration et initialisation :

Retourner un tableau

La syntaxe est la suivante :

int (ma_fonction(void))[4];

Mais si vous essayez de le faire le compilateur vous rejettera :) . Ce qui est normal, car le langage C ne le permet pas, pour la raison suivante :

On est donc obligé d'allouer dynamiquement notre tableau et le retourner sous la forme d'un pointeur.

Ou d'utiliser une structure :) , dans laquelle on mettra un tableau, et le fait de retourner cette structure, ne retournera qu'une copie de cette dernière. Donc cette méthode est correcte, et bien qu'elle soit pratique, mais relève du "bourrin" dans le codage (c'est pourquoi je ne vous encouragerai pas à l'utiliser :p ).

Exemple :

typedef struct {
     int tableau[5];
}STableauDe5;

STableauDe5 ma_fonction(void){
     STableauDe5 tab = {{0}};
     
     return tab;
}

Ceci est valable à la seule condition de réceptionner ce retour dans une variable de type STableaDe5 :

int main (void){
     STableauDe5 tab;

     tab = ma_fonction();
     return 0;
}

Quant à la méthode d'allocation dynamique nous la verrons plus tard.

Allocation dynamique d'un tableau à une dimension

Une allocation dynamique consiste à demander au système d'exploitation de nous allouer un espace d'une taille donnée dans la mémoire (dans le tas).

Par abus de langage on utilise les termes "allocation dynamique de tableau". Il est donc important de dissocier une allocation d'une taille en mémoire et le type tableau qu'on a vu précédemment.

La fonction malloc

La fonction malloc est la plus populaire des fonctions d'allocation, j'en rappelle le prototype :

void *malloc (size_t size);

Donc si l'on souhaite allouer un espace de mémoire pour notre dit "tableau", on utiliserait malloc comme ceci :

pointeur = malloc(nombreElements * sizeof(*pointeur));

Ainsi on alloue une taille de : nombreElement x la taille d'un élément (représentée par sizeof(*pointeur) ).

La fonction calloc

Le prototype de la fonction calloc est :

void *calloc (size_t nmemb, size_t size);

La différence avec la fonction malloc, est que calloc en plus de l'allocation, elle initialise l'espace alloué avec des 0 (elle met tous les bits à 0).
Il faut noter qu'elle est déconseillée pour allouer des espaces de type float ou double .

Pour allouer dynamiquement un tableau, on procèderait ainsi :

pointeur = calloc(nombreElements , sizeof(*pointeur));

Ainsi la fonction calloc nous alloue un nombre d'éléments égal à 'nombreElements' du type pointé par 'pointeur'. Cet espace sera initialisé automatiquement par des zéros (0).

La fonction realloc

Elle permet de modifier la taille allouée pour un objet, son prototype est :

void *realloc (void *ptr, size_t size);

Si l'on souhaite modifier la taille qu'on a allouée préalablement pour un objet, la fonction realloc s'utilise comme suit :

pointeur = realloc(pointeur, nouvelleTaille);

Retourner un espace alloué dynamiquement

Si l'on souhaite effectuer notre allocation dynamique dans une fonction, pour la retourner à la fonction appelante. Il faudrait donc utiliser l'une des fonctions présentées ci-dessus (à savoir malloc, calloc ou realloc). Et de retourner le pointeur sur l'espace alloué.

Exemple :

int * fonctionAllocation(int nombreElements){
      int * ptr = malloc( nombreElements * sizeof(*ptr) );

      return ptr;
}

int main(void){
      int * ptr = fonctionAllocation(10);
      if(ptr == NULL)
           //........
      //.......
      return 0;
}

Il se peut que l'on souhaite allouer de la mémoire, sans utiliser le retour d'une fonction mais à l'aide d'un passage par référence (vous vous en rappelez ? :) ).

Exemple :

void fonctionAllocation(int **ptr , int nombreElements){
      *ptr = malloc( nombreElements * sizeof(**ptr) );
}

int main(void){
      int * ptr;
      fonctionAllocation(&ptr , 10);
      if(ptr == NULL)
           //........
      //.......
      return 0;
}

Vous remarquerez que j'ai utilisé un double pointeur (int **), et oui c'est le piège :p . Avec l'utilisation d'un simple pointeur (int *), on serait entrain de faire un passage par valeur, et donc notre pointeur dans la fonction ne sera qu'une variable locale. Ainsi tous les changements qu'on apportera dessus seront de portée locale également (ne seront pas pris en compte dans la fonction main). D'où le double pointeur.


Les pointeurs (rappel) Les tableaux de tableaux ("tableaux à plusieurs dimensions")

Les tableaux de tableaux ("tableaux à plusieurs dimensions")

Les tableaux unidimensionnels Exercices

Déclaration

Un tableau de tableaux (appelé tableau à plusieurs dimensions) se déclare par précision de la taille de chaque "dimension". Si par exemple je souhaite déclarer un tableau "tridimensionnel" alors je ferais ainsi :

int t[taille1][taille2][taille3];

'taille1', 'taille2' et 'taille3' sont les tailles de chaque dimension. Autrement dit, 't' est un tableau de taille1 tableaux de taille2 tableaux de taille3ints.

Utilisation

Initialisation

L'initialisation de ce type de tableaux n'est pas très différente de celle d'un tableau à une dimension :

Exemple 1 :

int tableau1[2][3] = {{1,8,9},{0,6,4}};

Exemple 2 :

int tableau1[2][3][2] = {{ {1,8}   , {0,6} , {0,0} },
                         { {31,52} , {4,8} , {11,5}}
                        };

Ou en ne précisant pas la première dimension :

Exemple :

int tableau1[][3] = {{1,8,9},{0,6,4},{5,3,7},{2,2,2}};

Ainsi on a créé un tableau de 4x3 (équivalent à int tableau[4][3]), et initialisé ainsi :

1

8

9

0

6

4

5

3

7

2

2

2

Ou en n'initialisant que quelques cases du tableau :

Exemple :

int tableau1[][3] = {{1,9},{0,4},{5,3,7},{2,2,2}};

Les cases restantes de chaque ligne seront donc initialisées à 0.

Ou pour initialiser toutes les cases à 0 :

Exemple :

int tableau1[][3] = {{0},{0},{0},{0}};

Comme expliqué dans la partie "tableaux unidimensionnels", on pourrait également initialiser certaines cases de notre tableau; ceci s'applique bien évidemment à des tableaux multidimensionnels. Voici quelques exemples, je vous propose de les faire sous forme d'exercice pour voire si vous avez bien compris.

Exemple 1 :

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

1

0

0

5

6

0

0

0

0

0

0

0

0

1

0

2

4

0

0

10

La première ligne a été initialisée suivant la règle que nous avons vu pour un tableau à une dimension (si vous ne vous en rappelez pas vous pouvez relire cette partie.
La deuxième ligne n'a pas été initialisée manuellement car nous avons sauté cette ligne pour aller directement à celle d'indice 2 ([2]={...}).
Naturellement l'initialisateur suivant est utilisé pour initialiser la ligne suivante donc celle d'indice 3.

Exemple 2 :

int t[4][5] = {[3]={1,[3]=5,6},
               [0]={[3]=1},
                   {2,4,[4]=10}};

0

0

0

1

0

2

4

0

0

10

0

0

0

0

0

1

0

0

5

6

Exemple 3 :

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

Il se peut que vous ayez un warning vous indiquant que vous avez dépassé la taille pour l'initialisateur {0,7,5,3,8,4,7}.

0

0

0

0

0

1

0

0

5

6

7

1

5

0

0

2

4

0

0

10

0

7

5

3

8

0

0

0

0

0

Exemple 4 :

int t[][5] = {[5]={0}};

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

Exemple 5 :

int t[][5] = {[1]=1,[3]=5,6,2,4,[4]=10,0,7,5,3,8,4,7,[18]=7,9,[23]=13};

0

0

0

0

0

1

0

0

0

0

0

0

0

0

0

5

6

2

4

0

10

0

7

5

3

8

4

7

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

7

9

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

13

0

0

0

0

Parcours

Le parcours d'un tableau à plusieurs dimensions s'effectue en précisant l'indice pour chaque dimension.

Pour un exemple de tableau de trois dimensions, et à l'aide de trois boucles imbriquées, on arriverait à accéder à toutes les cases de notre tableau.

Exemple :

int tableau[taille1][taille2][taille3];
int i,j,k;

for(i=0 ; i < taille1 ; i++){               //Première dimension
      for(j=0 ; j < taille2 ; j++){         //Deuxième dimension
             for(k=0 ; k < taille3 ; k++){  //troisième dimension
                   tableau[i][j][k] = 0;
             }
      }
}

Les pointeurs sur tableaux

Vous pensez peut être au double pointeur (int **) :) , non ce n'est pas de cela qu'il s'agit.

La déclaration d'un pointeur sur tableau s'effectue en utilisant des parenthèses, l'astérisque '*' et en définissant la taille du tableau sur lequel on souhaite pointer. Notez que les parenthèses sont très importantes car en leur absence, ce sera un tableau de pointeurs qu'on aura déclaré, ce qui n'est pas la même chose.

Exemple :

int (*ptrtableau)[4];

Dans cet exemple, il s'agit d'une déclaration d'un pointeur sur tableaux de 4 entiers.

Utilité des pointeurs sur tableaux

C'est un moyen très pratique pour déclarer un tableau à deux dimensions :) . Sachant qu'une dimension est déjà pré allouée, il ne reste plus qu'à allouer la deuxième dimension.

Exemple :

int (*tableau)[4];

tableau = malloc(5 * sizeof(*tableau));

Ainsi on aura déclaré un tableau de 5 tableaux de 4 entiers chacun (équivalent à int tableau[5][4];).

Libération de mémoire allouée

Pour libérer la mémoire, on doit utiliser la fonction free toujours, et lui donner en paramètre le pointeur sur l'espace alloué à l'aide de malloc :

int (*tableau)[4];

//----Allocation-----
tableau = malloc(5 * sizeof(*tableau));
if(tableau == NULL){
    //Notifier l'erreur
    exit(EXIT_FAILURE);
}

//----Libération en cas d'allocation réussie-----
free(tableau);

La deuxième dimension sera libérée automatiquement donc pas besoin de free pour le faire.
Ainsi on remarque que ce type est beaucoup plus rapide d'utilisation (en terme d'allocation/libération de mémoire) du fait qu'il ne nécessite pas de boucles.

Les tableaux de pointeurs

Un tableau de pointeurs, est un outil pour ranger un ensemble de pointeurs sur différentes variables.
Il peut également servir pour déclarer un tableau à deux dimensions, en allouant plusieurs espaces et stockant leurs pointeurs dans notre tableau.

Exemple de déclaration :

int * tableauDePtr[5];

Exemple d'utilisation pour déclarer un tableau à deux dimensions :

int * tableauDePtr[5];
int i;

for(i=0 ; i < 5 ; i++){
       tableauDePtr[i] = malloc(4 * sizeof(tableau[0]));
}

Ainsi on aura créé un tableau de 5 tableaux de 4 entiers chacun (équivalent à int tableau[5][4];).

Libération de mémoire allouée

La libération de mémoire allouée doit se faire en parcourant le tableau, et en allouant pointeur par pointeur dans ce tableau :

int i;
int * tableauDePtr[5];

//---Allocation-----
for(i=0 ; i < 5 ; i++){
       tableauDePtr[i] = malloc(4 * sizeof(tableau[0]));
       if(tableauDePtr[i] == NULL){    //En cas d'erreur d'allocation
            //N'oubliez pas de notifier l'erreur
            for(i=i-1 ; i >= 0 ; i--)  //Libération de l'espace déjà alloué
                 free(tableauDePtr[i]);

            exit(EXIT_FAILURE);
       }
}

//---Libération en cas d'allocation réussie-----
for(i=0 ; i < 5 ; i++){
       free(tableauDePtr[i]);
}

Allocation dynamique

Cette partie je la qualifie étant la plus dure à suivre, donc mettez vos ceintures :) .

L'allocation dynamique d'un dit "tableau" à plusieurs dimensions, s'effectue en allouant les dimensions une par une. Si je prends l'exemple d'un tableau tridimensionnel, il faudrait un triple pointeur pour y arriver. Donc veillez à avoir le même nombre d'astérisques '*' dans la déclaration de pointeur, que le nombre de dimensions de votre tableau.

Si je prends un exemple de tableau à 3 dimensions que je souhaite allouer dynamiquement, j'utiliserais un pointeur déclaré ainsi :

int ***ptr;     //3 étoiles pour 3 dimensions

Et un tableau à deux dimensions nécessiterait un double pointeur :

int **ptr;     //2 étoiles pour 2 dimensions

Tableaux bidimensionnels

Ce type de tableaux, est un simple tableau de pointeurs, chacun de ces pointeurs va pointer sur un espace représentant un tableau à une dimension, une petite image est la bienvenue je pense :) :

Image utilisateur

L'allocation doit se faire en respectant la démarche suivante :

Ainsi on obtient le code suivant :

int i , taille1 = 2 , taille2 = 3;
int **ptr;

ptr = malloc(taille1 * sizeof(*ptr));       //On alloue 'taille1' pointeurs.
if(ptr == NULL)
      //Ne pas oublier de notifier l'erreur et de quitter le programme.

for(i=0 ; i < taille1 ; i++){
     ptr[i] = malloc(taille2 * sizeof(**ptr) );       //On alloue des tableaux de 'taille2' variables.
     if(ptr[i] == NULL){                              //En cas d'erreur d'allocation
         //Il faut libérer la mémoire déjà allouée
         //Ne pas oublier de notifier l'erreur et de quitter le programme.
     }
}

//notre tableau ptr[2][3] est maintenant utilisable...

Quant à la libération de mémoire elle se fait suivant l'ordre inverse à l'allocation :

On obtient donc le code suivant :

int i;

//Après allocation....

//--------La libération---------
for(i=0 ; i < taille1 ; i++){
     free(ptr[i]);
}

free(ptr);
ptr = NULL;   //Ceci est par mesure de sécurité (ce n'est donc pas obligatoire).
//--------------------------------

Cette libération n'est utilisable que si l'allocation s'est bien déroulé (sans erreurs), Que faire donc en cas d'erreur d'allocation o_O ?

Et bien il s'agit des if( ptr[i] == NULL) que vous avez peut être remarquées dans le code d'allocation :) . On va maintenant essayer de les remplir avec ce qu'il faut.

Tableaux tridimensionnels

Passant maintenant à un tableau à 3 dimensions :) . Il s'agit d'un tableau de pointeurs sur pointeurs, chacun d'eux va pointer sur un pointeur qui lui pointe sur un tableau :) bref, trop de pointeurs et de blabla, une image serait donc plus explicite :

Image utilisateur

L'allocation dynamique de ce tableau, sera divisée en 3 étapes qui doivent correspondre à l'ordre suivant :

Nous nous retrouvons donc avec le code suivant :

int ***ptr;
int i,j;

ptr = malloc(taille1 * sizeof(*ptr));
if(ptr == NULL)          //Pas de libération de mémoire à ce niveau
     return -1;          //Exemple de code d'erreur

for(i=0 ; i < taille1 ; i++){
     ptr[i] = malloc(taille2 * sizeof(**ptr));
     if( ptr[i] == NULL) //Pensez à libérer la mémoire déjà allouée et fermer le programme.
          return -1;     //Exemple de code d'erreur
}
for(i=0 ; i < taille1 ; i++){
     for(j=0 ; j < taille2 ; j++){
           ptr[i][j] = malloc(taille3 * sizeof(***ptr));
           if(ptr[i][j] == NULL)   //Pensez à libérer correctement la mémoire déjà allouée
                return -1;           //Exemple de code d'erreur
     }
}

Pour un tableau de dimensions supérieurs, il faut procéder de la même manière, en allouant les dimensions une à une, et en respectant l'ordre des allocations.

Pour la libération de mémoire déjà allouée, il faut que cela soit fait dans l'ordre inverse à celui de l'allocation. Pour le même exemple présenté ci-dessus on procèderait ainsi :

Le code de libération est donc comme ceci :

for(i=0 ; i < tailleDejaAllouee1 ; i++){
     for(j=0 ; j < tailleDejaAllouee2 ; j++){
           free(ptr[i][j]);
     }
     free(ptr[i]);
}
free(ptr);

Voici un code montrant la méthode de libération de mémoire si l'allocation échoue avant d'allouer le tableau entièrement :

#include <stdlib.h>

void * my_free(int***,int,int);

void * my_free(int ***ptr,int tailleDejaAllouee1,int tailleDejaAllouee2){
    int i,j;

    for(i=0 ; i < tailleDejaAllouee1 ; i++){
         for(j=0 ; j < tailleDejaAllouee2 ; j++){
               free(ptr[i][j]);
         }
         free(ptr[i]);
    }
    free(ptr);

    return NULL;
}

int main(void)
{
    /*---------------------------------------------------*/
    int taille1 = 2, taille2 = 2, taille3 = 2;
    int ***ptr;
    int i,j;

    /*-----------------Allocation------------------------*/
    ptr = malloc(taille1 * sizeof(*ptr));
    if(ptr == NULL)
         return -1;

    for(i=0 ; i < taille1 ; i++){
         ptr[i] = malloc(taille2 * sizeof(**ptr));

         if( ptr[i] == NULL){         //Erreur d'allocation

              for(--i ; i>=0 ; i--)   //On libère l'espace déjà alloué
                  free(ptr[i]);

              free(ptr);

              return -1;              //Fin du programme
         }
    }

    for(i=0 ; i < taille1 ; i++){
         for(j=0 ; j < taille2 ; j++){
               ptr[i][j] = malloc(taille3 * sizeof(***ptr));

               if(ptr[i][j] == NULL){         //Erreur d'allocation

                   for(--j ; j >= 0 ; j--)    //On libère l'espace déjà alloué
                        free(ptr[i][j]);

                   free(ptr[i]);

                   my_free(ptr , i , taille2);

                   for(--i ; i >= 0 ; i--)
                        free(ptr[i]);

                   return -2;                 //Fin du programme
               }
         }
    }

    /*---------------Libération--------------------------*/
    ptr = my_free(ptr,taille1,taille2);       //On libère la mémoire et on met la valeur de ptr à NULL
    /*---------------------------------------------------*/

    return 0;
}

Fonctions : mettre un tableau en argument

Si vous pensez à utiliser le double pointeur (int **), non il ne s'agit pas de cela :) .

Si le tableau est alloué automatiquement, il ne faut pas l'envoyer à une fonction attendant un pointeur sur pointeur (cf : code ci-dessus).
Les méthodes correctes correspondent globalement aux différentes déclarations d'un tableau telles que nous l'avons vu plus haut :) .

Méthode classique

Lors de la déclaration des arguments d'une fonction, le type tableau à plusieurs dimensions est désigné par des crochets [], avec ou sans taille.

void ma_fonction(int tableau[taille1][taille2][taille3]){
      //Corps de la fonction...
}

Exemple de déclaration correcte :

void ma_fonction(int tableau[][taille2][taille3]){
      //Corps de la fonction...
}

Et l'appel sera par simple envoi du nom du tableau, c'est valable pour les deux cas, à savoir avec ou sans précision de la taille de la première dimension :

int tableau[taille1][taille2][taille3];  //La déclaration du tableau

ma_fonction(tableau);   //L'appel à la fonction avec passage d'un tableau en argument.

Utiliser un pointeur sur tableau

Exemple :

int fonction(int (*matrice)[3]){
        //......
        matrice[0][2] = 15;
}

Dans cet exemple matrice est un pointeur sur tableau de 3 int.
Il permettra de réceptionner la matrice envoyée sous forme de pointeur sur le premier élément (en l'occurrence, pointeur sur tableau de trois entiers) lors de la l'appel à cette fonction.
Exemple :

int fonction(int (*matrice)[3]){
        //......
        matrice[0][2] = 15;
}
int main(void){
        int matrice[4][3];  //Notez que la taille 3 correspond à celle spécifiée dans la déclaration de la fonction!

        fonction(matrice);
        return 0;
}

Jusqu'ici, vous avez peut être remarqué que si l'on souhaite parcourir la matrice passée en argument, on serait obligé d'avoir une information sur la taille de la matrice. Vous pensez peut être à utiliser l'opérateur sizeof ? Alors je vous mets de suite en garde, cela ne fonctionnera pas (du moins pour la première dimension).

Contrairement à ce qu'on pourrait s'attendre à voir, dans ces trois cas le résultat de l'opérateur sizeof ne donnera pas la taille des tableaux déclarés en paramètre, mais celle d'un pointeur (souvent 4 octets). Ce qui démontre (en quelque sorte) que le passage d'un paramètre formel de type 'tableau' n'est pas faisable à partir des déclarations ci-dessus.
Dans le cas général, on préfère passer la taille de notre matrice (ou au moins la taille des dites "lignes") en paramètre à la fonction.

void fonction1(int matrice[][3], size_t Nlignes){
        sizeof matrice[0] * Nlignes;   //Ceci permet de récupérer la taille de toute la matrice.
        sizeof *matrice * Nlignes;     //équivalente à celle ci-dessus
}
void fonction2(int matrice[10][3], size_t Nlignes){ //La taille 10 ici sera ignorée, donc est inutile. Contrairement à la taille 3 qu'il faudra respecter!
        sizeof matrice[0] * Nlignes;   //Ceci permet de récupérer la taille de toute la matrice.
        sizeof *matrice * Nlignes;     //équivalente à celle ci-dessus
}
void fonction3(int (*matrice)[3], size_t Nlignes){
        sizeof matrice[0] * Nlignes;   //Ceci permet de récupérer la taille de toute la matrice.
        sizeof *matrice * Nlignes;     //équivalente à celle ci-dessus
}
int main (void){
        int matrice[4][3];
        fonction1(matrice,4);
        fonction2(matrice,4);
        fonction3(matrice,4);
}

Ainsi nous disposerons de toutes les informations nécessaires pour parcourir notre matrice à l'aide d'une boucle.

Utilisation d'un double pointeur int **ptr

Oui je vous ai mentit :p en vous disant qu'il n'est pas possible de réceptionner une matrice statique à partir d'un double pointeur. Mais cela n'est tout de même pas si simple à manier, et demanderait un peu de bricolage pour y parvenir, vous pouvez en juger vous même.

La méthode consisterait à réceptionner l'adresse envoyée lors de l'appel de la fonction, qui représente l'adresse mémoire du premier élément de la première dimension de la matrice. Et à l'aide d'un tableau intermédiaire de pointeurs, ainsi que de la mise en équation des adresses mémoire de toutes les cases de la matrice, on arriverait à manier correctement une matrice 2D.
Je vous propose d'analyser ce code :

int fonction(int **mat)
{
        int i, j, *index[3];   //Index est le tableau de pointeurs qu'on utilisera sous forme de mémoire tampon.

        for (i = 0 ; i < 3 ; i++)  //On initialise notre tableau intermédiaire 'index'
                index[i] = (int *)mat + 3 * i; 

        for(i = 0 ; i < 3 ; i++)
        {
                printf("\n");
                for(j = 0 ; j < 3 ; j++)
                {
	                printf("%d", index[i][j]);
                }
        }
        printf("\n");

        return 0;
}

Pour bien comprendre le pourquoi du comment, je vous fais un petit rappel (qui ne sera pas qu'un simple rappel pour certains).

Imaginons que l'on veuille mettre en équation les adresses des cases constituant une matrice, afin de pouvoir les indexer en utilisant uniquement comme données l'adresse du premier élément dans cette matrice ainsi que sa taille (lignes et colonnes).
Avant d'attaquer les explications, je tiens à vous informer/rappeler que le langage C garantie qu'un tableau à deux dimensions sera placé en mémoire selon la disposition Row-major order.

En quoi cela nous intéresse ?

Cela signifie que seulement à partir de l'adresse du premier élément, et en connaissant la taille d'une matrice, on arriverait à connaître l'adresse de chacune de ses cases.

Exemple :

1

2

3

4

5

6

7

8

8

Cette matrice est ramenée dans la mémoire à la forme :

1

2

3

4

5

6

7

8

8

(disposition Row-major order).

Pour la suite de l'explication, nous allons prendre trois variables de type pointeurs avec les noms adresse_1, adresse_2 et adresse_3, qui sont respectivement les adresses en mémoire des nombres 1, 4 et 7.
Nous allons dans un premier temps récupérer l'adresse de la première case (celle contenant le chiffre 1), puis nous allons nous décaler de trois cases (trois étant le nombre de colonnes de la matrice) pour obtenir les adresses des élément 4 et 7;

int matrice[3][3] = {{1,2,3},{4,5,6},{7,8,9}}; //La matrice
int **adresse_matrice = (int**)matrice;  //Ceci est pour simuler un passage d'une matrice à une fonction
int *adresse_1, *adresse_2, *adresse_3;

adresse_1 = (int*)adresse_matrice;
adresse_2 = (int*)adresse_matrice + 3;
adresse_3 = (int*)adresse_matrice + 6;

Ensuite nous allons regrouper maintenant nos pointeurs adresse_1/2/3 dans un tableau :

int matrice[3][3] = {{1,2,3},{4,5,6},{7,8,9}}; //La matrice
int **adresse_matrice = (int**)matrice;  //Ceci est pour simuler un passage d'une matrice à une fonction
int *adresse[3];                  //Les pointeurs intermédiaires

adresse[0] = (int*)adresse_matrice + (0*3);
adresse[1] = (int*)adresse_matrice + (1*3);
adresse[2] = (int*)adresse_matrice + (2*3);

D'où le code de la fonction présenté précédemment.

int fonction(int **mat)
{
        int i, j, *index[3];   //Index est le tableau de pointeurs qu'on utilisera sous forme de mémoire tampon.

        for (i = 0 ; i < 3 ; i++)  //On initialise notre tableau intermédiaire 'index'
                index[i] = (int *)mat + 3 * i; 

        for(i = 0 ; i < 3 ; i++)
        {
                printf("\n");
                for(j = 0 ; j < 3 ; j++)
                {
	                printf("%d", index[i][j]);
                }
        }
        printf("\n");

        return 0;
}

En conclusion

Cette méthode n'est pas utilisée dans la pratique, de part son caractère complexe et bricolé, sans oublier qu'elle fait appel à un tableau intermédiaire, et peut poser des problèmes du fait qu'on fait des affectations aux pointeurs avec types différents.
Je ne vous conseillerai donc pas de l'utiliser dans le cas général; je tenais simplement à vous la présenter à titre informatif, ça nous a aussi permis de voir la nature d'une tableau multidimensionnel en mémoire.

Utilisation d'un typedef

L'utilisation du typedef n'est qu'un raccourci, pour alléger les déclarations de tableaux.

Exemple :

typedef int tableau3x3x3[3][3][3];

void ma_fonction1(tableau3x3x3 tab){   //Déclaration d'un argument de type tableau tridimensionnel de 3x3x3
      //Corps de la fonction...
      printf("%zu\n",sizeof(tab[0][0]));//Pour un test
}

int main(void)
{
    tableau3x3x3 tableau;    //Déclaration du tableau 3D

    ma_fonction1(tableau);   //Appel de la fonction en lui passant notre tableau en paramètre
    return 0;
}

Fonctions : retourner un tableau

Comme nous l'avons vu dans la partie des tableaux unidimensionnels, il est interdit de renvoyer un tableau automatique (alloué statiquement) :

Il est donc impératif de procéder à une allocation dynamique, puis de retourner le pointeur sur l'espace alloué dynamiquement (tel que nous l'avons vu avant dans la partie d'allocation dynamique ;) ).

Exemple :

int *** fonction_allocation(int taille1, int taille2, int taille3){
     int ***ptr;
     int i,j;

     ptr = malloc(taille1 * sizeof(*ptr));
     if(ptr == NULL)          //Pas de libération de mémoire à ce niveau
          return -1;          //Exemple de code d'erreur

     for(i=0 ; i < taille1 ; i++){
          ptr[i] = malloc(taille2 * sizeof(**ptr));
          if( ptr[i] == NULL) //Pensez à libérer la mémoire déjà allouée et fermer le programme.
               return -1;     //Exemple de code d'erreur
     }
     for(i=0 ; i < taille1 ; i++){
          for(j=0 ; j < taille2 ; j++){
                ptr[i][j] = malloc(taille3 * sizeof(***ptr));
                if(ptr[i][j] == NULL)     //Pensez à libérer correctement la mémoire déjà allouée
                     return -1;           //Exemple de code d'erreur
          }
     }
     return ptr;                          //retour du pointeur sur l'espace alloué
}

int main(void){
     int taille1 = 2, taille2 = 2, taille3 = 2;
     int *** ptr = fonction_allocation(taille1,taille2,taille3);
     //.....
     ptr = my_free(ptr,taille1,taille2);   //Cette fonction est décrite dans la partie allocation dynamique
     return 0;
}

Les tableaux unidimensionnels Exercices

Exercices

Les tableaux de tableaux ("tableaux à plusieurs dimensions")

Pointeurs

Exercice 1

Solution

//Q1 :
int * pointeur = NULL;
//Q2 :
int ** pointeurSurPointeur;
//Q3 :
int variable;
//Q4 :
pointeur = &variable;
//Q5 :
pointeurSurPointeur = &pointeur;
//Q6 :
pointeur = malloc(sizeof(int));
//Ou
pointeur = malloc(sizeof( *pointeur ));

Exercice 2

//Q1 :
float * ptr1 , * ptr2 ;
//Q2 :
float variable1 , variable2 ;
//Q3 :
ptr1 = &variable1;
ptr2 = &variable2;
//Q4 :
*ptr1 = 12.5;
//Q5 :
*ptr2 = 5.76;
//Q6 :
float variableIntermediaire;

variableIntermediaire = * ptr1;
*ptr1 = *ptr2;
*ptr2 = variableIntermediaire;

Exercice 3 (problème)

Exercice 4 (problème)

Pointeurs et références(exercice wikipedia).

#include <stdio.h>
#define taille_max 5
 
void parcours(int *tab)
{
     int *q=tab;
     do
     {
         printf("%d:%d\n", q-tab, *q-*tab);
     }
     while (++q-tab < taille_max);
}
 
void bizarre(int **copie, int *source)
{
     *copie=source;
}
 
int main(void)
{
     int chose[taille_max] = {1,3,2,4,5}, *truc;
     printf("chose : \n");

     parcours(chose);

     bizarre(&truc, chose);

     printf("truc : \n");

     parcours(truc);
 
     return 0;
}

Tableaux unidimensionnels

Exercice 1

Solution

double tab[15];
int i;

for(i=0 ; i< 15 ; i++)
    tab[i] = 0;

Exercice 2 (problème)

Exercice 3 (problème)

Exercice 4 (problème)

Rappel : Un palindrome est un mot qui reste le même qu'on le lise de gauche à droite ou de droite à gauche.

Exemples :

PIERRE : n'est pas un palindrome

OTTO : est un palindrome

23432 : est un palindrome

Exercice 5 (problème)

Exercice 6 (problème)

Tableaux multidimensionnels

Exercice 1 (problème)

Exercice 2 (problème)

En conclusion nous allons donc retenir les choses suivantes :

Ayez également en esprit que les codes les plus simples sont souvent les plus robustes et fiables. Ne cherchez pas automatiquement la solution compliquée, appliquez ce que vous avez appris dans ce cours qui en fin de compte a été rédigé dans un but de vous montrer comment utiliser correctement chaque technique (d'allocation, libération, traitement d'erreur, initialisation...). Définissez vos contraintes de portabilité et de conformité à l'une des normes dés la rédaction de votre cahier des charges, et avancez dans le développement de vos programmes en respectant cette contrainte.

Ainsi vous amoindrirez le risque de bogue, et vous obtiendrez un programme fiable en gestion d'erreurs en ce qui concerne tableaux et pointeurs :) .

Merci pour votre attention, et n'hésitez pas à me faire part de vos remarques.


Les tableaux de tableaux ("tableaux à plusieurs dimensions")