Tous droits réservés

Premiers pas avec la POO

Publié :

La programmation orientée objet. C’est un terme qu’on rencontre souvent en programmation. Sa définition exacte et son utilisation varient en fonction des points de vue, des langages qui l’utilisent, des contraintes. Une chose néanmoins est sûre, c’est qu’il s’agit d’un paradigme très répandu, très utilisé et trop souvent mal enseigné.

Ce chapitre a pour but de vous introduire à la programmation orientée objet, en définissant les principes qui la régissent et en expliquant ce qu’elle peut nous apporter.

Le principe : des objets bien serviables

Le principe

Penser en terme de données…

Depuis que nous avons commencé ce cours, nous avons pensé et codé en termes de structures de données et d'algorithmes. Nous imaginions notre code comme un suite d’opérations, avec tel algorithme appliqué à telle structure ou tel type, afin d’obtenir telle information en sortie. Le code se concevait et se découpait comme une suite d’instructions et d’opérations.

Prenons un exemple tiré de la vie réelle et parlons d’une laverie (merci à @lmghs pour cet exemple). Dans celle-ci, on met à la disposition du client des machines à laver, mais reste à sa charge de faire le reste. C’est à lui de préparer son linge, d’amener sa lessive, de trier par couleur, de vérifier le poids inséré dans la machine, de lancer celle-ci avec le programme adapté au type de vêtement, de les essorer, etc.

On est typiquement dans le cas d’une solution assez complexe, avec plusieurs opérations à effectuer les unes à la suite des autres, des traitements particuliers (tri du linge, tri par couleur, etc). Tout tourne autour du linge, la « donnée centrale » de notre programme de laverie. Si on devait coder ça, on aurait sans doute une structure pour le linge et plusieurs fonctions comme tri_linge_par_couleur, ajouter_lessive, etc.

…ou de services ?

La programmation orientée objet elle se base sur le principe du service. Plutôt que de fournir une liste de fonctions, d’opérations, d’algorithmes à appliquer, on va abstraire cette complexité technique, la cacher. On va enfermer, camoufler tout ce qui est complexe et n’offrir qu’une interface beaucoup plus simple à utiliser.

Appliquer ce principe à notre laverie revient à créer un pressing. Dans ce cas, toute la complexité de tri par couleur, de bonne quantité de lessive et autres est cachée, déléguée. L’utilisation est beaucoup plus simple. Il suffit de venir, déposer le linge sale et repartir avec du linge propre. Nous n’avons rien d’autre à faire qu’attendre (et accessoirement, payer). Le pressing s’occupe de nous rendre un service « Laver votre linge » et nous nous contentons de l’utiliser, sans chercher plus loin.

On n’a pas besoin de maîtriser les arcanes du nettoyage, des gens/spécialistes en qui on peut avoir confiance vont en assumer la responsabilité. On fait une abstraction et ce principe est une des pierres angulaires de la programmation orientée objet.

En résumé

La programmation orientée objet, c’est créer des services.

Exemple concret

Vous voulez un exemple concret, avec du code ? Ça tombe bien, la bibliothèque standard en est remplie ! Prenez le cas des chaînes de caractères. Plusieurs fois dans le cours, j’ai fait remarquer qu’il existe en C++ un type de chaînes de caractères hérité du C. Dans ce langage, manipuler les chaînes est plus compliqué. Il faut passer par des fonctions pour les concaténer, pour supprimer un caractère, bref, du paradigme impératif typique.

Le type std::string permet quand à lui de manipuler les chaînes de caractères de façon beaucoup plus aisée. C’est la raison pour laquelle on l’utilise tout le temps. Sauf qu’en interne, std::string manipule des chaînes de caractères version C. Simplement voilà, la tambouille qu’il fait pour concaténer deux chaînes ou en vider une troisième ne nous intéresse pas et n’est justement pas exposée. C’est donc un très bon exemple de service.

Un peu de vocabulaire

L’objet

Un objet n’est ni plus ni moins qu'une donnée qu’on manipule. Ainsi, dans le code std::string chaine, chaine n’est ni plus ni moins qu’un objet de type std::string. De même pour std::vector<int> tableau, où notre variable tableau est un objet de type std::vector<int>.

Le type

Afin de définir quels services sont rendus par un objet, il faut définir son modèle, son type. On parle de la classe d’un objet. À partir de cette classe, on pourra créer autant d’objets de ce type qu’on veut. On parle d'instancier la classe. Quand on écrit std::string chaine, on créé une instance du type std::string, instance qui se nomme chaine et qui est notre objet.

Point vocabulaire

Des types très basiques, comme int, double ou bool, peuvent être considérés comme des objets d’un point de vue théorique. En pratique, en C++, on fera la distinction entre ces types dits natifs et les instances de classes, comme std::string.

L’interface d’une classe

Un objet peut nous rendre plus ou moins de services, en fonction de son type. Dans le cas de std::string, on peut accéder au premier caractère avec front, on peut ajouter un caractère à la fin avec push_back, etc. Ces fonctions, qui s’appliquent à une instance donnée d’une classe, sont appelées les fonctions membres. Nous en avions déjà parlé un peu dans le chapitre sur la documentation. C’est elles que nous retrouvons sous le titre Member functions.

Point vocabulaire

Ce terme « fonction membre » est à mettre en relation avec « fonction libre » (ou Non-member functions sur la documentation ), qui désigne les fonctions qui ne font partie d’aucun objet. Toutes les fonctions que nous avons écrites jusque là étaient des fonctions libres. Beaucoup de celles que nous avons aussi utilisé aussi, comme la fonction std::size.

Toutes les fonctions membres d’une classe, ainsi que toutes les fonctions libres qui peuvent manipuler cette classe, composent ce qu’on nomme l’interface d’une classe.

Teaser

L’interface d’une classe regroupe encore d’autres concepts, pas uniquement les fonctions membres et les fonctions libres qui manipule des instances de cette classe. On peut citer, entres autres, la surcharge d’opérateurs. Nous verrons le reste plus tard dans ce cours.

En C++, ça donne quoi ?

Créer des types, ça, nous savons le faire depuis que nous avons découvert les structures. Jusqu’ici, nous les avons traitées comme de simples agrégats de données. Il est temps de changer notre vision des choses et de passer à une approche objet.

Penser services

Reprenons un exemple de la partie II, où nous avions manipulés des fractions. Quels services peut-on attendre de la part d’une fraction ? Comment celle-ci se manipule-t-elle ? Qu’est ce qui fait sens de faire avec une fraction ? La liste ci-dessous décrit les services sur lesquels nous allons travailler. Elle n’est pas exhaustive, mais constitue un bon début.

  • Obtenir sa valeur réelle.
  • La simplifier.
  • Calculer sa puissance au degré xx.

Penser tests

Maintenant que nous avons défini une liste des services que nous aimerions appliquer à un objet de type Fraction, il faut réfléchir aux tests que nous allons écrire pour vérifier que notre implémentation est correcte. Prenez le temps de les écrire, c’est eux qui vous apporterons la garantie que votre code est de qualité.

Ci-dessous, voici les tests que j’ai écrits.

#include <cassert>

int main()
{
    Fraction f1 { 5, 2 };
    assert(f1.valeur_reelle() == 2.5 && "5/2 équivaut à 2.5.");
    f1.simplification();
    assert(f1.numerateur == 5 && f1.denominateur == 2 && "5/2 après simplification donne 5/2.");
    f1 = pow(f1, 2);
    assert(f1.numerateur == 25 && f1.denominateur == 4 && "5/2 au carré équivaut à 25/4.");

    Fraction f2 { 258, 43 };
    assert(f2.valeur_reelle() == 6 && "258/43 équivaut à 6.");
    f2.simplification();
    assert(f2.numerateur == 6 && f2.denominateur == 1 && "258/43 après simplification donne 6/1.");
    f2 = pow(f2, 2);
    assert(f2.numerateur == 36 && f2.denominateur == 1 && "6/1 au carré équivaut à 36/1.");

    return 0;
}

Notez que les tests montrent qu’on aura à manipuler tant des fonctions membres que des fonctions libres.

Définition de la classe

Fonctions membres

Nous sommes maintenant prêts à définir notre classe. Commençons par les fonctions membres. Celles-ci se définissent au sein même de la classe, comme des fonctions classiques.

struct Fraction 
{
    double valeur_reelle();
    void simplification();
};

Le prototype n’a rien d’extraordinaire, mais l’implémentation est quand même un poil plus inhabituelle. En effet, pour écrire l’implémentation d’une fonction membre, il faut préfixer son identifiant par le nom de la classe, avec l’opérateur :: en guise de séparateur. C’est nécessaire pour que le compilateur sache que la fonction définie est membre d’une classe.

struct Fraction 
{
    double valeur_reelle();
    void simplification();
};

double Fraction::valeur_reelle() 
{

}

void Fraction::simplification()
{

}
Un mot sur ::

On retrouve le même principe avec des objets que vous manipulez depuis le premier jour, comme std::string ou std::vector. C’est aussi lui qu’on utilise avec les énumérations. L’opérateur :: est ce qu’on appelle un opérateur de résolution de portée. Il permet de lever toute ambiguïté éventuelle sur l’origine d’un type, d’une fonction ou d’une variable.

Dans le cas de Fraction::valeur_reelle, on indique qu’il existe une fonction valeur_reelle dans la portée Fraction, qui est ici une classe. Dans le cas de std::string, on indique cette fois qu’il existe un type string dans une portée std. Cette fois, ce n’est pas d’une classe dont il s’agit, mais d’un espace de noms (en anglais namespace), qui permet de regrouper plusieurs identificateurs sous une portée commune.

Grâce à ces portées, on peut mieux « cloisonner » son code, ce qui est plus propre. L’espace de nom std permet, par exemple, de regrouper tout ce qui fait partie de la bibliothèque standard. De même, Fraction:: regroupe tout ce qui a trait à notre classe Fraction.

Fonctions libres

Continuons sur notre lancée en définissant des fonctions libres. De la même manière que size est à la fois une fonction membre des classes std::string et std::vector et à la fois une fonction libre, nous sommes libres de faire pareil pour une classe Fraction. Nous devons simplement réfléchir au pour et au contre.

  • Certaines fonctions sont obligatoirement implémentées sous la forme membre, comme l’opérateur [].
  • D’autres sont obligatoirement implémentées sous la forme libre, comme l’opérateur <<.
  • Enfin, pour celles qui peuvent être implémentées tant libres que membres, il faut comparer pour savoir quelle façon de faire est la plus simple. Notamment, si un service peut être rendu en utilisant uniquement d’autres services, mieux vaut l’écrire sous forme de fonction libre. Si, au contraire, il a besoin de manipuler des détails internes, alors il vaut mieux l’implémenter en tant que fonction membre.

Dans le cas d’une fraction, il semble logique d’attendre d’une instance qu’elle nous donne sa valeur réelle et sa version simplifiée. Par contre, mettre une fraction à une puissance xx, c’est comme std::pow, mais pour une fraction. Il parait donc bien d’offrir ce service sous forme de fonction libre. Cela uniformise l’utilisation de pow, qui a alors la même utilisation que ce soit pour un entier ou pour une Fraction.

Fraction pow(Fraction const & fraction, int puissance)
{
}

Les attributs

Maintenant que nous avons défini l’interface, il est temps de se plonger dans les détails d’implémentation. N’oublions pas que, même si nous pensons et concevons services, il faut bien des données à manipuler. Dans notre cas, il s’agit d’un numérateur et d’un dénominateur. On peut maintenant compléter la définition de la classe.

#include <cassert>
#include <cmath>
#include <numeric>

struct Fraction 
{
    // Nos fonctions membres.
    double valeur_reelle();
    void simplification();

    // Nos attributs.
    int numerateur;
    int denominateur;
};

double Fraction::valeur_reelle() 
{
    return static_cast<double>(numerateur) / denominateur;
}

void Fraction::simplification()
{
    int const pgcd { std::gcd(numerateur, denominateur) };
    numerateur /= pgcd;
    denominateur /= pgcd;
}

// Déclaration et implémentation d'un service sous forme libre.
Fraction pow(Fraction const & fraction, int puissance)
{
    Fraction copie { fraction };
    copie.numerateur = std::pow(fraction.numerateur, puissance);
    copie.denominateur = std::pow(fraction.denominateur, puissance);
    return copie;
}
Note sur static_cast

Si l’on divise un entier par un autre entier, on obtient un entier. Notre résultat serait alors faussé, puisqu’on perdrait toute la partie décimale. Pour éviter ce problème, il suffit que l’un des deux opérandes soit de type double. C’est ce que fait static_cast, en indiquant au compilateur que cette variable est en fait un réel et non plus un entier.

Notez que les fonctions membres manipulent sans problème les attributs de la classe. Chaque instance aura ses propres attributs, donc quand j’écris f1.simplification(), c’est le numérateur et le dénominateur de f1 qui sont modifiés et uniquement eux.

Désolé, cet objet n'est pas modifiable

Revenons à un mot-clé que nous connaissons depuis longtemps, que l’on utilise beaucoup mais que j’ai volontairement laissé de côté depuis l’introduction à la POO. Il s’agit de const. Il nous permet de déclarer qu’un objet est constant et ainsi empêcher d’éventuelles modifications. Sauf que si on déclare notre objet Fraction comme étant const, ça ne compile plus.

#include <cmath>
#include <iostream>
#include <numeric>

struct Fraction 
{
    double valeur_reelle();
    void simplification();

    int numerateur;
    int denominateur;
};

double Fraction::valeur_reelle() 
{
    return static_cast<double>(numerateur) / denominateur;
}

void Fraction::simplification()
{
    int const pgcd { std::gcd(numerateur, denominateur) };
    numerateur /= pgcd;
    denominateur /= pgcd;
}

Fraction pow(Fraction const & fraction, int puissance)
{
    Fraction copie { fraction };
    copie.numerateur = std::pow(fraction.numerateur, puissance);
    copie.denominateur = std::pow(fraction.denominateur, puissance);
    return copie;
}

int main()
{
    Fraction const f1 { 5, 2 };
    std::cout << "Voici la valeur réelle de 5/2 : " << f1.valeur_reelle() << "\n"; 
    return 0;
}
[Visual Studio]
E1086   l'objet a des qualificateurs de type incompatibles avec le membre fonction "Fraction::valeur_reelle"
C2662   'double Fraction::valeur_reelle(void)' : impossible de convertir un pointeur 'this' de 'const Fraction' en 'Fraction &'

------------------------------------------------------------

[GCC]
tests.cpp: In function 'int main()':
tests.cpp:48:74: error: passing 'const Fraction' as 'this' argument discards qualifiers [-fpermissive]
   48 |     std::cout << "Voici la valeur réelle de 5/2 : " << f1.valeur_reelle() << "\n";
      |                                                                          ^
tests.cpp:18:8: note:   in call to 'double Fraction::valeur_reelle()'
   18 | double Fraction::valeur_reelle()
      |        ^~~~~~~~
      
------------------------------------------------------------

[Clang]
prog.cc:43:57: error: 'this' argument to member function 'valeur_reelle' has type 'const Fraction', but function is not marked const
    std::cout << "Voici la valeur réelle de 5/2 : " << f1.valeur_reelle() << "\n"; 
                                                       ^~
prog.cc:15:18: note: 'valeur_reelle' declared here
double Fraction::valeur_reelle() 
                 ^
1 error generated.

Le message émis par GCC est un peu moins clair que les autres, mais les trois indiquent que notre fonction membre n’est pas utilisable si l’instance est déclarée comme constante, parce que cette fonction membre ne l’est pas.

Une fonction membre constante, c’est une fonction qui promet de ne pas modifier l’objet auquel elle appartient. Par exemple, dans le cas de std::vector, la fonction membre size retourne simplement la taille du tableau. Cette fonction donc est déclarée comme étant constante et on peut l’utiliser sur des tableaux constants. Au contraire, push_back modifie le tableau et n’est donc utilisable que sur un tableau non const.

Le mieux est d’avoir le plus de fonctions constantes possibles. Moins on modifie de choses, moins on a de chance d’introduire des erreurs et des bugs. Il convient donc de réfléchir, dès la conception d’une classe, de façon à maximiser les fonctions constantes. Ici, on voit que valeur_reelle est un très bon candidat pour ça.

Pour qu’une fonction membre soit utilisable même quand l’objet est constant, il faut qu’elle soit explicitement marquée comme constante. Cela se fait très simplement, en rajoutant le mot-clef const après la signature de la fonction en question, tant dans le prototype que dans l’implémentation.

struct Fraction 
{
    double valeur_reelle() const;
};

// Ne pas oublier const dans l'implémentation aussi.
double Fraction::valeur_reelle() const
{
    return static_cast<double>(numerateur) / denominateur;
}

Bien sûr, certaines fonctions ne pourront jamais être const. Par exemple, vouloir déclarer simplification comme étant constante n’a aucun sens, puisque c’est sa raison d’être que de modifier l’instance sur laquelle elle est appelée. Si vous essayez, le compilateur va vous envoyer balader.

[Visual Studio]
C3490   impossible de modifier 'numerateur' car il est accessible via un objet const
C3490   impossible de modifier 'denominateur' car il est accessible via un objet const

------------------------------------------------------------

[GCC]
tests.cpp: In member function 'void Fraction::simplification() const':
tests.cpp:24:16: error: assignment of member 'Fraction::numerateur' in read-only object
   24 |     numerateur /= pgcd;
      |     ~~~~~~~~~~~^~~~~~~
tests.cpp:25:18: error: assignment of member 'Fraction::denominateur' in read-only object
   25 |     denominateur /= pgcd;
      |     ~~~~~~~~~~~~~^~~~~~~

------------------------------------------------------------

[Clang]
prog.cc:24:16: error: cannot assign to non-static data member within const member function 'simplification'
    numerateur /= pgcd;
    ~~~~~~~~~~ ^
prog.cc:21:16: note: member function 'Fraction::simplification' is declared const here
void Fraction::simplification() const
~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~
prog.cc:25:18: error: cannot assign to non-static data member within const member function 'simplification'
    denominateur /= pgcd;
    ~~~~~~~~~~~~ ^
prog.cc:21:16: note: member function 'Fraction::simplification' is declared const here
void Fraction::simplification() const
~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~
2 errors generated.

On ne fait pas d'exception

Dans le chapitre sur la documentation, nous avons vu qu’il existe une section Exceptions, qui nous indique si une fonction peut potentiellement lancer une exception, ou non. Pour celles qui ont la garantie de ne rien lancer, il y a le mot-clé noexcept. Nous avons la possibilité de faire de même pour les classes que nous créons.

Par exemple, la fonction membre valeur_reelle consiste en une simple division et ne lève donc jamais d’exception. De même, simplification ne fait appel qu’à std::gcd en interne. Si on regarde la documentation, on a la garantie que std::gcd ne lance jamais d’exception non plus. Quant à std::pow, aucune information n’est donnée. Nous la laisserons telle quelle.

Comme pour const, le mot-clé noexcept s’écrit deux fois, tant après le prototype au sein de la classe qu’après la signature au moment de l’implémentation.

#include <cmath>
#include <iostream>
#include <numeric>

struct Fraction 
{
    double valeur_reelle() const noexcept;
    void simplification() noexcept;

    int numerateur;
    int denominateur;
};

double Fraction::valeur_reelle() const noexcept
{
    return static_cast<double>(numerateur) / denominateur;
}

void Fraction::simplification() noexcept
{
    int const pgcd { std::gcd(numerateur, denominateur) };
    numerateur /= pgcd;
    denominateur /= pgcd;
}

Fraction pow(Fraction const & fraction, int puissance)
{
    Fraction copie { fraction };
    copie.numerateur = std::pow(fraction.numerateur, puissance);
    copie.denominateur = std::pow(fraction.denominateur, puissance);
    return copie;
}

int main()
{
    Fraction const f1 { 5, 2 };
    std::cout << "Voici la valeur réelle de 5/2 : " << f1.valeur_reelle() << "\n"; 
    return 0;
}

Découpage en fichiers

Bien que tout écrire dans un fichier soit plus rapide et bien plus rapide pour des codes d’exemples, dans la réalité, on sépare les définitions des implémentations, vous le savez depuis le chapitre sur le découpage en fichier. Le principe est le même que ce que vous connaissez.

Fraction.hpp
#ifndef FRACTION_HPP
#define FRACTION_HPP

struct Fraction 
{
    double valeur_reelle() const noexcept;
    void simplification() noexcept;

    int numerateur;
    int denominateur;
};

Fraction pow(Fraction const & fraction, int puissance);

#endif
Fraction.cpp
#include <cmath>
#include <numeric>

#include "Fraction.hpp"

double Fraction::valeur_reelle() const noexcept
{
    return static_cast<double>(numerateur) / denominateur;
}

void Fraction::simplification() noexcept
{
    int const pgcd { std::gcd(numerateur, denominateur) };
    numerateur /= pgcd;
    denominateur /= pgcd;
}

Fraction pow(Fraction const & fraction, int puissance)
{
    Fraction copie { fraction };
    copie.numerateur = std::pow(fraction.numerateur, puissance);
    copie.denominateur = std::pow(fraction.denominateur, puissance);
    return copie;
}
main.cpp
#include <iostream>
#include "Fraction.hpp"

int main()
{
    Fraction const f1 { 5, 2 };
    std::cout << "5/2 = " << f1.valeur_reelle() << "\n";
    return 0;  
}

Pour la suite du cours, la majorité des exemples resteront écrits dans un seul fichier, par simplicité et facilité. Néanmoins, quand vous commencerez des projets plus gros, découpez et vous gagnerez en lisibilité et compréhension.

Exercices

Cercle

Imaginons que l’on veuille représenter un cercle. De nombreuses opérations sont possibles.

  • On peut calculer sa surface.
  • On peut calculer son périmètre.
  • On peut déterminer si un point fait partie du cercle ou non.

Si on se base sur ces informations, alors on peut déjà imaginer les tests qui suivent.

#include <cassert>
#include <iostream>

bool compare(double lhs, double rhs)
{
    return std::abs(lhs - rhs) < 0.001;
}

int main()
{
    Point const centre { 0, 0 };
    Cercle const c1 { 1, centre };

    std::cout << "Voici l'aire du cercle : " << c1.aire() << "\n";
    assert(compare(c1.aire(), 3.1415) && "L'aire du cercle c1 doit valoir pi.");

    std::cout << "Voici le périmètre du cercle : " << c1.perimetre() << "\n"; 
    assert(compare(c1.perimetre(), 3.1415 * 2) && "Le périmètre du cercle doit valoir pi * 2.");

    std::cout << std::boolalpha;
    Point const p1 { 2, -18 };
    std::cout << "Est-ce que le point p1 appartient au cercle c1 ? " << appartient(c1, p1) << "\n";
    assert(!appartient(c1, p1) && "Le point p1 n'appartient pas au cercle c1.");
    return 0;
}
Un mot sur la fonction compare

Je vous ai ajouté une fonction compare, qui permet de dire si deux nombres réels sont égaux en ne se basant que sur les trois premières décimales. Sans ça, si on les compare directement avec ==, alors la comparaison se fera sur les milliers de décimales qui composent nos double.

On risque d’avoir des résultats faussés, car le programme nous dira alors que deux nombres sont différents car leurs 20000ème décimales sont différentes, ce qui est négligeable à notre niveau.

Indices

Petits rappels mathématiques.

  • L’aire d’un cercle de rayon rr vaut r2×πr^2 \times \pi.
  • Le périmètre d’un cercle de rayon rr vaut 2×r×π2 \times r \times \pi.
  • La distance entre deux points P1(x;y)P1 (x;y) et P2(a;b)P2 (a;b) se calcule avec la formule (xa)2+(yb)2\sqrt{(x - a)^2 + (y - b)^2}.
Correction

Comme un cercle a un point central, j’ai séparé les opérations concernant les points de celles concernant les cercles. En effet, le calcul de la distance entre deux points n’est pas une opération réservée aux seuls cercles, mais peut être effectuée pour n’importe quel point.

J’ai aussi décidé de fournir le service distance et appartient comme des fonctions libres, car, de mon point de vue, ces opérations ne sont pas « liées à une instance en particulier », mais sont générales. C’est un choix de conception.

Point.hpp
#ifndef POINT_HPP
#define POINT_HPP

struct Point
{
    int x;
    int y;
};

double distance(Point const & lhs, Point const & rhs);

#endif
Point.cpp
#include <cmath>
#include "Point.hpp"

double distance(Point const & lhs, Point const & rhs)
{
    return std::sqrt(std::pow(lhs.x - rhs.x, 2) + std::pow(lhs.y - rhs.y, 2));
}
Cercle.hpp
#ifndef CERCLE_HPP
#define CERCLE_HPP

#include <cmath>
#include "Point.hpp"

struct Cercle
{
    double aire() const noexcept;
    double perimetre() const noexcept;

    double rayon;
    Point centre;
};

bool appartient(Cercle const & cercle, Point const & point);

// Il n'y a malheureusement pas (encore) de constante PI dans la bibliothèque standard.
// On doit donc créer la notre en utilisant la propriété que arcosinus(-1) vaut PI. 
double const pi { std::acos(-1) };

#endif
Cercle.cpp
#include "Cercle.hpp"

double Cercle::aire() const noexcept
{
    return rayon * rayon * pi;
}

double Cercle::perimetre() const noexcept
{
    return 2 * rayon * pi;
}

bool appartient(Cercle const & cercle, Point const & point)
{
    return distance(cercle.centre, point) <= cercle.rayon; 
}

Informations personnelles

Vous souvenez-vous de la structure InformationsPersonnelles, que nous avons écrite au chapitre 5 de la partie II ? Pourrait-on la réécrire avec un point de vue objet ? Quels services pourrait-elle rendre ? Quels invariants pourraient être mis en place ? Je laisse ces questions ouvertes, à vous de décider. :)


En résumé

  • La programmation orientée objet, c’est penser en termes de services rendus et non en terme de données.
  • Une classe est un type personnalisé que l’on définit comme on veut.
  • On parle d’instancier une classe quand on crée un objet de ce type.
  • L’interface d’une classe comprend notamment les fonctions membres et les fonctions libres qui manipulent les instances de cette classe.
  • Seules les fonctions membres constantes peuvent manipuler des instances déclarées const.
  • Les fonctions membres doivent être déclarées constantes le plus possible.
  • De la même manière, il est bien de déclarer noexcept toutes les fonctions membres qui le sont.
  • La séparation en fichiers nous permet de séparer la définition d’une classe de l’implémentation de ses fonctions membres.