Révision – Solutions

Ce qui suit liste des solutions pour les problèmes proposés dans la série d'exercices Révision.

Q00

  Question Réponse
Q00.0

En C++, un tableau est une structure de données.

Vrai. Une structure un peu « brute » (une suite contiguë en mémoire d'éléments de même type), mais une structure tout de même.

Q00.1

En C++, un tableau est un objet.

Faux. On n'y retrouve pas un regroupement sous un même nom d'attributs et de méthodes.

Q00.2

Il est possible de programmer une pile sans avoir recours à de l'allocation dynamique de mémoire.

Vrai. On peut le faire avec un tableau et quelques entiers, par exemple. Il faut par contre décider a priori d'une capacité maximale pour la pile. Êtes-vous capables de le faire sans aide?

Q00.3

Il est possible de programmer une pile en ayant recours à de l'allocation dynamique de mémoire.

Vrai. Êtes-vous capables de le faire sans aide?

Q00.4

Il est possible de programmer une file sans avoir recours à de l'allocation dynamique de mémoire.

Vrai. On peut le faire avec un tableau et quelques entiers, par exemple. Il faut par contre décider a priori d'une capacité maximale pour la file. Êtes-vous capables de le faire sans aide?

Q00.5

Il est possible de programmer une file en ayant recours à de l'allocation dynamique de mémoire.

Vrai. Êtes-vous capables de le faire sans aide?

Q00.6

Que vaut l'expression 0=='\0'?

Vrai.

Q00.7

Qu'affichera l'extrait de code suivant :

#include <iostream>
int main() {
   using std::cout;
   if(3)
      cout << "Vrai";
   else
      cout << "Faux";
}

Vrai. Le littéral 3 est un entier, et dû à l'héritage du langage C, les entiers non-nuls de C++ sont considérés vrais pour fins d'évaluation en tant que booléens.

Q00.8

Qu'affichera l'extrait de code suivant :

#include <iostream>
int main() {
   using std::cout;
   int val = 0;
   if(val = 3)
      cout << "Vrai";
   else
      cout << "Faux";
}

Vrai. L'opérateur = en C++ réalise l'affectation, pas la comparaison, et le résultat de l'affectation est la valeur de l'opérande de gauche suite à l'affectation.

Q01

Q01.0

Littéral
Type
Note
3
int

 

"allo"
const char*

Notez qu'autrefois, ces littéraux n'étaient pas const, ce qui pouvait mener à des résultats très vilains. Heureusement, l'état de la situation est aujourd'hui un peu plus... rationnel

1.0f
float

 

-8
int

 

"A"
const char*

Notez qu'autrefois, ces littéraux n'étaient pas const, ce qui pouvait mener à des résultats très vilains. Heureusement, l'état de la situation est aujourd'hui un peu plus... rationnel

'A'
char

En C++, sizeof(char)==1. En C, sizeof(char)==1 mais sizeof('A')==sizeof(int) car les littéraux char sont considérés comme des int... Il y a des raisons historiques pour cela, mais ça entraîne des conséquences vraiment pas agréables (par exemple, 'abc' est légal, mais le résultat – entier – est éminemment non-portable alors ne faites pas ça!)

4.0
double

Notez que C# permet d'écrire 4.0d dans ce cas, mais le d est redondant

7L
long

 

L"allo"
const wchar_t*

 

L'A'
wchar_t

Il existe aussi plusieurs types de littéraux caractères plus sophistiqués, pour tenir compte d'encodages tels que UTF-16 et UTF-32, mais notre compilateur ne les supporte pas encore

3U
unsigned int

 

3UL
unsigned long

 

0x0a
int

 

12.0L
long double

 

Q01.1

Littéral Valeur décimale Remarque
3
3
 
0x0a
10
 
0x000a
10
 
0xff
255 ou -1

Subtil. Vaut 255 si déposé dans un unsigned char ou dans un type entier autre, mais vaut -1 si déposé dans un char signé (présumant huit bits par byte)

03
3

Prudence : notation octale! Notez que cette notation n'est plus supportée par C#

8
8
 
2.7f
2 
(décimale tronquée)

La partie décimale est tronquée, pas arrondie

08

Illégal

En notation octale (qui, je le rappelle, n'est plus supportée par C#), cette écriture n'a pas de sens (ce n'est même pas un nombre!)

Q02

Q02.0

bool full_cool(const bool*, const bool*);

Q02.1

bool full_cool(const bool *b0, const bool *b1) {
   return *b0 == *b1;
}

Q02.2

#include <iostream>
int main() {
   using namespace std;
   bool b0 = true, b1 = false; // arbitraire
   if (full_cool(&b0, &b1))
      cout << "Sont pareils... full cool!" << endl;
   else
      cout << "Sont pas pareils... full poche!" << endl;
}

Q03

Le littéral "Coucou" occupera un espace d'au moins sept bytes, soit 'C', 'o', 'u', 'c', 'o', 'u' et enfin le caractère '\0' (valeur 0 encodée sur un byte) qui délimite la fin de la chaîne. Le compilateur est responsable de cette mémoire; nous ne savons pas s'il en réservera plus pour des raisons qui lui appartiennent.

Q04

Non, car aucune allocation dynamique de mémoire n'y est faite. Le compilateur « brûle » à même le segment de données du programme les valeurs en mémoire pour les littéraux "Coucou" et "Yo", alors que le programme fait simplement pointer texte vers l'un ou l'autre de ces littéraux. Notre programme n'est pas responsable de la gestion de cette mémoire.

Q05

Oui, car il est probable que seul le premier int de tableau soit libéré – ou encore, que le tout plante royalement, dû à un comportement indéfini dans le standard. En C++, la contrepartie de l'opérateur new[] est l'opérateur delete[]. L'instruction correcte dans ce programme serait delete[] tableau;.

Cela dit, en C++, utiliser directement new et new[] est typiquement une mauvaise idée. Nous y reviendrons.

Q06

Non. Dans ce programme, t est un pointeur de caractères, pas un objet. Ainsi, l'opérateur += appliqué à ce pointeur change l'adresse vers laquelle il pointe. Ce programme pointe un peu partout en mémoire de manière absolument illégale (sauf dans de rares cas [mal]chanceux et aléatoires).

Notons aussi que même si t était une std::string plutôt qu'un const char*, la répétitive demeurerait mal écrite, ne testant pas convenablement le succès de la lecture sur le flux std::cin. Une meilleure écriture serait :

#include <iostream>
#include <string>
int main() {
   using namespace std;
   string t = "Wow";
   for (char c; cin >> c; && c != '.'; )
      t += c;
}

Cette structure teste convenablement le succès de la lecture et la condition d'arrêt; de plus, elle restreint la portée de la variable c locale à la répétitive qui s'en sert.

Q07

Ce programme est légal car t0 est un conteneur dont les éléments sont modifiables et car il n'y a pas de dépassement de capacité. Notez que t0 et t1 sont de la même taille, tous eux ayant une capacité de quatre éléments. Exprimé en termes de bytes, sizeof(t0)==sizeof(t1), et le nombre d'éléments de t0 peut être calculé à la compilation par l'expression suivante :

enum { N = sizeof(t0) / sizeof(t0[0]) };

Q08

L'expression suivante semble faire le travail demandé :

string s2 = s;

Cependant, elle ne copie pas le contenu de s dans s2; elle fait simplement pointer s2 au même endroit que s. Avec C#, comme avec Java, nous manipulons tous les objets à travers des indirections (des références, au sens de C# ou de Java). Nous n'avons pas directement accès à ces objets. Ceci pose parfois problème, car on tend alors à partager des objets entre des fonctions ou des threads; heureusement, une classe telle que string (ou String en Java) est immuable (ses instances ne peuvent être modifiées une fois construites), et par conséquent, partager ces objets est sans risque.

Q09

Non, car on ne parle que d'un déplacement de la référence d'un référé vers un autre. Dans une Personne, la propriété Nom tient à l'interne une référence qui lui est propre, est distincte de la variable s dans Main(), même si une fois p construite les deux références pointent au même endroit.

Q10

Q10.0

size_t lg_chaine(const char *p) {
   size_t i = 0;
   while (p[i])
      ++i;
   return i;
}

Une variante serait la suivante, où le transtypage tient du fait que la différence entre deux pointeurs est de type std::ptrdiff_t, qui est un type signé, alors que std::size_t est non-signé :

Variante
size_t lg_chaine(const char *p) {
   const char *q = p;
   while(++p)
      ;
   return static_cast<size_t>(p-q);
}

Notez que C++ permet de faire mieux, du moins dans certaines circonstances. Ainsi, si on examine le code client suivant :

int main() {
   return lg_chaine("J'aime mon prof");
}

... il se trouve que le littéral "J'aime mon prof" n'est pas tant un const char* qu'une référence sur un tableau de 16 char (incluant le délimiteur nul à la fin du littéral), donc un char(&)[16]. Dans un tel cas, la taille est connue du compilateur, et il est possible d'en profiter. On pourrait donc ajouter une surcharge constexpr comme la suivante, en plus de celle proposée ci-dessus :

template <std::size_t N>
   constexpr std::size_t lg_chaine(const char (&arr)[N]) {
      return N - 1; // ne pas compter le '\0'
   }

... et faire en sorte que, lorsque cela s'avère possible, la taille du littéral soit une constante.

Q10.1

int comp_chaines(const char *p0, const char *p1) {
   int i = 0;
   while (p0[i] && p1[i] && p0[i] == p1[i])
      ++i;
   return static_cast<int>(p0[i]) - static_cast<int>(p1[i]);
}

Une variante serait :

Variante
int comp_chaines(const char *p0, const char *p1) {
   for(; *p0 && *p1 && *p0 == *p1; ++p0, ++p1)
      ;
   return static_cast<int>(*p0) - static_cast<int>(*p1);
}

Q10.2

//
// Précondition: l'espace réservé pour dest doit être au moins aussi
// grand que celui réservé pour src, délimiteur inclus
//
char* copier_chaine(char *dest, const char *src) {
   int i = 0;
   while(src[i]) {
      dest[i] = src[i];
      ++i;
   }
   dest[i] = src[i]; // copie du délimiteur
   return dest + i;
}

Une variante serait :

Variante
//
// Précondition: l'espace réservé pour dest doit être au moins aussi
// grand que celui réservé pour src, délimiteur inclus
//
char* copier_chaine(char *dest, const char *src) {
   for (; *dest++ = *src++; )
      ;
   return dest;
}

Q10.3


//
// Précondition: l'espace réservé pour dest doit être au moins aussi
// grand que la somme de celui réservé pour src et de celui occupé par
// dest, incluant un délimiteur (pas deux).
//
char* concat_chaines(char *dest, const char *src) {
   int i = lg_chaine(dest), j = 0;
   while (src[j])
      dest[i++] = src[j++];
   dest[i] = src[j]; // copie du délimiteur
   return dest;
}

Q10.4

#include <iostream>
#include <locale>
// ...
void afficher_majuscules(const char *p) {
   using namespace std;
   for (int i = 0; i < lg_chaine(p); )
      cout << toupper(p[i++], locale{ "" });
}

Son code est mauvais car il demande de recalculer la longueur de la chaîne à chaque itération de la boucle, ce qui implique parcourir toute la chaîne à chaque fois. Notez aussi qu'un objet locale{""} distinct est créé à chaque appel de toupper(), ce qui suggère que créer une variable temporaire serait (très!) avantageux.

Si on a une chaîne d'un million de caractères, ce programme parcourra un million de fois un million de caractères alors qu'il n'a, au fond, à le faire qu'une seule fois.

On aurait pu utiliser une variable (ou une constante) temporaire pour entreposer cette longueur et on aurait économisé beaucoup de calculs inutiles. On aurait aussi pu simplement tester pour le délimiteur de fin dans la condition de la répétitive et ne pas se préoccuper de la longueur.

En fin de compte, on pourrait en arriver à quelque chose comme :

#include <iostream>
#include <locale>
// ...
void afficher_majuscules(const char *p) {
   using namespace std;
   const auto lg = lg_chaine(p);
   const auto loc = locale{ "" };
   for (int i = 0; i < lg; )
      cout << toupper(p[i++], loc);
}

... ce qui serait monstrueusement plus rapide.

Q10.5

Si on utilise le premier byte pour entreposer la taille de la chaîne, on n'a plus besoin de délimiteur pour savoir où cette chaîne se termine.

Connaître la longueur de la chaîne devient une opération instantanée.

Par contre, on est alors limités à 255 caractères maximum par chaîne. On peut contourner ce problème en utilisant plus d'un byte pour entreposer la taille de la chaîne, mais on aura toujours un plafond sur la taille maximale d'une chaîne si on utilise une stratégie préfixée.


//
// Connaître la longueur d'une chaîne devient simplement extraire la
// valeur du premier byte et traiter cette valeur comme un entier
//
unsigned int lg_chaine_prefixe(const char *p) {
   return static_cast<unsigned int>(*p);
}

//
// Notez que les chaînes utilisent maintenant les positions 1 à n
// plutôt que 0 à n-1, la position 0 servant à entreposer la
// taille. La stratégie ASCIIZ, elle, utilise la position n pour
// entreposer un délimiteur.
//
int comp_chaines_prefixe(const char *p0, const char *p1) {
   int i = 1;
   while (i <= *p0 && i <= *p1 && p0[i] == p1[i])
      ++i;
   return static_cast<int>(p0[i]) - static_cast<int>(p1[i]);
}

//
// Avertissement (voir plus haut)
//
char* copier_chaine(char *dest, const char *src) {
   int i = 1;
   while (i <= *src) {
      dest[i] = src[i];
      i++;
   }
   return dest;
}
//
// Avertissement: (voir plus haut)
//
char* concat_chaines(char *dest, const char *src) {
   int i = lg_chaine(dest) + 1, j = 1;
   while (j <= *src)
      dest[i++] = src[j++];
   return dest;
}

Le code de afficher_majuscules() de votre collègue deviendrait à peu près aussi rapide que si on avait utilisé une variable temporaire, mais il faudrait y utiliser des indices allant de 1 à la taille de la chaîne (inclusivement) plutôt que de 0 à la taille de la chaîne (exclusivement) :

void afficher_majuscules(const char *p) {
   int i = 1;
   while (i <= lg_chaine(p))
      cout << toupper(p[i++], locale{ "" });
}

Évidemment, on peut faire (beaucoup) plus rapide...


Valid XHTML 1.0 Transitional

CSS Valide !