C++ en profondeur

Tutoriel sur le langage C++

Les premières sections de ce tutoriel couvrent les sujets de base déjà présentés dans les deux derniers modules et fournissent plus d'informations sur les concepts avancés. Dans ce module, nous allons nous concentrer sur la mémoire dynamique, et obtenir plus d'informations sur les objets et les classes. Certains sujets avancés sont également présentés, tels que l'héritage, le polymorphisme, les modèles, les exceptions et les espaces de noms. Nous les étudierons plus tard dans le cours C++ avancé.

Conception orientée objet

Il s'agit d'un excellent tutoriel sur la conception orientée objet. Nous appliquerons la méthodologie présentée ici dans le projet de ce module.

Apprendre à l'aide de l'exemple 3

Dans ce module, nous allons apprendre à vous entraîner davantage avec les pointeurs, la conception orientée objet, les tableaux multidimensionnels et les classes/objets. Parcourez les exemples suivants. On ne saurait jamais assez insister sur le fait que la clé pour devenir un bon programmeur est de s'entraîner et de s'entraîner !

Exercice n° 1: S'entraîner davantage avec les pointeurs

Si vous avez besoin de vous entraîner davantage avec les pointeurs, consultez cette ressource qui couvre tous les aspects des pointeurs et fournit de nombreux exemples de programmes.

Quelle est la sortie du programme suivant ? Veuillez ne pas exécuter le programme, mais dessiner l'image de la mémoire pour déterminer la sortie.

void Unknown(int *p, int num);
void HardToFollow(int *p, int q, int *num);

void Unknown(int *p, int num) {
  int *q;

  q = #
  *p = *q + 2;
  num = 7;
}

void HardToFollow(int *p, int q, int *num) {
  *p = q + *num;
  *num = q;
  num = p;
  p = &q;
  Unknown(num, *p);
}

main() {
  int *q;
  int trouble[3];

  trouble[0] = 1;
  q = &trouble[1];
  *q = 2;
  trouble[2] = 3;

  HardToFollow(q, trouble[0], &trouble[2]);
  Unknown(&trouble[0], *q);

  cout << *q << " " << trouble[0] << " " << trouble[2];
}

Une fois que vous avez déterminé manuellement la sortie, exécutez le programme pour voir si vous avez raison.

Exercice n° 2: S'entraîner davantage avec les classes et les objets

Si vous avez besoin d'entraînement supplémentaire avec les classes et les objets, vous trouverez sur cette page une ressource qui explique l'implémentation de deux petites classes. Prenez le temps de faire les exercices.

Exercice n° 3: Tableaux multidimensionnels

Voici un exemple de programme: 

const int kStudents = 25;
const int kProblemSets = 10;

// This function returns the highest grade in the Problem Set array.
int get_high_grade(int *a, int cols, int row, int col) {
  int i, j;
  int highgrade = *a;

  for (i = 0; i < row; i++)
    for (j = 0; j < col; j++)
      if (*(a + i * cols + j) > highgrade)  // How does this line work?
        highgrade = *(a + i*cols + j);
  return highgrade;
}

int main() {
 int grades[kStudents][kProblemSets] = {
   {75, 70, 85, 72, 84},
   {85, 92, 93, 96, 86},
   {95, 90, 83, 76, 97},
   {65, 62, 73, 84, 73}
 };
 int std_num = 4;
 int ps_num = 5;
 int highest;

 highest = get_high_grade((int *)grades, kProblemSets, std_num, ps_num);
 cout << "The highest problem set score in the class is " << highest << endl;

 return 0;
}

Ce programme comporte une ligne intitulée "How does this line work?" (Comment fonctionne cette ligne ?) - tu peux le comprendre ? Pour en savoir plus, consultez cette page.

Écrivez un programme qui initialise un tableau à 3 dimensions et remplit la valeur de la 3e dimension avec la somme des trois index. La solution que nous proposons est disponible ici.

Exercice n° 4: exemple complet de conception d'un bureau extérieur

Voici un exemple de conception orientée objet détaillé, qui passe en revue l'intégralité du processus du début à la fin. Le code final est écrit dans le langage de programmation Java, mais sa lecture vous permettra de tenir compte de votre progression.

Prenez le temps de parcourir cet exemple dans son intégralité. Elle illustre parfaitement le processus et les outils de conception sur lesquels il repose.

Tests unitaires

Introduction

Les tests constituent une partie essentielle du processus d'ingénierie logicielle. Un test unitaire est un type particulier de test qui vérifie le fonctionnement d'un petit module unique du code source.Les tests unitaires sont toujours effectués par l'ingénieur et, généralement, au moment où il code le module. Les pilotes de test que vous avez utilisés pour tester les classes Composer et Database sont des exemples de tests unitaires.

Les tests unitaires présentent les caractéristiques suivantes. Ils...

  • tester un composant de manière isolée
  • sont déterministes ;
  • correspondent généralement à une seule classe
  • Évitez les dépendances sur des ressources externes, comme les bases de données, les fichiers, le réseau
  • s'exécutent rapidement
  • peuvent être exécutées dans n'importe quel ordre

Il existe des méthodologies et des frameworks automatisés qui assurent l'assistance et la cohérence des tests unitaires dans les grandes organisations d'ingénierie logicielle. Il existe des frameworks de tests unitaires Open Source sophistiqués, que nous aborderons dans la suite de cette leçon. 

Les tests effectués dans le cadre de tests unitaires sont illustrés ci-dessous.

Dans un monde idéal, nous testons les éléments suivants:

  1. L'interface du module est testée pour s'assurer que les informations entrent et sortent correctement.
  2. Les structures de données locales sont examinées pour s’assurer qu’elles stockent correctement les données.
  3. Les conditions des limites sont testées pour s'assurer que le module fonctionne correctement aux limites qui limitent ou restreignent le traitement.
  4. Nous testons les chemins d'accès indépendants dans le module pour nous assurer que chaque chemin, et donc chaque instruction du module, est exécuté au moins une fois. 
  5. Enfin, nous devons vérifier que les erreurs sont traitées correctement.

Couverture de code

En réalité, nos tests ne nous permettent pas d'obtenir une "couverture de code" complète. La couverture de code est une méthode d'analyse qui détermine quelles parties d'un système logiciel ont été exécutées (couvertes) par la suite de scénarios de test et lesquelles ne l'ont pas été. Si nous essayons d'atteindre une couverture de 100 %, nous passerons plus de temps à écrire des tests unitaires qu'à écrire le code réel. Envisagez de proposer des tests unitaires pour tous les chemins indépendants suivants. Cela peut rapidement devenir un problème exponentiel.

Dans ce schéma, les lignes rouges ne sont pas testées, tandis que les lignes non colorées le sont.

Au lieu d'essayer d'atteindre une couverture de 100 %, nous nous concentrons sur les tests qui nous permettent d'avoir l'assurance que le module fonctionne correctement. Nous testons les éléments suivants:

  • Cas nuls
  • Tests de la plage (par exemple, tests de valeurs positives/négatives)
  • Cas extrêmes
  • Cas d'échec
  • Tester les chemins les plus susceptibles de s'exécuter la plupart du temps

Frameworks de tests unitaires

La plupart des frameworks de test unitaire utilisent des assertions pour tester les valeurs lors de l'exécution d'un chemin d'accès. Les assertions sont des instructions qui vérifient si une condition est vraie. Le résultat d'une assertion peut être un succès, un échec non fatal ou un échec fatal. Une fois l'assertion effectuée, le programme continue normalement si le résultat est une réussite ou un échec non fatal. En cas d'échec fatal, la fonction actuelle est annulée.

Les tests se composent de code qui définit un état ou manipule votre module, associés à un certain nombre d'assertions qui vérifient les résultats attendus. Si toutes les assertions d'un test réussissent (autrement dit, si elles renvoient "true"), le test réussit. Sinon, il échoue.

Un scénario de test contient un ou plusieurs tests. Nous regroupons les tests dans des scénarios de test qui reflètent la structure du code testé. Dans ce cours, nous allons utiliser CPPUnit comme framework de test unitaire. Avec ce framework, nous pouvons écrire des tests unitaires en C++ et les exécuter automatiquement, ce qui génère un rapport sur la réussite ou l'échec des tests.

Installation de CPPUnit

Téléchargez le code CPPUnit depuis SourceForge. Recherchez un répertoire approprié et placez-y le fichier tar.gz. Saisissez ensuite les commandes suivantes (sous Linux et Unix) en remplaçant le nom du fichier cppunit approprié:

gunzip filename.tar.gz
tar -xvf filename.tar

Si vous travaillez sous Windows, vous aurez peut-être besoin d'un utilitaire permettant d'extraire les fichiers tar.gz. L'étape suivante consiste à compiler les bibliothèques. Accédez au répertoire "cppunit". Il y a un fichier INSTALL qui fournit des instructions spécifiques. En général, vous devez exécuter la commande suivante:

./configure
make install

Si vous rencontrez des problèmes, reportez-vous au fichier INSTALL. Les bibliothèques se trouvent généralement dans le répertoire cppunit/src/cppunit. Pour vérifier que la compilation a fonctionné, accédez au répertoire cppunit/examples/simple et saisissez "make". Si tout se compile correctement, vous êtes prêt.

Vous trouverez un excellent tutoriel sur cette page. Veuillez suivre ce tutoriel pour créer la classe de nombres complexes ainsi que les tests unitaires associés. Vous trouverez plusieurs autres exemples dans le répertoire cppunit/examples.

Pourquoi dois-je effectuer cette procédure ??

Les tests unitaires sont d'une importance capitale dans le secteur pour plusieurs raisons. Vous connaissez déjà une raison: nous avons besoin d'un moyen de vérifier notre travail lors du développement du code. Même lorsque nous développons un tout petit programme, nous écrivons instinctivement une sorte de vérificateur ou de pilote pour nous assurer que notre programme répond à nos attentes.

Grâce à leur longue expérience, les ingénieurs savent que les chances qu'un programme fonctionne du premier coup sont très faibles. Les tests unitaires s'appuient sur cette idée en permettant aux programmes de test de s'autocontrôler et d'être reproductibles. Les assertions remplacent l'inspection manuelle des résultats. De plus, comme les résultats sont faciles à interpréter (le test réussit ou échoue), les tests peuvent être exécutés plusieurs fois, ce qui fournit un filet de sécurité qui rend votre code plus résistant aux modifications.

Concrètement, cela fonctionne parfaitement lorsque vous envoyez votre code finalisé pour la première fois dans CVS. Et il continuera de fonctionner parfaitement pendant un certain temps. Un jour, quelqu'un d'autre modifie votre code. Tôt ou tard, quelqu'un va casser votre code. Pensez-vous qu'ils le remarqueront tout seul ? Peu probable. Toutefois, lorsque vous écrivez des tests unitaires, certains systèmes peuvent les exécuter automatiquement tous les jours. Il s'agit de systèmes d'intégration continue. Ainsi, lorsque cet ingénieur X casse votre code, le système lui envoie des e-mails désagréables jusqu'à ce qu'il le corrige. Même si vous êtes ingénieur X !

En plus de vous aider à développer des logiciels et à les protéger contre les changements, les tests unitaires vous aident à:

  • Crée une spécification exécutable et une documentation qui reste synchronisée avec le code. En d'autres termes, vous pouvez lire un test unitaire pour connaître le comportement compatible avec le module.
  • Elle vous aide à séparer les exigences de l'implémentation. Comme vous revendiquez un comportement visible de l'extérieur, vous avez la possibilité d'y réfléchir explicitement au lieu de mélanger des idées sur la façon de mettre en œuvre ce comportement.
  • Possibilité d'effectuer des tests Si vous disposez d'un filet de sécurité pour vous avertir en cas de défaillance du comportement d'un module, vous aurez plus de chances d'essayer et de reconfigurer vos conceptions.
  • Améliore vos conceptions. Pour écrire des tests unitaires complets, vous devez souvent faciliter le test de votre code. Le code testable est souvent plus modulaire que le code non testable.
  • Qualité élevée Un petit bug dans un système critique peut faire perdre des millions de dollars à une entreprise, ou pire encore, le bonheur ou la confiance d'un utilisateur. Le filet de sécurité fourni par les tests unitaires réduit ce risque. En détectant rapidement les bugs, elles permettent également aux équipes de contrôle qualité de se consacrer à des scénarios de défaillance plus complexes et plus complexes, au lieu de signaler des défaillances évidentes.

Prenez le temps d'écrire des tests unitaires avec CPPUnit pour l'application de base de données Composer. Reportez-vous au répertoire cppunit/examples/ pour obtenir de l'aide.

Fonctionnement de Google

Introduction

Imaginez qu'un moine, au Moyen Âge, consulte les milliers de manuscrits dans les archives de son monastère."Où est-ce que celui-ci d'Aristote..."

bibliothèque monastary

Heureusement pour lui, les manuscrits sont organisés par contenu et portant des symboles spéciaux pour faciliter la récupération des informations qu'ils contiennent. Sans une telle organisation, il serait très difficile de trouver le manuscrit pertinent.

L'activité de stockage et de récupération d'informations écrites à partir de grandes collections est appelée récupération d'informations (IR). Cette activité a pris de l'importance au fil des siècles, en particulier avec des inventions comme le papier et l'imprimerie. Avant, seules peu de personnes s'occupaient de ce lieu. Toutefois, aujourd'hui, des centaines de millions de personnes recherchent chaque jour des informations lorsqu'elles utilisent un moteur de recherche ou effectuent une recherche sur leur ordinateur.

Premiers pas dans la récupération d'informations

chat dans le chapeau

Le Dr Seuss a écrit 46 livres pour enfants en 30 ans. Dans ses livres, il racontait des chats, des vaches et des éléphants, des animaux, des sourires et le lorax. Tu te rappelles quelles créatures étaient dans quelle histoire ? À moins que vous ne soyez parent, seuls les enfants peuvent vous dire quelle série d'histoires du Dr Seuss contient ces créatures:

(COW et BEE) ou CROWS

Nous appliquerons des modèles classiques de récupération d'informations pour résoudre ce problème.

Une approche évidente est la force brute: récupérez les 46 histoires du Dr Seuss et commencez à lire. Pour chaque livre, notez lesquels contiennent les mots COW et BEE, et en même temps, recherchez les livres qui contiennent le mot CROWS. Les ordinateurs sont beaucoup plus rapides que nous. Si nous disposons de tout le texte des livres du Dr Seuss au format numérique, par exemple sous forme de fichiers texte, il nous suffit de lancer une commande grep sur les fichiers. Pour une petite collection telle que les livres du Dr Seuss, cette technique convient bien.

Cependant, il existe de nombreuses situations où nous en avons besoin. Par exemple, la collecte de toutes les données actuellement en ligne est beaucoup trop volumineuse pour que la commande grep ne puisse la gérer. De plus, nous ne voulons pas seulement obtenir les documents qui correspondent à notre état. Nous avons pris l'habitude de les classer en fonction de leur pertinence.

En plus de la commande grep, une autre approche consiste à créer un index des documents d'une collection avant d'effectuer la recherche. L'indice IR est semblable à l'indice situé au dos d'un manuel. Nous dressons une liste de tous les mots (ou termes) de chaque témoignage du Dr Seuss, en laissant des mots comme "le" et "et", ainsi que d'autres connexions, prépositions, etc. (il s'agit de mots vides). Nous représentons ensuite ces informations de manière à faciliter la recherche de termes et l'identification des articles dans lesquels ils apparaissent.

Une représentation possible est une matrice dans laquelle les articles apparaissent dans la partie supérieure et les termes sont listés sur chaque ligne. Un "1" dans une colonne indique que le terme apparaît dans la story de cette colonne.

tableau de livres et de mots

Nous pouvons visualiser chaque ligne ou chaque colonne sous la forme d'un vecteur de bits. Le vecteur de bits d'une ligne indique dans quelles histoires le terme apparaît. Le vecteur de bits d'une colonne indique quels termes apparaissent dans l'histoire.

Pour en revenir à notre problème d'origine:

(COW et BEE) ou CROWS

Nous prenons les vecteurs de bits pour ces termes et effectuons d'abord une opération AND au niveau du bit, puis un opérateur OR au niveau du bit sur le résultat.

(100001 et 010011) ou 000010 = 000011

La réponse : "M. Brown Can Moo ! et "Le Lorax". Il s'agit d'une illustration du modèle de récupération booléenne, qui est un modèle "à correspondance exacte".

Supposons que nous devions étendre la matrice pour inclure tous les articles du Dr Seuss et tous les termes pertinents. La matrice augmenterait considérablement, et il est important de noter que la plupart des entrées sont nulles. Une matrice n'est probablement pas la meilleure représentation de l'index. Nous devons trouver un moyen de ne stocker que les 1.

Quelques améliorations

La structure utilisée en infrarouge pour résoudre ce problème est appelée indice inversé. Nous conservons un dictionnaire des termes, puis pour chaque terme, nous disposons d'une liste répertoriant les documents dans lesquels le terme apparaît. Cette liste s'appelle une liste de publications. Une liste à lien unique permet bien de représenter cette structure, comme illustré ci-dessous.

Si vous ne connaissez pas bien les listes associées, effectuez simplement une recherche Google sur "listes associées en C++". Vous y trouverez de nombreuses ressources expliquant comment en créer une et comment elle est utilisée. Nous aborderons ce point plus en détail dans un module ultérieur.

Notez que nous utilisons les ID de document (DocIDs) au lieu du nom de l'histoire. Nous trions également ces DocID pour faciliter le traitement des requêtes.

Comment traitons-nous une requête ? Pour le problème initial, nous trouvons d'abord la liste des publications COW, puis la liste des publications du BEE. Nous les "fusionnons" ensuite:

  1. Conservez les repères dans les deux listes et parcourez les deux listes de messages simultanément.
  2. À chaque étape, comparez le DocID indiqué par les deux pointeurs.
  3. S'ils sont identiques, placez le DocID dans une liste de résultats, sinon faites avancer le pointeur vers le plus petit docID.

Voici comment créer un index inversé:

  1. Attribuez un DocID à chaque document qui vous intéresse.
  2. Identifiez les termes pertinents pour chaque document (tokeniser).
  3. Pour chaque terme, créez un enregistrement comprenant le terme, l'ID de document où il se trouve et la fréquence dans ce document. Notez qu'il peut y avoir plusieurs enregistrements pour un terme particulier s'il apparaît dans plusieurs documents.
  4. Triez les enregistrements par terme.
  5. Créez le dictionnaire et la liste de publications en traitant les enregistrements uniques d'un terme, et en combinant les différents enregistrements de termes qui apparaissent dans plusieurs documents. Créez une liste associée des ID de document (par ordre de tri). Chaque terme est également associé à une fréquence, qui correspond à la somme des fréquences de tous les enregistrements pour un terme.

Le projet

Trouvez plusieurs longs documents en texte brut que vous pouvez tester. Le projet consiste à créer un index inversé à partir des documents, à l'aide des algorithmes décrits ci-dessus. Vous devez également créer une interface pour la saisie des requêtes et un moteur pour les traiter. Vous trouverez un partenaire du projet sur le forum.

Voici un processus possible pour mener à bien ce projet:

  1. La première chose à faire est de définir une stratégie d'identification des termes dans les documents. Dressez une liste de tous les mots vides auxquels vous pouvez penser, puis rédigez une fonction qui lit les mots dans les fichiers, enregistre les termes et élimine les mots vides. Vous devrez peut-être ajouter d'autres mots vides à votre liste lorsque vous examinez la liste des termes à partir d'une itération.
  2. Créez des scénarios de test CPPUnit pour tester votre fonction, ainsi qu'un fichier makefile pour regrouper tous les éléments nécessaires à votre build. Vérifiez vos fichiers dans CVS, en particulier si vous travaillez avec des partenaires. Vous voudrez peut-être rechercher comment ouvrir votre instance CVS aux ingénieurs distants.
  3. Ajouter un traitement pour inclure les données de localisation, c'est-à-dire quel fichier et où se trouve un terme dans le fichier ? Vous pouvez effectuer un calcul pour définir un numéro de page ou de paragraphe.
  4. Écrivez des scénarios de test CPPUnit pour tester cette fonctionnalité supplémentaire.
  5. Créez un index inversé et stockez les données de localisation dans l'enregistrement de chaque terme.
  6. Rédigez d'autres scénarios de test.
  7. Concevez une interface pour permettre à un utilisateur de saisir une requête.
  8. À l'aide de l'algorithme de recherche décrit ci-dessus, traitez l'index inversé et renvoyez les données de localisation à l'utilisateur.
  9. Veillez également à inclure des scénarios de test pour cette dernière partie.

Comme nous l'avons fait pour tous les projets, utilisez le forum et le chat pour trouver des partenaires et partager vos idées.

Une fonctionnalité en plus

Dans de nombreux systèmes infrarouges, l'étape de traitement courante est la recherche de radical. L'idée principale derrière la recherche de radical est que les utilisateurs qui recherchent des informations sur le terme "récupération" s'intéresseront également à des documents contenant des informations telles que "récupérer", "récupérée", "récupérée", etc. Les systèmes peuvent être sensibles à des erreurs en raison d'une mauvaise recherche de radicaux, ce qui est donc un peu délicat. Par exemple, un utilisateur intéressé par la "récupération d'informations" peut voir un document intitulé "Informations sur les Golden Retrievers" en raison de la recherche de radical. L'algorithme Porter est un algorithme utile pour la recherche de radical.

Application: vous pouvez partout

Découvrez l'application de ces concepts sur le site Panoramas.dk.