Chapitre suivantIndex des CoursChapitre précedentChapitre 7

LES TABLEAUX ET LES POINTEURS

 

Objectifs

• comprendre la non on de tableau en langage c

• connaître la syntaxe de déclaration des tableaux à une dimension et à plusieurs dimensions

• Comprendre la notion pointeur en langage C

•Identifier le lien entre les pointeurs et les tableaux

• Appliquer l’utilisation des pointeurs  en argument  d’une fonction

Eléments de contenu

•      Les tableaux à une dimension

•      Les tableaux a plusieurs dimension

•  Classe d’allocation des tableaux et initialisation

•Notion de pointeurs - Les opérateur * et &

• Utilisation des pointeurs en argument  d’une fonction

Lien  entre les pointeurs et les tableaux

•Les tableaux transmis en  arguments d’une fonction

Comme tous les langages, C permet d’utiliser des tableaux. On nomme ainsi Un ensemble d’éléments de même type désignés par un identificateur unique chaque élément est repéré par un indice précisant Sa position au sein de l’ensemble

Par ailleurs, comme certains langages, C dispose (le ‘pointeurs”, c’est à dire des variables destinées à contenir des adresses d’autres “objets” (variables, fonctions,...).

A priori, ces deux notions de tableau et de pointeur peuvent paraître éloignées l’une de ‘autre. Toutefois, il se trouve qu’en C un lien indirect existe entre les deux notions, à savoir qu’un identificateur de tableau est un constant pointeur.

I-LES TABLEAUX A UNE DIMENSION

Supposons que nous souhaitions déterminer, à partir de 20 notes d’élèves (fournies on données), combien d’entre elles sont supérieures à la moyenne de la classe.

S’il ne s’agissait que de calculer simplement la moyenne de ces notes, il nous. suffirait d’en calculer la somme on les cumulant dans une variable, au fur  et à mesure de leur lecture. Mais, ici, il nous faut à nouveau pouvoir consulter Les notes pour déterminer combien d’entre elles sont supérieures à la moyenne ainsi obtenue, il est donc nécessaire de mémoriser ces 20 notes.

Pour ce faire, il paraît peu raisonnable de prévoir 20 variables scalaires (méthode qui serait. Difficilement  transposable à un nombre important de notes).

Le tableau va nous offrir une solution convenable à ce problème, comme le montre le programme suivant

Main()

{

int i,som,nbm ;

float moy;
int t[20] ;

for (i=0; i<20;I++)

{          
printf( “donner la note  numéro %d :’’,i+1’’)

scanf(‘’%d’’,&t[i]) ;

}
som=0;
for(I=0; i< 20 ;i++)som+=t[i] ;

moy=som\20;
printf,(‘’\n\n La moyenne de cette classe est :%f\n’’,som) ;

nbm=0 ;
for(i=0 ;i<20 ;i++)
    if(t[i]>moy) nbm++;
printf(‘’%d élève ont plus de cette moyenne \n’’, nbm);
}
La déclaration :
int t [20]

réserve emplacement pour 20 élément de type int , Chaque élément est repéré par Sa position

dans le tableau ,nommée ‘‘indice”. Conventionnellement, en langage C, la première position porte le numéro 0. Ici, donc, nos indices vont de O à 19, le premier élément du tableau sera désigné par t[0], le troisième par t[2], le dernier par t[l9].

Plus généralement, la notion t[i] désigne un élément dont la position dans le tableau est fournie parla valeur de i. Elle joue le même rôle qu’une variable scalaire de type int.

La notation &t[i] désigne l’adresse de cet élément t[i] de même que &n désigne l’adresse de n

D’une façon générale

1) Un élément d’un tableau est une ivalue. Il peut donc apparaître à gauche d’un opérateur d’affectation comme dans :

t[2] = 5

Il peut aussi apparaître comme opérande d’un opérateur d’incrémentation, comme dans :

            t[3]++                        - -t[i]

2) L’indice peut prendre la forme de n’importe quelle expression arithmétique de type entier (ou caractère compte tenu des règle de conversion implicite). Par exemple, si n, p, k, et j sont de type int Ces notations sont correctes :

t[n-3]

t[3*p-2*k+j%n]

Il  en va de même si c1 et c2 de type char, de :

t [c1+3]

t[c2-c1]

3) Aucun contrôle de “débordement d’indice’’ n’est, mis en place par le compilateur. Pour en comprendre les conséquences, il faut savoir que lorsque le compilateur rencontre une L’aine telle que t[i], il en détermine l’adresse en ajoutant à l’adresse de début t, un décaLage proportionnel à. la valeur de i (et aussi proportionnel à la taille de chaque élément du tableau). De sorte qui est très facile de désigner et de modifier un emplacement situé avant ou après le tableau.

Il- LES TABLEAUX A PLUSIEURS DIMENSIONS

Comme tous les langages, C autorise les tableaux à plusieurs indices (ou à plusieurs dimensions).

Par exemple, la déclaration :

int t[5][3] ;

réserve un tableau de 15(5*3) éléments. Un élément quelconque de ce tableau se trouve alors repéré par deux indices comme dans ces notations :

             t[3][2]      t[i] [j]        t[i-3][i+j]

il  faut noter que là encore, la notation désignant un élément d’un tel tableau est une Ivalue Arrangement en mémoire des tableaux à plusieurs dimensions

les éléments d’un tableau sont rangés suivant l’ordre obtenu en faisant varier le dernier indice le premier. Ainsi, dans la déclaration suivante :

int[5][3] ;

les éléments du tableau t sont ordonnés comme suit :

          t[0][0]

t[0][1]

t[0][2]

t[1][0]

            t[1][1]

t[1][2]

            t[2] [0]

            …

t[4][0]

t[4][1]

             t[4][2]

Nous verrons que Cet ordre a une incidence dans au moins trois circonstances :

•     lorsque l’on omet de préciser certaines dimensions d’un tableau,

•     lorsque l’on souhaite accéder à laide d’un pointeur aux différents éléments d’un tableau,

•     lorsque l’un des indices déborde. Suivant l’indice concerné et les valeurs qu’il prend, il peut y avoir débordement d’indice sans ‘’sortie’’ du tableau. Par exemple, toujours avec notre tableau t de 5*3 éléments, vous voyez que la notation t[0][5] désigne en fait l’élément t[1][2]. Par contre la notation t[5][0] désigne un emplacement mémoire situé juste au delà du tableau.

III- CLASSE D’ALLOCATION DES TABLEAUX ET INITIALISATION

111.1 Règles concernant l’initialisation des tableaux

    •  Rappelons quelques points que nous avons déjà évoqués

Il est possible d’initialiser explicitement dans leur déclaration les tableaux, qu ils soient statiques ou automatiques. Les tableaux statiques sont initialisées une fois pour toutes avant l’exécution du programme. Les tableaux automatiques par contre, sont initialisés à chaque appel de la Fonction où ils sont déclarés.

 • En l’absence d’initialisation explicite, les tableaux statiques sont par défaut initialisés à zéro. Par contre les tableau automatiques ne sont pas initialisés.

III.2 Initialisation des tableaux à une dimension

La déclaration

Int tab[5] ={ 10,20,5,0,3}

place les valeurs 10,20,5,0 et 3 dans chacun des 5 éléments du tableau tab.

Il  est possible de ne mentionner dans les accolades que certaines valeurs seulement comme dans ces exemples :

int tab[5] ={10, 20}

int tab[5] ={,,5,, 3}

Les valeurs manquantes seront, suivant la classe d’allocation du tableau initialisés à zéro (statique) ou aléatoires (automatique).

De plus, il est possible, d’omettre la dimension du tableau, le compilateur pourra la déterminer par le nombre des valeurs énumérés dans l’initialisation. Ainsi, on peut écrire :

int tab []={I0,20,5,0,3}

III.3 Initialisation des tableaux à plusieurs dimensions

Voyons ces deux exemples équivalents :

             int tab [3][4] =            {{ 1,2,3,4 },
                                                 {5,6,7,8 },
                                                  {9,10,11,12}}

             int tab [3][4] =            {1,2,3,4,5,6,7, 8,9, 10, 11,12}

La première forme revient à considérer notre tableau comme formé de 3 tableaux de 4  éléments. La seconde exploite la manière dont les éléments sont effectivement rangés en mémoire.

Certaines valeurs peuvent être omises !

            int tab [3][4] =      {1,,2,,, 6,7}

IV- NOTION DE POINTEURS - LES OPERATEUR * ET &

IV.1 Introduction

Nous avons déjà été amenés à utiliser l’opérateur & pour désigner l’adresse d’une Ivalue. D’une manière générale, le langage C permet de manipuler des adresses par intermédiaire de variables nommées “pointeurs“.

En guise d’introduction à cette section, considérons les instructions suivantes :

int*ad;

                    int n;

        n=20 ;

ad=&n;

                   *ad =30;

La première instruction réserve une variable nommée ad comme étant un pointeur sur des entiers. On verra que * est un opérateur qui désigne le contenu de l’adresse qui le suit. Ainsi, on peut ditre que cette déclaration signifie que *ad c’est à dire l’objet d’adresse ad, est de type int ; ce qui signifie bien que ad est l’adresse d’un, entier.

L’instruction:

Ad=&n;

affecte à la variable ad la valeur de l’expression &n. L’opérateur & est un opérateur unaire qui fournit comme résultat adresse de son opérande. Ainsi, cette instruction place dans la variable ad l‘adresse de la variable n.

l’instruction :

*ad = 30;

signifie : affecter à la valuer *ad la valeur 30. Or, *ad représente l’entier ayant pour adresse ad (notez bien qu’on parle d’un entier et pas simplement d’une valeur quelconque car ad est un pointeur sur des entiers)

Nous aurions obtenus le même résultat avec :

n=30;

IV.2 Quelques exemples :

Voici quelques exemples d’utilisation de ces deux opérateurs. Supposons que nous ayons effectué ces déclarations :

            int ad 1, ad2,ad ;

int n= 10,p =20;

Considérons maintenant les instructions suivantes :

Ad 1 =&n ;

ad2=&p;

*ad 1=*ad2 +2;

Les deux premières placent dans ad 1 et ad2 les adresses de n et de p. La troisième affecte à

*ad1 la valeur de l’expression

*a<2 +2

Autrement dit, elle place dans l’adresse désignée par ad1 la valeur entière d’adresse ad2, augmentée de 2. Cette instruction est donc équivalctte à

n = p + 2

De manière comparable, l’expression :

            ad1+=3 ;

joue le même rôle que :

            n =n +3 ;

et l‘expression :

            (*ad1)++ ;                               

est équivalente à :

n+ +;

Remarques :

·         Si ad est un pointeur, les expressions ad et *ad sont des Ivalue. Par Contre, il n’en est pas de même pour &ad. En effet, cette expression désigne non plus une variable pointeur comme ad, mais l’adresse de la variable ad telle quelle est définie par le compilateur. Cette adresse est nécessairement fixe et on ne peut pas la modifier (la même remarque s’applique à &n où n est une variable scalaire quelconque). D’une manière, générale, des expressions telles que:

(&ad) ++ ou(&p)++

            seront rejetées à la compilation.

      Une déclaration telle que :

int *ad ;

·         réserve un emplacement pour un pointeur sur un entier, bile ne réserve pas on plus un emplacement pour un tel entier. Cette remarque est valable lorsque les objets pointés seront des chaînes ou des tableaux.

1V.3 Arithmétiques sur les pointeurs

Il est possible de manipuler, non seulement les variables pointées, mais aussi Les variables pointeurs. Soit La déclaration :

int *ad ;

une expression telle que :

ad+1

a un sens pour C.

En effet, est censé contenir l’adresse d’un entier, et pour C. L’expression ci-dessus représente l’adresse de l’entier suivant ceci n’a pas dans notre exemple, mais nous verrons que cela peut être très utile dans le traitement de chaînes ou de tableaux.

Notez bien qu’il ne faut pas confondre un pointeur avec un nombre entier. En effet, l’expression ci-dessus ne présente pas l’adresse de ad augmentée de un (octet). plus précisément, la différence entre ad +1et ad est ici de deux octets si.

D’une manière générale, expression :

ad+ +

Incrément l’adresse contenue dans ad de manière à ce qu’elle désigne objet suivant.

D’une manière générale, les opérations arithmétiques autorisées sur les pointeurs sont :

• addition ou soustraction d’un entier à un pointeur. Le résultat est du même type pointeur.

•    différence entre deux pointeurs de même type (c’est à dire qui pointent sur des objets de même type) le résultat est un entier.

V-  UTILISATION DES POINTEURS EN ARGUMENT D’UNE FONCTION

Nous avons vu que le mode de transmission par valeur interdisait à une fonction de modifier la valeur de ses arguments effectifs et nous avons mentionné que les pointeurs fourniraient une solution à ce problème.

Voici un exemple de programme qui réalise la permutation des valeurs de deux entier

main()

{

int n = 20, p = 30 ;

printf(‘’avant appel %d %d\n”, n ,p) ;                                    avant appel 20 30
échange( &n, &p) ;                                                                 après appel 30 20
printf (‘‘après appel %d %d\n’’ ,n , p) ;

}

échange( int* ad 1,int*ad2)

{

int x;

x  = *ad 1 ;

*ad1 = *ad2;

*ad2 = x;

}

Les argument effectifs de 1appel de échange sont, cette fois, Les adresses des variables n et p (et non plus leurs valeurs). On transmet à la fonction échange les valeur de &n et &p.

VI- LIEN ENTRE LES POINTEURS ET LES TABLEAUX

En langage C l’identificateur d’un tableau, lorsqu’il est employé seul (sans indice à sa suite) désigne on fait adresse de début de ce tableau, c’est à dire l’adresse de son premier élément.

VI.1 Cas des tableaux à une dimension

Soit la déclaration suivante:

int t[20]

La notation t est alors équivalente à &t[0].

De plus cet identificateur est considéré comme étant de type pointeur sur le type correspondant aux éléments du tableau, c’est à dire, ici, int, Ainsi, voici quelques exemples de notations équivalentes :

t+1                   &t[1]

                        t+i                    &t[i]

                       t[i]                    *(t+I)

• Pour illustrer Ces nouvelles possibilités de notation, plusieurs façons de placer la valeur I dans chacun des 20 éléments de notre tableau t.

int i;

for (i = 0; 1<20; i++)

            *(t+I)=1;

int I;
int*p;

for (p = t, =i =0;i<20; i++, p++)

                               *p=1

Notez bien que dans la seconde façon, nous avons dû recopier a valeur représentée par t dans un pointeur nommé p. Fn effet il ne faut pas oublier que le symbole t est une  adresse constante (t est une constante de type pointeur sur des entiers). Autrement dit, une expression telle que

t++ aurait été invalide, au même titre que, par exemple, 3++.un nom de tableau n’est pas une Ivalue.

Remarque importante :

nous venons de voir que la notation t[i] est équivalente à *(t+i) lorsque t est déclaré comme un tableau. En fait cela reste vrai, quelque soit la manière dont t a été déclaré. Ainsi, avec :

int *t ;

les deux notations précédentes resteraient équivalentes, autrement dit, on peut utiliser t[i] dans un programme où t est simplement déclaré comme un pointeur (sous condition d’avoir alloué l’espace mémoire nécessaire).

Vl.2 Cas des tableaux à plusieurs dimensions

Comme pour les tableaux à une dimension, l’identificateur d’un tableau à plusieurs dimensions employé seul, en représente toujours l’adresse de début. Mais de nouvelle possibilités apparaissent. Ainsi, si L’on a déclaré

                              int t[3][4] ;

t est effectivement équivalent à &t[0][0] mais, de plus, les notation

              t[0]                   t[1]                t[i]

ont un sens. En effet, comme nous l’avons vu, notre tableau t peut être considéré comme une succession de 3 tableaux de 4 éléments. Pour C, t[0] représente l’adresse de début du premier de ces “sous” tableaux, t[1] est l’adresse de début du second et ainsi de suite.

Autrement dit, les notations suivantes sont équivalentes :

              t[0]                  &t[0][0]

t [1]                &t[1][0]

              t[2]                 &t[2][0]

D’autre part ,ces notations de la forme t[i] sont considérées comme des constantes pointeur sur des entiers. Ainsi, ces deux expressions sont équivalentes :

              t[i]+1                &t[i][0]+1

Remarques :

t[i] est une constante ; ce n’est pas une Ivalue l’expression :

              t[i]++

est invalide. Par contre t[1][2] est bien une Ivalue.

VII- LES TABLEAUX TRANSMIS EN ARGUMENTS D’UNE FONCTION

Lorsqu’un identificateur de tableau apparaît en argument effectif de l’appel d’une fonction, il représente on fait l’adresse. De sorte que c’est effectivement cette adresse qui sera transmise à la fonction, laquelle pourra effectuer toutes les manipulations voulues à partir de cette adresse.

VII.1  Tableau à nombre fixe d’éléments

Voici un exemple qui met la valeur 1 dans tous les éléments (l’un tableau de 10 éléments l’adresse de ce tableau étant transmise on argument :

Void fct(int t[10])

{

int i;

for (i=0;I< 10;I++)

t[I]=1;

}

Voici deux exemples d’appel possibles de cette fonction :

Int t1[10],t2[10]


fct(t1) ;

                 …

             fct(t2) ;
                  

Il existe en fait d’autres formulations possibles pour cette fonction. Ainsi, par exemple, son en­tête peut être remplacée par :

        Void fct(int t[])

En effet, t désigne un argument. muet la réservation de l’emplacement mémoire du tableau dont on recevra ici l’adresse est réalisé dans la fonction appelante. D’autre part la connaissance de la taille exacte du tableau n’est pas indispensable au compilateur ; il est capable de déterminer l’adresse d’un élément quelconque à partir de son rang et de l’adresse de début.

La fonction peut s’écrire sans faire apparaître de tableau en utilisant simplement des pointeurs sur des entiers. En voici un exemple:

Void fct(int/*p)

{

int I;

for (I=0;i<10;I++,p++)                                  *p=1;

}

Il  faut noter que les deux en-têtes

Void fct (int* p)

et

void fct (int p[])

Sont parfaitement  équivalentes.

Dans les deux cas, vous pouvez utiliser le formalisme pointeur ou le formalisme tableau. Les trois instruction suivantes sont équivalentes :

for (i=0; i<10; i++,p++)*p=1;

for (i=0; i<10; i++)        *(p+i)=1;

for (i=0; i<10; i++)        p[i]=1;

VII.2   Tableau à nombre variable d’éléments

Soit l’exemple d’une fonction qui calcule la somme des éléments d’un tableau d’entiers de taille quelconque :

int som, (int[]int nb)

{

int s=0,i;

for (I=0;i<nb;i++)

s+=t[i];

return(s);

}

Voici quelque exemple d’appel de cette fonction :

main()

{

int t1[30],t2[15],t3[10] ;

int s1,s2,s3

     ….

S1=som(t1,30);

s2= son(t2,15)+som(t3,10) ;

    ….

}

Remarque en pratique, les dimensions effectives des tableaux seront plutôt exprimées sous forme de symboles définis par #difine, plutôt que directement sous forme de constante numérique. Cela facilite l’adaptation éventuelle des programmes. Ainsi le programme ci-dessus peut s’écrive, plus judicieusement, de la façon suivante :

#difineN1        30
#define N2       15
#define N3       10
main()

}

int t1[30],t2[15],t3[10];

      int s1,s2,s3 ;

      ….

S1=som(t1,N1);

     S2=som(t2,N2)+som(t3,N3);

}

  VII.3 Cas des tableaux à plusieurs dimensions

Nous avons vu comment réaliser une fonction travaillant sur un tableau à un seul indice, de taille quelconque. Si l’on souhaite généraliser cela à un tableau plusieurs dimension, il faut être en mesure de tenir compte de la manière dont un tel tableau est organisé en mémoire. Car il ne suffit pas de connaître l’adresse de début d’un tableau et qu’il possède deux indices pour retrouver l’adresse d’un élément quelconque t[i][j]. En effet son emplacement exacte dépend de la seconde dimension du tableau.

Supposons par exemple que nous souhaitions écrire une fonction qui place la valeur 0 dans chacun des éléments de la diagonale d’un tableau carré, Voici une manière de procéder :

Diag(int t[10][10])

{

int I,j;

for (i=0;i>10;i++)

        for(j=0;j<10;j++)

                  t[i][j]=0;

}

l’en-tête de cette fonction aurait pu être :

diag(int t[][10])

Diag( int t[][])

aurait été inutilisable puisqu’il serait impossible au compilateur de calculer l‘adresse Cette fonction ne convient pas pour un tableau de dimensions différente de 10*10. Plus précisément nous pourrons toujours l’appeler, comme dans cet exemple

Int mat[12][12] ;

                 ….

Diag(mat) ;

L’exécution de ces instructions conduira à placer 10 zéros dans le tableau mat, à des emplacements pour la plupart incorrects.

Le résultat serait plus catastrophique si le tableau comporte moins de 100 éléments puisque nous trouverions des zéros en dehors du tableau

Une façon de résoudre ce problème consiste à adresser les éléments voulus par des pointeurs en effectuant le “calcul d’adresse approprié”. Voici, par exemple une fonction qui place des zéros dans tous les éléments de la diagonale d’un tableau carré de taille quelconque.

Diag(int*p,int n)

{

int I;

for (i= O;i<n;I++)

           {

            *p=0;

             p+=n+1;
        }

}

Ici nous avons tenu compte que deux éléments consécutifs de la diagonale sont séparés par n

éléments. Si un pointeur désigne un élément de la diagonale, pour pointer Sur le suivant, il

suffit d’incrémenter ce pointeur de n+1 unités (l’unité étant ici la taille d’un entier)

Chapitre précedentIndex des CoursChapitre suivant

Révisé le :23-Sep-2007| ©2007 www.technologuepro.com