Ce qui suit liste des solutions pour les problèmes proposés dans la série d'exercices Révision.
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 :
|
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 :
|
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. |
Littéral
|
Type
|
Note
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
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!) |
|
|
Notez que C# permet d'écrire 4.0d dans ce cas, mais le d est redondant |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
Littéral | Valeur décimale | Remarque |
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
Prudence : notation octale! Notez que cette notation n'est plus supportée par C# |
|
|
|
|
(décimale tronquée) |
La partie décimale est tronquée, pas arrondie |
|
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!) |
bool full_cool(const bool*, const bool*);
bool full_cool(const bool *b0, const bool *b1) {
return *b0 == *b1;
}
#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;
}
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.
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.
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.
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.
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]) };
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.
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.
C++ | Java |
---|---|
|
|
Initialisation des variables :
À proprement dit, ces deux programmes ne font pas tout à fait la même chose puisque la version Java lit une ligne à la fois alors que la version C++ lit un mot à la fois (deux mots étant séparés l'un de l'autre par au moins un blanc).
Le comportement du programme C++ serait plus proche de celui du programme Java si nous consommions du flux avec std::getline() plutôt qu'avec l'opérateur>>appliqué au flux std::cin.
Consommation de données sur un flux :
Gestion des chaînes de caractères :
#include <string>
#include <iostream>
int main() {
using namespace std;
wstring s, texte;
while (wcin >> s && s != L".")
texte += s;
wcout << texte << endl;
}
En C++, le type std::string est en fait un cas particulier du type générique nommé std::basic_string, instancié pour le type char. Ceci tient de raisons historiques (le langage C utilise des chaînes de caractère reposant sur des char, et sizeof(char)==1 au sens où le type char est encodé sur un byte, peu importe l'ordinateur, donc "allo" est un const char *). C et C++ supportent aussi les chaînes de caractères étendus à travers le type wchar_t. Ainsi, le littéral L'A' (préfixe L devant le littéral) est un wchar_t (caractère étendu, capacble de représenter – avec C++ 11 – ces caractères sur 16 et sur 32 bits; la représentation des caractères étendus est une question très complexe) alors que 'A' est encodé sur un seul byte. Le littéral "allo" est un const char * alors que le littéral L"allo" est un const wchar_t *. Enfin, là où le type std::string est un basic_string<char>, le type std::wstring est un basic_string<wchar_t>.
Les objets std::wcerr, std::wcin, std::wclog et std::wcout équivalent respectivement aux objets std::cerr, std::cin, std::clog et std::cout, mais pour des séquences de caractères étendus. De manière générale, tous les flux standards sont génériques sur la base du type de caractère utilisé : par exemple, il existe un std::istream et un std::wistream, les deux étant des spécialisations de std::basic_istream pour un char et pour un wchar_t, respectivement).
C++ | Java |
---|---|
|
|
Remarquez d'abord la syntaxe : en C++, un tableau (en particulier s'il est alloué dynamiquement) est un pointeur, alors qu'en Java il s'agit d'une entité spéciale (remarquez la position des crochets [] entre le type des éléments et le nom de la variable).
En Java, le tableaux sont des objets et connaissent leur taille (attribut final – sorte de constante d'instance – nommé length). Le tableau est immuable une fois instancié, comme le sont les String, mais son contenu ne l'est pas (autrement dit, on peut remplacer ses éléments, on peut faire pointer la référence ailleurs – un tableau Java est un objet, donc est manipulé indirectement; ce n'est pas tant un objet qu'une référence sur un objet – mais on ne peut pas changer sa structure, par exemple sa capacité).
Comme tous les objets du langage Java, un tableau sera réclamé par le moteur de collecte d'ordures si le moteur de la JVM juge nécessaire et pertinent de le faire. En C++, un tableau est une séquence contiguë en mémoire d'éléments du même type, sans plus, et doit être détruit explicitement (opérateur delete[]) au moment jugé opportun. Prudence, d'ailleurs : l'opérateur delete détruit un scalaire alors que l'opérateur delete[] détruit un tableau. La distinction est nécessaire du fait qu'aux yeux du compilateur, un pointeur sur le 1er élément d'une séquence d'un type donné (ici, le pointeur p qui pointe sur une séquence de int) est du même type qu'un pointeur sur un seul élément de ce type, mais les stratégies d'allocation de mémoire peuvent différer pour l'allocation de scalaires et de tableaux.
La consommation d'entiers sur un flux se fait, dans le code Java, avec une méthode parseInt() appliquée au texte lu. Cette méthode lèvera une exception si la traduction de texte à entier pose problème. Remarquez que le code client doit expliciter le type à même le nom de la méthode de lecture; en C++, le type des opérandes de l'opérateur permet au compilateur de faire un choix éclairé, et le code source est le même peu importe le type des données consommées du flux.
Remarquez enfin la stratégie de tri de part et d'autre : en C++, l'algorithme std::sort() opère sur des itérateurs, de manière générique, alors qu'en Java, il opère strictement sur un tableau; l'algorithme y est implémenté dans une méthode statique de la classe Array.
C++ | Java |
---|---|
|
|
Petite note : en Java, un tableau est un objet et doit être instancié avec new (parfois en cachette, par un peu de sucre syntaxique) peu importe que sa capacité soit connue à la compilation ou non. En C++, quand la capacité du tableau est connue à la compilation, il est possible d'allouer l'espace pour ce tableau sur la pile (pour les variables locales) ou en mémoire globale. Ce n'est pas toujours une bonne idée, mais ça peut être pratique.
using System;
namespace q10_4
{
public class Swapper
{
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
}
class Program
{
static void Main(string[] args)
{
int a = 2, b = 3;
Console.WriteLine($"a: {a}, b: {b}");
Swapper.Swap(ref a, ref b);
Console.WriteLine($"a: {a}, b: {b}");
}
}
}
À titre de rappel, la fonction std::swap(a,b) en C++ est une fonction globale générique sur la base du type de a et b. Les deux paramètres doivent être du même type. L'algorithme std::swap() avant C++ 11 utilise des références, mais depuis l'avènement de ce standard, il utilise des rvalue references (une optimisation importante).
Le langage C# permet le passage de paramètre par référence, mais l'appelant (ici, la méthode q08_4.Program.Main()) doit expliciter cette particularité (indiquer ref a et ref b à l'appel). La méthode Swapper.Swap() peut être appliquée à des objets comme à des primitifs, mais ce qui sera échangé dans le cas des objets sera les références, pas le contenu des référés.
Notez que les classes et les espaces nommés sont obligatoires en C#. Les fonctions globales n'y sont pas supportées. Cela explique le recours à la méthode de classe Swapper.Swap() plutôt qu'à une simple fonction globale.
Java ne supporte pas le passage de paramètres par référence. Seul le passage par valeur y est supporté. On peut simuler un swap en Java en utilisant des classes dont les instances ne sont pas immuables, mais le langage ne permet pas d'exprimer une solution générale à ce problème.