Étapes suivantes

Présentation de la programmation et C++

Ce tutoriel en ligne continue avec des concepts plus avancés. Veuillez consulter la Partie III. Dans ce module, nous allons nous concentrer sur l'utilisation des pointeurs et les premiers objets avec les objets.

Apprendre à l'aide de l'exemple 2

L'objectif de ce module est de vous familiariser avec la décomposition, de comprendre les pointeurs, et de faire vos premiers pas avec les objets et les classes. Parcourez les exemples suivants. Lorsque vous y êtes invité, écrivez les programmes vous-même ou faites des expériences. On ne saurait jamais assez insister sur le fait que la clé pour devenir un bon programmeur est de s'entraîner, de s'entraîner et de s'entraîner !

Exemple n° 1: Autres pratiques de décomposition

Prenons le résultat suivant d'un jeu simple:

Welcome to Artillery.
You are in the middle of a war and being charged by thousands of enemies.
You have one cannon, which you can shoot at any angle.
You only have 10 cannonballs for this target..
Let's begin...

The enemy is 507 feet away!!!
What angle? 25<
You over shot by 445
What angle? 15
You over shot by 114
What angle? 10
You under shot by 82
What angle? 12
You under shot by 2
What angle? 12.01
You hit him!!!
It took you 4 shots.
You have killed 1 enemy.
I see another one, are you ready? (Y/N) n

You killed 1 of the enemy.

La première observation est le texte d'introduction qui s'affiche une fois par exécution de programme. Nous avons besoin d'un générateur de nombres aléatoires pour définir la distance de l'ennemi à chaque manche. Nous avons besoin d'un mécanisme permettant d'obtenir l'angle d'entrée de la part du joueur. Cette structure est évidemment en boucle, car elle se répète jusqu'à ce que nous atteignions l'ennemi. Nous avons également besoin d'une fonction pour calculer la distance et l'angle. Enfin, nous devons effectuer le suivi du nombre de coups de feu utilisés pour frapper l'ennemi, ainsi que du nombre d'ennemis touchés pendant l'exécution du programme. Voici un aperçu possible du programme principal.

StartUp(); // This displays the introductory script.
killed = 0;
do {
  killed = Fire(); // Fire() contains the main loop of each round.
  cout << "I see another one, care to shoot again? (Y/N) " << endl;
  cin >> done;
} while (done != 'n');
cout << "You killed " << killed << " of the enemy." << endl;

La procédure Fire gère le déroulement du jeu. Dans cette fonction, nous appelons un générateur de nombres aléatoires pour connaître la distance de l'ennemi, puis nous configurons la boucle pour obtenir l'entrée du joueur et calculer s'il a touché l'ennemi ou non. La condition de garde en boucle indique à quel point nous sommes sur le point d'atteindre l'ennemi.

In case you are a little rusty on physics, here are the calculations:

Velocity = 200.0; // initial velocity of 200 ft/sec Gravity = 32.2; // gravity for distance calculation // in_angle is the angle the player has entered, converted to radians. time_in_air = (2.0 * Velocity * sin(in_angle)) / Gravity; distance = round((Velocity * cos(in_angle)) * time_in_air);

En raison des appels à cos() et sin(), vous devrez inclure math.h. Essayez d'écrire ce programme, car il s'agit d'une excellente pratique de décomposition des problèmes et d'une bonne évaluation du langage C++ de base. N'oubliez pas de n'effectuer qu'une seule tâche dans chaque fonction. Comme il s'agit du programme le plus sophistiqué que nous ayons écrit jusqu'à présent, cela peut vous prendre un peu de temps.Voici la solution

Exemple n° 2: S'entraîner avec des pointeurs

Lorsque vous utilisez des pointeurs, il y a quatre points à retenir :
  1. Les pointeurs sont des variables qui contiennent des adresses mémoire. Lors de l'exécution d'un programme, toutes les variables sont stockées en mémoire, chacune à sa propre adresse ou à son propre emplacement. Un pointeur est un type spécial de variable qui contient une adresse mémoire plutôt qu'une valeur de données. Tout comme les données sont modifiées lorsqu'une variable normale est utilisée, la valeur de l'adresse stockée dans un pointeur est modifiée au fur et à mesure qu'une variable de pointeur est manipulée. Exemple :
    int *intptr; // Declare a pointer that holds the address
                 // of a memory location that can store an integer.
                 // Note the use of * to indicate this is a pointer variable.
    
    intptr = new int; // Allocate memory for the integer.
    *intptr = 5; // Store 5 in the memory address stored in intptr.
          
  2. On dit généralement qu'un pointeur "pointe" vers l'emplacement qu'il stocke (le "pointeur"). Ainsi, dans l'exemple ci-dessus, "intptr" pointe vers la pointe 5.

    Notez l'utilisation de l'opérateur "new" (nouveau) afin d'allouer de la mémoire pour nos pointes d'entiers. Nous devons effectuer cette opération avant d'essayer d'accéder à la pointe.

    int *ptr; // Declare integer pointer.
    ptr = new int; // Allocate some memory for the integer.
    *ptr = 5; // Dereference to initialize the pointee.
    *ptr = *ptr + 1; // We are dereferencing ptr in order
                     // to add one to the value stored
                     // at the ptr address.
          

    L'opérateur * est utilisé pour déréférencer dans C. L'une des erreurs les plus courantes commises par les programmeurs C/C++ lors de l'utilisation des pointeurs est d'oublier d'initialiser le pointee. Cela peut parfois entraîner un plantage de l'environnement d'exécution, car nous accédons à un emplacement en mémoire qui contient des données inconnues. Si nous essayons de modifier ces données, cela peut entraîner une légère corruption de la mémoire, ce qui rend le bug difficile à détecter. 

  3. L'affectation de deux pointeurs les fait pointer vers la même pointe. Ainsi, l'affectation y = x; pointe y pointe sur le même pointee que x. L'attribution du pointeur ne touche pas la pointe. Elle modifie simplement un pointeur pour qu'il prenne le même emplacement qu'un autre. Après l'affectation des deux pointeurs, les deux pointeurs "partagent" le pointe. 
  4. void main() {
     int* x; // Allocate the pointers x and y
     int* y; // (but not the pointees).
    
     x = new int; // Allocate an int pointee and set x to point to it.
    
     *x = 42; // Dereference x and store 42 in its pointee
    
     *y = 13; // CRASH -- y does not have a pointee yet
    
     y = x; // Pointer assignment sets y to point to x's pointee
    
     *y = 13; // Dereference y to store 13 in its (shared) pointee
    }
      

Voici une trace de ce code:

1. Attribuez deux pointeurs x et y. L'attribution des pointeurs n'entraîne pas l'allocation de pointes.
2. Attribuez une pointe à une pointe et définissez x pour qu'elle pointe vers celle-ci.
3. Déréférencez-en x pour stocker 42 dans sa pointe. Voici un exemple de base de l'opération de déréférencement. Commencez à x, puis suivez la flèche pour accéder à sa pointe.
4. Essayez de déréférencer y pour en stocker 13 dans sa pointe. Ce problème plante, car vous n'avez pas de pointe. Elle ne lui a jamais été attribuée.
5. Attribuez y = x; de sorte que y pointe sur x's pointee. Placez ensuite le pointeur sur "X" et "Y" vers le même point : ils "partagent".
6. Essayez de déréférencer y pour en stocker 13 dans sa pointe. Cette fois-ci, ça marche, car le devoir précédent m’a donné une pointe.

Comme vous pouvez le voir, les images sont très utiles pour comprendre l'utilisation des pointeurs. Voici un autre exemple.

int my_int = 46; // Declare a normal integer variable.
                 // Set it to equal 46.

// Declare a pointer and make it point to the variable my_int
// by using the address-of operator.
int *my_pointer = &my_int;

cout << my_int << endl; // Displays 46.

*my_pointer = 107; // Derefence and modify the variable.

cout << my_int << endl; // Displays 107.
cout << *my_pointer << endl; // Also 107.

Notez dans cet exemple que nous n'avons jamais alloué de mémoire avec l'opérateur "new". Nous avons déclaré une variable entière normale et l'avons manipulée à l'aide de pointeurs.

Cet exemple illustre l'utilisation de l'opérateur de suppression qui libère de la mémoire de tas de mémoire et la façon dont nous pouvons allouer des ressources pour des structures plus complexes. Nous aborderons l'organisation de la mémoire (tas de mémoire et pile d'exécution) dans une autre leçon. Pour l'instant, considérez le tas de mémoire comme un espace de stockage sans frais disponible pour l'exécution de programmes.

int *ptr1; // Declare a pointer to int.
ptr1 = new int; // Reserve storage and point to it.

float *ptr2 = new float; // Do it all in one statement.

delete ptr1; // Free the storage.
delete ptr2;

Dans ce dernier exemple, nous verrons comment les pointeurs sont utilisés pour transmettre des valeurs en référence à une fonction. C'est ainsi que nous modifions les valeurs des variables dans une fonction.

// Passing parameters by reference.
#include <iostream>
using namespace std;

void Duplicate(int& a, int& b, int& c) {
  a *= 2;
  b *= 2;
  c *= 2;
}

int main() {
  int x = 1, y = 3, z = 7;
  Duplicate(x, y, z);
  // The following outputs: x=2, y=6, z=14.
  cout << "x="<< x << ", y="<< y << ", z="<< z;
  return 0;
}

Si nous excluons le "&" des arguments dans la définition de la fonction "Dupliquer", nous transmettons les variables "par valeur", c'est-à-dire que la valeur de la variable est copiée. Toute modification apportée à la variable dans la fonction modifie la copie. Ils ne modifient pas la variable d'origine.

Lorsqu'une variable est transmise par référence, nous ne transmettons pas de copie de sa valeur, mais l'adresse de la variable à la fonction. Toute modification apportée à la variable locale modifie en fait la variable d'origine transmise. 

Si vous êtes un programmeur C, il s'agit d'une nouvelle touche. Nous pourrions procéder de la même manière en C en déclarant Duplicate() comme Duplicate(int *x), auquel cas x est un pointeur vers un int, puis en appelant Duplicate() avec l'argument &x (adresse de x) et en utilisant le déréférencement de x dans Duplicate() (voir ci-dessous). Mais C++ fournit un moyen plus simple de transmettre des valeurs aux fonctions par référence, même si l'ancienne méthode "C" fonctionne toujours.

void Duplicate(int *a, int *b, int *c) {
  *a *= 2;
  *b *= 2;
  *c *= 2;
}

int main() {
  int x = 1, y = 3, z = 7;
  Duplicate(&x, &y, &z);
  // The following outputs: x=2, y=6, z=14.
  cout << "x=" << x << ", y=" << y << ", z=" << z;
  return 0;
}

Notez qu'avec les références C++, il n'est pas nécessaire de transmettre l'adresse d'une variable, ni de déréférencer la variable dans la fonction appelée.

Que produit le programme suivant ? Dessinez une image de souvenir pour le comprendre.

void DoIt(int &foo, int goo);

int main() {
  int *foo, *goo;
  foo = new int;
  *foo = 1;
  goo = new int;
  *goo = 3;
  *foo = *goo + 3;
  foo = goo;
  *goo = 5;
  *foo = *goo + *foo;
  DoIt(*foo, *goo);
  cout << (*foo) << endl;
}

void DoIt(int &foo, int goo) {
  foo = goo + 3;
  goo = foo + 4;
  foo = goo + 3;
  goo = foo;
} 

Exécutez le programme pour voir si vous avez trouvé la bonne réponse.

Exemple n° 3: Transmettre des valeurs par référence

Écrivez une fonction appelée "Accelerate()" comme entrée, et qui prend en entrée la vitesse d'un véhicule et une quantité. Cette fonction augmente la vitesse du véhicule. Le paramètre de vitesse doit être transmis par référence et le montant par valeur. Voici notre solution.

Exemple 4: Classes et objets

Prenons la classe suivante:

// time.cpp, Maggie Johnson
// Description: A simple time class.

#include <iostream>
using namespace std;

class Time {
 private:
  int hours_;
  int minutes_;
  int seconds_;
 public:
  void set(int h, int m, int s) {hours_ = h; minutes_ = m; seconds_ = s; return;}
  void increment();
  void display();
};

void Time::increment() {
  seconds_++;
  minutes_ += seconds_/60;
  hours_ += minutes_/60;
  seconds_ %= 60;
  minutes_ %= 60;
  hours_ %= 24;
  return;
}

void Time::display() {
  cout << (hours_ % 12 ? hours_ % 12:12) << ':'
       << (minutes_ < 10 ? "0" :"") << minutes_ << ':'
       << (seconds_ < 10 ? "0" :"") << seconds_
       << (hours_ < 12 ? " AM" : " PM") << endl;
}

int main() {
  Time timer;
  timer.set(23,59,58);
  for (int i = 0; i < 5; i++) {
    timer.increment();
    timer.display();
    cout << endl;
  }
}

Notez que les variables des membres de classe se terminent par un trait de soulignement. Cela permet de différencier les variables locales des variables de classe.

Ajoutez une méthode de diminution à cette classe. Voici notre solution.

Les merveilles de la science: l'informatique

Exercices

Comme dans le premier module de ce cours, nous ne fournissons pas de solutions aux exercices ni aux projets.

N'oubliez pas qu'un bon programme...

... est décomposée de manière logique en fonctions, où une fonction n'effectue qu'une seule tâche.

... dispose d'un programme principal qui se lit comme un aperçu de ce qu'il fera.

... possède des noms descriptifs de fonctions, de constantes et de variables.

... utilise des constantes pour éviter tout nombre "magique" dans le programme.

... propose une interface utilisateur conviviale.

Exercices d'échauffement

  • Exercice n° 1

    L'entier 36 a une propriété particulière: c'est un carré parfait. Il correspond également à la somme des entiers compris entre 1 et 8. Le prochain nombre est 1 225, soit 352, et la somme des entiers compris entre 1 et 49. Trouver le nombre suivant, qui est un carré parfait, ainsi que la somme d'une série 1...n. Le nombre suivant peut être supérieur à 32 767. Vous pouvez utiliser des fonctions de bibliothèque que vous connaissez (ou des formules mathématiques) pour accélérer l'exécution de votre programme. Il est également possible d'écrire ce programme à l'aide de boucles for pour déterminer si un nombre est un carré parfait ou la somme d'une série. (Remarque: selon votre machine et votre programme, la recherche de ce nombre peut prendre un certain temps.)

  • Exercice 2

    La librairie de votre université a besoin de votre aide pour estimer son activité pour l'année prochaine. L'expérience a montré que la vente dépend grandement de la nécessité d'un livre pour un cours ou simplement de son utilisation facultative. Un nouveau manuel obligatoire sera vendu à 90% des participants potentiels, mais s'il a déjà été utilisé lors d'un cours, seuls 65% l'achèteront. De même, 40% des élèves potentiels achèteront un nouveau manuel facultatif, mais s'ils ont déjà été utilisés en classe avant seulement 20% d'entre eux l'achèteront. Notez que le terme "occasion" ne fait pas référence aux livres d'occasion.

  • Écrivez un programme qui accepte comme entrée une série de livres (jusqu'à ce que l'utilisateur ait saisi une sentinelle). Pour chaque livre, demandez un code, le coût de l'exemplaire unique, le nombre actuel de livres disponibles, l'inscription potentielle au cours et les données indiquant si le livre est obligatoire/facultatif, neuf ou d'occasion par le passé. En sortie, affichez toutes les informations d'entrée sur un écran bien formaté, ainsi que le nombre de livres à commander (le cas échéant, notez que seuls les nouveaux livres sont commandés) et le coût total de chaque commande.

    Une fois toutes les données saisies, affichez le coût total de toutes les commandes de livres et le bénéfice attendu si le magasin paie 80% du prix catalogue. Comme nous n'avons pas encore parlé des façons de gérer un grand ensemble de données entrant dans un programme (restez à l'écoute !), il vous suffit de traiter un livre à la fois et d'afficher l'écran de sortie correspondant. Ensuite, une fois que l'utilisateur a terminé de saisir toutes les données, votre programme doit générer les valeurs de total et de bénéfice.

    Avant de commencer à écrire du code, prenez le temps de réfléchir à la conception de ce programme. Décomposer en un ensemble de fonctions, puis créez une fonction main() qui se lit comme un aperçu de votre solution au problème. Assurez-vous que chaque fonction effectue une tâche.

    Voici un exemple de résultat:

    Please enter the book code: 1221
     single copy price: 69.95
     number on hand: 30
     prospective enrollment: 150
     1 for reqd/0 for optional: 1
     1 for new/0 for used: 0
    ***************************************************
    Book: 1221
    Price: $69.95
    Inventory: 30
    Enrollment: 150
    
    This book is required and used.
    ***************************************************
    Need to order: 67
    Total Cost: $4686.65
    ***************************************************
    
    Enter 1 to do another book, 0 to stop. 0
    ***************************************************
    Total for all orders: $4686.65
    Profit: $937.33
    ***************************************************

Projet de base de données

Dans ce projet, nous allons créer un programme C++ entièrement fonctionnel qui implémente une application de base de données simple.

Notre programme nous permettra de gérer une base de données de compositeurs et d'informations pertinentes les concernant. Le programme propose les fonctionnalités suivantes:

  • Possibilité d'ajouter un compositeur
  • Capacité à classer un compositeur (c'est-à-dire à indiquer à quel point nous aimons ou n'aimons pas sa musique)
  • Possibilité d'afficher tous les compositeurs de la base de données
  • Possibilité d'afficher tous les compositeurs par classement

"Il existe deux façons d'élaborer une conception logicielle: la première consiste à la simplifier de sorte qu'elle ne présente évidemment pas de défauts, et l'autre à la rendre si compliquée qu'il n'y a pas de défauts évidents. La première méthode est beaucoup plus difficile. » – C.A.R. Hoare

Beaucoup d'entre nous ont appris à concevoir et à coder en utilisant une approche « procédurale ». La principale question qui nous intéresse est "Que doit faire le programme ?". Nous décomposerons la solution à un problème en tâches, chacune permettant de résoudre une partie du problème. Ces tâches sont mappées aux fonctions de notre programme, qui sont appelées de manière séquentielle à partir de main() ou d'autres fonctions. Cette approche étape par étape est idéale pour certains problèmes à résoudre. Mais le plus souvent, nos programmes ne sont pas de simples séquences de tâches ou d'événements linéaires.

Avec une approche orientée objet, nous commençons par la question "Quels objets du monde réel suis-je en train de modéliser ?". Au lieu de diviser un programme en tâches comme décrit ci-dessus, nous le divisons en modèles d'objets physiques. Ces objets physiques ont un état défini par un ensemble d'attributs, ainsi qu'un ensemble de comportements ou d'actions qu'ils peuvent effectuer. Ces actions peuvent modifier l'état de l'objet ou appeler des actions d'autres objets. Le principe de base est qu'un objet "sait" faire les choses par lui-même. 

Dans la conception OO, nous définissons les objets physiques en termes de classes et d'objets, d'attributs et de comportements. Un programme OO contient généralement un grand nombre d'objets. Cependant, beaucoup de ces objets sont fondamentalement identiques. Tenez compte des points suivants.

Une classe est un ensemble d'attributs et de comportements généraux pour un objet, qui peuvent exister physiquement dans le monde réel. Dans l'illustration ci-dessus, nous avons une classe Apple. Toutes les pommes, quelle que soit leur espèce, ont des caractéristiques de couleur et de goût. Nous avons également défini le comportement d'Apple pour qu'il affiche ses attributs.

Dans ce schéma, nous avons défini deux objets de la classe Apple. Chaque objet possède les mêmes attributs et actions que la classe, mais définit les attributs pour un type de pomme spécifique. En outre, l'action d'affichage affiche les attributs de cet objet particulier, par exemple « Vert » et « Aigre ».

Une conception d'exécution (OO) se compose d'un ensemble de classes, des données associées à ces classes et de l'ensemble d'actions que les classes peuvent effectuer. Nous devons également identifier les interactions entre les différentes classes. Cette interaction peut être effectuée par des objets d'une classe appelant les actions d'objets d'autres classes. Par exemple, nous pourrions avoir une classe AppleOutputer qui génère la couleur et le goût d'un tableau d'objets Apple, en appelant la méthode Display() de chaque objet Apple.

Voici les étapes que nous effectuons pour la conception OO:

  1. Identifier les classes et définir de manière générale ce qu'un objet de chaque classe stocke en tant que données et ce qu'un objet peut faire.
  2. Définir les éléments de données de chaque classe
  3. Définissez les actions de chaque classe et la manière dont certaines actions d'une classe peuvent être implémentées à l'aide d'actions d'autres classes associées.

Pour un système volumineux, ces étapes se déroulent de manière itérative à différents niveaux de détail.

Pour le système de base de données Composer, nous avons besoin d'une classe Composer qui encapsule toutes les données que nous souhaitons stocker sur un compositeur individuel. Un objet de cette classe peut se promouvoir ou se rétrograder (modifier son rang) et afficher ses attributs.

Nous avons également besoin d'une collection d'objets Composer. Pour cela, nous définissons une classe Database qui gère les enregistrements individuels. Un objet de cette classe peut ajouter ou récupérer des objets Composer, et en afficher individuellement en appelant l'action d'affichage d'un objet Composer.

Enfin, nous avons besoin d'une sorte d'interface utilisateur pour fournir des opérations interactives sur la base de données. Il s'agit d'une classe d'espace réservé, c'est-à-dire que nous ne savons pas encore à quoi ressemblera l'interface utilisateur, mais que nous savons que nous en aurons besoin. Elle sera peut-être graphique, voire basée sur du texte. Pour l'instant, nous définissons un espace réservé que nous pourrons remplir ultérieurement.

Maintenant que nous avons identifié les classes pour l'application de base de données Composer, l'étape suivante consiste à définir les attributs et les actions de ces classes. Dans le cas d'une application plus complexe, nous nous étions avec un crayon et du papier, des cartes UML, des cartes CRC ou des outils d'édition (OOD) pour cartographier la hiérarchie des classes et la manière dont les objets interagissent.

Pour notre base de données de compositeurs, nous définissons une classe Composer qui contient les données pertinentes que nous souhaitons stocker sur chaque compositeur. Il contient également des méthodes permettant de manipuler les classements et d'afficher les données.

La classe Database requiert une structure pour contenir les objets Composer. Nous devons être en mesure d'ajouter un nouvel objet Composer à la structure et de récupérer un objet Composer spécifique. Nous aimerions également afficher tous les objets, soit par ordre d'entrée, soit par classement.

La classe Interface utilisateur implémente une interface pilotée par un menu, avec des gestionnaires qui appellent des actions dans la classe Database. 

Si les classes sont faciles à comprendre, et si leurs attributs et actions sont clairs, comme dans l'application Composer, il est relativement facile de les concevoir. Toutefois, si vous avez des questions sur la façon dont les classes interagissent et interagissent, il est préférable de les extraire d'abord et d'examiner les détails avant de commencer à coder.

Une fois que nous avons une idée claire de la conception et que nous l'avons évaluée (plus d'informations bientôt à ce sujet), nous définissons l'interface pour chaque classe. À ce stade, nous ne nous préoccupons pas des détails de l'implémentation. À ce stade, nous ne nous intéressons pas aux attributs et aux actions, ni aux parties de l'état et des actions d'une classe qui sont disponibles pour les autres classes.

En C++, nous le faisons normalement en définissant un fichier d'en-tête pour chaque classe. La classe Composer comporte des membres de données privées pour toutes les données que nous souhaitons stocker sur un composer. Nous avons besoin d'accesseurs (méthodes "get") et de mutateurs (méthodes "set"), ainsi que des actions principales de la classe.

// composer.h, Maggie Johnson
// Description: The class for a Composer record.
// The default ranking is 10 which is the lowest possible.
// Notice we use const in C++ instead of #define.
const int kDefaultRanking = 10;

class Composer {
 public:
  // Constructor
  Composer();
  // Here is the destructor which has the same name as the class
  // and is preceded by ~. It is called when an object is destroyed
  // either by deletion, or when the object is on the stack and
  // the method ends.
  ~Composer();

  // Accessors and Mutators
  void set_first_name(string in_first_name);
  string first_name();
  void set_last_name(string in_last_name);
  string last_name();
  void set_composer_yob(int in_composer_yob);
  int composer_yob();
  void set_composer_genre(string in_composer_genre);
  string composer_genre();
  void set_ranking(int in_ranking);
  int ranking();
  void set_fact(string in_fact);
  string fact();

  // Methods
  // This method increases a composer's rank by increment.
  void Promote(int increment);
  // This method decreases a composer's rank by decrement.
  void Demote(int decrement);
  // This method displays all the attributes of a composer.
  void Display();

 private:
  string first_name_;
  string last_name_;
  int composer_yob_; // year of birth
  string composer_genre_; // baroque, classical, romantic, etc.
  string fact_;
  int ranking_;
};

La classe Database est également simple.

// database.h, Maggie Johnson
// Description: Class for a database of Composer records.
#include  <iostream>
#include "Composer.h"

// Our database holds 100 composers, and no more.
const int kMaxComposers = 100;

class Database {
 public:
  Database();
  ~Database();

  // Add a new composer using operations in the Composer class.
  // For convenience, we return a reference (pointer) to the new record.
  Composer& AddComposer(string in_first_name, string in_last_name,
                        string in_genre, int in_yob, string in_fact);
  // Search for a composer based on last name. Return a reference to the
  // found record.
  Composer& GetComposer(string in_last_name);
  // Display all composers in the database.
  void DisplayAll();
  // Sort database records by rank and then display all.
  void DisplayByRank();

 private:
  // Store the individual records in an array.
  Composer composers_[kMaxComposers];
  // Track the next slot in the array to place a new record.
  int next_slot_;
};

Notez que nous avons soigneusement encapsulé les données spécifiques à composer dans une classe distincte. Nous aurions pu placer une structure ou une classe dans la classe Database pour représenter l'enregistrement Composer et y accéder directement. Mais cela serait une "sous-objectification", c'est-à-dire que nous ne modélisons pas avec des objets autant que nous le pourrions.

Lorsque vous commencerez à travailler sur l'implémentation des classes Composer et Database, il est beaucoup plus propre de disposer d'une classe Composer distincte. En particulier, le fait d'effectuer des opérations atomiques distinctes sur un objet Composer simplifie considérablement l'implémentation des méthodes Display() dans la classe Database.

Bien entendu, il existe également une "objectification excessive", qui consiste à essayer de faire de tout une classe, ou à disposer de plus de classes que nécessaire. Il faut de la pratique pour trouver le bon équilibre. Vous constaterez que certains programmeurs ont des opinions différentes.

Il est souvent possible de déterminer si l'objectification est excessive ou sous-objective en créant soigneusement un diagramme de vos classes. Comme indiqué précédemment, il est important de définir la conception d'une classe avant de commencer à coder. Cela peut vous aider à analyser votre approche. UML (Unified Modeling Language) est une notation couramment utilisée à cet effet. Maintenant que nous disposons des classes définies pour les objets Composer et Database, nous avons besoin d'une interface permettant à l'utilisateur d'interagir avec la base de données. Un simple menu fera l'affaire:

Composer Database
---------------------------------------------
1) Add a new composer
2) Retrieve a composer's data
3) Promote/demote a composer's rank
4) List all composers
5) List all composers by rank
0) Quit

Nous pourrions implémenter l'interface utilisateur en tant que classe ou en tant que programme procédural. Tout un programme C++ ne doit pas nécessairement être une classe. En fait, si le traitement est séquentiel ou orienté tâche, comme dans ce programme de menu, vous pouvez l'implémenter de manière procédurale. Il est important de la mettre en œuvre de sorte qu'elle reste un "espace réservé", c'est-à-dire que si nous voulons créer une interface utilisateur graphique à un moment donné, nous ne devrions pas avoir à modifier quoi que ce soit dans le système, mais l'interface utilisateur.

La dernière chose dont nous avons besoin pour terminer l'application est un programme de test des classes. Pour la classe Composer, nous voulons un programme main() qui reçoit des entrées, insère un objet Composer, puis l'affiche pour s'assurer que la classe fonctionne correctement. Nous souhaitons également appeler toutes les méthodes de la classe Composer.

// test_composer.cpp, Maggie Johnson
//
// This program tests the Composer class.

#include <iostream>
#include "Composer.h"
using namespace std;

int main()
{
  cout << endl << "Testing the Composer class." << endl << endl;

  Composer composer;

  composer.set_first_name("Ludwig van");
  composer.set_last_name("Beethoven");
  composer.set_composer_yob(1770);
  composer.set_composer_genre("Romantic");
  composer.set_fact("Beethoven was completely deaf during the latter part of "
    "his life - he never heard a performance of his 9th symphony.");
  composer.Promote(2);
  composer.Demote(1);
  composer.Display();
}

Nous avons besoin d'un programme de test similaire pour la classe Database.

// test_database.cpp, Maggie Johnson
//
// Description: Test driver for a database of Composer records.
#include <iostream>
#include "Database.h"
using namespace std;

int main() {
  Database myDB;

  // Remember that AddComposer returns a reference to the new record.
  Composer& comp1 = myDB.AddComposer("Ludwig van", "Beethoven", "Romantic", 1770,
    "Beethoven was completely deaf during the latter part of his life - he never "
    "heard a performance of his 9th symphony.");
  comp1.Promote(7);

  Composer& comp2 = myDB.AddComposer("Johann Sebastian", "Bach", "Baroque", 1685,
    "Bach had 20 children, several of whom became famous musicians as well.");
  comp2.Promote(5);

  Composer& comp3 = myDB.AddComposer("Wolfgang Amadeus", "Mozart", "Classical", 1756,
    "Mozart feared for his life during his last year - there is some evidence "
    "that he was poisoned.");
  comp3.Promote(2);

  cout << endl << "all Composers: " << endl << endl;
  myDB.DisplayAll();
}

Notez que ces programmes de test simples constituent une bonne première étape, mais qu'ils nous obligent à inspecter manuellement la sortie pour nous assurer que le programme fonctionne correctement. À mesure que le système s'agrandit, l'inspection manuelle des résultats devient rapidement difficile. Dans une leçon ultérieure, nous présenterons des programmes d'autovérification sous la forme de tests unitaires.

La conception de notre application est maintenant terminée. L'étape suivante consiste à implémenter les fichiers .cpp pour les classes et l'interface utilisateur.Pour commencer, copiez-collez le code .h et le code du pilote de test ci-dessus dans des fichiers, puis compilez-les.Utilisez les pilotes d'essai pour tester vos cours. Ensuite, implémentez l'interface suivante:

Composer Database
---------------------------------------------
1) Add a new composer
2) Retrieve a composer's data
3) Promote/demote a composer's rank
4) List all composers
5) List all composers by rank
0) Quit

Utilisez les méthodes que vous avez définies dans la classe Database pour implémenter l'interface utilisateur. Assurez-vous que vos méthodes sont résistantes aux erreurs. Par exemple, un classement doit toujours se situer entre 1 et 10. Ne laissez personne ajouter 101 compositeurs non plus, sauf si vous prévoyez de modifier la structure des données dans la classe Database.

N'oubliez pas que tout votre code doit respecter nos conventions de codage, qui sont répétées ici pour plus de commodité:

  • Chaque programme que nous écrivons commence par un commentaire d'en-tête, fournissant le nom de l'auteur, ses coordonnées, une brève description et son utilisation (le cas échéant). Chaque fonction ou méthode commence par un commentaire sur l'opération et l'utilisation.
  • Nous ajoutons des commentaires explicatifs à l'aide de phrases complètes lorsque le code ne se documente pas, par exemple si le traitement est délicat, non évident, intéressant ou important.
  • Utilisez toujours des noms descriptifs: les variables sont des mots en minuscules séparés par "_", comme dans "my_variable". Les noms de fonctions/méthodes utilisent des lettres majuscules pour marquer les mots, comme dans MyExcitingFunction(). Les constantes commencent par un "k" et utilisent des lettres majuscules pour marquer les mots (kDaysInWeek, par exemple).
  • La mise en retrait est par multiples de deux. Le premier niveau est composé de deux espaces. Si un retrait supplémentaire est nécessaire, nous utilisons quatre espaces, six espaces, etc.

Bienvenue dans le monde réel !

Dans ce module, nous présenterons deux outils très importants utilisés dans la plupart des organisations d'ingénierie logicielle. Le premier est un outil de compilation, et le second est un système de gestion des configurations. Ces deux outils sont essentiels en ingénierie logicielle industrielle, où de nombreux ingénieurs travaillent souvent sur un seul grand système. Ces outils aident à coordonner et à contrôler les modifications apportées au code base, et constituent un moyen efficace de compiler et de lier un système à partir de nombreux fichiers de programme et d'en-tête.

Fichiers Make

Le processus de compilation d'un programme est généralement géré à l'aide d'un outil de compilation, qui compile et associe les fichiers requis, dans le bon ordre. Très souvent, les fichiers C++ ont des dépendances. Par exemple, une fonction appelée dans un programme réside dans un autre programme. Un fichier d'en-tête est peut-être nécessaire pour plusieurs fichiers .cpp différents. Un outil de compilation détermine l'ordre de compilation correct à partir de ces dépendances. De plus, seuls les fichiers qui ont été modifiés depuis la dernière compilation sont compilés. Cela peut vous faire gagner beaucoup de temps dans les systèmes constitués de plusieurs centaines ou milliers de fichiers.

Un outil de compilation Open Source appelé "make" est souvent utilisé. Pour en savoir plus, consultez cet article. Essayez de créer un graphique de dépendances pour l'application de base de données Composer, puis convertissez-le en fichier makefile.Notre solution est disponible ici.

Systèmes de gestion de configuration

Le deuxième outil utilisé en ingénierie logicielle industrielle est la gestion de configuration (CM). Ceci est utilisé pour gérer le changement. Supposons que Bob et Susan sont tous deux rédacteurs techniques et qu'ils travaillent tous les deux sur la mise à jour d'un manuel technique. Lors d'une réunion, son responsable lui attribue à chacun une section du même document à mettre à jour.

Le manuel technique est stocké sur un ordinateur auquel Bob et Suzanne peuvent accéder. Sans outil ou processus de gestion de la configuration, un certain nombre de problèmes peuvent survenir. Un scénario possible est que l'ordinateur qui stocke le document peut être configuré de sorte que Bob et Suzanne ne puissent pas travailler simultanément sur le manuel. Cela les ralentirait considérablement.

Une situation plus dangereuse survient lorsque l'ordinateur de stockage autorise l'ouverture du document par Bob et Susan en même temps. Voici ce qui pourrait se produire:

  1. Bob ouvre le document sur son ordinateur et travaille sur sa section.
  2. Susan ouvre le document sur son ordinateur et travaille sur sa section.
  3. Bob effectue ses modifications et enregistre le document sur l'ordinateur de stockage.
  4. Susan effectue ses modifications et enregistre le document sur l’ordinateur de stockage.

Cette illustration montre le problème qui peut survenir si aucune commande ne figure dans la copie du manuel technique. Lorsque Susan enregistre ses modifications, elle remplace celles effectuées par Bob.

C'est exactement le type de situation qu'un système CM peut contrôler. Avec un système de gestion de la configuration, Bob et Susan "consultent" leur propre exemplaire du manuel technique et travaillent dessus. Lorsque Bob vérifie ses modifications, le système sait que Susan a sa propre copie validée. Lorsque Susan vérifie sa copie, le système analyse les modifications apportées par Bob et Susan, puis crée une version qui fusionne les deux ensembles de modifications.

Les systèmes CM disposent d'un certain nombre de fonctionnalités en plus de la gestion des modifications simultanées, comme décrit ci-dessus. De nombreux systèmes stockent les archives de toutes les versions d'un document, dès sa création initiale. Dans le cas d'un manuel technique, cela peut être très utile lorsqu'un utilisateur possède une ancienne version du manuel et pose des questions à un rédacteur technique. Un système CM permettrait au rédacteur technique d'accéder à l'ancienne version et de voir ce que l'utilisateur voit.

Les systèmes CM sont particulièrement utiles pour contrôler les modifications apportées aux logiciels. Ces systèmes sont appelés systèmes de gestion de configuration logicielle (SCM, Software Configuration Management). Si l'on considère le grand nombre de fichiers de code source individuels dans une grande organisation d'ingénierie logicielle et le grand nombre d'ingénieurs qui doivent y apporter des modifications, il est évident qu'un système SCM est essentiel.

Gestion de la configuration logicielle

Les systèmes SCM sont basés sur une idée simple: les copies définitives de vos fichiers sont conservées dans un dépôt central. Les utilisateurs extraient les copies des fichiers du dépôt, travaillent sur ces copies, puis les vérifient lorsqu'elles sont terminées. Les systèmes SCM gèrent et suivent les révisions de plusieurs personnes sur un seul ensemble principal. 

Tous les systèmes SCM offrent les fonctionnalités essentielles suivantes:

  • Gestion de la simultanéité
  • Gestion des versions
  • Synchronisation

Examinons chacune de ces fonctionnalités plus en détail.

Gestion de la simultanéité

La simultanéité désigne la modification simultanée d'un fichier par plusieurs personnes. Avec un dépôt volumineux, nous voulons que les utilisateurs puissent effectuer cette opération, mais cela peut entraîner des problèmes.

Prenons un exemple simple dans le domaine de l'ingénierie: supposons que nous permettions aux ingénieurs de modifier le même fichier simultanément dans un dépôt central de code source. Les clients 1 et 2 doivent tous deux modifier un fichier en même temps:

  1. Le client1 ouvre bar.cpp.
  2. Le client2 ouvre bar.cpp.
  3. Client1 modifie le fichier et l'enregistre.
  4. Client2 modifie le fichier et l'enregistre en écrasant les modifications du Client1.

Évidemment, nous ne voulons pas que cela se produise. Même si nous contrôlons la situation en demandant aux deux ingénieurs de travailler sur des copies distinctes plutôt que directement sur un ensemble principal (comme dans l'illustration ci-dessous), les copies doivent être rapprochées. La plupart des systèmes SCM résolvent ce problème en permettant à plusieurs ingénieurs de vérifier un fichier ("synchroniser" ou "mettre à jour") et d'apporter des modifications si nécessaire. Le système SCM exécute ensuite des algorithmes pour fusionner les modifications à mesure que les fichiers sont vérifiés ("submit" ou "commit") dans le dépôt.

Ces algorithmes peuvent être simples (demander aux ingénieurs de résoudre les modifications conflictuelles) ou moins simples (déterminer comment fusionner intelligemment les modifications conflictuelles et demander à un ingénieur uniquement si le système est vraiment bloqué). 

Gestion des versions

La gestion des versions fait référence au suivi des révisions de fichiers, ce qui permet de recréer une version précédente du fichier (ou d'effectuer un rollback vers celle-ci). Pour ce faire, vous pouvez créer une copie d'archive de chaque fichier lorsqu'il est enregistré dans le dépôt, ou enregistrer chaque modification apportée à un fichier. Nous pouvons à tout moment utiliser les archives ou modifier les informations pour créer une version précédente. Les systèmes de gestion des versions peuvent également créer des rapports de journal indiquant les utilisateurs qui ont effectué les modifications, la date de leur enregistrement et les modifications apportées.

Synchronisation

Avec certains systèmes SCM, les fichiers individuels sont enregistrés et retirés du dépôt. Des systèmes plus puissants vous permettent de consulter plusieurs fichiers à la fois. Les ingénieurs consultent leur propre copie complète du dépôt (ou une partie de celui-ci) et travaillent sur les fichiers selon les besoins. Ils valident ensuite régulièrement leurs modifications dans le dépôt principal et mettent à jour leurs copies personnelles pour rester informés des modifications apportées par d'autres personnes. Ce processus est appelé synchronisation ou mise à jour.

Subversion

Subversion (SVN) est un système de contrôle des versions Open Source. Il possède toutes les fonctionnalités décrites ci-dessus.

Le SVN adopte une méthodologie simple en cas de conflit. Un conflit se produit lorsque deux ingénieurs ou plus apportent des modifications différentes à la même zone du code base, puis soumettent leurs modifications. Le SVN avertit uniquement les ingénieurs en cas de conflit : il appartient aux ingénieurs de le résoudre.

Nous allons utiliser le SVN tout au long de ce cours pour vous aider à vous familiariser avec la gestion de configuration. Ces systèmes sont très courants dans l'industrie.

La première étape consiste à installer le SVN sur votre système. Cliquez ici pour obtenir des instructions. Recherchez votre système d'exploitation et téléchargez le binaire approprié.

Quelques terminologies concernant les SVN

  • Révision: modification apportée à un fichier ou à un ensemble de fichiers. Une révision est un "instantané" dans un projet en constante évolution.
  • Dépôt: copie maître dans laquelle le SVN stocke l'historique complet des révisions d'un projet. Chaque projet possède un dépôt.
  • Copie de travail: copie dans laquelle un ingénieur modifie un projet. Il peut exister de nombreuses copies de travail d'un projet donné, chacune appartenant à un ingénieur individuel.
  • Payez: demandez une copie de travail à partir du dépôt. Une copie de travail correspond à l'état du projet lorsqu'il a été finalisé.
  • Commit: pour envoyer les modifications de votre copie de travail vers le référentiel central. Également appelé "enregistrement" ou "envoi".
  • Mise à jour: pour intégrer dans votre copie de travail les modifications apportées par d'autres utilisateurs depuis le dépôt, ou pour indiquer si votre copie de travail comporte des modifications non validées. Cette opération s'apparente à une synchronisation, comme décrit ci-dessus. L'option "update/sync" met à jour votre copie de travail avec la copie du dépôt.
  • Conflit: situation dans laquelle deux ingénieurs tentent de valider des modifications dans la même zone d'un fichier. Le SVN indique des conflits, mais les ingénieurs doivent les résoudre.
  • Message de journal: commentaire décrivant vos modifications que vous joignez à une révision lorsque vous la validez. Le journal fournit un résumé de ce qui se passe dans un projet.

Maintenant que SVN est installé, nous allons exécuter quelques commandes de base. La première chose à faire est de configurer un dépôt dans un répertoire spécifié. Voici les commandes:

$ svnadmin create /usr/local/svn/newrepos
$ svn import mytree file:///usr/local/svn/newrepos/project -m "Initial import"
Adding         mytree/foo.c
Adding         mytree/bar.c
Adding         mytree/subdir
Adding         mytree/subdir/foobar.h

Committed revision 1.

La commande import copie le contenu du répertoire mytree dans le projet de répertoire du dépôt. Nous pouvons consulter le répertoire du dépôt à l'aide de la commande list.

$ svn list file:///usr/local/svn/newrepos/project
bar.c
foo.c
subdir/

L'importation ne crée pas de copie fonctionnelle. Pour ce faire, vous devez utiliser la commande svncheckout. Cela crée une copie de travail de l'arborescence de répertoires. Faisons-le maintenant:

$ svn checkout file:///usr/local/svn/newrepos/project
A    foo.c
A    bar.c
A    subdir
A    subdir/foobar.h
…
Checked out revision 215.

Maintenant que vous disposez d'une copie de travail, vous pouvez modifier les fichiers et les répertoires qui s'y trouvent. Votre copie de travail est comme n'importe quelle autre collection de fichiers et de répertoires : vous pouvez en ajouter de nouvelles, les modifier, les déplacer, et même supprimer l'intégralité de la copie de travail. Notez que si vous copiez et déplacez des fichiers dans votre copie de travail, il est important d'utiliser les commandes svn copy et svn move plutôt que les commandes de votre système d'exploitation. Pour ajouter un fichier, utilisez svn add. Pour supprimer un fichier, utilisez svn delete. Si vous souhaitez simplement apporter des modifications, il vous suffit d'ouvrir le fichier avec votre éditeur et de le modifier.

Il existe certains noms de répertoire standard souvent utilisés avec Subversion. Le répertoire "jonction" contient la ligne de développement principale de votre projet. Un répertoire "branches" contient toutes les versions de branche sur lesquelles vous êtes susceptible de travailler.

$ svn list file:///usr/local/svn/repos
/trunk
/branches

Supposons que vous ayez apporté toutes les modifications requises à votre copie de travail et que vous souhaitiez la synchroniser avec le dépôt. Si de nombreux autres ingénieurs travaillent dans cette section du dépôt, il est important de maintenir votre copie de travail à jour. Vous pouvez utiliser la commande svn status pour afficher les modifications que vous avez apportées.

A       subdir/new.h      # file is scheduled for addition
D       subdir/old.c        # file is scheduled for deletion
M       bar.c                  # the content in bar.c has local modifications

Notez que la commande status comporte de nombreux indicateurs pour contrôler cette sortie. Pour afficher des modifications spécifiques dans un fichier modifié, utilisez svn diff.

$ svn diff bar.c
Index: bar.c
===================================================================
--- bar.c	(revision 5)
+++ bar.c	(working copy)
## -1,18 +1,19 ##
+#include
+#include

 int main(void) {
-  int temp_var;
+ int new_var;
...

Enfin, pour mettre à jour votre copie de travail à partir du dépôt, utilisez la commande svn update.

$ svn update
U  foo.c
U  bar.c
G  subdir/foobar.h
C  subdir/new.h
Updated to revision 2.

C'est là qu'un conflit peut survenir. Dans la sortie ci-dessus, le "U" indique qu'aucune modification n'a été apportée aux versions de dépôt de ces fichiers et qu'une mise à jour a été effectuée. Le "G" signifie qu'une fusion a eu lieu. La version du dépôt avait été modifiée, mais les modifications n'étaient pas en conflit avec la vôtre. La lettre "C" indique un conflit. Cela signifie que les modifications du dépôt ont chevauché les vôtres. Vous devez maintenant choisir entre elles.

Pour chaque fichier présentant un conflit, Subversion place trois fichiers dans votre copie de travail:

  • file.mine: votre fichier tel qu'il existait dans votre copie de travail avant la mise à jour de votre copie de travail.
  • file.rOLDREV: fichier que vous avez extrait du dépôt avant d'apporter vos modifications.
  • file.rNEWREV: ce fichier correspond à la version actuelle dans le dépôt.

Vous pouvez effectuer l'une des trois actions suivantes pour résoudre le conflit:

  • Parcourez les fichiers et procédez à la fusion manuellement.
  • Copiez l'un des fichiers temporaires créés par SVN sur votre version de copie de travail.
  • Pour supprimer toutes vos modifications, exécutez la commande svn restauration.

Une fois le conflit résolu, signalez le problème à SVN en exécutant la commande svnresolve (Svn résolu). Cette opération supprime les trois fichiers temporaires, et le SVN ne l'affiche plus en état de conflit.

La dernière chose à faire est de valider votre version finale dans le dépôt. Pour ce faire, exécutez la commande svn commit. Lorsque vous effectuez un commit d'une modification, vous devez fournir un message de journal décrivant vos modifications. Ce message de journal est joint à la révision que vous créez.

svn commit -m "Update files to include new headers."  

Il y a tant à apprendre sur le SVN et sur la façon dont il peut soutenir des projets d'ingénierie logicielle de grande envergure. De nombreuses ressources sont disponibles sur le Web. Effectuez simplement une recherche Google sur "Subversion".

Pour vous entraîner, créez un dépôt pour votre système de base de données Composer et importez tous vos fichiers. Ensuite, récupérez une copie de travail et exécutez les commandes décrites ci-dessus.

Références

Livre Subversion en ligne

Article Wikipédia sur les numéros SVN

Site Web Subversion

Application: une étude en anatomie

Découvrez les eSkeletons de l'Université du Texas à Austin