TD 4 : Des fonctions, des tests et de la documentation

TD 4 : Des fonctions, des tests et de la documentation#

Notes aux enseignants

La notion d’appel de fonction vs définition de la fonction n’est pas simple pour les débutants. Une erreur classique est d’indiquer le type au moment de l’appel (ex : cout << int factorielle(3);). Faire le parallèle avec les variables : on déclare le type d’une variable une fois pour toutes et ensuite on l’utilise sans le rappeler (ex : cout << x;) et c’est pareil avec les fonctions.

Une autre difficulté est de comprendre ce qu’est réellement un paramètre, et la différence entre paramètre et variable locale. Par exemple dans l’exercice 1 beaucoup d’élèves ont tendance à mettre pi en paramètre.

Exercice 1 : Premières fonctions

Voici une fonction qui calcule la surface d’un rectangle :

float surfaceRectangle(float longueur, float largeur) {
    return longueur * largeur;
}
  1. Définissez une fonction surfaceDisque qui calcule la surface d’un disque de rayon donné. On prendra \(\pi=3,1415926\).

    /// BEGIN SOLUTION

    float surfaceDisque(float rayon) {
        return 3.1415926 * rayon * rayon;
    }
    

    /// END SOLUTION

  2. Définissez une fonction surfaceTriangle qui calcule la surface d’un triangle de base et de hauteur données.

    /// BEGIN SOLUTION

    float surfaceTriangle(float base, float hauteur) {
        return base * hauteur / 2;
    }
    

    /// END SOLUTION

Exercice 2 : En route vers l’exponentielle

  1. Nous avons vu en cours et en TP une fonction factorielle(n) qui calcule la factorielle d’un entier positif \(n\). Pour un exercice du TP à venir, et pour éviter les problèmes de dépassement de capacité, il est souhaitable que les calculs intermédiaires et le résultat soient des double. Adaptez en conséquence la fonction factorielle.

    /// BEGIN SOLUTION

    /** La fonction factorielle
     *  @param n un nombre entier positif
     *  @return n! comme un nombre à virgule flottante à double précision
     **/
    double factorielle(int n) {
        double resultat = 1;
        for ( int k = 1; k <= n; k++ ) {
            resultat = resultat * k;
        }
        return resultat;
    }
    

    /// END SOLUTION

  2. On considère la fonction dont la documentation et l’entête sont donnés ci-dessous :

    /** La fonction puissance
     *  @param a un nombre à virgule flottante en double précision
     *  @param n un nombre entier positif
     *  @return la n-ième puissance a^n de a
     **/
    

    Quels sont les types de ses paramètres formels et de sa valeur de retour?

    /// BEGIN SOLUTION

    Le paramètre formel a est de type double, le paramètre formel n est de type int, la valeur de retour est de type double.

    /// END SOLUTION

  3. Écrivez quelques exemples d’utilisation de la fonction puissance. Éditez-les sous forme de tests, en vous inspirant du test suivant pour la fonction surfaceRectangle:

        CHECK( surfaceRectangle(4, 5) == 20 );
    

    /// BEGIN SOLUTION

        CHECK( puissance(2.5, 0) == 1.0  );
        CHECK( puissance( 35, 1) == 35.0  );
        CHECK( puissance( -2, 4) == 16.0  );
        CHECK( puissance( 10, 5) == 100000.0  );
    

    /// END SOLUTION

  4. Définissez la fonction puissance.

    /// BEGIN SOLUTION

    /** La fonction puissance
     *  @param a un nombre à virgule flottante en double précision
     *  @param n un nombre entier positif
     *  @return la n-ième puissance a^n de a
     **/
    

    /// END SOLUTION

  5. Cherchez dans les notes de cours la sémantique simplifiée de l’appel d’une fonction.

  6. Exécutez pas à pas le programme suivant :

        int k = 3;
        double x = 6;
        double a = 4;
        double resultat = puissance(x - a, k) / factorielle(k);
        cout << resultat << endl;
    

    Quelle est la valeur de la variable resultat à la fin?

    /// BEGIN SOLUTION

    Les trois premières lignes initialisent les variables k, x et a. Ensuite on calcule puissance(x - a, k) / factorielle(k), qu’on stocke dans resultat.

    Au moment du calcul, les expressions passées aux fonctions auxiliaires sont remplacées par leur valeur. Ainsi l’appel effectif est puissance(2, 3) / factorielle(3).

    Il ne reste plus qu’à exécuter pas à pas la fonction puissance avec \(2\) et \(3\) comme valeurs initiales pour ses paramètres formels a et n: on obtient \(8.0\) comme valeur de retour.

    On fait de même avec factorielle avec \(3\) comme valeur initiale pour le paramètre formel n, ce qui donne \(6.0\).

    Il ne reste qu’à faire la division 8.0 / 6.0, ce qui donne environ \(1.333\), et c’est la valeur de resultat.

    /// END SOLUTION

  7. \(\clubsuit\) Définissez les fonctions factorielle et puissance en récursif cette fois, puis refaites l’exécution pas à pas. Qu’est-ce qui change?

    /// BEGIN SOLUTION

    /** La fonction factorielle récursive
     *  @param n un nombre entier positif
     *  @return n! comme un nombre à virgule flottante à double précision
     **/
    double factorielle(int n) {
        double resultat = n;
        if ( n == 0 ) {
        	resultat = 1;
        } else {
        	resultat = resultat * factorielle(n-1);
        }
        return resultat;
    }
    

    /// END SOLUTION

    /// BEGIN SOLUTION

    /** La fonction puissance récursive
     *  @param a un nombre à virgule flottante en double précision
     *  @param n un nombre entier positif
     *  @return la n-ième puissance a^n de a
     **/
    double puissance(double a, int n) {
        if ( n == 0 ) {
        	return 1;
        } else {
        	return a * puissance(a , n - 1);
        }
    }
    

    /// END SOLUTION

Notes aux enseignants

Les étudiants ont vu en cours la sémantique détaillée de l’appel de fonctions, mais c’est encore fragile. Dans l’exercice suivant, forcer les étudiants à suivre pas à pas les étapes décrites dans la sémantique de l’appel de fonction, en utilisant la même représentation de la pile que dans le poly. À corriger soigneusement au tableau, avec le même formalisme que dans le cours (donc pas comme la présentation compacte utilisée dans la correction ci-dessous).

À faire

Dessins de pile dans les corrections. 2024-2025?

Mettre à jour la correction pour utiliser le même type de représentation que dans le poly (voir au passage comment on peut produire des représentations de ce type en markdown).

Exercice 3 : Variables locales/globales, pile et exécution pas à pas

On considère les deux programmes suivants :

int i = 0;
int f(int j) {
    i = i + j;
    return i;
}

int resultat = f(1) + f(2) + f(3);
int f(int j) {
    int i = 0;
    i = i + j;
    return i;
}

int resultat = f(1) + f(2) + f(3);
  1. Mettez en évidence les différences entre les deux programmes (par exemple au surligneur).

  2. Cherchez dans les notes de cours la sémantique détaillée de l’appel d’une fonction (formalisation suivant le modèle d’exécution).

  3. Exécutez pas à pas les deux programmes en décrivant au fur et à mesure l’état de la mémoire (pile). Quelle est la valeur des variables i et resultat à la fin de l’exécution?

  4. Décrivez la différence de comportement entre ces programmes, et retrouvez dans les notes de cours le commentaire à ce propos.

/// BEGIN SOLUTION

À faire

Solution exercice 3 à rédiger

/// END SOLUTION

Exercice 4 : La trilogie code, documentation, tests

Analysez la fonction volumePiscine suivante :

/** Calcule le volume d'une piscine parallélépipédique
 *  @param profondeur la profondeur de la piscine (en mètres)
 *  @param largeur la largeur de la piscine (en mètres)
 *  @param longueur la longueur de la piscine (en mètres)
 *  @return le volume de la piscine (en litres)
 **/
double volumePiscine(double profondeur, double largeur, double longueur) {
    return 100 * profondeur * largeur * longueur;
}

Munie des tests :

    CHECK( volumePiscine(5, 12, 5) == 30000 );
    CHECK( volumePiscine(1,  1, 5) == 500   );
  1. Est-ce que les tests passent?

    /// BEGIN SOLUTION

    Oui!

    /// END SOLUTION

  2. La documentation, le code et les tests sont-ils cohérents?

    /// BEGIN SOLUTION

    Les tests et l’implantation ne sont pas cohérents avec la documentation, car cette dernière spécifié que le résultat doit être exprimé en litres, alors que dans le test et l’implantation le résultat calculé est exprimé en décalitres.

    /// END SOLUTION

  3. Corrigez les anomalies éventuelles.

    /// BEGIN SOLUTION

    Le facteur de conversion devrait être de \(10^3=1000\) et pas de \(100\).

    /// END SOLUTION

Exercice 5 : \(\clubsuit\)

Analysez la fonction mystere suivante :

string mystere(int blop) {
    string schtroumpf = "";
    for ( int hip = 1; hip <= blop; hip++ ) {
        for ( int hop = 1; hop <= hip; hop++ ) {
            schtroumpf += "*";
        }
        schtroumpf += "\n";
    }
    return schtroumpf;
}

Munie des tests :

    CHECK( mystere(0) == ""             );
    CHECK( mystere(1) == "*\n"          );
    CHECK( mystere(2) == "*\n**\n"      );
    CHECK( mystere(3) == "*\n**\n***\n" );
  1. Comment fait-on appel à cette fonction (quelle est sa syntaxe)?

    /// BEGIN SOLUTION

    La signature de la fonction :

    string mystere(int blop);
    

    nous indique que la fonction attend un seul paramètre entier (blop). On écrit donc mystere(x)x est un entier pour appeler la fonction.

    /// END SOLUTION

  2. Que fait cette fonction (quelle est sa sémantique)?
    Indications : pour les chaînes de caractères, l’opérateur + représente la concaténation (par exemple "Cou" + "cou" a pour valeur "Coucou"); comme pour les entiers, x += expression est un raccourci pour x = x + expression; enfin, dans une chaîne de caractères, «\n» représente un saut de ligne.

    /// BEGIN SOLUTION

    La fonction crée une chaîne de caractères représentant un triangle d’étoiles ayant blop lignes et la renvoie. La boucle externe se charge des lignes, tandis que la boucle imbriquée se charge des colonnes.

    Ainsi, mystere(4) renvoie la chaîne de caractères :

    *
    **
    ***
    ****
    

    /// END SOLUTION

  3. Ré-écrivez la fonction en choisissant des noms pertinents pour la fonction et ses variables et en la faisant précéder de sa documentation.

    /// BEGIN SOLUTION

    /** Triangle d'étoiles
     *  @param nbLignes un nombre entier positif
     *  @return une chaîne de caractère représentant un triangle d'étoiles
     *    ayant nbLignes lignes
     **/
    string triangleEtoiles(int nbLignes) {
        string resultat = "";
        for ( int ligne = 1; ligne <= nbLignes; ligne++ ) {
            for ( int etoile = 1; etoile <= ligne; etoile++ ) {
                resultat += "*";
            }
            resultat += "\n";
        }
        return resultat;
    }
    

    /// END SOLUTION

Exercice 6 : \(\clubsuit\)

Le but de cet exercice est de coder une fonction point_de_chute qui calcule l’abscisse \(x_c\) à laquelle tombe un projectile lancé en \(x = 0\) avec une vitesse \(v\) suivant un angle \(\alpha\) (exprimé en degrés par rapport à l’horizontale). Définissez la fonction point_de_chute. On commencera par écrire sa documentation ainsi que des tests (voir TD 1).

Rappels :

  • l’abscisse est donnée par la formule : \(x_c=(2v_xv_y)/g\)\(v_x = v\cos(\alpha)\), \(v_y = v\sin(\alpha)\) et \(g\) est l’accélération gravitationnelle (environ \(\SI{9,8}{\meter\per\second\squared}\) sur la planète Terre).

  • en C++, les fonctions mathématiques sinus et cosinus sont implantées par les fonctions prédéfinies sin(arg) et cos(arg) dans <cmath>, où l’angle arg est exprimé en radians.

/// BEGIN SOLUTION

/** Fonction qui calcule le point de chute d'un objet lancé
 *  @param v la vitesse de lancer en m/s
 *  @param angle l'angle de lancer en degrés, entre 0 et 180
 *  @return l'abscisse du point de chute comme un nombre à virgule flottante
 *          à double precision
 **/
double pointDeChute(int v, int angle) {
    double angleRadians = angle * 3.141592 / 180;
    return 2 * v * cos(angleRadians) * v * sin(angleRadians) / 9.8;
}

void pointDeChuteTest() {
    CHECK(pointDeChute(3, 0) == 0);
    CHECK(pointDeChute(3, 90) == 0);
    CHECK(pointDeChute(0, 45) == 0);
    CHECK(pointDeChute(3, 30) + pointDeChute(3, 150) == 0);
}

/// END SOLUTION

Exercice 7 : \(\clubsuit\)

Le but de cet exercice est de calculer la hauteur en fonction du temps \(z(t)\) à laquelle se trouve un pot de fleur (\(m=\SI{3}{\kilogram}\)) lâché à \(t=0\) depuis le 10ème étage (\(h_0=\SI{27}{\meter}\)), en chute libre avec résistance de l’air; puis de calculer le temps de chute.

  1. Définissez une fonction chute_libre(t) calculant \(z(t)\) pour un \(V_0\) donné (\(V_0=\SI{80}{\meter\per\second}\)).
    Indications :

    • La hauteur s’exprime en fonction du temps par

      \[z(t)=h_0-(V_0t+\frac{V_0^2}{g} \ln\left(\frac{1}{2}\left(1+e^{-2tg/V_0}\right)\right)\,,\]

      \(V_0\) est la vitesse limite de chute de l’objet et \(g=\SI{9.81}{\meter\per\second\squared}\).

    • La fonction logarithme népérien est prédéfinie sous la forme log(arg) dans <cmath>.

    /// BEGIN SOLUTION

    /** Calcul de la hauteur d'un objet en chute libre
     *  @param time: le temps de chute
     *  @return la hauteur de l'objet au temps time
     **/
    double chute_libre_simple(double t) {
        double g = 9.81;
        double Vo=80;
        double ho=27;
        return ho - Vo*t - Vo*Vo/g*log( (1 + exp(-2*t*g/Vo)) / 2 );
    }
    

    /// END SOLUTION

  2. Que se passe-t-il si on varie \(h_0\) et \(V_0\)? Généralisez votre fonction pour prendre en paramètres additionnels la hauteur initiale \(h_0\) et la vitesse limite de chute \(V_0\). Pour la gravité, définir une variable globale \(g\).
    Bonus : définir cette variable globale comme une constante (nous irons sur Mars une autre fois).

    /// BEGIN SOLUTION

    double g = 9.81;
    
    /** Calcul de la hauteur d'un objet en chute libre
     *  @param time: le temps de chute
     *  @param h0: le hauteur au début de la chute
     *  @param V0: la vitesse limite
     *  @return la hauteur de l'objet au temps time
     **/
    double chute_libre(double t, double ho, double Vo) {
        return ho - Vo*t - Vo*Vo/g*log( (1 + exp(-2*t*g/Vo)) / 2 );
    }
    

    /// END SOLUTION

    Écrivez les appels à la fonction précédente pour calculer \(z(t)\) pour \(t=\SI{2}{s}\) et pour différentes valeurs de \(V_0\): \(10\), \(40\), \(60\), \(120\), \(\SI{180}{\meter\per\second}\).

    /// BEGIN SOLUTION

    chute_libre(2,27,10);
    chute_libre(2,27,40);
    chute_libre(2,27,60);
    chute_libre(2,27,120);
    chute_libre(2,27,180);
    

    /// END SOLUTION

  3. Écrivez une fonction temps_de_chute qui prend les même paramètres que précédemment et utilise chute_libre de façon répétée pour déterminer une approximation de la durée \(t_c\) de la chute du pot de fleur jusqu’au sol.

    /// BEGIN SOLUTION

    /** Calcul d'une approximation du temps de chute
     *  @param precision: la precision attendue, un reel strictement positif
     *  @param h0: le hauteur au début de la chute
     *  @param V0: la vitesse limite
     *  @return une approximation du temps de chute
     **/
    double temps_de_chute(double precision, double ho, double Vo) {
        double t1=0;
        double t2=1;
        double t3;
        //on commence par chercher un temps t2 qui soit plus grand que
        //le temps de chute,
        //le temps t1 doit lui rester plus petit que le temps de chute
        while (chute_libre(t2,ho,Vo)>0){
            t1=t2;
    	t2=2*t2;
        }
        //on cherche ensuite t par dichotomie
        while (t2-t1>precision){
            t3=(t1+t2)/2;
            if (chute_libre(t3,ho,Vo)<0){
    	  t2=t3;
    	} else {
    	  t1=t3;
    	}
        }
        return t1;
    }
    

    /// END SOLUTION

  4. La vitesse limite peut être obtenue en fonction de la masse volumique de l’air \(\rho\), du coefficient de résistance aérodynamique \(C_x\) et de la section de l’objet \(S\) à l’aide de la formule \(V_0=\sqrt{\frac{2mg}{C_x \rho S}}\). Définissez une fonction vitesse_limite pour calculer cette formule. Puis implantez de nouvelles fonctions utilisant les précédentes pour calculer \(z(t)\) et le temps de chute \(t_c\) en fonction des paramètres \(C_x\), \(S\), et \(m\). On suppose que \(\rho\) est une variable globale déjà définie.

    /// BEGIN SOLUTION

    double vitesse_limite(double Cx, double S, double m){
      return sqrt(2*m*g/(Cx*rho*S));
    }
    

    /// END SOLUTION

Exercice 8 : \(\clubsuit\) Triangles rectangles à côtés entiers

Inspiré du problème 39 du Projet Euler, Integer right triangles

Pour un périmètre \(p=120\) donné, il n’existe que trois configurations pour un triangle rectangle dont les côté sont de longueurs entières : \(\{20, 48, 52\}, \{24, 45, 51\}, \{30, 40, 50\}\).

Documentez et écrivez une fonction nombreTrianglesRectanglesEntiers qui prend en entrée un entier, le périmètre fixé, et renvoie le nombre de configurations possibles de triangles rectangles à côtés de longueurs entières.

Indications : On pensera à écrire des fonctions intermédiaires utiles, par exemple une fonction qui vérifie que la longueur de l’hypothénuse d’un triangle rectangle est entière et renvoie sa valeur à partir des longueurs de ses deux autres côtés, ou une fonction qui vérifie si la somme des longueurs des trois côtés vaut bien le périmètre fixé. Ne pas oublier de prévoir des tests automatiques pour chaque fonction qui le permet.

/// BEGIN SOLUTION

/** Une fonction qui calcule la longueur de l'hypothénuse d'un triangle 
 *  rectangle à partir des longueurs de ses deux autres côtés
 * @param x un entier, longueur d'un côté du triangle
 * @param y un entier, longueur d'un côté du triangle
 * @return la longueur de l'hypothénuse si elle est entière, -1 sinon
 * */
int longueurHypothenuse(int x, int y) {
    int carreHypothenuse = x*x + y*y;
    int racine = 1;
    while (racine * racine < carreHypothenuse) {
        racine++;
    }
    if (racine * racine == carreHypothenuse) {
        return racine;
    } else {
        return -1;
    }
}

/** Une fonction qui calcule le paramètre d'un triangle à partir des longueurs 
 *  de ses côtés
 * @param x un entier, longueur d'un côté du triangle
 * @param y un entier, longueur d'un côté du triangle
 * @param z un entier, longueur d'un côté du triangle
 * @return le périmètre du triangle
 * */
int perimetre(int x, int y, int z) {
    return x + y + z;
}

/** Une fonction qui renvoie le nombre de triangles rectangles entiers de 
 *  périmètre donné
 * @param p un entier positif, le périmètre donné
 * @return le nombre de triangles rectangles de côtés entiers de périmètre p
 * */
int nombreTrianglesRectanglesEntiers(int p) {
    int c;
    int nombre = 0;
    for ( int b = 1; b <= p / 2; b++ ) {
        for ( int a = 1; a <= b; a ++ ){ 
            c = longueurHypothenuse(a, b);
            if ((c > 0) && (perimetre(a, b, c) == p)){
                nombre++;
            }
        }
    }
    return nombre;
}

Avec les tests :

    CHECK( perimetre(1, 2, 3) == 6 );
    CHECK( perimetre(15, 5, 3) == 23 );

    CHECK( longueurHypothenuse(20, 48) == 52 );
    CHECK( longueurHypothenuse(24, 45) == 51 );

    CHECK( nombreTrianglesRectanglesEntiers(120) == 3 );

/// END SOLUTION