Choisissez votre style : colorisé, impression

Série 19 :
Surcharge d'opérateurs.

Exercice 0  : illustration de la surcharge dans le cas des polynômes (POO, niveau 0)

Introduction

Le but de cet exercice est d'illustrer les notions de constructeur et de surcharge d'opérateur.

On cherche à définir une classe Polynome permettant de représenter et manipuler des polynômes réels.

Définition de la classe

Commencez par ouvrir le fichier Polynome.cc et définissez y la classe Polynome contenant un tableau dynamique de nombres réels.

Ce tableau stockera les coefficients du polynôme.
Par exemple 2.5 X4 + X + 9.2 sera stocké sous la forme du tableau de coefficients {9.2, 1.0, 0, 0, 2.5}.

Ajoutez y la méthode publique degre() retournant le degré du polynôme :

#include <vector>
using namespace std;

typedef int Degre;

class Polynome {
public:
  Degre degre() const { return p.size()-1; }

private:
  vector<double> p;

};

Constructeurs

Il nous faut maintenant définir les constructeurs. Il nous faut au moins le constructeur par défaut et le constructeur de copie :

class Polynome {
public:
  Polynome(); // constructeur par defaut
  Polynome(Polynome const &); // copy-constructor

  Degre degre() const { return p.size()-1; }

private:
  vector<double> p;
};

Pour le construteur par défaut, choisissons que ce soit le polynôme nul qui soit construit:

Polynome::Polynome() : p(1, 0.0) {}
(revoir si nécessaire l'initialisation des vector)

Concernant le constructeur de copie, pas de surprise, on copie les données en utilisant simplement le constructeur de copie de la classe vector :

Polynome::Polynome(const Polynome& autre) : p(autre.p) {}

On peut maintenant aussi ajouter d'autres constructeurs « pratiques », comme par exemple le plongement du corps des réels dans l'anneau des polynômes réels (c.-à-d. que tout nombre réel peut être vu comme un polynôme de degré 0), ce qui permet par exemple d'écrire :

 Polynome un_polynome(2.3); 

On écrirait alors pour cela :

class Polynome {
public:
  Polynome(); // constructeur par defaut
  Polynome(Polynome const &); // copy-constructor
  Polynome (double); // plongement du corps des reels

mais on peut faire mieux et utiliser la valeur par défaut des arguments, pour fusionner le constructeur par défaut et ce dernier constructeur :
class Polynome {
public:
	Polynome (double = 0.0); //ici
  Polynome(Polynome const &); 
  ...

Sa définition ne change pas fondamentalement du constructeur par défaut précédent :

Polynome::Polynome(double x) : p(1, x) {}

On peut pour finir avec les constructeurs ajouter une façon de déclarer un monôme de degré quelconque. Par exemple p=3X^2 pourrait s'écrire :

Polynome p(3.0, 2);

3.0 est le coefficient et 2 le degré.

Ceci est possible en complétant le constructeur par défaut :

class Polynome {
public:
  Polynome(double coef = 0.0, unsigned int degre = 0);
  Polynome(Polynome const &); // copy-constructor
  ...
avec pour définition
Polynome::Polynome(double coef, unsigned int deg)
 : p(deg+1, 0.0) // garantit que le degr'e de p est deg
{
  p[deg] = coef; // change la valeur du coef de plus haut degre
}

Pour résumer, à ce stade nous avons :

#include <vector>
using namespace std;

typedef int Degre;

class Polynome {
public:
  // constructeurs
  Polynome(double coef = 0.0, unsigned int degre = 0);
  Polynome(Polynome const &); // copy-constructor

  // methodes publiques
  Degre degre() const { return p.size()-1; }

private:
  // attributs privés
  vector<double> p;
};

Polynome::Polynome(double coef, unsigned int deg)
 : p(deg+1, 0.0) // garantit que le degr'e de p est deg
{
  p[deg] = coef; // change la valeur du coef de plus haut degre
}

Polynome::Polynome(const Polynome& autre) : p(autre.p) {}

Opérateurs

Ajoutons maintenant quelques opérateurs simples à nos Polynomes.

Nous ne présenterons ici que la multiplication et l'affichage.

Commençons par l'affichage, c'est-à-dire l'opérateur << de la classe ostream (l'opérateur qui s'applique à cout). Cet opérateur est donc un opérateur externe à la classe Polynome (puisque son opérande de gauche est cout, il devrait être interne à la classe ostream).

De plus, comme cet opérateur doit afficher le contenu du polynôme, il doit avoir accès à l'attribut vector<double> p;. Mais comme cet attribut est privé et que nous n'avons pas fait de méthode «get» correspondante (ce qui serait la meilleure solution, beaucoup plus «propre» !), il nous faut déclarer cet opérateur comme friend de la classe Polynome.

Tout ceci se fait de la façon suivante :

#include <iostream>
...
class Polynome {
public:
  ... // comme avant

  friend ostream& operator<<(ostream&, const Polynome&);
  ...
};

...

void affiche_coef(ostream& out, double c, Degre puissance,
                  bool signe = true)
{
  if (c != 0) {
    if (signe && (c > 0.0))  out << "+";
    out << c;
    if (puissance > 1)
      out << "*X^" << puissance;
    else if (puissance == 1) out << "*X";
  }
}

ostream& operator<<(ostream& sortie, const Polynome& polynome)
{
  // comme la fonction affiche du cours 13

  // plus haut degré : pas de signe + devant
  Degre i(polynome.degre());
  affiche_coef(sortie, polynome.p[i], i, false);

  // degré de N à 0 : +a*X^i
  for (i--; i >= 0; i--) affiche_coef(sortie, polynome.p[i], i);

  // degré 0 : afficher quand meme le 0 si rien d'autre
  if ((polynome.degre() == 0) && (polynome.p[0] == 0.0))
    sortie.operator<<(0);

  return sortie;
}

On peut maintenant tester nos développements à ce stade, par exemple en ajoutant la fonction main

int main() {
  Polynome p(3.2, 4);
  cout << "p=" << p << endl;
  return 0;
}

Voyons maintenant la multiplication.
Il y a plusieurs cas qui nous intéressent :

  1. multiplication de 2 polynômes
  2. multiplication par un réel (sans passer par 1)

Pour 1 nous disposons de 2 opérateurs : * et *=. Commençons par le premier.

* nous permet d'écrire des choses comme r = p * q;, et doit donc prendre un polynôme (de plus : q) en argument et retourner la valeur du calcul après l'opération.
Son prototype devient donc tout naturellement :

Polynome operator*(Polynome q)

Plusieurs remarques cependant :

  1. cet opérateur retourne le résultat du calcul mais ne modifie pas l'objet concerné :
    dans r = p * q;, c'est-à-dire r = p.operator*(q);, p n'est pas modifié
    donc c'est une méthodes const :
    Polynome operator*(Polynome q) const;
    
    
  2. il est beaucoup plus efficace de passer q par référence que par valeur pour éviter des copies intermédiaire inutiles et coûteuses.
    Polynome operator*(Polynome& q) const;
    
    Polynome operator*(Polynome& q) const
  3. mais q n'est pas modifié par cet opérateur, il est donc const :
    Polynome operator*(const Polynome& q) const;
    
    

La définition de cet opérateur est ensuite sans difficulté (définition classique de la multiplication de polynômes) :

Polynome Polynome::operator*(const Polynome& q) const {
  Polynome r(0.0);

  // Prépare la place pour le polynôme résultat (de degre degre()+q.degre()).
  // Notez que r = 0, donc il contient déjà un monome (0.0 de degre 0)
  // et donc il suffit d'aller jusqu'au degré 1 (et non 0) pour avoir
  // le bon nombre de coefficients
  for (Degre i(degre() + q.degre()); i > 1; i--) r.p.push_back(0.0);

  // fait le calcul
  for (Degre i(0); i <= degre(); ++i)
    for (Degre j(0); j <= q.degre(); ++j)
      r.p[i+j] += p[i] * q.p[j];

  // retourne le resultat
  return r;
}

Pour le second opérateur, *=, il nous permet d'écrire p *= q;, mais doit aussi nous permettre (norme ISO C++) d'écrire des choses comme r = s + (p *= q);, bien que je vous déconseille fortement d'utiliser ce genre d'expressions.

Donc *= doit aussi retourner la valeur du calcul après l'opération.

Le plus simple et le plus efficace pour ceci est encore d'utiliser les références et de choisir comme prototype :

Polynome& operator*=(const Polynome&);

La définition de cet opérateur peut ensuite utiliser l'opérateur * défini ci-dessus et l'opérateur = qui nous reste à définir :

Polynome& Polynome::operator*=(const Polynome& q) {
  return (*this = *this * q);
}

Note : dans le cas général on fait plutôt le contraire : on définit l'opérateur Op en utilisant l'opérateur Op= avec la définition suivante

Classe Classe::operatorOp(const Classe& arg) {
   return (Classe(*this) Op= arg);
}
qui utilise le copy-constructor.

Nous ne l'avons pas fait ici car la multiplication de polynômes nécéssite de toutes façons un polynôme intermédiaire (r ci-dessus) pour le calcul, même dans le cas de l'opérateur *=. On ne gagne donc rien à procéder comme cela ici.

(Si vous ne comprennez pas cette note, laissez tomber. Ce n'est pas fondamental.)

Terminons donc cette première étape par la définition de l'opérateur = (sans difficulté). Nous avons à ce stade :

#include <iostream>
#include <vector>
using namespace std;

typedef int Degre;

class Polynome {
public:
  // constructeurs
  Polynome(double coef = 0.0, unsigned int degre = 0);
  Polynome(Polynome const &); // copy-constructor

  // methodes publiques
  Degre degre() const { return p.size()-1; }

  // operateurs internes
  Polynome operator*(const Polynome& q) const;
  Polynome& operator*=(const Polynome& q);
  Polynome& operator=(const Polynome& q);

  // operateurs externes
  friend ostream& operator<<(ostream&, const Polynome&);

private:
  // attributs privés
  vector<double> p;
};

// ======================================================================
// definition des methodes

// ----------------------------------------------------------------------
Polynome::Polynome(double coef, unsigned int deg)
 : p(deg+1, 0.0) // garantit que le degré de p est deg
{
  p[deg] = coef; // change la valeur du coef de plus haut degre
}

// ----------------------------------------------------------------------
Polynome::Polynome(Polynome const & autre) : p(autre.p) {}

// ----------------------------------------------------------------------
Polynome Polynome::operator*(Polynome const & q) const {
  Polynome r(0.0);

  // Prépare la place pour le polynôme résultat (de degre degre()+q.degre()).
  // Notez que r = 0, donc il contient déjà un monome (0.0 de degre 0)
  // et donc il suffit d'aller jusqu'au degré 1 (et non 0) pour avoir
  // le bon nombre de coefficients
  for (Degre i(degre() + q.degre()); i >= 1; --i) r.p.push_back(0.0);

  // fait le calcul
  for (Degre i(0); i <= degre(); ++i)
    for (Degre j(0); j <= q.degre(); ++j)
      r.p[i+j] += p[i] * q.p[j];

  // retourne le resultat
  return r;
}

// ----------------------------------------------------------------------
Polynome& Polynome::operator*=(Polynome const & q) {
  return (*this = *this * q);
}

// ----------------------------------------------------------------------
Polynome& Polynome::operator=(Polynome const & q) {
  if (&q != this) { p = q.p; }
  return *this;
}

// ======================================================================
// fonctions intermédiaires et operateurs externes

// ----------------------------------------------------------------------
void affiche_coef(ostream& out, double c, Degre puissance,
                  bool signe = true)
{
  if (c != 0) {
    if (signe && (c > 0.0))  out << "+";
    out << c;
    if (puissance > 1)
      out << "*X^" << puissance;
    else if (puissance == 1) out << "*X";
  }
}

// ----------------------------------------------------------------------
ostream& operator<<(ostream& sortie, const Polynome& polynome)
{
  // plus haut degré : pas de signe + devant
  Degre i(polynome.degre());
  affiche_coef(sortie, polynome.p[i], i, false);

  // degré de N à 0 : +a*X^i
  for (i--; i >= 0; i--) affiche_coef(sortie, polynome.p[i], i);

  // degré 0 : afficher quand meme le 0 si rien d'autre
  if ((polynome.degre() == 0) && (polynome.p[0] == 0.0))
    sortie.operator<<(0);

  return sortie;
}

// ======================================================================
// ----------------------------------------------------------------------
int main() {
  Polynome p(3.2, 4);
  cout << "p=" << p << endl;

  Polynome q(1.1, 2), r;

  r = p * q;
  cout << p << " * " << q << " = " << r << endl;

  return 0;
}

Terminons maintenant par la multiplication par les réels.
Nous allons pour cela définir les opérateurs

Polynome& operator*=(const double);
Polynome operator*(const double) const;
friend Polynome operator*(const double, const Polynome&);

Les deux premiers sont des opérateurs internes, utilisés respectivement pour des opérations de type p *= x et q = p * x, où p et q sont des polynômes et x est un double.

Le dernier est un opérateur externe permettant d'effectuer des opération du type q = x * p.

Leurs définitions ne présentent pas non plus de problème majeur, les deux derniers utilisant le premier de ces opérateurs :

 ----------------------------------------------------------------------
Polynome& Polynome::operator*=(const double x) {
  for (Degre i(0); i <= degre(); ++i)
    p[i] *= x;
  return *this;
}

// ----------------------------------------------------------------------
Polynome Polynome::operator*(const double x) const {
  return Polynome(*this) *= x;
}

// ----------------------------------------------------------------------
Polynome operator*(const double x, const Polynome& p) {
  return Polynome(p) *= x;
}

On peut tester en ajoutant

  r *= 2.0;
  cout << " * 2" << " = " << r << endl;

Code source

Pour avoir accès / sauvegarder le programme complet, cliquez ici.


Dernière mise à jour : 2022/03/15 18:03:40