Académique Documents
Professionnel Documents
Culture Documents
Eléments de contenu
• Les tableaux à une dimension
• Les tableaux a plusieurs dimensions
• 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
VII.1. LES TABLEAUX A UNE DIMENSION
Supposons que nous souhaitions déterminer, à partir de 20 notes d’élèves (fournies en 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 en 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’’, moy) ;
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 un 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 par la
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 notation 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 qu’il est très
facile de désigner et de modifier un emplacement situé avant ou après le tableau.
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 valeur *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;
VII.4.b. Quelques exemples :
Voici quelques exemples d’utilisation de ces deux opérateurs. Supposons que nous ayons effectué ces
déclarations :
int ad1, ad2, ad ;
int n= 10, p = 20;
Considérons maintenant les instructions suivantes :
ad1 = &n ;
ad2 = &p;
*ad1 = *ad2 + 2;
Les deux premières placent dans ad1 et ad2 les adresses de n et de p. La troisième affecte à
*ad1 la valeur de l’expression *ad2 + 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 équivalente à 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, il ne
réserve pas en plus un emplacement pour un tel entier. Cette remarque est valable lorsque les objets
pointés seront des chaînes ou des tableaux.
VII.4.c. 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, ad 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 de sens 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 +1 et 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.
VII.5. 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
echange( &n, &p) ;
printf (‘‘après appel %d %d\n’’ ,n , p) ; //après appel 30 20
}
echange( int *ad1,int *ad2)
{
int x;
x = *ad1 ;
*ad1 = *ad2;
*ad2 = x;
}
Les argument effectifs de l’appel 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.
VII.6. 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 l’adresse de début de ce tableau, c’est à dire l’adresse de son premier élément.
VII.6.a. 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 la 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).
VII.6.b. Cas des tableaux à plusieurs dimensions
Comme pour les tableaux à une dimension, l’identificateur d’un tableau à plusieurs dimensions
employé seul, 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.7. 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.7.a. Tableau à nombre fixe d’éléments
Voici un exemple qui met la valeur 1 dans tous les éléments (d’un tableau de 10 éléments; l’adresse
de ce tableau étant transmise en 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 l’entê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.7.b. 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 t[ ], 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 #define, 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 :
#define N1 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.7.c. 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 :
void diag(int t[10][10])
{
int i,j;
for (i=0;i>10;i++)
for(j=0;j<10;j++)
t[i][j]=0;
}
int i,j;
int mat[10][10];
int main(){
diag(mat);
return 0;
}
}
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= 0; 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)