Saturday, February 2, 2013




 

Exo 1

Solution avec boucle tant que canonique (un grand classique de la programmation structurée) :
#include <stdio.h>

float pht, pttc;

main() {
    printf("Prix HT ? ");
    scanf("%f", &pht);

    while (pht > 0) {
        pttc = pht * (1 + 19.6 / 100); 
        printf("Prix TTC : %f\n", pttc);
        
        printf("Prix HT ? ");
        scanf("%f", &pht);
    }
    printf("Au revoir...");
}
[1] Vous rencontrerez très souvent cette situation : répéter un certain traitement jusqu'à ce qu'une certaine condition cesse d'être vraie, sans savoir à l'avance combien de fois le traitement devra être exécuté.
On peut passer un peu de temps sur cet exemple, qui a une grande importance pour la suite. Il faut se convaincre qu'il est une bonne manière de résoudre le problème posé, même s'il semble comporter des redondances inutiles. Dans certains langages, comme Pascal, c'est la seule ; en C on en a une autre.
Allons-y par étapes. Un peu de réflexion fait apparaître que la solution cherchée se composera de trois éléments :
  • lecture d'une donnée ; ici, soit un vrai prix HT, soit 0 pour indiquer la fin,
  • traitement de la donnée lue (supposée valide), ici calcul et écriture du prix TTC demandé,
  • boucle contrôlée par la condition « la donnée est-elle valide ? » (soit, ici, le prix HT est-il non nul ?).
Dans l'ordre de l'exécution on doit lire la donnée avant de la traiter. L'organisation suivante parait naturelle
tant que la donnée est valide faire         Attention, ce modèle est erroné !
        lire une donnée
        traiter la donnée lue
Mais cela ne peut pas marcher! Deux défauts : (1) la première fois que la condition « la donnée est valide » est évaluée, le programme n'a encore rien lu et (2) la lecture est suivie par le traitement, que la donnée soit valide ou non. Dans beaucoup de situations, traiter une donnée invalide provoque des erreurs graves et est donc une conduite inacceptable.
Notez que remplacer la boucle tant que faire (while) par une boucle faire tant que (do...while) corrige le premier défaut mais pas le second : la lecture est toujours suivie du traitement :
faire             Attention, ce modèle est encore erroné !
        lire une donnée
        traiter la donnée lue
tant que la donnée est valide
On en arrive donc à la configuration proposée, qui obéit au schéma suivant :
lire une donnée
tant que la donnée est valide faire

        traiter la donnée lue
        lire une donnée
Les deux défauts constatés plus haut sont corrigés. D'une part la première fois que la condition « la donnée est valide » est évaluée une donnée a déjà été lue. D'autre part, dans l'ordre de l'exécution, entre la lecture d'une donnée et son traitement se place toujours la condition précédente, assurant que seules les données valides seront traitées.
[2] Sans rien enlever à la qualité de la solution précédente, notons qu'en C l'existence de l'instruction break nous permet d'écrire des boucles dans lesquelles la condition de continuation (ou d'arrêt) n'est pas obligatoirement placée à l'entrée de la boucle, mais n'importe où à son intérieur
#include <stdio.h>

float pht, pttc;

main() {
    while (1) {
        printf("Prix HT ? ");
        scanf("%f", &pht);
        
        if (pht == 0)
            break;
            
        pttc = pht * (1 + 19.6 / 100); 
        printf("Prix TTC : %f\n", pttc);    
    }
    printf("Au revoir...");
}
On peut trouver cette solution plus naturelle. Elle corrige bien les défauts constatés plus haut, car l'évaluation de la condition est toujours précédée d'une lecture et entre la lecture et le traitement il y a toujours une validation.
Puisque, pris pour un booléen, 1 signifie vrai, l'expression while(1) veut dire « répéter indéfiniment ». En C cela peut s'écrire aussi for(;;). Nous nous sommes ramenés au schéma suivant :
répéter indéfiniment
        lire une donnée
        si la donnée est invalide abandonner la boucle
        traiter la donnée lue
[3] N'écoutez pas les intégristes qui vous disent que l'instruction break n'est pas de la programmation structurée. Obscurantisme et oppression du prolétariat ! La programmation structurée consiste essentiellement à organiser son programme sous forme de blocs vérifiant :
  • un bloc est fait d'une suite d'instructions, chacune composée à son tour d'un ou plusieurs blocs,
  • le contrôle n'entre dans un bloc autrement qu'en passant par sa première instruction,
  • quand le contrôle quitte un bloc, il se retrouve toujours immédiatement après sa dernière instruction,
Comme on peut le voir, notre boucle ci-dessus respecte scrupuleusement ces règles.
[4] On nous demande une version de notre programme qui affiche, à la fin du travail, le nombre de calculs faits et le total des prix TTC calculés. Il nous faut introduire deux éléments très fréquents et utiles :
  • un compteur, c.-à-d. une variable entière qui vaut constamment le nombre de calculs qui ont été faits,,
  • un accumulateur, c.-à-d. une variable du type qu'il faut (ici float) qui vaut constamment la somme des prix TTC calculés.
Ce qui donne le programme :
#include <stdio.h>

float pht, pttc, cumul;
int nombre;

main() {
    nombre = 0;
    cumul = 0;
    
    printf("Prix HT ? ");
    scanf("%f", &pht);
    while (pht > 0) {
        pttc = pht * (1 + 19.6 / 100); 
        printf("Prix TTC : %f\n", pttc);
        
        nombre = nombre + 1;
        cumul = cumul + pttc;
        
        printf("Prix HT ? ");
        scanf("%f", &pht);
    }
    printf("En tout %d calculs faits - Cumul des prix TTC : %f\n", nombre, cumul);    
}
N.B. Ce n'est pas une obligation mais, quand nous serons plus habités au langage C, les deux lignes concernant nombre et cumul nous les écrirons plutôt comme ceci :
    ...
    nombre++;
    cumul += pttc;
    ...

Exo 2

[1] La seule difficulté est de se rappeler que les fonctions trigonométriques de la bibliothèque requièrent leur argument exprimé en radians (180 degrés = PI radians) alors que nous devons l'afficher en degrés. D'où une conversion à faire au moment de l'appel de la fonction sin :
#include <stdio.h>
#include <math.h>

int a, pi = 4 * atan(1);

main() {
    a = 0;
    while (a <= 90) {
        printf("sin(%d) = %f\n", a, sin(a * (pi / 180)));
        a = a + 15;
    }
}
Notez que ce programme n'a aucune chance de fonctionner si on oublie la ligne « #include <math.h> » . Cela est dû à la fonction sin, dont la déclaration est donnée dans le fichier math.h (sans cette déclaration le compilateur fait l'hypothèse que sin rend un int, cela ne peut pas être juste).
[2] La bibliothèque mathématique standard ne comporte pas de définition de la constante pi (3.141592653589793 etc.). Une manière d'en obtenir une expression en accord avec les besoins de la bibliothèque consiste à le faire calculer par une fonction de la bibliothèque. Si on se souvient que la tangente de pi / 4 est 1 on en déduit que pi vaut quatre fois l'arc tangente de 1.
[3] On peut rendre ce programme encore plus compact avec l'instruction for
...
main() {
    for (a = 0; a <= 90; a = a + 15)
        printf("sin(%d) = %f\n", a, sin(a * pi / 180));
}
[4] Les versions B et C demandées sont d'infimes variations de ce qui précède. Par exemple, la version C peut être ainsi écrite :
#include <stdio.h>
#include <math.h>

int a, b, p, pi = 4 * atan(1);

main() {
    printf("donnez a b p : ");
    scanf("%d%d%d", &a, &b, &p);
    for ( ; a <= b; a = a + p)
        printf("sin(%d) = %f\n", a, sin(a * pi / 180));
}

Exo 3

#include <stdio.h>

int nombre, somme, x;
float moyenne; 

main() {
    nombre = 0;
    somme = 0;
    
    printf("? ");
    scanf("%d", &x);
    while (x >= 0) {
        nombre = nombre + 1;
        somme = somme + x;
        
        printf("? ");
        scanf("%d", &x);
    }
    
    if (nombre > 0) {
       moyenne = (float) somme / nombre; 
       printf("la moyenne est : %f\n", moyenne);
    }
    else
       printf("pour calculer une moyenne il faut au moins un nombre!\n");
}
[1] Il ne faut pas se faire avoir par l'énoncé de l'exercice ! Ce n'est pas parce que le sujet parle d'une suite (x0, x1, x2, ...) qu'il faut nécessairement introduire un tableau X et parler de X[0], X[1], X[2], etc. En effet, compte tenu du travail à faire, les valeurs x0, x1, x2, etc. n'ont pas besoin d'exister simultanément et une unique variable simple, appelée ci-dessus x, suffit à les représenter à tour de rôle.
[2] Ce programme est un avatar du problème « traiter une suite de données sans savoir à l'avance combien il y en a ». Sa résolution à l'aide d'une boucle while a été commentée à l'occasion de l'exercice 1.
[3] Les nombres à traiter étant garantis positifs ou nuls, nous n'avons pas eu de mal à déterminer une valeur « intruse » pour indiquer la fin des données. Mais il ne faut pas croire qu'il en sera toujours ainsi ; souvent toutes les données possibles sont valides et on a du mal à trouver comment indiquer la fin de la suite (le moment venu on verra comment mettre à profit des talents cachés de la fonction scanf).
Ce programme illustre un point important mentionné à l'exercice 1 : la marque de fin des données est une donnée invalide qu'il ne faut pas traiter comme les autres. Ici, ce serait une erreur grave que de faire intervenir le nombre négatif de la fin dans le calcul de la moyenne!
[4] Notez enfin un piège subtil qui vous est tendu ici. Les nombres lus étant des entiers, nous avons déclaré de type int les variables x et somme. Tout est fait pour vous pousser à écrire
moyenne = somme / nombre;        Attention, erreur !
C'est une erreur, car somme et nombre étant tous deux entiers, la division faite est le quotient par défaut : ce n'est pas ce qu'il faut pour la moyenne (le fait que le membre gauche de l'affectation, moyenne, soit une variable de type float ne rattrape rien, au moment de l'affectation les décimales sont déjà perdues).

Exo 4

#include <stdlib.h>

#define MAX 100

int table[MAX], nombre, x, i;

main() {
    nombre = 0;
    
    printf("? ");
    scanf("%d", &x);
    while (x >= 0) {
        if (nombre >= MAX) {
            printf("Débordement du tableau");
            break;
        }
        
        table[nombre] = x;
        nombre = nombre + 1;
        
        printf("? ");
        scanf("%d", &x);
    }
    
    printf("Voici le tableau à l'envers:\n");
    for (i = nombre - 1; i >= 0; i = i - 1)
        printf("%d ", table[i]);
    printf("\n");
}
Contrairement à l'exercice précédent, il nous faut ici mémoriser tous les termes de la suite pour pouvoir les afficher à l'envers, d'où la nécessité de déclarer un tableau. Deux points importants dans ce genre de situations :
  • estimer correctement une taille maximum bien adaptée aux jeux de données envisagés,
  • vérifier l'absence de débordement au fur et à mesure que le tableau se remplit.
Ici, la taille maximum nous est explicitement suggérée par l'énoncé, qui indique qu'il n'y aura pas plus de 100 nombres.

Exo 5

[1] Pour commencer il nous faut écarter les solutions inutilement onéreuses, voire ridicules. Chef d'œuvre : lire toutes les données en comptant le nombre d'apparitions de la valeur 0, puis redemander à l'utilisateur de refrapper les données afin de compter le nombre de 1, puis les demander à nouveau pour compter les 2, et ainsi de suite (notre programme sera à la poubelle bien avant le 21ème tour!). [2] Raffinement de la même idée : lire les données en les mémorisant dans un tableau comme à l'exercice précédent, ensuite parcourir ce tableau 21 fois : une fois pour compter le nombre de valeurs égales à 0, une autre pour compter le nombre de 1, encore une pour compter les 2, etc. Ce programme fonctionnerait correctement et, en principe, suffisamment vite du point de vue de l'utilisateur. Mais le tableau des données peut être de taille importante et il est probablement bien peu efficace de le parcourir 21 fois. De plus, comment déclarer ce tableau alors qu'on n'a aucune indication quant à sa taille ?
[3] Voici un programme qui traite les nombres donnés au vol, c'est-à-dire au moment où ils sont lus, sans avoir besoin d'y revenir dessus. Pour cela il nous faut un tableau de 21 compteurs, pour compter séparément le nombre de fois que se sont produits chacun des 21 événements possibles : x = 0, x = 1, x = 2, etc.
Étant donnée une valeur x, une manière de savoir quel compteur doit être incrémenté est une cascade de if :
if (x == 0)
    compteur[0]++;
else if (x == 1)
    compteur[1]++;
else if (x == 2)
    compteur[2]++; 
...
etc. Or, il y a une simplification évidente
compteur[x]++;
d'où notre programme :
#include <stdio.h>

int compteur[21], x, i;

main() {
    for (i = 0; i <= 20; i++)
        compteur[i] = 0;

    printf("? ");
    scanf("%d", &x);
    while (0 <= x && x <= 20) {
        compteur[x]++;

        printf("? ");
        scanf("%d", &x);
    }

    for (i = 0; i <= 20; i++)
        if (compteur[i] > 0)
            printf("nombre de %d : %d\n", i, compteur[i]);
} 

Exo 6

#include <stdio.h>

int p, q, i, j;

main() {
    printf("p q ? ");
    scanf("%d%d", &p, &q);

    for (i = 1; i <= p; i++) {
        for (j = 1; j <= q; j++)
            printf("( %d , %d ) ", i, j);
        printf("\n");
    }
}
Ne cherchez pas la matrice, il n'y en a pas. Cet exercice est une plaisanterie, un entraînement basique sur les boucles imbriquées et la manière d'obtenir un affichage en lignes et colonnes (cela nous servira plus tard). Ce qui se fait par
  • une boucle interne dont chaque itération fait un affichage élémentaire sans aller à la ligne,
  • une boucle externe, dont chaque itération se compose d'une exécution complète de la boucle interne (dont l'affichage de toute une ligne) puis un saut à la ligne suivante.

Exo 7

Il s'agit de réaliser une sorte d'ancêtre (très éloigné) d'un tableur comme Excel.
En première analyse on peut penser que ce problème va nous obliger à parcourir intégralement les lignes et les colonnes d'un tableau rectangulaire (ou matrice) en additionnant les coefficients. En réalité il n'y en a aucun besoin, puisque le tableau est construit de proche en proche, en ne changeant qu'un coefficient à la fois et à partir d'une matrice entièrement faire de zéros.
Il suffira, à chaque lecture d'un coefficient, de mettre à jour la somme de la ligne correspondante, celle de la colonne et la somme générale. Cela ne demande aucun parcours.
Voici notre programme, en supposant que 3 lignes et 4 colonnes sont souhaitées :
#include <stdio.h>

#define NL 3
#define NC 4

float t[NL + 1][NC + 1], x;
int i, j, k;

main() {
    for (i = 0; i <= NL; i++)
        for (j = 0; j <= NC; j++)
            t[i][j] = 0;

    for (;;) {
                        /* affichage du tableau */
        for (i = 0; i < NL; i++) {
            for (j = 0; j < NC; j++)
                printf("%8.2f ", t[i][j]);
            printf("|%8.2f\n", t[i][NC]);
        }
        for (j = 0; j < NC; j++)
            printf("---------");
        printf("+--------\n");
        for (j = 0; j < NC; j++)
            printf("%8.2f ", t[NL][j]);
        printf("|%8.2f\n", t[NL][NC]);

                        /* lecture d'un coefficient */
        printf("\ni j x ? ");
        scanf("%d", &i);
        if (i < 0)
            break;
        scanf("%d%f", &j, &x);

                        /* mise à jour du tableau et des sommes */
        t[i][NC]  = t[i][NC]  - t[i][j] + x;
        t[NL][j]  = t[NL][j]  - t[i][j] + x; 
        t[NL][NC] = t[NL][NC] - t[i][j] + x; 
        t[i][j] = x;
    }
    
    printf("Au revoir...");
}
[1] Le plus grosse partie de ce programme est celle qui affiche le tableau. Nous nous sommes donnés du mal pour obtenir un affichage proche de celui du sujet. Voici ce que cela donne :
La deuxième partie du programme se charge de la lecture de l'indice de ligne, l'indice de colonne et le coefficient lui même. La frappe de -1 pour i produit l'abandon de la boucle for(;;) et donc la fin du programme. Enfin, la troisième partie se charge de mettre à jour les trois sommes concernées, en soustrayant l'ancienne valeur du coefficient (qui vaut 0, la première fois) et en ajoutant la nouvelle valeur.
[2] Ce programme commence par une double boucle qui initialise la matrice avec des zéros. En réalité ce travail est superflu, la matrice ayant été déclarée comme une variable globale, son remplissage par des zéros est garanti lorsque le programme démarre.
[3] Ce n'est qu'un détail, mais il est instructif de comprendre comment fonctionne la lecture des trois variables i, j et x. Nous l'avons découpée en deux appels de scanf de sorte que quand l'utilisateur souhaite arrêter le programme, il lui suffit de donner une valeur négative (avec une instruction comme scanf("%d%d%f", &i, ij, &x) il serait obligé de taper trois valeurs, y compris pour arrêter).
Il est intéressant de constater que cette découpe en deux scanf ne change rien aux lectures normales : l'utilisateur tape trois nombres sur la même ligne (ou sur plusieurs lignes, s'il préfère), le premier scanf consomme la première donnée tapée, le second scanf les deux autres. L'utilisateur ne voit même pas que deux scanf se sont succédés pour lire les trois nombres tapés.

0 comments:

Post a Comment