Penser en C++

Volume 1


précédentsommairesuivant

7. Fonctions surchargée et arguments par défaut

Un des dispositifs importants dans n'importe quel langage de programmation est l'utilisation commode des noms.

Quand vous créez un objet (une variable), vous donnez un nom à une région de stockage. Une fonction est un nom pour une action. En composant des noms pour décrire le système actuel, vous créez un programme qu'il est plus facile pour d'autres personnes de comprendre et de changer. Cela se rapproche beaucoup de la prose d'écriture - le but est de communiquer avec vos lecteurs.

Un problème surgit en traçant le concept de la nuance dans la langue humaine sur un langage de programmation. Souvent, le même mot exprime un certain nombre de différentes significations selon le contexte. C'est-à-dire qu'un mot simple a des significations multiples - il est surchargé. C'est très utile, particulièrement quand en on vient aux différences insignifiantes.Vous dites : « lave la chemise, lave la voiture. » Il serait idiot d'être forcé de dire : « je fais un lavage de chemise à la chemise, je fais un lavage de voiture à la voiture ». Ainsi l'auditeur ne doit faire aucune distinction au sujet de l'action a exécuté. Les langues humaines ont la redondance intégrée, ainsi même si vous manquez quelques mots, vous pouvez déterminer la signification. Nous n'avons pas besoin de marques uniques - nous pouvons déduire la signification à partir du contexte.

La plupart des langages de programmation, cependant, exigent que vous ayez une marque unique pour chaque fonction. Si vous avez trois types différents de données que vous voulez imprimer : int, char, et float, vous devez généralement créer trois noms de fonction différents, par exemple, print_int (), print_char (), et print_float (). Ceci ajoute du travail supplémentaire pour l'écriture du programme, et pour les lecteurs pendant qui essayent de comprendre votre code.

En C++, un autre facteur force la surcharge des noms de fonction : le constructeur. Puisque le nom du constructeur est prédéterminé par le nom de la classe, il semblerait qu'il ne puisse y avoir qu'un constructeur. Mais si vous voulez en créer un en plus du constructeur classique ? Par exemple, supposer que vous voulez établir une classe qui peut s'initialiser d'une manière standard et également par une information de lecture à partir d'un dossier. Vous avez besoin de deux constructeurs, un qui ne prennent aucun argument (le constructeur par défaut)et un qui prennent une chaine de caractère comme argument, qui est le nom du dossier pour initialiser l'objet. Tous les deux sont des constructeurs, ainsi ils doivent avoir le même nom : le nom de la classe. Ainsi, la surcharge de fonction est essentielle pour permettre au même nom de fonction - le constructeur dans ce cas-ci - d'être employée avec différents types d'argument.

Bien que la surcharge de fonction soit une nécessité pour des constructeurs, c'est une convenance générale et elle peut être employé avec n'importe quelle fonction, pas uniquement avec une fonction membre d'une classe. En plus, les fonctions surchargent les significations, si vous avez deux bibliothèques qui contiennent des fonctions du même nom, elles n'entrerons pas en conflit tant que les listes d'arguments sont différentes. Nous regarderons tous ces facteurs en détail dans ce chapitre.

Le thème de ce chapitre est l'utilisation correcte des noms de fonction. La surcharge de fonction vous permet d'employer le même nom pour différentes fonctions, mais il y a une deuxième manière de faire appel à une fonction de manière plus correcte. Que se passerait-il si vous voudriez appeler la même fonction de différentes manières ? Quand les fonctions ont de longues listes d'arguments, il peut devenir pénible d'écrire (et difficile à lire) des appels de fonction quand la plupart des arguments sont les mêmes pour tous les appels. Un dispositif utilisé généralement en C++ s'appelle les arguments par défaut. Un argument par défaut est inséré par le compilateur si on ne l'indique pas dans l'appel de fonction. Ainsi, les appels f ("bonjour"), f ("salut", 1), et f ("allo", 2, `c') peuvent tout être des appels à la même fonction. Cela pourraient également être des appels à trois fonctions surchargées, mais quand les listes d'argument sont semblables, vous voudrez habituellement un comportement semblable, qui réclame une fonction simple.

Les fonctions surchargées et les arguments par défaut ne sont pas vraiment très compliqués. Avant la fin de ce chapitre, vous saurez quand les employer et les mécanismes fondamentaux qui les mettent en application pendant la compilation et l'enchaînement.

7.1. Plus sur les décorations de nom

Dans le chapitre 4, le concept de décoration de nom a été présenté. Dans ce code :

 
Sélectionnez

void f();
class X { void f(); };

la fonction f() à l'intérieur de la portée de la classe X n'est pas en conflit avec la version globale de f(). Le compilateur réalise cette portée par différents noms internes fabriqués pour la version globale de f() et de X : : f(). Dans le chapitre 4, on a suggéré que les noms soient simplement le nom de classe « décoré » avec le nom de fonction, ainsi les noms internes que le compilateur utilise pourraient être _f et _X_f. Cependant, il s'avère que la décoration de nom pour les fonctions implique plus que le nom de classe.

Voici pourquoi. Supposez que vous vouliez surcharger deux noms de fonction :

 
Sélectionnez

void printf(char);
void printf(float);

Il n'importe pas qu'ils soient à l'intérieur d'une classe ou à portée globale. Le compilateur ne peut pas produire des identifiants internes uniques s'il emploie seulement la portée des noms de fonction. Vous finiriez avec _print dans les deux cas. L'idée d'une fonction surchargée est que vous employez le même nom de fonction, mais les listes d'arguments sont différentes. Ainsi, pour que la surcharge fonctionne, le compilateur doit décorer le nom de fonction avec les noms des types d'argument. Les fonctions ci-dessus, définies à la portée globale produisent des noms internes qui pourraient ressembler à quelque chose comme _print_char et _print_float. Il vaut la peine de noter qu'il n'y a aucune norme pour la manière dont les noms doivent être décorés par le compilateur, ainsi vous verrez des résultats très différents d'un compilateur à l'autre. (Vous pouvez voir à quoi ça ressemble en demandant au compilateur de générer une sortie en langage assembleur.) Ceci, naturellement, pose des problèmes si vous voulez acheter des bibliothèques compilées pour un compilateur et un éditeur de liens particuliers - mais même si la décoration de nom étaient normalisées, il y aurait d'autres barrages en raison de la manière dont les différents compilateurs produisent du code.

C'est vraiment tout ce qu'il y a dans le surchargement de fonction : vous pouvez employer le même nom de fonction pour différentes fonctions tant que les listes d'arguments sont différentes. Le compilateur décore le nom, la portée, et les listes d'argument pour produire des noms internes que lui et l'éditeur de liens emploient.

7.1.1. Valeur de retour surchargée :

Il est commun de se demander, « pourquoi juste les portées et les listes d'arguments ? Pourquoi pas les valeurs de retour ? » Il semble à première vue qu'il soit raisonnable de décorer également la valeur de retour avec le nom interne de fonction. Alors vous pourriez surcharger sur les valeurs de retour, également :

 
Sélectionnez

void f();
int f();

Ceci fonctionne très bien quand le compilateur peut sans équivoque déterminer la signification du contexte, comme dans x = f( ) ;. Cependant, en C vous avez toujours pu appeler une fonction et ignorer la valeur de retour (c'est-à-dire que vous pouvez l'appeler pour ses effets secondaires). Comment le compilateur peut-il distinguer quel appel est voulu dans ce cas-là ? La difficulté pour le lecteur de savoir quelle fonction vous appelez est probablement encore pire. La surcharge sur la valeur de retour seulement est trop subtile, et n'est donc pas permise en C++.

7.1.2. Edition de liens sécurisée

Il y a un avantage supplémentaire à la décoration de noms. Un problème particulièrement tenace en C se produit quand un programmeur client déclare mal une fonction, ou, pire, lorsqu'il appelle une fonction sans la déclarer d'abord, et que le compilateur infère la déclaration de fonction d'après la façon dont elle est appelée. Parfois cette déclaration de fonction est correcte, mais quand elle ne l'est pas, ce peut être un bug difficile à trouver.

Puisque toutes les fonctions doivent être déclarées avant d'être employées en C++, le risque que ce problème se produise est considérablement diminuée. Le compilateur C++ refuse de déclarer une fonction automatiquement pour vous, ainsi il est probable que vous inclurez le fichier d'en-tête approprié. Cependant, si pour une raison quelconque vous parvenez toujours à mal déclarer une fonction, soit en la déclarant à la main soit en incluant le mauvais fichier d'en-tête (peut-être un qui est périmé), la décoration de nom fournit une sécurité qui est souvent désignée sous le nom de édition de liens sécurisée.

Considérez le scénario suivant, une fonction est définie dans un fichier :

 
Sélectionnez

//: C07:Def.cpp {O}
// définition de fonction
void f(int) {}
///:~ 

Dans le second fichier, la fonction est mal déclarée puis appelée :

 
Sélectionnez

//: C07:Use.cpp
//{L} Def
// Mauvaise déclaration de fonction 
void f(char);
 
int main() {
//!  f(1); // Cause une erreur d'édition de liens
} ///:~

Même si vous pouvez voir que la fonction est réellement f(int), le compilateur ne le sait pas parce qu'on lui a dit - par une déclaration explicite - que la fonction est f(char). Ainsi, la compilation réussit. En C, l'édition de liens aurait également réussi, mais pas en C++. Puisque le compilateur décore les noms, la définition devient quelque chose comme f_int, tandis que l'utilisation de la fonction est f_char. Quand l'éditeur de liens essaye de trouver la référence à f_char, il trouve seulement f_int, et il envoie un message d'erreur. C'est l'édition de liens sécurisée. Bien que le problème ne se produise pas très souvent, quand cela arrive il peut être incroyablement difficile à trouver, particulièrement dans un grand projet. C'est un des cas où vous pouvez trouver facilement une erreur difficile dans un programme C simplement en le compilant avec un compilateur C++.

7.2. Exemple de surchargement

Nous pouvons maintenant modifier les exemples vus précédemment pour employer la surcharge de fonction. Comme indiqué avant, un élément immédiatement utile à surcharger est le constructeur. Vous pouvez le voir dans la version suivante de la classe Stash:

 
Sélectionnez

//: C07:Stash3.h
// Fonction surchargée
#ifndef STASH3_H
#define STASH3_H
 
class Stash {
  int size;      // Taille pour chaque emplacement
  int quantity;  // Nombre d'espaces mémoire
  int next;      // Emplacement vide suivant
  //Tableau de bits dynamiquement alloué :
  unsigned char* storage;
  void inflate(int increase);
public:
  Stash(int size); // Quantité zéro
  Stash(int size, int initQuantity);
  ~Stash();
  int add(void* element);
  void* fetch(int index);
  int count();
};
#endif // STASH3_H ///:~

Le premier constructeur de Stash() est identique à celui d'avant, mais le second a un argument Quantity pour indiquer le nombre initial d'emplacements de stockage à allouer. Dans la définition, vous pouvez voir que la valeur interne de la quantité est placée à zéro, de même que le pointeur de stockage. Dans le deuxième constructeur, l'appel à Inflate(initQuantity) augmente Quantity à la taille assignée :

 
Sélectionnez

//: C07:Stash3.cpp {O}
// Fonction surchargée
#include "Stash3.h"
#include "../require.h"
#include <iostream>
#include <cassert>
using namespace std;
const int increment = 100;
 
Stash::Stash(int sz) {
  size = sz;
  quantity = 0;
  next = 0;
  storage = 0;
}
 
Stash::Stash(int sz, int initQuantity) {
  size = sz;
  quantity = 0;
  next = 0;
  storage = 0;
  inflate(initQuantity);
}
 
Stash::~Stash() {
  if(storage != 0) {
    cout << "freeing storage" << endl;
    delete []storage;
  }
}
 
int Stash::add(void* element) {
  if(next >= quantity) // Suffisamment d'espace libre ?
    inflate(increment);
  // Copie l'élément dans l'emplacement mémoire,
  // Commence a l'emplacement vide suivant:
  int startBytes = next * size;
  unsigned char* e = (unsigned char*)element;
  for(int i = 0; i < size; i++)
    storage[startBytes + i] = e[i];
  next++;
  return(next - 1); // Index
}
 
void* Stash::fetch(int index) {
  require(0 <= index, "Stash::fetch (-)index");
  if(index >= next)
    return 0; // To indicate the end
  // Produit un pointeur vers l'élément désiré :
  return &(storage[index * size]);
}
 
int Stash::count() {
  return next; // Nombre d'éléments dans CStash
}
 
void Stash::inflate(int increase) {
  assert(increase >= 0);
  if(increase == 0) return;
  int newQuantity = quantity + increase;
  int newBytes = newQuantity * size;
  int oldBytes = quantity * size;
  unsigned char* b = new unsigned char[newBytes];
  for(int i = 0; i < oldBytes; i++)
    b[i] = storage[i]; // Copie l'ancien vers le nouveau
  delete [](storage); // Libère l'ancien emplacement mémoire
  storage = b; // Pointe sur la nouvelle mémoire
  quantity = newQuantity; // Ajuste la taille
} ///:~

Quand vous utilisez le premier constructeur aucune mémoire n'est assignée pour le stockage. L'allocation se produit la première fois que vous essayez d'ajouter ( add() en anglais, ndt) un objet et à chaque fois que le bloc de mémoire courant est excédé à l'intérieur de add().

Les deux constructeurs sont illustrés dans le programme de test :

 
Sélectionnez

//: C07:Stash3Test.cpp
//{L} Stash3
// Surcharge de fonction 
#include "Stash3.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
 
int main() {
  Stash intStash(sizeof(int));
  for(int i = 0; i < 100; i++)
    intStash.add(&i);
  for(int j = 0; j < intStash.count(); j++)
    cout << "intStash.fetch(" << j << ") = "
         << *(int*)intStash.fetch(j)
         << endl;
  const int bufsize = 80;
  Stash stringStash(sizeof(char) * bufsize, 100);
  ifstream in("Stash3Test.cpp");
  assure(in, "Stash3Test.cpp");
  string line;
  while(getline(in, line))
    stringStash.add((char*)line.c_str());
  int k = 0;
  char* cp;
  while((cp = (char*)stringStash.fetch(k++))!=0)
    cout << "stringStash.fetch(" << k << ") = "
         << cp << endl;
} ///:~

L'appel du constructeur pour stringStash utilise un deuxième argument ; vraisemblablement vous savez quelque chose au sujet du problème spécifique que vous résolvez qui vous permet de choisir une première taille pour le Stash.

7.3. unions

Comme vous l'avez vu, la seule différence entre struct et class en C++ est que struct est public par défaut et class, private. Une struct peut également avoir des constructeurs et des destructeurs, comme vous pouvez vous y attendre. Mais il s'avère qu'une union peut aussi avoir constructeur, destructeur, fonctions membres et même contrôle d'accès. Vous pouvez encore une fois constater l'usage et le bénéfice de la surcharge dans les exemples suivants :

 
Sélectionnez
//: C07:UnionClass.cpp
// Unions avec constructeurs et fonctions membres
#include<iostream>
using namespace std;
 
union U {
private: // Contrôle d'accès également !
  int i;
  float f;
public:  
  U(int a);
  U(float b);
  ~U();
  int read_int();
  float read_float();
};
 
U::U(int a) { i = a; }
 
U::U(float b) { f = b;}
 
U::~U() { cout << "U::~U()\n"; }
 
int U::read_int() { return i; }
 
float U::read_float() { return f; }
 
int main() {
  U X(12), Y(1.9F);
  cout << X.read_int() << endl;
  cout << Y.read_float() << endl;
} ///:~

Vous pourriez penser d'après le code ci-dessus que la seule différence entre une union et une class est la façon dont les données sont stockées (c'est-à-dire, le int et le float sont superposés sur le même espace de stockage). Cependant, une union ne peut pas être utilisée comme classe de base pour l'héritage, ce qui est assez limitant d'un point de vue conception orientée objet (vous étudierez l'héritage au Chapitre 14).

Bien que les fonctions membres rendent l'accès aux union quelque peu civilisé, il n'y a toujours aucun moyen d'éviter que le programmeur client sélectionne le mauvais type d'élément une fois que l' union est initialisée. Dans l'exemple ci-dessus, vous pourriez écrire X.read_float( ) bien que ce soit inapproprié. Cependant, une union"sécurisée" peut être encapsulée dans une classe. Dans l'exemple suivant, observez comme enum clarifie le code, et comme la surcharge est pratique avec le constructeur :

 
Sélectionnez
//: C07:SuperVar.cpp
// Une super-variable
#include <iostream>
using namespace std;
 
class SuperVar {
  enum {
    character,
    integer,
    floating_point
  } vartype;  // Définit une
  union {  // Union anonyme
    char c;
    int i;
    float f;
  };
public:
  SuperVar(char ch);
  SuperVar(int ii);
  SuperVar(float ff);
  void print();
};
 
SuperVar::SuperVar(char ch) {
  vartype = character;
  c = ch;
}
 
SuperVar::SuperVar(int ii) {
  vartype = integer;
  i = ii;
}
 
SuperVar::SuperVar(float ff) {
  vartype = floating_point;
  f = ff;
}
 
void SuperVar::print() {
  switch (vartype) {
    case character:
      cout << "character: " << c << endl;
      break;
    case integer:
      cout << "integer: " << i << endl;
      break;
    case floating_point:
      cout << "float: " << f << endl;
      break;
  }
}
 
int main() {
  SuperVar A('c'), B(12), C(1.44F);
  A.print();
  B.print();
  C.print();
} ///:~

Dans le code ci-dessus, le enum n'a pas de nom de type (c'est une énumération sans label). C'est acceptable si vous définissez immédiatement les instances de l' enum, comme c'est le cas ici. Il n'y a pas besoin de faire référence au type de l' enum à l'avenir, si bien que le nom du type est optionnel.

L' union n'a aucune nom de type ni de nom de variable. Cela s'appelle une union anonyme, et créé de l'espace pour l' union mais ne requiert pas d'accéder aux éléments de l' union avec un nom de variable et l'opérateur point. Par exemple, si votre union anonyme est :

 
Sélectionnez
//: C07:AnonymousUnion.cpp
int main() {
  union { 
    int i; 
    float f; 
  };
  // Accès aux membres sans utiliser de qualification :
  i = 12;
  f = 1.22;
} ///:~

Notez que vous accédez aux membres d'une union anonyme exactement comme s'ils étaient des variables ordinaires. La seule différence est que les deux variables occupent le même espace. Si l' union anonyme est dans la portée du fichier (en dehors de toute fonction ou classe) elle doit alors être déclarée static afin d'avoir un lien interne.

Bien que SuperVar soit maintenant sécurisée, son utilité est un peu douteuse car le motif pour lequel on a utilisé une union au début était de gagner de l'espace, et l'addition de vartype occupe un peu trop d'espace relativement aux données dans l' union, si bien que les économies sont de fait perdues. Il y a quelques alternatives pour rendre ce procédé exploitable. Vous n'auriez besoin que d'un seul vartype s'il contrôlait plus d'une instance d' union- si elles étaient toutes du même type - et il n'occuperait pas plus d'espace. Une approche plus utile est d'avoir des #ifdef s tout autour des codes utilisant vartype, ce qui peut alors garantir que les choses sont utilisées correctement durant le développement et les tests. Pour le code embarqué, l'espace supplémentaire et le temps supplémentaire peut être éliminé.

7.4. Les arguments par défaut

Examinez les deux constructeurs de Stash() dans Stash3.h. Ils ne semblent pas si différents, n'est-ce pas ? En fait, le premier constructeur semble être un cas spécial du second avec la taille ( size) initiale réglée à zéro. C'est un peu un gaspillage d'efforts de créer et de maintenir deux versions différentes d'une fonction semblable.

C++ fournit un remède avec les arguments par défaut. Un argument par défaut est une valeur donnée dans la déclaration que le compilateur insère automatiquement si vous ne fournissez pas de valeur dans l'appel de fonction. Dans l'exemple Stash(), nous pouvons remplacer les deux fonctions :

 
Sélectionnez

Stash(int size); // Quantitée zéro
Stash(int size, int initQuantity);

par la seule fonction :

 
Sélectionnez

Stash(int size, int initQuantity = 0);

La définition de Stash(int) est simplement enlevée -tout ce qui est nécessaire est la définition unique Stash(int,int).

Maintenant, les deux définitions d'objet :

 
Sélectionnez

Stash A(100), B(100, 0);

Produiront exactement les mêmes résultats. Le même constructeur est appelé dans les deux cas, mais pour A, le deuxième argument est automatiquement substitué par le compilateur quand il voit que le premier argument est un int et qu'il n'y a pas de second argument. Le compilateur a vu l'argument par défaut, ainsi il sait qu'il peut toujours faire l'appel à la fonction s'il substitue ce deuxième argument, ce qui est ce que vous lui avez dit de faire en lui donnant une valeur par défaut.

Les arguments par défaut sont une commodité, comme la surcharge de fonction est une commodité. Les deux dispositifs vous permettent d'employer un seul nom de fonction dans différentes situations. La différence est qu'avec des arguments par défaut le compilateur ajoute les arguments quand vous ne voulez pas mettre vous-même. L'exemple précédent est un cas idéal pour employer des arguments par défaut au lieu de la surcharge de fonction ; autrement vous vous retrouvez avec deux fonctions ou plus qui ont des signatures semblables et des comportements semblables. Si les fonctions ont des comportements très différents, il n'est généralement pas logique d'employer des arguments par défaut (du reste, vous pourriez vous demander si deux fonctions avec des comportements très différents doivent avoir le même nom).

Il y a deux règles que vous devez prendre en compte quand vous utilisez des arguments par défaut. En premier lieu, seul les derniers arguments peuvent être mis par défauts. C'est-à-dire que vous ne pouvez pas faire suivre un argument par défaut par un argument qui ne l'est pas. En second lieu, une fois que vous commencez à employer des arguments par défaut dans un appel de fonction particulier, tous les arguments suivants dans la liste des arguments de cette fonction doivent être par défaut (ceci dérive de la première règle).

Les arguments par défaut sont seulement placés dans la déclaration d'une fonction (typiquement placée dans un fichier d'en-tête). Le compilateur doit voir la valeur par défaut avant qu'il puisse l'employer. Parfois les gens placeront les valeurs commentées des arguments par défaut dans la définition de fonction, pour des raisons de documentation.

 
Sélectionnez

void fn(int x /* = 0 */) { // ...

7.4.1. Paramètre fictif

Les arguments dans une déclaration de fonction peuvent être déclarés sans identifiants. Quand ceux-ci sont employés avec des arguments par défaut, cela peut avoir l'air un peu bizarre. Vous pouvez vous retrouver avec :

 
Sélectionnez

void f(int x, int = 0, float = 1.1);

En C++ vous n'avez pas besoin des identifiants dans la définition de fonction, non plus :

 
Sélectionnez

void f(int x, int, float flt) { /* ... */ }

Dans le corps de la fonction, on peut faire référence à x et flt, mais pas à l'argument du milieu, parce qu'il n'a aucun nom. Cependant, les appels de fonction doivent toujours fournir une valeur pour le paramètre fictif : f(1) ou f(1.2.3.0). Cette syntaxe permet de passer l'argument comme un paramètre fictif sans l'utiliser. L'idée est que vous pourriez vouloir changer la définition de la fonction pour employer le paramètre fictif plus tard, sans changer le code partout où la fonction est appelée. Naturellement,vous pouvez accomplir la même chose en employant un argument nommé, mais si vous définissez l'argument pour le corps de fonction sans l'employer, la plupart des compilateurs vous donneront un message d'avertissement, supposant que vous avez fait une erreur logique. En omettant intentionnellement le nom d'argument, vous faite disparaître cet avertissement.

Plus important, si vous commencez à employer un argument de fonction et décidez plus tard que vous n'en avez pas besoin, vous pouvez effectivement l'enlever sans générer d'avertissements, et sans pour autant déranger de code client qui appelait la version précédente de la fonction.

7.5. Choix entre surcharge et arguments par défaut

La surcharge et les arguments par défauts procurent tout deux un moyen pratique d'appeler les noms de fonction. Toutefois, savoir quelle technique utiliser peut être parfois peu évident. Par exemple, considérez l'outil suivant conçu pour gérer automatiquement les blocs de mémoire pour vous :

 
Sélectionnez
//: C07:Mem.h
#ifndef MEM_H
#define MEM_H
typedef unsigned char byte;
 
class Mem {
  byte* mem;
  int size;
  void ensureMinSize(int minSize);
public:
  Mem();
  Mem(int sz);
  ~Mem();
  int msize();
  byte* pointer();
  byte* pointer(int minSize);
}; 
#endif // MEM_H ///:~

Un objet Mem contient un bloc de byte s (octets, ndt) et s'assure que vous avez suffisamment d'espace. Le constructeur par défaut n'alloue aucune place, et le second constructeur s'assure qu'il y a sz octets d'espace de stockage dans l'objet Mem. Le destructeur libère le stockage, msize( ) vous dit combien d'octets sont présents à ce moment dans l'objet Mem, et pointer( ) produit un pointeur vers l'adresse du début de l'espace de stockage ( Mem est un outil d'assez bas niveau). Il y a une version surchargée de pointer( ) dans laquelle les programmeurs clients peuvent dire qu'ils veulent un pointeur vers un bloc d'octets de taille minimum minSize, et la fonction membre s'en assure.

Le constructeur et la fonction membre pointer( ) utilisent tout deux la fonction membre privateensureMinSize( ) pour augmenter la taille du bloc mémoire (remarquez qu'il n'est pas sûr de retenir le résultat de pointer( ) si la mémoire est redimensionnée).

Voici l'implémentation de la classe :

 
Sélectionnez
//: C07:Mem.cpp {O}
#include "Mem.h"
#include <cstring>
using namespace std;
 
Mem::Mem() { mem = 0; size = 0; }
 
Mem::Mem(int sz) {
  mem = 0;
  size = 0;
  ensureMinSize(sz); 
}
 
Mem::~Mem() { delete []mem; }
 
int Mem::msize() { return size; }
 
void Mem::ensureMinSize(int minSize) {
  if(size < minSize) {
    byte* newmem = new byte[minSize];
    memset(newmem + size, 0, minSize - size);
    memcpy(newmem, mem, size);
    delete []mem;
    mem = newmem;
    size = minSize;
  }
}
 
byte* Mem::pointer() { return mem; }
 
byte* Mem::pointer(int minSize) {
  ensureMinSize(minSize);
  return mem; 
} ///:~

Vous pouvez constater que ensureMinSize( ) est la seule fonction responsable de l'allocation de mémoire, et qu'elle est utilisée par le deuxième constructeur et la deuxième forme surchargée de pointer( ). Dans ensureMinSize( ), il n'y a rien à faire si size est suffisamment grand. Si un espace de stockage nouveau doit être alloué afin de rendre le bloc plus grand (ce qui est également le cas quand le bloc a une taille nulle après la construction par défaut), la nouvelle portion "supplémentaire" est fixée à zéro en utilisant la fonction memset( ) de la bibliothèque C Standard, qui a été introduite au Chapitre 5. L'appel de fonction ultérieur est à la fonction memcpy( ) de la bibliothèque C Standard, qui, dans ce cas, copie les octets existant de mem vers newmem(de manière généralement efficace). Finalement, l'ancienne mémoire est libérée et la nouvelle mémoire et la nouvelle taille sont assignées aux membres appropriés.

La classe Mem est conçue pour être utilisée comme un outil dans d'autres classes pour simplifier leur gestion de la mémoire (elle pourrait également être utilisée pour dissimuler un système de gestion de la mémoire plus sophistiqué, par exemple, par le système d'exploitation). On le teste ici de manière appropriée en créant une classe "string" simple :

 
Sélectionnez
//: C07:MemTest.cpp
// Testing the Mem class
//{L} Mem
#include "Mem.h"
#include <cstring>
#include <iostream>
using namespace std;
 
class MyString {
  Mem* buf;
public:
  MyString();
  MyString(char* str);
  ~MyString();
  void concat(char* str);
  void print(ostream& os);
};
 
MyString::MyString() {  buf = 0; }
 
MyString::MyString(char* str) {
  buf = new Mem(strlen(str) + 1);
  strcpy((char*)buf->pointer(), str);
}
 
void MyString::concat(char* str) {
  if(!buf) buf = new Mem;
  strcat((char*)buf->pointer(
    buf->msize() + strlen(str) + 1), str);
}
 
void MyString::print(ostream& os) {
  if(!buf) return;
  os << buf->pointer() << endl;
}
 
MyString::~MyString() { delete buf; }
 
int main() {
  MyString s("Ma chaine test");
  s.print(cout);
  s.concat(" un truc supplémentaire");
  s.print(cout);
  MyString s2;
  s2.concat("Utilise le constructeur par défaut");
  s2.print(cout);
} ///:~

Tout ce que vous pouvez faire avec cette classe est créer un MyString, concaténer du texte, et l'imprimer vers un ostream. La classe contient seulement un pointeur vers un Mem, mais notez la distinction entre le contructeur par défaut, qui initialise le pointeur à zéro, et le deuxième constructeur, qui créé un Mem et copie des données dedans. L'avantage du constructeur par défaut est que vous pouvez créer, par exemple, un grand tableau d'objets MyString vide à faible coût, comme la taille de chaque objet est seulement un pointeur et le seul délai supplémentaire du constructeur par défaut est l'assignation à zéro. Le coût d'un MyString commence à s'accroître seulement quand vous concaténez des données ; à ce point, l'objet Mem est créé, s'il ne l'a pas déjà été. Cependant, si vous utilisez le constructeur par défaut et ne concaténez jamais de données, l'appel au destructeur est toujours sûr car appeler delete pour zéro est défini de telle façon qu'il n'essaie pas de libérer d'espace ou ne cause pas de problème autrement.

Si vous examinez ces deux constructeurs, il pourrait sembler au premier abord que c'est un bon candidat pour des arguments par défaut. Toutefois, si vous abandonnez le constructeur par défaut et écrivez le constructeur restant avec un argument par défaut :

 
Sélectionnez
MyString(char* str = "");

tout fonctionnera correctement, mais vous perdrez le bénéfice d'efficacité puisqu'un objet Mem sera systématiquement créé. Pour recouvrer l'efficacité, vous devez modifier le constructeur :

 
Sélectionnez
MyString::MyString(char* str) {
  if(!*str) { // Pointe vers une chaîne vide
    buf = 0;
    return;
  }
  buf = new Mem(strlen(str) + 1);
  strcpy((char*)buf->pointer(), str);
} 

Ceci signifie, en effet, que la valeur par défaut devient un signal qui entraîne l'exécution d'une partie différente du code de celui utilisé pour une valeur quelconque. Bien que cela semble sans trop d'importance avec un constructeur aussi petit que celui-ci, en général cette pratique peut causer des problèmes. Si vous devez chercher le constructeur par défaut plutôt que l'exécuter comme une valeur ordinaire, cela devrait être une indication que vous finirez avec deux fonctions différentes dans un seul corps de fonction : une version pour le cas normal et une par défaut. Vous feriez aussi bien de la séparer en deux corps de fonction et laisser le compilateur faire la sélection. Ceci entraîne une légère (mais généralement imperceptible) augmentation d'efficacité, parce que l'argument supplémentaire n'est pas passé et le code conditionnel supplémentaire n'est pas exécuté. Plus important, vous conservez le code de deux fonctions différentes dans deux fonctions distinctes plutôt que de les combiner en une seule utilisant les arguments par défaut, ce qui résulte en une maintenance plus facile, surtout si les fonctions sont grandes.

D'un autre point de vue, considérez le cas de la classe Mem. Si vous regardez les définitions des deux constructeurs et des deux fonctions pointer( ), vous constatez qu'utiliser les arguments par défaut, dans les deux cas, n'entraînera aucune modification de la définition de la fonction membre. Ainsi, la classe peut devenir sans problème :

 
Sélectionnez
//: C07:Mem2.h
#ifndef MEM2_H
#define MEM2_H
typedef unsigned char byte;
 
class Mem {
  byte* mem;
  int size;
  void ensureMinSize(int minSize);
public:
  Mem(int sz = 0);
  ~Mem();
  int msize();
  byte* pointer(int minSize = 0);
}; 
#endif // MEM2_H ///:~

Notez qu'un appel à ensureMinSize(0) sera toujours assez efficace.

Bien que dans chacun de ces cas j'ai fondé une part du processus de décision sur la question de l'efficacité, vous devez faire attention à ne pas tomber dans le piège de ne penser qu'à l'efficacité (aussi fascinante soit-elle). La question la plus importante dans la conception de classes est l'interface de la classe (ses membres public, qui sont disponibles pour le programmeur client). Si cela produit une classe qui est facile à utiliser et réutiliser, alors c'est un succès ; vous pouvez toujours faire des réglages pour l'efficacité si nécessaire, mais l'effet d'une classe mal conçue parce que le programmeur se concentre trop sur la question de l'efficacité peut être terrible. Votre souci premier devrait être que l'interface ait un sens pour ceux qui l'utilisent et qui lisent le code résultant. Remarquez que dans MemTest.cpp l'utilisation de MyString ne change pas selon qu'on utilise un constructeur par défaut ou que l'efficacité soit grande ou non.

7.6. Résumé

D'une manière générale, vous ne devriez pas utiliser un argument par défaut comme un signal pour l'exécution conditionnelle de code. A la place, vous devriez diviser la fonction en deux ou plusieurs fonctions surchargées si vous le pouvez. Un argument par défaut devrait être une valeur utilisée ordinairement. C'est la valeur la plus probablement utilisée parmi toutes celles possibles, si bien que les programmeurs clients peuvent généralement l'ignorer ou l'utiliser uniquement s'ils veulent lui donner une valeur différente de la valeur par défaut.

L'argument par défaut est utilisé pour rendre l'appel aux fonctions plus facile, en particulier lorsque ces fonctions ont beaucoup d'arguments avec des valeurs types. Non seulement il devient plus facile d'écrire les appels, mais il est également plus facile de les lire, spécialement si le créateur de la classe peut ordonner les arguments de telle façon que les valeurs par défauts les moins modifiées apparaissent en dernier dans la liste.

Une utilisation particulièrement importante des arguments pas défaut est quand vous commencez l'utilisation d'une fonction avec un ensemble d'arguments, et après l'avoir utilisée quelques temps, vous découvrez que vous devez utiliser plus d'arguments. En donnant une valeur par défaut à tous les nouveaux arguments, vous garantissez que tout le code client utilisant l'interface précédente n'est pas perturbé.

7.7. Exercices

Les solutions des exercices suivants peuvent être trouvées dans le document électronique The Thinking in C++ Annotated Solution Guide, disponible à petit prix sur www.BruceEckel.com.

  1. Créez une classe Text contenant un objet string pour stocker le texte d'un fichier. Munissez-le de deux constructeurs : un constructeur par défaut et un constructeur prenant un argument string qui est le nom du fichier à ouvrir. Quand le second constructeur est utilisé, ouvrez le fichier et lisez le contenu dans le membre string de l'objet. Ajoutez une fonction membre contents( ) retournant la string pour (par exemple) qu'elle puisse être affichée. Dans le main( ), ouvrez un fichier en utilisant Text et affichez le contenu.
  2. Créez une classe Message avec un constructeur qui prend un unique argument string avec une valeur par défaut. Créez un membre privé string, et dans le constructeur assignez simplement l'argument string à votre string interne. Créez deux fonctions membres surchargées appelées print( ): l'une qui ne prend aucun argument et affiche simplement le message stocké dans l'objet, et l'autre qui prend un argument string, lequel sera affiché en plus du message stocké. Cela a t-il plus de sens d'utiliser cette approche plutôt que celle utilisée pour le constructeur ?
  3. Déterminez comment générer l'assembly avec votre compilateur, et faites des expériences pour déduire le sytème de décoration des noms.
  4. Créez une classe contenant quatre fonctions membres avec 0, 1, 2 et 3 arguments int respectivement. Créez un main( ) qui crée un objet de votre classe et appelle chacune des fonctions membres. Maintenant modifiez la classe pour qu'elle n'ait maintenant qu'une seule fonction membre avec tous les arguments possédant une valeur pas défaut. Cela change-t-il votre main( )?
  5. Créez une fonction avec deux arguments et appelez-la dans le main( ). Maintenant rendez l'un des argument “muet” (sans identifiant) et regardez si votre appel dans main( ) change.
  6. Modifiez Stash3.h et Stash3.cpp pour utiliser les arguments par défaut dans le constructeur. Testez le constructeur en créant deux versions différentes de l'objet Stash.
  7. Créez une nouvelle version de la classe Stack(du Chapitre 6) contenant le constructeur par défaut comme précédemment, et un second constructeur prenant comme argument un tableau de pointeurs d'objets et la taille de ce tableau. Ce constructeur devra parcourir le tableau et empiler chaque pointeur sur la Stack. Testez la classe avec un tableau de string.
  8. Modifiez SuperVar pour qu'il y ait des #ifdef autour de tout le code vartype comme décrit dans la section sur les enum. Faites de vartype une énumération régulière et privée (sans instance) et modifiez print( ) pour qu'elle requiert un argument vartype pour lui dire quoi faire.
  9. Implémentez Mem2.h et assurez-vous que la classe modifiée fonctionne toujours avec MemTest.cpp.
  10. Utilisez la classe Mem pour implémenter Stash. Notez que parce que l'implémentation est privée et ainsi cachée au programmeur client, le code de test n'a pas besoin d'être modifié.
  11. Dans la classe Mem, ajoutez une fonction membre bool moved( ) qui prend le résultat d'un appel à pointer( ) et vous indique si le pointeur a bougé (en raison d'une réallocation). Ecrivez un main( ) qui teste votre fonction membre moved( ). Cela a t-il plus de sens d'utiliser quelque chose comme moved( ) ou d'appeler simplement pointer( ) chaque fois que vous avez besoin d'accéder à la mémoire d'un Mem?

précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2005 Bruce Eckel. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.