Vous êtes sur la page 1sur 25

COURS LANGAGE C & C++

Enseignant : M. EBOA Michel Anicet


Plan du cours
Chapitre 1 : Les bases de la programmation en C
Pour valider le module :
Chapitre 2 : Structure d’un programme C
Chapitre 3 : Les types de base
• CC : Présence, TP et TPE
Chapitre 4 : Les operateurs et les expressions
• Un examen de fin de Chapitre 5 : Les instructions de contrôle
semestre Chapitre 6 : Les fonctions

Code B2033 Chapitre 7 : Les tableaux et les pointeurs


Volume Horaire 32 h Chapitre 8 : Les chaines de caractères
Crédits 3
Chapitre 9 : Les structures et les unions
Niveau 1
Semestre 1 Chapitre 10 : Introduction au Langage C++
Pré-requis Raisonnement logique
Chapitre 7 : Les tableaux et les pointeurs
Objectifs
• Comprendre le 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 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.

VII.2. 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
VII.2.a. 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 en
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[1][0] t[2] [0] t[3][0] t[4][0]


t[0][1] t[1][1] t[2] [1] t[3][1] t[4][1]
t[0][2] t[1][2] t[2] [2] t[3][2] 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 à l’aide 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.
VII.3. CLASSE D’ALLOCATION DES TABLEAUX ET INITIALISATION
VII.3.a. 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 (dynamiques). 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.
VII.3.b. 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};
VII.3.c. 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}

VII.4. NOTION DE POINTEURS - LES OPERATEUR * ET &


VII.4.a. 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 dire 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 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;
}

l’en-tête de cette fonction aurait pu être : diag(int t[ ][10])


#include<stdio.h>
#include<string.h>

int i,j;
int mat[10][10];

int main(){

diag(mat);

return 0;
}

void diag(int t[10][10])


{
for(i=0;i<10;i++){
for(j=0;j<10;j++){
t[i][j]=0;
if(i == j)
t[i][j]=1;
printf("%d",t[i][j]);
}
printf("\n");
}

}
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)

Vous aimerez peut-être aussi