Security-X

Forum Security-X => Programmation => Cours => Discussion démarrée par: XmichouX le octobre 30, 2011, 13:27:14

Titre: Pointeurs et tableaux statiques : Utilisation et différences
Posté par: XmichouX le octobre 30, 2011, 13:27:14
Les tableaux statiques

1) Unidimensionnel

Un tableau est une structure de données particulière qui représente en fait une liste d'éléments de même type (variables, structures, ...), située dans un espace contigu en mémoire. On accède à chaque élément d'un tableau grâce à un index.

On crée un tableau en renseignant le type d'élément que l'on veut y insérer et le nombre d'éléments.

#include <stdio.h>

int main () {
          int tab[5];

          tab[0] = 4; tab[1] = 2; tab[5] = 2; tab[3]= 3; tab[4] = 26000;
}

Dans cet exemple, on voit que la création d'un tableau se fait comme pour une variable basique, on renseigne le type, mais on ajoute deux crochets après le nom de la variable pour indiquer qu'il s'agit d'un tableau. On renseigne le nombre d'éléments entre les crochets. Ici, un tableau de 5 int (entier) est défini.
Pour accéder et modifier tel élément du tableau, on utilise l'indexation. On représente le numéro d'index de l'élément auquel on veut accéder. Vu qu'en informatique, la numérotation commence souvent par 0, l'index du dernier élément est égal à au nombre total d'éléments -1.
Notons qu'ici, on peut initialiser le tableau plus rapidement de cette manière (grâce à des accolades, la virgule séparant chaque élément) :

int tab[4] = {4, 2, 3, 26000};
int tab[] = {4, 2, 3, 26000};

La première instruction réserve 4 entiers en mémoire car c'est précisé dans la taille du tableau (crochets) alors que la deuxième instruction en réserve 4 parce qu'il y a quatre données précisées entre les accolades. Pour la première instruction, si on avait la même ligne avec [3] au lieu de [], l'entier 26000 ne serait pas mis en mémoire. La taille est donc prioritaire sur les données précisées entre les accolades.

Si on déclare un tableau sans l'initialiser, celui-ci peut contenir n'importe quoi, il n'est pas mis à zéro.
En revanche, si on initialise un tableau, comme ça par exemple :

int tab[4] = {0};
Tout le tableau sera mis à zéro. Et si on avais mis par exemple {1}, le premier élément aurait été mis à un et tous les autres à zéro.
De manière générale, quand on initialise que partiellement un tableau, les données non précisées sont mises à zéro.

Quel est le rapport avec les pointeurs ? Un tableau est un ensemble d'éléments qui ont chacun une adresse, on peut donc aussi bien utiliser l'indexation pour manipuler un tableau que les pointeurs ! tab correspond à l'adresse du tableau, et non à un pointeur.
En effet, lorsque je crée tab, il n'y a pas de variable tab créée en mémoire et contenant l'adresse du premier élément du tableau :

int tab[3] = {1,2,3};
printf ("Adresse tab : %p, valeur tab : %p\n", &tab, tab);
printf ("Adresse premier element tableau : %p\n", &tab[0]);

Ceci affiche :
Citer
Adresse tab : 0022FF34, valeur tab : 0022FF34
Adresse premier element tableau : 0022FF34

(https://forum.security-x.fr/proxy.php?request=http%3A%2F%2Fsecurity-x.fr%2Fimg%2Fpublic%2Ftableau%21%3Dpointeur.png&hash=912974f2969110aaf3df24866b8c0810e995c417)

On voit que l'adresse de "tab" et la valeur sont identiques, et sont égales à l'adresse du premier élément du tableau (suite d'éléments) créé en mémoire. Le premier élément du tableau est bien une variable qui a une adresse et une valeur. Voyez plutôt tab comme une simple "étiquette" gérée par le compilateur C. Lorsque je crée un pointeur, il y a véritablement une variable créée ayant sa propre adresse et en valeur l'adresse de l'élément sur lequel elle pointe. Autrement dit : tab est l'adresse du tableau, "rien de plus" (dans la mémoire en tout cas)

Il en découle qu'avec un tableau, on ne peut pas faire une instruction du type :
Citer
tab = &variable;
tab = autre_tableau;

L'adresse du tableau créé ne peut pas changer (les éléments sont alloués dans la mémoire de manière statique) car il n'y pas de variable tab en mémoire et que le compilateur ne le permet pas ! C'est une des différences avec un pointeur. Un pointeur peut être déréférencé, à nouveau référencé, etc.

Si vous souhaitez manipuler un vrai pointeur, il suffit de faire par exemple :

int tab[]= {données};
int *ptr = &tab[0];

Ensuite, il vous suffit de manipuler le pointeur qui pointe sur votre tableau de données en mémoire.

Dans la plupart des cas, un tableau (dans la fonction où il a été créé) se comporte comme un pointeur constant sur son élément initial (ce que l'on vient de voir plus haut). Et l'adresse du tableau (&tab) se comporte comme un pointeur sur le tableau de n éléments.
Ainsi, tab est considéré par le compilateur comme de type int *, alors que &tab est de type int(*)[3] : pointeur sur un tableau de 3 int, à ne pas confondre avec int*[3] qui est de type tableau de trois pointeurs.
Pour ne pas vous embrouiller, on peut déclarer :
int(*pt)[3] qui est un pointeur sur un tableau de trois entiers,
int*pt[3] qui est un tableau de trois pointeurs sur entier.

De manière générale, on peut tout de même manipuler les tableaux soit en notation (comme un) pointeur, soit avec la notation tableau.

L'adresse d'un tableau est égal à l'adresse de son premier élément -conceptuellement et concrètement- qui se note :

printf ("Adresse du tableau = %p = %p\n", tab, &tab[0]);tab, se comporte là comme un pointeur sur le tableau créé, c'est-à-dire, sur son premier élément (tab[0]). Sa valeur correspond donc à l'adresse du tableau (premier élément). &tab[0] signifie l'adresse du premier élément du tableau, ce qui revient au même résultat.

De la même manière, l'adresse du deuxième élément du tableau est :

printf("Adresse du deuxième élément = %p = %p\n", (tab+1), &tab[1]);
Ici, la première manière de représenter l'adresse est particulière. Le fait de mettre entre parenthèses signifie que toute addition (ici +1) se rapporte à un élément du tableau (plus précisément à tab qui ici se comporte comme un int*, c'est-à-dire un pointeur sur entier) et non à un octet (+1 = plus un élément du tableau # plus un octet). Chaque élément prend ici 4 octets en mémoire.
En vérité, ce code tab+1 correspond à tab + 1 * sizeof(*tab), voir plus loin.
Cela dépend peut-être du compilateur, néanmoins, j'ai le même résultat en enlevant les parenthèses. Par convention, je trouve plus propre de les mettre, pour ne pas confondre avec "plus un octet".
La deuxième manière est la manière formelle de dire "l'adresse du deuxième élément du tableau". On n'oublie pas que 1er élément = 0, 2e élément = 1, etc.

Pour accéder à la valeur d'un élément, on retrouve encore la notion de pointeur, on peut accéder à la valeur du premier élément comme ceci :

printf("Valeur du premier élément = %i = %i",*(tab), tab[0]);
De la même manière, on accède au deuxième élément comme cela :

printf("Valeur du deuxième élément = %i = %i",*(tab+1), tab[1]);
Ces exemples reflètent bien l'utilisation des pointeurs dans la partie ci-dessus : *(adresse) = valeur. J'ai bien mis adresse, et non pointeur.
Cela revient au même qu'une variable pointeur qui pointe sur l'adresse d'une variable : *(variable pointeur = adresse variable pointée) = valeur.

Dans chaque exemple que nous avons vu, la notation tab[..] est traduite par le compilateur dans la notation pointeur (tab se comporte comme un pointeur). Par exemple, l'instruction "tab[1] = 2;" est traduite en "*(tab+1) = 2;", ce qui signifie que la notation pointeur/tableau sont équivalentes. On peut donc également utiliser la notation [] avec un véritable pointeur, voir en dessous.
Personnellement, je préfère utiliser les crochets pour un tableau, et l'étoile pour un pointeur afin de bien les différencier.

De la même manière, on peut utiliser un pointeur externe pour accéder aux éléments du tableau (pas l'inverse). Cette fois, on a un véritable pointeur.

int main () {
        int tab[4] = {4, 2, 3, 26000};
        printf ("Adresse du tableau = %p = %p\n", tab, &tab[0]);
        printf("Adresse du deuxieme element = %p = %p\n", (tab+1), &tab[1]);
        printf("Valeur du premier element = %i = %i\n",*tab, tab[0]);
        printf("Valeur du deuxieme element = %i = %i\n\n",*(tab+1), tab[1]);

        int *pt = tab;
        printf ("Adresse du tableau = %p\n", pt);
        printf("Adresse du second element = %p\n", (pt+1));
        printf("Valeur du second element  = %i\n", *(pt+1));
        pt +=2; // On incrémente le pointeur de 2. Maintenant pt pointe sur le troisième élément du tableau.
printf("Valeur du troisieme element = %i, du premier element = %i\n", pt[0], pt[-1]);
}

Puisqu'on veut accéder à un tableau d'entiers, on crée un pointeur d'entiers qu'on fait pointer sur l'adresse du tableau, autrement dit tab ou &tab[0].
Ensuite, on peut accéder aux différents éléments du tableau, de la même manière qu'avec tab. Le pointeur pointant sur un tableau d'entiers, les calculs utilisés avec sont basé sur l'unité = élément et non octet, comme on a vu. On déplace donc le pointeur de deux éléments. pt[0] est donc équivalent à tab[2]. Et vu qu'on s'est déplacé, on peut même faire l'instruction pt[-1] (qu'on ne pourrait surtout pas faire avec un tableau !), car on ne sort pas de l'espace mémoire qui nous est réservé.

Le code affiche ceci :

Citer
Adresse du tableau = 0028FEFC = 0028FEFC
Adresse du deuxieme element = 0028FF00 = 0028FF00
Valeur du premier element = 4 = 4
Valeur du deuxieme element = 2 = 2

Adresse du tableau = 0028FEFC
Adresse du second element = 0028FF00
Valeur du second element  = 2
Valeur du troisieme element = 3, du premier element = 2

On remarque bien ici que les éléments d'un tableau sont contigus en mémoire. En effet, 0022FF30 -1 0022FF2C = 4, soit la taille d'un entier, c'est-à-dire d'un élément du tableau.

Nous venons de voir qu'on peut associer un pointeur à un tableau, et utiliser l'indexation avec un pointeur. Cela signifie qu'on peut également faire :

char *ch = "maman";
Contrairement à ce qu'on pourrait croire, le pointeur ne contient pas maman, mais la chaine maman est convertie en un tableau de variables de type char sur lequel le pointeur "ch" pointe. Cette instruction est donc correcte. Cependant, à la différence de char[] = "maman" qui créée dans la mémoire (enfin la pile principale du programme) les différents caractères accessible en lecture et écriture, char * crée une chaine en lecture seule dans une zone spéciale (section .r(o)data = Read Only Data, voir ici (http://www.siteduzero.com/forum-83-508979-p1-difference-entre-char-tab-et-char-tab.html#r4868011)). C'est pourquoi, dans ce cas, on ne peut pas faire chaine[0]= 'b';, mais on peut tout à fait l'afficher dans un printf par exemple.

On peut parcourir un tableau de la manière suivante :

#include <stdio.h>

int main () {
          int tab[5] = {1,3,5,6,4};
          int i;
          for (i=0; i<sizeof(tab)/sizeof(int); i++) {
                    printf ("cellule %i vaut %i\n", i, tab[i]);
          }
          return 0;
}

L'opérateur sizeof en C retourne la taille de l'objet passé en argument (variable, tableau, structure, etc.). En divisant la taille totale du tableau (ici : 4*5) par la taille du type du tableau (ici int = 4), on trouve le nombre d'éléments du tableau. Dans l'affiche, on aurait tout aussi bien pu mettre *(tab+i), comme nous l'avons vu avant. On remarque ici quelque chose. D'habitude, le tableau se comporte comme un pointeur sur son premier élément, or sizeof() ne retourne pas la taille d'une adresse (4 octets), comme ça aurait été le cas pour "sizeof(&tab[0])" mais la taille de tous les éléments du tableau. tab se comporte donc ici comme "tous les éléments du tableau".

Ce parcours du tableau peut aussi s'écrire de cette manière :
for (i=0; i<sizeof(tab)/sizeof(tab[0]); i++) {
          printf ("cellule %i vaut %i\n", i, tab[i]);
}

tab[0] étant la valeur du premier élément du tableau, on obtient la taille du premier élément, donc d'un élément (puisque les tableaux en C sont d'un et un seul type). On peut également mettre "sizeof(*tab)". À vous de choisir vos conventions !

La deuxième manière est bien plus intéressante à mettre, dans le cas où on ne connaît pas le type, où le type est changé. Cela ne nécessite aucune modification, alors que si on codait en dur le nombre d'éléments ou le type du tableau, il faudrait le changer par la suite (si modification du tableau).

2) Multidimensionnels

Les tableaux que nous avons vu sont des tableaux unidimensionnels (une dimension). On peut aussi en créer à plusieurs dimensions (n), en 2D aussi appelés matrices (lignes, colonnes), 3D (longueur, largeur, hauteur), etc....

Pour cela, il suffit de rajouter une autre paire de crochets avec le nombre d'éléments souhaités. Une paire de crochets représente donc une dimension.

Exemple :

#include <stdio.h>

int main () {
int matrice[2][3] = {{0, 2, 4}, {1, 3, 5}};

/*
Équivalent à :
matrice[0][0] = 0;
matrice[0][1] = 2;
matrice[0][2] = 4;
matrice[1][0] = 1;
matrice[1][1] = 3;
matrice[1][2] = 5;
*/}

On déclare ici une matrice de 2x3 cases (vous pouvez imaginer ça comme un tableau qu'on dessine). On peut initialiser directement la matrice comme pour un tableau d'une dimension. Cette fois-ci, il faut rajouter un jeu d'accolades à chaque index de la première dimension.
C'est équivalent que le code en commentaire. Pour assigner une valeur, il faut donc bien renseigner l'index des deux dimensions.
On remarque qu'il y a différents niveaux d'accolades lors de l'initialisation :  { {}, {} }
Il y a donc un niveau d'accolades par dimension. S'il y avait trois dimensions, on aurait quelque chose du type : { { {}, {}}, {{}, {}} }
Par exemple, on pourrait faire : tab3[2][2][2] = {{{0 ,1}, {2, 3}}, {{4, 5}, {6, 7}}}... ce qui équivaut à :
tab3[0][0][0] = 0; tab3[0][0][1] = 1; tab3[0][1][0] = 2; tab3[0][1][1] = 3; tab3[1][0][0] = 4;tab3[1][0][1] = 5;tab3[1][1][0] = 6;tab3[1][1][1] = 7;
Ouais, je sais, ça ressemble à du code binaire, enfin c'était pas fait exprès ^^  ;D
Pour résumer, le premier niveau d'accolades représente la première dimension, le deuxième niveau la deuxième dimension, et ainsi de suite.
Je ne sais pas si c'est très clair, mais en gros : dans le niveau d'une dimension, lorsque vous changez d'index, vous écrivez un nouveau jeu d'accolades pour la dimension de niveau inférieur (niveau 1 > niveau 2). Vous pouvez pour cette explication, vous imaginer un arbre, comme sur ce schéma. Comme ça, c'est sûrement plus aisé à comprendre et modéliser :

(https://forum.security-x.fr/proxy.php?request=http%3A%2F%2Fsecurity-x.fr%2Fimg%2Fpublic%2Ftab_arbre.png&hash=a1ed285bb85895821b0322792a307f79f90995ec)

A chaque niveau de l'arbre (en partant du haut), on ajoute un jeu d'accolades.
Notez que la vraie représentation dans la mémoire du tableau est celle du dernier niveau. Les éléments sont consécutifs dans une mémoire unidimensionnelle.
On remarque donc qu'en C, tab, tab[0], tab[1]... ont une signification : se comporter comme un pointeur sur tel élément, etc. Dans la mémoire, il n'y a que les véritables éléments (dernier niveau).

#include <stdio.h>

int main () {
int matrice[2][3] = {{0, 2, 4}, {1, 3, 5}};

/*
Équivalent à :
matrice[0][0] = 0;
matrice[0][1] = 2;
matrice[0][2] = 4;
matrice[1][0] = 1;
matrice[1][1] = 3;
matrice[1][2] = 5;
*/
printf ("Taille de la matrice en octets : %i\n", sizeof(matrice));
printf ("Nombre d'éléments de la matrice : %i\n", sizeof(matrice)/sizeof(matrice[0][0]));

printf ("Nombre d'éléments de la deuxième dimension : %i\n", sizeof(matrice[0])/sizeof(matrice[0][0]));
/* pareil : sizeof(*matrice)/sizeof(**matrice) */
printf ("Nombre d'éléments de la première dimension : %i\n", sizeof(matrice)/sizeof(matrice[0]));

return 0;
}

Revenons au code ci-dessus : pour trouver le nombre d'éléments des deux dimensions, ce n'est pas beaucoup plus compliqué qu'avec une seule ! Comme pour un tableau à une dimension, pour trouver le nombre d'éléments de la matrice, on divise la taille en octets de la matrice (toutes dimensions confondues) par la taille d'un élément. matrice[0][0] (==*matrice[0]) est le premier élément de la matrice et non pas matrice[0], qui se comporte comme un pointeur sur (l'adresse de) la deuxième dimension (premier élément)  pour l'index 0 de la première dimension du tableau (adresse de la colonne 0 pour la ligne 0).

Note : On peut voir sur cet affichage que les éléments sont consécutifs en mémoire, quel que soit le nombre de dimensions.

adresse matrice[0][0] = 0xbf4f0f90
adresse matrice[0][1] = 0xbf4f0f94
adresse matrice[1][0] = 0xbf4f0f98
adresse matrice[1][1] = 0xbf4f0f9c
adresse matrice[2][0] = 0xbf4f0fa0
adresse matrice[2][1] = 0xbf4f0fa4

Je le répète, "matrice" n'existe pas en mémoire. Seules les données sont présentes, il n'y a pas de pointeurs lorsqu'on crée un tableau statique.
Comme on un tableau à deux dimensions (conceptuellement), matrice[0], matrice[1] et matrice[2] sont chacun considérés comme des tableaux à une dimensions : ils n'existent donc pas non plus en mémoire. C'est le compilateur qui gère tout ça.
En mémoire, vous n'avez que ce qu'il y a au dessus, les données, consécutives en mémoire.

On obtient le schéma suivant :

(https://forum.security-x.fr/proxy.php?request=http%3A%2F%2Fsecurity-x.fr%2Fimg%2Fpublic%2Fmatrice_statique.png&hash=2d0eec51500fa0744a7968cf59e1430db61388f2)

Nous avons vu comment calculer le nombre d'éléments total du tableau. Si on veut trouver le nombre d'éléments de chaque dimension, il faut partir de la dernière dimension, et venir progressivement vers la première. Par exemple ici, pour connaître le nombre d'éléments, il faut obligatoirement connaître le nombre d'éléments total du tableau et le nombre d'éléments de la deuxième dimension.
Pour trouver le nombre d'éléments de la deuxième dimension, il suffit de faire sizeof(matrice[0]), qui donne donc le nombre d'octets pour la deuxième dimension correspondant à l'index 0 de la première dimension, et de le diviser par la taille d'un élément, comme toujours.
Pour la première dimension, il suffit alors de diviser le nombre d'éléments total du tableau, par le nombre d'éléments de la deuxième dimension pour tel index de la première dimension (ce qui revient plus simplement à diviser directement la taille en octets du tableau par celle de la 2e dimension, cf code). Sachant qu'un tableau statique en C contient le même nombre d'éléments pour n'importe quel index. On ne peut pas avoir 6 éléments pour l'index 0 de la première dimension et 4 pour l'index 1 par exemple. Cette méthode est donc valable avec n'importe quel index.
Plus il y a de dimensions, plus il y a de divisions à faire, mais le principe est le même. Pour un tableau 3D, il va falloir diviser la taille totale du tableau par la taille de la 3e dimension * la taille de la 2e dimension (ce qu'on vient de faire).

3) Passer un tableau à une fonction

Nous allons finir en voyant comment passer un tableau à une fonction.

#include <stdio.h>

void zeroTab1D (int tab[]) {
int i;
for (i=0; i<sizeof(tab)/sizeof(tab[0]); i++)
tab[i] = 0;
}

int main() {

int tab1D[] = {1, 2, 3, 4};
int i;
zeroTab1D(tab1D);
for (i=0; i<sizeof(tab1D)/sizeof(tab1D[0]); i++)
printf ("tab[%i] = %i\n", i, tab1D[i]);

return 0;
}

Contrairement à ce que l'on aurait attendu (initialisation du tableau à 0), l'affichage est :

Citer
tab[0] = 0
tab[1] = 2
tab[2] = 3
tab[3] = 4

Les 0 n'ont pas été assignés pour tout le tableau, sauf pour le premier index (0).
Le problème ici est qu'à l'intérieur de la fonction, on ne connait pas la taille du tableau et ceci pour une raison simple, tab[] n'est pas un tableau, mais un pointeur.
En effet, même si vous passez un tableau à votre fonction, le tableau sera automatiquement converti en un pointeur sur &nom_tableau[0]. Or l'opérateur sizeof() retourne la taille d'une adresse sur un pointeur (généralement 4). C'est pourquoi ici, la fonction n'a modifié que le premier élément (4 octets).
Un argument d'une fonction n'est donc jamais un tableau, mais toujours un pointeur. C'est pourquoi les fonctions comme printf prennent en paramètre un const char *, et non un char[].
On remarque ici concrètement la différence entre un tableau (tab qui se comporte comme un pointeur) et un pointeur (une véritable variable en mémoire pointant sur un objet).
Ainsi, f(int tab[]) est identique à f(int tab[5]), qui finalement se ramènent à f(int *tab). Il est donc pour moi plus logique de toujours mettre un pointeur en argument, pour bien montrer qu'il s'agit d'un pointeur et non d'un tableau. Malgré cette conversion, on peut tout de même accéder aux éléments du tableau car on connaît le type (int) sur lequel pointe le pointeur.

Pourquoi l'argument n'est pas un tableau, mais un pointeur ? Quand on passe une variable en paramètre à une fonction, une copie est envoyée à la fonction (empilée), ou bien on peut spécifier son adresse au lieu de sa valeur avec l'opérateur &. Mais imaginez qu'on manipule des tableaux de milliers/millions d'octets. Quel gaspillage horrible ce serait de copier toutes ces données dans chaque fonction appelée. C'est la raison principale pour laquelle un tableau est toujours passé par "adresse".  Vous envoyez à votre fonction votre "tab" créé, qui est une adresse comme nous l'avons vu. L'argument de la fonction ne peut donc être qu'un pointeur, prenant en valeur cette adresse.

void zeroTab1D (int *tab, int taille) {
int i;
for (i=0; i<taille; i++)
*(tab+i) = 0; // tab[i] marche aussi ;)
}

int main() {

int tab1D[] = {1, 2, 3, 4};
int i;
zeroTab1D(tab1D, sizeof(tab1D)/sizeof(tab1D[0]));
for (i=0; i<sizeof(tab1D)/sizeof(tab1D[0]); i++)
printf ("tab[%i] = %i\n", i, tab1D[i]);

return 0;
}

Ce code marche ! On doit donc connaître la taille du tableau si on veut la parcourir. Or, on ne peut pas la connaître dans la fonction (du moins, pour ce qui n'est pas tableau de caractères qui possèdent une particularité), donc on la transmet en paramètre.

Maintenant, voyons comment peut-on passer un tableau à deux dimensions statique à une fonction.

Le problème de base :

#include <stdio.h>
#include <stdlib.h>

void zero (int **tab, int lignes, int col) {
int i, j;
for (i=0; i<lignes; i++) {
for (j=0; j<col; j++) {
tab[i][j] = 0;
}
}
}

int main () {
int matrice[5][4] = {{1,2,3,4}, {5,6,7,8}, {9,1,2,3}, {4,5,6,7}, {8,9,1,2}};
zero(matrice, sizeof(matrice)/sizeof(matrice[0]), sizeof(matrice[0])/sizeof(matrice[0][0]));
int i , j;
for (i=0; i<sizeof(matrice)/sizeof(matrice[0]); i++) {
for (j=0; j<sizeof(matrice[0])/sizeof(matrice[0][0]); j++) {
printf ("tab[%i][%i] = %i\n", i , j, matrice[i][j]);
}
}
}

On voudrait faire ceci d'office... sauf qu'un problème se pose (le programme plante). Ceci est du au fait que le compilateur ne sait pas comment accéder aux cellules demandées pour une raison simple.
Dans un tableau à une dimension, il n'y a pas ce problème, car il y a une seule dimension. Pour accéder à la cellule 3 du tableau, il suffit au compilateur de faire : &tableau + (3 * sizeof(élément_tableau)). Or dans un tableau à deux dimensions (données alignées), il faut connaître la taille de la deuxième dimension. Pourquoi ? La mémoire est unidimensionnelle (une ligne continue), et donc les deux dimensions sont alignées sur une dimension.
Les dimensions sont une abstraction pour le programmeur. Elles n'existent pas dans la mémoire.
Pour passer au 2e index de la première dimension du tableau, il faut connaître la taille de la deuxième dimension, pour la sauter
Exemple : À la fin de la partie b) (un peu plus haut), on voyait les adresses mémoire de tous les éléments d'une matrice.
Ainsi, comment passer de l'index 0 réprésentant l'adresse tab[0][0] = 0xbf4f0f90 à l'index 1 représentant l'adresse tab[1][0] = 0xbf4f0f9c ?
Pour cela, il faut connaître le nombre d'éléments de la deuxième dimension (qui est de 4) : tab[1][0] = le_pointeur_passé (pointe sur tab[0][0]) + 4 * sizeof(élément_tableau). Pour accéder à tab[1][0], on saute donc dans l'ordre tab[0][0], tab[0][1], tab[0][2] et tab[0][3].
Puisque le tableau est converti en pointeur lors du passage à une fonction, on perd cette information qui est conceptualisée dans un tableau (ex : tab[1] se comporte comme un pointeur sur tab[1][0], mais tab[1] n'existe pas en mémoire, alors qu'un pointeur si !).
De plus, le type passé en paramètre ((*)[4]) ne correspond pas au type attendu (**).
Comme pour tab pour un tableau de première dimension, tab[0] pour une matrice n'est pas un pointeur, ni une variable. tab[0] se comporte vis-à-vis de tab[0][...] comme tab vis-à-vis de tab[...]. Puisque notre tableau est converti en pointeur, le compilateur ne connaît pas ensuite l'adresse de tab[1]. Il lui faut connaître la deuxième dimension.
Ce code ne marche donc pas !

Il y a plusieurs solutions :

1) On peut préciser la taille de la deuxième dimension dans le prototype de la fonction, ce qui réduit les possibilités de la fonction (on ne peut lui passer qu'un tableau dont la deuxième dimension fait tel grandeur) :

#include <stdio.h>
#include <stdlib.h>

void zero (int (*tab)[4], int lignes, int col) { // égal à tab[][4] puisque conversion en pointeur sur tableau de 4 int ce que veut dire int(*tab)[4].
int i, j;
for (i=0; i<lignes; i++) {
for (j=0; j<col; j++) {
(*(tab+i))[j]= 0; // tab[i][j]
}
}
}

int main () {
int matrice[5][4] = {{1,2,3,4}, {5,6,7,8}, {9,1,2,3}, {4,5,6,7}, {8,9,1,2}};
zero(matrice, sizeof(matrice)/sizeof(matrice[0]), sizeof(matrice[0])/sizeof(matrice[0][0]));
int i , j;
for (i=0; i<sizeof(matrice)/sizeof(matrice[0]); i++) {
for (j=0; j<sizeof(matrice[0])/sizeof(matrice[0][0]); j++) {
printf ("tab[%i][%i] = %i\n", i , j, matrice[i][j]);
}
}
}

Ce code marche. Le compilateur sait que la deuxième dimension contient 4 éléments de type int. Il lui suffit donc de calculer les bons indices (on va voir comment dans la solution qui suit).

2) Puisque les donnés d'une matrice statique sont alignées en mémoire, on peut considérer qu'on transmet un tableau à une seule dimension et faire nous-même les calculs que ferait le compilateur si on choisissait la première solution (vous allez comprendre).

#include <stdio.h>
#include <stdlib.h>

void zero (int *tab, int lignes, int col) {
int i, j;
for (i=0; i<lignes; i++) {
for (j=0; j<col; j++) {
*(tab + i * col + j) = 0;
}
}
}

int main () {
int matrice[5][4] = {{1,2,3,4}, {5,6,7,8}, {9,1,2,3}, {4,5,6,7}, {8,9,1,2}};
zero(matrice[0], sizeof(matrice)/sizeof(matrice[0]), sizeof(matrice[0])/sizeof(matrice[0][0]));
int i , j;
for (i=0; i<sizeof(matrice)/sizeof(matrice[0]); i++) {
for (j=0; j<sizeof(matrice[0])/sizeof(matrice[0][0]); j++) {
printf ("tab[%i][%i] = %i\n", i , j, matrice[i][j]);
}
}
}

Le prototype de fonction prend cette fois un pointeur sur entier, et non un pointeur sur tableau, on ne transmet donc pas matrice qui se comporte comme un int(*)[4] (pointeur sur tableau de 4 éléments), mais matrice[0] qui se comporte comme un int*. :) C'est histoire de ne pas avoir de warning de la part du compilateur (incompatible type), mais sinon cela marche également de passer directement matrice.
Ensuite, pour accéder à chaque élément, c'est très facile, puisque les données sont alignées.
Pour accéder à la cellule [2][3] par exemple, il suffit de multiplier : 2 * nombre_élément_2e_dimension + 3. Pas besoin de multiplier par la taille de l'élément, car le compilateur sait que la taille d'un élément est celle d'un int.
Toutes les fois où j'ai écrit * sizeof (*tab) ne sont pas à écrire, car le compilateur le fait déjà !
Si vous ne comprenez pas, regardez la citation que j'ai fait plus haut où on voit les adresses mémoire d'une matrice et vous comprendrez !
Titre: Re : Pointeurs et tableaux statiques : Utilisation et différences
Posté par: igor51 le juillet 18, 2012, 22:21:47
UP !