Structure d’un programme C avec exercices 

Cours C avec exercices , tutoriel & guide de travaux pratiques langage C en pdf.

Pointeurs

La notion de pointeur est tres importante en C, elle va vous permettre de comprendre le fonctionnement d’un programme, de programmer de facon davantage propre et performante, et surtout de concevoir des programmes que vous ne pourriez pas mettre au point sans cela.
Une des premieres choses a comprendre quand on programme, c’est qu’une variable est un emplace-ment de la memoire dans lequel vous allez placer une valeur. En programmant, vous utilisez son nom pour y lire ou ecrire une valeur. Mais ce qui se passe au coeur de la machine est quelque peu plus com-plexe, les noms que l’on donne aux variables servent a masquer cette complexit .
Pour le compilateur, une variable est un emplacement dans la memoire, cet emplacement est identi par une adresse memoire. Une adresse est aussi une valeur, mais cette valeur sert seulement a speci er un emplacement dans la memoire. Lorsque vous utilisez le nom d’une variable, le compilateur le remplace par une adresse, et manipule la variable en utilisant son adresse.
Ce systeme est ouvert, dans le sens ou vous pouvez decider d’utiliser l’adresse d’une variable au lieu d’utiliser son nom. Pour ce faire, on utilise des pointeurs.

Introduction
Un pointeur est une variable qui contient l’adresse memoire d’une autre variable.
Declaration
T est le type d’une variable contenant l’adresse memoire d’une variable de type T . Si une variable p de type T contient l’adresse memoire d’une variable x de type T , on dit alors que p pointe vers x (ou bien sur x). &x est l’adresse memoire de la variable x. Exposons cela dans un exemple,
#include<s t d i o . h>
void main ( )
f
int x = 3 ;
int p ;
p = &x ;
g
x est de type int. p est de type int , c’est a dire de type pointeur de int, p est donc faite pour contenir l’adresse memoire d’un int. &x est l’adresse memoire de la variable x, et l’a ectation p = &x place l’adresse memoire de x dans le pointeur p. A partir de cette a ectation, p pointe sur x.
A chage
La cha^ne de format d’une adresse memoire est « %X ». On a che donc une adresse memoire (tres utile pour debugger 🙂 comme dans l’exemple ci-dessous,
#include<s t d i o . h>
void main ( )
f
int x = 3 ;
int p ;
p = &x ;
p r i n t f ( « p c o n t i e n t l a v a l e u r %X, q u i n ’ e s t a u t r e que l ’ a d r e s s e %X de x » , p , &x ) ;g
Acces a la variable pointee
Le lecteur impatient se demande probablement a quoi peuvent servir toutes ces etoiles ? Quel peut bien ^etre l’inter^et des pointeurs ?
Si p pointe sur x, alors il est possible d’acceder a la valeur de x en passant par p. Pour le compila-teur, p est la variable pointee par p, cela signi e que l’on peut, pour le moment du moins, utiliser indi erement p ou x. Ce sont deux facons de se referer a la m^eme variable, on appelle cela de l’aliasing. Explicitons cela sur un exemple,
#include<s t d i o . h>
void main ( )
f
int x = 3 ;
int p ;
p = &x ;
p r i n t f ( « x = %dnn » , x ) ;
p = 4 ;
p r i n t f ( « x = %dnn » , x ) ;
L’a ectation p = &x fait pointer p sur x. A partir de ce moment, p peut ^etre utilise pour designer la variable x. De ce fait, l’a ectation x = 4 peut aussi ^etre ecrite p = 4. Toutes les modi cations operees sur p seront repercutees sur la variable pointee par p. Donc ce programme a che
x = 3
x = 4
Recapitulons
Qu’a che, a votre avis, le programe suivant ?
#include<s t d i o . h>
void main ( )
f
int x = 3 ;
int y = 5 ;
int p ;
p = &x ;
p r i n t f ( « x = %dnn » , x ) ;
p = 4 ;
p r i n t f ( « x = %dnn » , x ) ;
p = &y ;
p r i n t f (  » p = %dnn » , p ) ;
p = p + 1 ;
p r i n t f ( « y = %dnn » , y ) ;
x est initialise a 3 et y est initialise a 5. L’a ectation p = &x fait pointer p sur x, donc p et x sont deux ecritures di erentes de la m^eme variable. Le premier printf a che la valeur de x : plus precisement
x = 3. Ensuite, l’a ectation p = 4 place dans la valeur pointee par p, a savoir x, la valeur 4. Donc le deuxieme printf a che x = 4. L’a ectation p = &y fait maintenant pointer p sur y, donc la valeur de p est la valeur de la variable pointee y. le troisieme printf a che donc *p = 5. N’oubliez pas que comme p pointe sur y, alors p et y sont deux alias pour la m^eme variable, de ce fait, l’instruction p = p + 1 peut tout a fait s’ecrire y = y + 1. Cette instruction place donc dans y la valeur 6, le quatrieme printf a che donc y = 6. Ce programme a che donc :
51
x = 3
x = 4
*p = 5
y = 6
Tableaux
Il est possible d’aller plus loin dans l’utilisation des pointeurs en les employant pour manipuler des tableaux. Tout d’abord, eclaircissons quelques points.
Demysti cation (et demythi cation) du tableau en C
Les elements d’un tableau sont juxtaposes dans la memoire. Autrement dit, ils sont places les uns a cote des autres. Sur le plan de l’adressage, cela a une consequence fort intuitive. Sachant qu’un int occupe 2 octets en memoire et que &T [0] est l’adresse memoire du premier element du tableau T , quelle est l’adresse memoire de T [1] ? La reponse est la plus simple : &T [0] + 2 (ne l’ecrivez jamais ainsi dans un programme, vous verrez pourquoi plus loin dans le cours…). Cela signi e que si l’on connait l’adresse d’un element d’un tableau (le premier en l’occurrence), il devient possible de retrouver les adresses de tous les autres elements de ce tableau.
J’ai par ailleurs, une assez mauvaise surprise pour vous. Vous utilisez sans le savoir des pointeurs depuis que vous utilisez des tableaux. Etant donne la declaration int T [50], vous conviendrez que les designations fT [0]; : : : ; T [49]g permettent de se referer aux 50 elements du tableau T . Mais vous est-il deja arrive, en passant un tableau en parametre, d’ecrire T sans ecrire d’indice entre crochets ? Par exemple,
#include<s t d i o . h>
void i n i t T a b ( int K [ ] , int n )
f i ;
int
for ( i = 0 ; i < n ; i ++)
K[ i ] = i + 1 ;
g
void a f f i c h e T a b ( int K[] , int n )
f i ;
int
for ( i = 0 ; i < n ; i ++)
p r i n t f ( « %dnn » , K[ i ] ) ;
g
int main ( )
f
int T [ 5 0 ] ;
i n i t T a b (T, 50);
a f f i c h e T a b (T, 50);
return 0 ;
Vous remarquez que lorsque l’on passe un tableau en parametre a un sous-programme, on mentionne seulement son nom. En fait, le nom d’un tableau, T dans l’exemple ci-avant, est l’adresse memoire du premier element de ce tableau. Donc, T est une variable contenant une adresse memoire, T est par consequent un pointeur. Lorsque dans un sous-programme auquel on a passe un tableau en parametre, on mentionne un indice, par exemple K[i], le compilateur calcule l’adresse du i-eme element de K pour lire ou ecrire a cette adresse.
Utilisation des pointeurs
Commencons par observer l’exemple suivant :
#include<s t d i o . h>
main ( )
f t [ 1 0 ] ;
char
char p ;
t [ 0 ] = ’ a ’ ;
p = t ;
p r i n t f (  » l e p r e m i e r e l e m e n t du t a b l e a u e s t %c . n n » , p ) ;
g
La variable t contient l’adresse memoire du premier element du tableau t. p est un pointeur de char, donc l’a ectation p = t place dans p l’adresse memoire du premier element du tableau t. Comme p pointe vers t[0], on peut indi erement utiliser p ou t[0]. Donc ce programme a che
le premier element du tableau est a.
Calcul des adresses memoire
Tout d’abord, je tiens a rappeler qu’une variable de type char occupe un octet en memoire. Considerons maintenant les declarations
char t[10] ;
et
char p = t ;
Nous savons que si p pointe vers t[0], il est donc aise d’acceder au premier element de t en utilisant le pointeur p. Mais comment acceder aux autres elements de t ? Par exemple T [1] ? Souvenez-vous que les elements d’un tableau sont juxtaposes, dans l’ordre, dans la memoire. Par consequent, si p est l’adresse memoire du premier element du tableau t, alors (p + 1) est l’adresse memoire du deuxieme element de ce tableau. Vous ^etes conviendrez que p est la variable dont l’adresse memoire est contenue dans p. Il est possible, plus generalement, d’ecrire (p + 1) pour designer la variable dont l’adresse memoire est (p + 1), c’est a dire (la valeur contenue dans p) + 1. Illustrons cela dans un exemple, le programme
#include<s t d i o . h>
main ( )
f t [ 1 0 ] ;
char
char p ;
t [ 1 ] = ’ b ’ ;
p = t ;
p r i n t f (  » l e deuxieme e l e m e n t du t a b l e a u e s t %c . n n » , ( p + 1 ) ) ;
g
a che
le deuxieme element du tableau est b.
En e et, p + 1 est l’adresse memoire du deuxieme element de t, il est donc possible d’utiliser indi erement (p + 1) et t[1]. Plus generalement, on peut utiliser (p + i) a la place de t[i]. En e et, (p + i) est l’adresse du i-eme element de t, et (p + i) est la variable dont l’adresse memoire est (p + i). Par exemple,
#include<s t d i o . h>
#define N 26
Arithmetique des pointeurs
Supposons que le tableau t contienne des int, sachant qu’un int occupe 2 octets en memoire. Est-ce que (t + 1) est l’adresse du deuxieme element de t ?
Mathematiquement, la reponse est non, l’adresse du deuxieme element est t+(la taille d0un int) = (t+2). Etant donne un tableau p d’elements occupant chacun n octets en memoire, l’adresse du i-eme element est alors p + i n.
Cependant, la ponderation systematique de l’indice par la taille occupee en memoire par chaque element est d’une part une lourdeur dont on se passerait volontier, et d’autre part une source d’erreurs et de bugs. Pour y remedier, le compilateur prend cette partie du travail en charge,on ne ponderera donc pas les indices ! Cela signi e, plus explicitement, que quel que soit le type des elements du tableau p, l’adresse memoire du i-eme element de p est p + i. On le veri e experimentalement en executant le programme suivant :
#include<s t d i o . h>
#define N 30
void i n i t T a b ( int k , int n )
f
int i ;
k = 1 ;
for ( i = 1 ; i < n ; i ++)
( k + i ) = ( k + i 1 ) + 1 ;
g
void a f f i c h e T a b ( int k , int n )
f
int i ;
for ( i = 0 ; i < n ; i ++)
p r i n t f ( « %d  » , ( k + i ) ) ;
p r i n t f ( « nn » ) ;
g
main ( )
f
int t [N ] ;
i n i t T a b ( t , N ) ;
a f f i c h e T a b ( t , N ) ;
g
 Allocation dynamique de la memoire
La lecture du chapitre precedent, si vous y avez survecu, vous a probablement men a une question que les eleves posent souvent : « Monsieur pourquoi on fait ca ? ». C’est vrai ! Pourquoi on manipulerait les tableaux avec des pointeurs alors que dans les exemples que je vous ai donne, on peut le faire sans les poin-teurs ? Dans la mesure ou un tableau est un pointeur, on peut, m^eme a l’interieur d’un sous-programme auquel un tableau a et passe en parametre, manipuler ce tableau avec des crochets. Alors dans quel cas utiliserons-nous des pointeurs pour parcourir les tableaux ?
De la m^eme facon qu’il existe des cas dans lesquels on connait l’adresse d’une variable scalaire mais pas son nom, il existe des tableaux dont on connait l’adresse mais pas le nom.
Un probleme de taille
Lorsque l’on declare un tableau, il est obligatoire de preciser sa taille. Cela signi e que la taille d’un tableau doit ^etre connue a la compilation. Alors que faire si on ne connait pas cette taille ? La seule solution qui se presente pour le moment est le surdimensionnement, on donne au tableau une taille tres (trop) elevee de sorte qu’aucun debordement ne se produise.
Nous aimerions proceder autrement, c’est a dire preciser la dimension du tableau au moment de l’execution. Nous allons pour cela rappeler quelques principes. Lors de la declaration d’un tableau t, un espace memoire alloue au stockage de la variable t, c’est a dire la variable qui contient l’adresse memoire du premier element du tableau. Et un autre espace memoire est alloue au stockage des elements du tableau. Il y a donc deux zones memoires utilisees.
La fonction malloc
Lorsque vous declarez un pointeur p, vous allouez un espace memoire pour y stocker l’adresse memoire d’un entier. Et p, jusqu’a ce qu’on l’initialise, contient n’importe quoi. Vous pouvez ensuite faire pointer p sur l’adresse memoire que vous voulez (choisissez de preference une zone contenant un int…). Soit cette adresse est celle d’une variable qui existe deja. Soit cette adresse est celle d’un espace memoire creee specialement pour l’occasion.
Vous pouvez demander au systeme d’exploitation de l’espace memoire pour y stocker des valeurs. La fonction qui permet de reserver n octets est malloc(n). Si vous ecrivez malloc(10), l’OS reserve 10 oc-tets, cela s’appelle une allocation dynamique, c’est a dire une allocation de la memoire au cours de l’execution. Si vous voulez reserver de l’espace memoire pour stocker un int par exemple, il su t d’appeler malloc(2), car un int occupe 2 octets en memoire.
En m^eme temps, c’est bien joli de reserver de l’espace memoire, mais ca ne sert pas a grand chose si on ne sait pas ou il se trouve ! C’est pour ca que malloc est une fonction. malloc retourne l’adresse memoire du premier octet de la zone reservee. Par consequent, si vous voulez creer un int, il convient d’executer l’instruction : p = malloc(2) ou p est de type int . Le malloc reserve deux octets, et retourne l’adresse memoire de la zone allouee. Cette a ectation place donc dans p l’adresse memoire du int nou-vellement cre .
Cependant, l’instruction p = malloc(2) ne peut pas passer la compilation. Le compilateur vous dira que les types void et int sont incompatibles (incompatible types in assignement). Pour votre culture generale, void est le type « adresse memoire » en C. Alors que int est le type « adresse memoire d’un int ». Il faut donc dire au compilateur que vous savez ce que vous faites, et que vous ^etes s^ur que c’est bien un int que vous allez mettre dans la variable pointee. Pour ce faire, il convient d’e ctuer ce que l’on appelle un cast, en ajoutant, juste apres l’operateur d’a ectation, le type de la variable se situant a gauche de l’a ectation entre parentheses. Dans l’exemple ci-avant, cela donne : int p = (int )malloc(2).
Voici un exemple illustrant l’utilisation de malloc.
#include<s t d i o . h>
#include<m a l l o c . h>
main ( )
f
int p ;
p = ( int ) m a l l o c ( 2 ) ; p = 2 8 ;
p r i n t f ( « %dnn » , p ) ;
g
Vous remarquez que nous sommes bien dans un cas ou l’on connait l’adresse d’une variable mais pas son nom. Le seul moyen de manier la variable allouee dynamiquement est d’utiliser un pointeur.
La fonction free
Lorsque que l’on e ectue une allocation dynamique, l’espace reserv ne peut pas ^etre alloue pour une autre variable. Une fois que vous n’en avez plus besoin, vous devez le liberer explicitement si vous souhaitez qu’une autre variable puisse y ^etre stockee. La fonction de liberation de la memoire est f ree. f ree(v) ou v est une variable contenant l’adresse memoire de la zone a liberer. A chaque fois que vous allouez une zone memoire, vous devez la liberer ! Un exemple classique d’utilisation est :
#include<s t d i o . h>
#include<m a l l o c . h>
main ( )
f
int p ;
p = ( int ) m a l l o c ( 2 ) ; p = 2 8 ;
p r i n t f ( « %dnn » , p ) ;
f r e e ( p ) ;
g
Notez bien que la variable p, qui a et allouee au debut du main, a et liber par le f ree(p).
La valeur N U LL
La pointeur p qui ne pointe aucune adresse a la valeur N U LL. Attention, il n’est pas necessairement initialise a N U LL, N U LL est la valeur que, conventionnellement, on decide de donner a p s’il ne pointe sur aucune zone memoire valide. Par exemple, la fonction malloc retourne N U LL si aucune zone memoire adequate n’est trouvee. Il convient, a chaque malloc, de veri er si la valeur retournee par malloc est di erente de N U LL. Par exemple,
#include<s t d i o . h>
#include<m a l l o c . h>
main ( )
Vous remarquez que le test de non nullite de la valeur retournee par malloc est e ectu immediatement apres l’allocation dynamique. Vous ne devez jamais utiliser un pointeur sans avoir veri sa validite, autrement dit, sa non-nullite. Un pointeur contenant une adresse non valide est appel un pointeur fou. Vous devrez, dans votre vie de programmeur, les traquer avec hargne !
L’allocation dynamique d’un tableau
Lors de l’allocation dynamique d’un tableau, il est necessaire de determiner la taille memoire de la zone de la zone a occuper. Par exemple, si vous souhaitez allouer dynamiquament un tableau de 10 variables de type char. Il su t d’executer l’instruction malloc(10), car un tableau de 10 char occupe 10 octets en memoire. Si par contre, vous souhaitez allouer dynamiquement un tableau de 10 int, il conviendra d’executer malloc(20), car chaque int occupe 2 octets en memoire.
Pour se simpli er la vie, le compilateur met a notre disposition la fonction sizeof, qui nous permet de calculer la place prise en memoire par la variable d’un type donne. Par exemple, soit T un type, la valeur sizeof(T ) est la taille prise en memoire par une variable de type T . Si par exemple on souhaite allouer dynamiquement un int, il convient d’executer l’instruction malloc(sizeof(int)). Attention, sizeof prend en parametre un type !
Si on souhaite allouer dynamiquement un tableau de n variables de type T , on execute l’instruction malloc(n sizeof(T )). Par exemple, pour allouer un tableau de 10 int, on execute malloc(10 sizeof(int)). Voici une variante du programme d’un programme precedent :
#include<s t d i o . h>
#include<m a l l o c . h>
#define N 26

1 Notes de cours 
1.1 Introduction
1.1.1 Denitions et terminologie
1.1.2 Hello World !
1.1.3 Structure d’un programme C
1.1.4 Commentaires
1.2 Variables
1.3 Operateurs
1.4 Traitements conditionnels
1.4.1 Si … Alors
1.4.2 Switch
1.4.3 Booleens
1.4.4 Les priorites
1.4.5 Preprocesseur
1.5 Boucles
1.6 Tableaux
1.7 Cha^nes de caracteres
1.8 Fonctions
1.9 Structures
1.10 Pointeurs
1.11 Fichiers
1.12 Listes Chaines
2 Exercices 
2.1 Variables et operateurs
2.2 Traitements conditionnels
2.3 Boucles
2.3.1 Comprehension
2.3.2 Utilisation de toutes les boucles
2.3.3 Choix de la boucle la plus appropriee
2.3.4 Morceaux choisis
2.3.5 Extension de la calculatrice
2.4 Tableaux
2.5 Cha^nes de caracteres
2.6 Fonctions
2.7 Structures
2.8 Pointeurs
2.8.1 Aliasing
2.9 Fichiers
2.10 Matrices
2.11 Listes Chainees
2.11.1 Pointeurs et structures
2.11.2 Maniement du chainage
2.11.3 Operations sur les listes chainees
2.11.4 Listes doublement chainees
2.11.5 Fonctions recursives et listes chainees

Cours gratuitTélécharger le cours complet

Télécharger aussi :

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *