Retour à l'index

Programmation modulaire (suite)

Les pointeurs et l'adressage indirect

Commençons par voir la notion d'adressage indirect, mise en place à l'aide des pointeurs, notion qui n'a rien à voir avec les fonctions a priori.


Notion générale
Principe. Jusqu'à maintenant nous avons vu, en ce qui concerne la désignation des entités, la notion de référence directe grâce à une variable. Ceci correspond en gros à une boîte noire dans laquelle on peut placer certaines entités, le type de la variable indiquant la taille des entités que l'on peut y placer.
L'indexation indirecte repose sur le principe suivant. On utilise une nouvelle sorte de variables, dites variables pointeurs. Le contenu d'une telle variable ne sera pas une entité du type voulu mais l'adresse de cette entité (dite entité pointée).
L'intérêt de cette façon de faire est que le contenu d'une telle variable pointeur a une taille fixe, quel que soit le type de l'entité pointée (qui peut être de taille très grande).
Types pointeurs. A tout type correspond un type pointeur. L'intérêt de ceci est dû au fait que, si le contenu d'une variable pointeur, lui, est de taille constante, la taille de l'entité pointée, elle, dépend du type de cette entité ; lorsqu'on réserve l'emplacement pour l'entité pointée il faut donc en connaître la taille, indiquée par le type du type pointeur.
Accès au contenu pointé. La référence à une variable pointeur donnera l'adresse de l'entité voulue et non l'entité elle­même. Il faut donc un moyen pour désigner cette entité. La façon de faire dépend du langage considéré.

Mise en place en langage C
Type pointeur.­ A chaque type 'a' est associé un type pointeur 'a*' correspondant, noté en faisant suivre le nom du premier type par une étoile.
Déclaration d'une variable d'un type pointeur. La syntaxe de déclaration d'une variable de nom 'ptr' (pour PoinTeuR) de type pointeur pointant vers une entité de type type est :
type *ptr; 

en utilisant l'astérisque, qui est l'opérateur de pointeur.
On dit aussi que la variable ptr est de type
type*
.
Remarque. Le type pointeur est type* mais l'astérisque se place traditionnellement devant la variable et non derrière le type, ce qui permet des déclarations du genre :
int *ptr, i ; 

Attention ! On déclare dans ce cas en même temps une variable i de type entier et une variable ptr de type pointeur vers un entier et non deux variables pointeur.
Initialisation des pointeurs. On peut initialiser une variable de type pointeur de plusieurs façons :
  • Affectation directe, par exemple :
    ptr = 24680; 

    évidemment à éviter, car on ne sait pas ce qui a été placé à cette adresse par le compilateur (sauf pour l'adresse fixe de certains périphériques).

  • Affectation grâce à l'opérateur d'adressage, par exemple :
    int i; 
    int *ptr;
    ptr = &i; 

  • Affectation d'un autre pointeur, par exemple :
    ptr1 = ptr2; 


Accès au contenu pointé. Pour accéder au contenu désigné par le pointeur ptr on utilise encore l'astérisque comme opérateur d'indirection (ou opérateur de contenu ou opérateur de référence). Le contenu désigné par ptr a pour nom (composé) *ptr.
On a par exemple :
		int *ptr,i;     /* déclaration d'un entier i */
		i = 3;			/* initialisation de l'entier i */
		ptr = &i; 		/* initialisation du pointeur, avec l'adresse de i */
		i = *ptr; 		/* affectation de i, avec la valeur de ptr */
		*ptr = 34; 		/* affectation de la valeur de ptr */
		

Passage des arguments de fonctions par adresse

Introduction. Nous l'avons déjà dit, et nous le répétons : la transmission par référence n'existe pas en langage C ; elle est émulée par le passage par adresse. Le principe en est le suivant :
  • On communique l'adresse d'une variable, ce qui permet d'en modifier le contenu (sans modifier l'adresse elle­même).
  • L'adresse d'une variable est manipulée par un pointeur. Les paramètres formels doivent être des pointeurs ; les paramètres effectifs doivent être des adresses, par exemple des noms de variables précédés de l'opérateur d'adressage &.
Exemple. Rappelons que le programme suivant :
		#include <stdio.h>  
		
		void affiche(int n)   
		{   
			n = 4;   
			printf("%d \n", n);   
		}   
		  
		int main(void)   
		{   
			int n = 3;   
			affiche(n);   
			printf("%d \n", n);   
		}   
		
avec passage des paramètres par valeur fait afficher 4 puis 3. Considérons le programme modifié suivant :
		#include <stdio.h>  
		
		void affiche(int *n)   
		{   
			*n = 4;   
			printf("%d \n", *n);   
		}   
		  
		int main(void)   
		{   
			int n = 3;   
			affiche(&n);   
			printf("%d \n", n);   
		}   
		
Son exécution fait afficher 4 puis 4. La fonction affiche() a donc changé la valeur de n dans la fonction principale.

Philosophie du raffinement successif

Nous avons vu plusieurs raisons pour utiliser des fonctions, c'est-à-dire des sous-programmes.
Une utilisation importante des sous­programmes conduit à la méthode de programmation connue sous le nom de raffinement successif.

Conception générale

Un bon plan d'attaque pour concevoir un algorithme est de décomposer la tâche à accomplir en quelques sous-tâches importantes, puis de décomposer chacune de ces sous-tâches en des sous­ tâches plus petites, et ainsi de suite.
A la fin les sous-tâches deviennent si petites qu'elles se programment très facilement en langage C, ou dans n'importe lequel des langages de program­mation choisi.
On considère en général que le corps du programme principal ou d'une fonction ne devrait jamais dépasser une dizaine de lignes.

Exemple : opérations sur les rationnels


Le problème
Nous voulons concevoir un programme concernant les opérations sur les nombres rationnels, simulant une petite calculette interactive permettant d'effectuer les quatre opérations sur les nombres rationnels. On termine en proposant le pseudo-rationnel 0/0. Un exemple de session d'utilisation de ce programme est le suivant :
1/2 + 1/3 = 5/6
1/2 x 1/3 = 1/6
1/2 : 1/3 = 3/2
­1/2 + 1/3 = ­ 1/6
245/28 + 26/49 = 1819/196
258/­48 + 25/491 = 2797/11784 
48/45 x 456/29 = 2432/145 
0/0 bye 

Ce programme ne peut être que de taille assez importante. Conformément à la philosophie pré­cédente nous allons donc programmer petit à petit ce que nous désirons et en le concevant de façon le plus modulaire possible de façon à pouvoir réutiliser des fonctions lorsqu'on passe d'un programme à l'autre.

Mise en place du programme
Les fonctions nécessaires Pour créer un programme qui effectue des calculs sur les rationnels, nous allons avoir besoin de fonctions pour :
  • la lecture des rationnels
  • l'addiciton de rationnel
  • la soustraction de rationnel
  • la multiplication de rationnel
  • la division de rationnel
  • la simplification de rationnel
  • l'affichage de rationnel

Sous fonction Après réflection, nous nous rendons compte qu'une certaine fonction a besoin d'une sous-fonction pour être optimal : la fonction de simplification.
En effet, selon les mathématiques, il faut calculer le PGCD du numérateur et du dénominateur pour pouvoir simplifier la fraction. Il nous faut donc implémenter la fonction
int PGCD (int,int,int,int);

D'où :
  • la lecture des rationnels
  • l'addiciton de rationnel
  • la soustraction de rationnel
  • la multiplication de rationnel
  • la division de rationnel
  • la simplification de rationnel
    • le calcul du pgcd
  • l'affichage de rationnel

Lecture du rationnel Il est possible pour l'utilisateur d'écrire :
1/2
et que le programme ne lise pas le '/'. (cf exemple)
Pour la lecture du rationnel, il va falloir différencier le numérateur (num) et du dénominateur (den). Or une fonction ne peut renvoyer qu'une valeur. Il va donc falloir passer par l'utilisation de pointeur. Dans le programme principal, les variables seront déclarées comme tel :
int *num, *den;

Il faut ensuite leur allouer de la place mémoire. Il existe deux possibilité :
  • Déclarer deux variables entière (int) :
     int a,b;

    et donner leur adresses aux pointeur :
    *num = &a ;
    *den = &b ;
  • Allouer dynamiquement de la place à ces ponteur :
    *num = (int) maaloc (sizeof(int)) ;
    *den  = (int) maaloc (sizeof(int)) ;
Du fait de l'utilisation des pointeurs, la fonction ne renvera aucune valeur (son type de retour sera donc 'void'). Les valeurs seront directement changées sur les arguments :
		void lect_ratio (int* num, int* den)  
		{  
			printf("Entrez un rationnel (a/b) : ");   
			scanf("%d %*c %d", num, den); 
			/* le format %*c de scanf permet de dire que le caractère en question doit être lu, 
			   mais ne doit pas être placé dans une variable. Il y a donc 3 formats et 2 variables. 
			   Le rationnel est lu, les variables sont modifié dans le programme principal, 
			   du fait de l'utilisation des pointeurs, la fonction est donc finie. */   
		}
		

Affichage du rationnel
		void affiche_ratio (int num, int den)	  	
		{  
			printf("%d/%d \n", num, den);  
		}  
		

Addition de rationnels Nous ne traiterons que l'addition, sachant que les autres opérations sont globalement identique.
Il faut dans un premier temps, lire le second membre de l'opération, effectuer le calcul et le stocker. Pour cela, nous avons le même problème que pour la saisie : on ne peux renvoyer qu'une valeur. Dans ce cas-ci, il est possible de réutiliser les variables arguments comme variable pour le résultat. En effet, les valeurs des opérandes ne nous interessent plus une fois le calcul effectué.

Ce qui donne :
		void add_ratio (int *num, int *den)  
		{  
		  int num_1, den_1;  
		  int *num_2=&num_1;  
		  int *den_2=&den_1;   
		    
		  lire_ratio( num_2, den_2 );   /* Lecture de la seconde opérande */  
		  
		  /* mise au même dénominateur */  
		  		  
		  *num   *= *den_2;  
		  *num_2 *= *den;  
		  *den *= *den_2;  
			
		  /* fin de mise au même dénominateur */
			
		  /* somme */  
		  *num += *num_2;  
		    
		}  
		

La simplification de rationnel Nous avons vu que pour simplifier les rationnel, if fallait préalablement calculer le PGCD. Cela est fait par la fonction PGCD qui doit être déclarée avant d'être appelé (dans le fonction simpl_ratio);
		int PGCD (int *a, int *b)  
		{  
		  int max,i,pgcd=1;  
		    
		  if (*a > *b)  
		    max=*a;  
		  else  
		    max=*b;  
		    
		  for(i=1;i<=max;i++)  /* N.B. 'i' doit être initialisé à 1, pour ne pas tenter une division par 0 */
		  {  
		        if ( *a%i==0 && *b%i==0)    /* '%' permet de savoir si 'i' divise la valeur de 'a' et la valeur de 'b' */
			  		pgcd=i;  
		  }  
		    
		  return pgcd;  
		}  
		  		
		void simpl_ratio (int *num, int *den)  
		{  
		  int pgcd=1;  
		  pgcd=PGCD(num, den);  
		    
		  *num /= pgcd;  
		  *den /= pgcd;  
		}  
		

L'affichage de rationnel
		void affiche_ratio (int num, int den) /* inutil de passer des pointeur ici. */
		{
		  printf("%d/%d \n", num,den);
		}
		

Le programme principal
		#include<stdio.h>
		#include<stdlib.h> /* permet le EXIT_FAILURE et le EXIT_SUCCESS. */

		#include"fract.h"   		/* Fichier en-tête, déclaration des prototypes des fonctions */
		#include"fract_fonction.c"  /* Fichier annexe, contient le code des fonctions */
		
		int main (void)
		{
			int rep,a,b;
			int *num=&a;
			int *den=&b;
			char c='o';

			while ( c=='o' || c=='O' )
			{
				*num=1;*den=1;
				lire_ratio(num, den);
			
				printf("Quelle action voulez vous?\n");
				printf("\t 1] Ajouter un rationnel\n");
				printf("\t 2] Soustraire un rationnel\n");
				printf("\t 3] Multipliser un rationnel\n");
				printf("\t 4] Divisier un rationnel\n");
				printf("\t 5] Simplifier un rationnel\n");
				printf("\nVotre réponse : ");
				scanf(" %d", &rep);
			
				if( rep <1 || rep >5)
				{
					printf("Cette réponse n'est pas acceptable.\nFin du programme.\n");
					return EXIT_FAILURE;
				}
				else
				{
					if( rep == 1)
					{
						add_ratio(num, den);
				
						simpl_ratio(num, den);
						affiche_ratio(*num, *den); /* la fonction n'est pas déclaré pour lire des pointeurs
													  il nous faut donc passer les valeurs, et non les adresses. */
					}
	
					if( rep == 2)
					{
						sub_ratio(num, den);
				
						simpl_ratio(num, den);
						affiche_ratio(*num, *den);
					}
				
					if( rep == 3)
					{
						mult_ratio(num,den);
				
						simpl_ratio(num, den);
						affiche_ratio(*num, *den);
					}
				
					if( rep == 4)
					{
						div_ratio(num/den);
						
						simpl_ratio(num, den);
						affiche_ratio(*num,*den);
					}
				
					if ( rep == 5)
					{
						simpl_ratio(num, den);
						affiche_ratio(*num, *den);
					}
			
					printf("Voulez vous continuer? (o/N)");
					scanf(" %c", &c);
				}
			}
		printf("Fin du programme.\n");
		return EXIT_SUCCESS;
	}}
			

Cours, éxercices ou graphismes libre de droit. Un mail est souhaitable | Webmestre : Aublet Bastien (bastien.aublet@hotmail.fr)