• Aucun résultat trouvé

3.8.4 - Economiser de la mémoire avec union

enum ShapeType { circle = 10, square = 20, rectangle = 50 };

Si vous donnez des valeurs à certains noms mais pas à tous, le compilateur utilisera la valeur entière suivante. Par exemple,

enum snap { crackle = 25, pop };

Le compilateur donne la valeur 26 à pop.

Vous pouvez voir alors combien vous gagnez en lisibilité du code en utilisant des types de données énumérés.

Cependant, d'une certaine façon, cela reste une tentative (en C) d'accomplir des choses que l'on peut faire avec une classen C++, c'est ainsi que vous verrez que les enumsont moins utilisées en C++.

Vérification de type pour les énumérations

Les énumérations du C sont très primitives, en associant simplement des valeurs intégrales à des noms, mais elles ne fournissent aucune vérification de type. En C++, comme vous pouvez vous y attendre désormais, le concept de type est fondamental, et c'est aussi vrai avec les énumérations. Quand vous créez une énumération nommée, vous créez effectivement un nouveau type tout comme vous le faites avec une classe : le nom de votre énumération devient un mot réservé pour la durée de l'unité de traduction.

De plus, la vérification de type est plus stricte pour les énumérations en C++ qu'en C. Vous noterez cela, en particulier, dans le cas d'une énumération colorappelée a. En C, vous pouvez écrire a++, chose que vous ne pouvez pas faire en C++. Ceci parce que l?incrémentation d?une énumération effectue en réalité deux conversions de type, l'une d'elle légale en C++, mais l'autre illégale. D'abord, la valeur de l'énumération est implicitement convertie de colorvers un int, puis la valeur est incrémentée, et reconvertie en color. En C++, ce n'est pas autorisé, parce que colorest un type distinct et n'est pas équivalent à un int. Cela a du sens, parce que comment saurait-on si le résultat de l'incrémentation de bluesera dans la liste de couleurs? Si vous souhaitez incrémenter un color, alors vous devez utiliser une classe (avec une opération d'incrémentation) et non pas une enum, parce que la classe peut être rendue plus sûre. Chaque fois que vous écrirez du code qui nécessite une conversion implicite vers un type enum, Le compilateur vous avertira du danger inhérent à cette opération.

Les unions (décrites dans la prochaine section) possèdent la même vérification additionnelle de type en C++.

3.8.4 - Economiser de la mémoire avec union

Parfois, un programme va manipuler différents types de donnée en utilisant la même variable. Dans de tels cas, deux possibilités: soit vous créez une structqui contient tous les types possibles que vous auriez besoin d'enregistrer, soit vous utilisez une union. Une unionempile toutes les données dans un même espace; cela signifie que la quantité de mémoire nécessaire sera celle de l'élément le plus grand que vous avez placé dans l' union. Utilisez une unionpour économiser de la mémoire.

Chaque fois que vous écrivez une valeur dans une union, cette valeur commence toujours à l'adresse de début de l' union, mais utilise seulement la mémoire nécessaire. Ainsi, vous créez une “super-variable” capable d'utiliser chacune des variables de l' union. Toutes les adresses des variables de l' unionsont les mêmes (alors que dans une classe ou une struct, les adresses diffèrent).

Voici un simple usage d'une union. Essayez de supprimer quelques éléments pour voir quel effet cela a sur la taille de l' union. Notez que cela n'a pas de sens de déclarer plus d'une instance d'un simple type de données dans une union(à moins que vous ne fassiez que pour utiliser des noms différents).

//: C03:Union.cpp // Simple usage d'une union

#include <iostream>

using namespace std;

union Packed { // Déclaration similaire à une classe char i;

short j;

int k;

long l;

float f;

double d;

// L'union sera de la taille d'un

// double, puisque c'est l?élément le plus grand

}; // Un point-virgule termine une union, comme une struct int main() {

cout << "sizeof(Packed) = "

<< sizeof(Packed) << endl;

Packed x;

x.i = 'c';

cout << x.i << endl;

x.d = 3.14159;

cout << x.d << endl;

} ///:~

Le compilateur effectuera l'assignation correctement selon le type du membre de l'union que vous sélectionnez.

Une fois que vous avez effectué une affectation, le compilateur se moque de ce que vous ferez par la suite de l'union. Dans l'exemple précédent, on aurait pu assigner une valeur flottante à x:

x.f = 2.222;

Et l'envoyer sur la sortie comme si c'était un int:

cout << x.i;

Ceci aurait produit une valeur sans aucun sens.

3.8.5 - Tableaux

Les tableaux sont une espèce de type composite car ils vous autorisent à agréger plusieurs variables ensemble, les unes à la suite des autres, sous le même nom. Si vous dites :

int a[10];

Vous créez un emplacement mémoire pour 10 variables intempilées les unes sur les autres, mais sans un nom unique pour chacune de ces variables. A la place, elles sont toutes réunies sous le nom a.

Pour accéder à l'un des élémentsdu tableau, on utilise la même syntaxe utilisant les crochets que celle utilisée pour définir un tableau :

a[5] = 47;

Cependant, vous devez retenir que bien que la taillede asoit 10, on sélectionne les éléments d'un tableau en commençant à zero (ceci est parfois appelé indexation basée sur zero), donc vous ne pouvez sélectionner que les éléments 0-9 du tableau, comme ceci :

//: C03:Arrays.cpp

#include <iostream>

using namespace std;

int main() { int a[10];

for(int i = 0; i < 10; i++) { a[i] = i * 10;

cout << "a[" << i << "] = " << a[i] << endl;

} } ///:~

L'accès aux tableaux est extrêmement rapide. Cependant, si votre index dépasse la taille du tableau, il n'y a aucun filet de sécurité – vous pointerez sur d'autres variables. L'autre inconvénient est que vous devez spécifier la taille du tableau au moment de la compilation ; si vous désirez changer la taille lors de l'exécution, vous ne pouvez pas le faire avec la syntaxe précédente (le C propose une façon de créer des tableaux dynamiquement, mais c'est assurément plus sale). Le type vectorfournit par C++, présenté au chapitre précédent, nous apporte un type semblable à un tableau qui redéfinit sa taille automatiquement, donc c'est généralement une meilleure solution si la taille de votre tableau ne peut pas être connue lors de la compilation.

Vous pouvez créer un tableau de n'importe quel type, y compris de structures :

//: C03:StructArray.cpp // Un tableau de struct

typedef struct { int i, j, k;

} ThreeDpoint;

int main() {

ThreeDpoint p[10];

for(int i = 0; i < 10; i++) { p[i].i = i + 1;

p[i].j = i + 2;

p[i].k = i + 3;

} } ///:~

Remarquez comment l'identifiant ide la structure est indépendant de celui de la boucle for.

Pour vérifier que tous les éléments d'un tableau se suivent, on peut afficher les adresses comme ceci :

//: C03:ArrayAddresses.cpp

#include <iostream>

using namespace std;

int main() { int a[10];

cout << "sizeof(int) = "<< sizeof(int) << endl;

for(int i = 0; i < 10; i++) cout << "&a[" << i << "] = "

<< (long)&a[i] << endl;

} ///:~

Quand vous exécutez ce programme, vous verrez que chaque élément est éloigné de son précédent de la taille d'un int. Ils sont donc bien empilés les uns sur les autres.

Pointeurs et tableaux

L'identifiant d'un tableau n'est pas comme celui d'une variable ordinaire. Le nom d'un tableau n'est pas une lvalue ; vous ne pouvez pas lui affecter de valeur. C'est seulement un point d'ancrage pour la syntaxe utilisant les crochets

‘[]’, et quand vous utilisez le nom d'un tableau, sans les crochets, vous obtenez l'adresse du début du tableau:

//: C03:ArrayIdentifier.cpp

#include <iostream>

using namespace std;

int main() { int a[10];

cout << "a = " << a << endl;

cout << "&a[0] =" << &a[0] << endl;

} ///:~

En exécutant ce programme, vous constaterez que les deux adresses (affichées en hexadécimal, puisque aucun cast en longn'est fait) sont identiques.

Nous pouvons considérer que le nom d?un tableau est un pointeur en lecture seule sur le début du tableau. Et bien que nous ne puissions pas changer le nom du tableau pour qu'il pointe ailleurs, nous pouvons, en revanche, créer un autre pointeur et l'utiliser pour se déplacer dans le tableau. En fait, la syntaxe avec les crochets marche aussi avec les pointeurs normaux également :

//: C03:PointersAndBrackets.cpp int main() {

int a[10];

int* ip = a;

for(int i = 0; i < 10; i++) ip[i] = i * 10;

} ///:~

Le fait que nommer un tableau produise en fait l'adresse de départ du tableau est un point assez important quand on s'intéresse au passage des tableaux en paramètres de fonctions. Si vous déclarez un tableau comme un argument de fonction, vous déclarez en fait un pointeur. Dans l'exemple suivant, func1( )et func2( )ont au final la même liste de paramètres :

//: C03:ArrayArguments.cpp

#include <iostream>

#include <string>

using namespace std;

void func1(int a[], int size) { for(int i = 0; i < size; i++)

a[i] = i * i - i;

}

void func2(int* a, int size) { for(int i = 0; i < size; i++)

a[i] = i * i + i;

}

void print(int a[], string name, int size) { for(int i = 0; i < size; i++)

cout << name << "[" << i << "] = "

<< a[i] << endl;

}

int main() {

int a[5], b[5];

// Probablement des valeurs sans signification:

print(a, "a", 5);

print(b, "b", 5);

// Initialisation des tableaux:

func1(a, 5);

func1(b, 5);

print(a, "a", 5);

print(b, "b", 5);

// Les tableaux sont toujours modifiés : func2(a, 5);

func2(b, 5);

print(a, "a", 5);

print(b, "b", 5);

} ///:~

Même si func1( )et func2( )déclarent leurs paramètres différemment, leur utilisation à l'intérieur de la fonction sera la même. Il existe quelques autres problèmes que l'exemple suivant nous révèle : les tableaux ne peuvent pas être passés par valeur A moins que vous ne considériez l'approche stricte selon laquelle “ tous les paramètres en C/C++ sont passés par valeur, et que la ‘valeur’ d'un tableau est ce qui est effectivement dans l'identifiant du tableau : son adresse.” Ceci peut être considéré comme vrai d'un point de vue du langage assembleur, mais je ne pense pas que cela aide vraiment quand on travaille avec des concepts de plus haut niveau. L'ajout des références en C++ ne fait qu'accentuer d'avantage la confusion du paradigme “tous les passages sont par valeur”, au point que je ressente plus le besoin de penser en terme de “passage par valeur” opposé à “passage par adresse”, car vous ne récupérez jamais de copie locale du tableau que vous passez à une fonction. Ainsi, quand vous modifiez un tableau, vous modifiez toujours l'objet extérieur. Cela peut dérouter au début, si vous espériez un comportement de passage par valeur tel que fourni avec les arguments ordinaires.

Remarquez que print( )utilise la syntaxe avec les crochets pour les paramètres tableaux. Même si les syntaxes de pointeurs et avec les crochets sont effectivement identiques quand il s'agit de passer des tableaux en paramètres, les crochets facilitent la lisibilité pour le lecteur en lui explicitant que le paramètre utilisé est bien un tableau.

Notez également que la tailledu tableau est passée en paramètre dans tous les cas. Passer simplement l'adresse d'un tableau n'est pas une information suffisante; vous devez toujours savoir connaître la taille du tableau à l'intérieur de votre fonction, pour ne pas dépasser ses limites.

Les tableaux peuvent être de n'importe quel type, y compris des tableaux de pointeurs. En fait, lorsque vous désirez passer à votre programme des paramètres en ligne de commande, le C et le C++ ont une liste d'arguments spéciale pour main( ), qui ressemble à ceci :

int main(int argc, char* argv[]) { // ...

Le premier paramètre est le nombre d'éléments du tableau, lequel tableau est le deuxième paramètre. Le second paramètre est toujours un tableau de char*, car les arguments sont passés depuis la ligne de commande comme des tableaux de caractères (et souvenez vous, un tableau ne peut être passé qu'en tant que pointeur). Chaque portion de caractères délimitée par des espaces est placée dans une chaîne de caractères séparée dans le tableau. Le programme suivant affiche tous ses paramètres reçus en ligne de commande en parcourant le tableau :

//: C03:CommandLineArgs.cpp

#include <iostream>

using namespace std;

int main(int argc, char* argv[]) { cout << "argc = " << argc << endl;

for(int i = 0; i < argc; i++) cout << "argv[" << i << "] = "

<< argv[i] << endl;

} ///:~

Notez que argv[0]est en fait le chemin et le nom de l'application elle-même. Cela permet au programme de récupérer des informations sur lui. Puisque que cela rajoute un élément de plus au tableau des paramètres du programme, une erreur souvent rencontrée lors du parcours du tableau est d'utiliser argv[0]alors qu'on veut en fait argv[1].

Vous n'êtes pas obligés d'utiliser argcet argvcomme identifiants dans main( ); ils sont utilisés par pure convention (mais ils risqueraient de perturber un autre lecteur si vous ne les utilisiez pas). Aussi, il existe une manière alternative de déclarer argv:

int main(int argc, char** argv) { // ...

Les deux formes sont équivalentes, mais je trouve la version utilisée dans ce livre la plus intuitive pour relire le code, puisqu'elle dit directement “Ceci est un tableau de pointeurs de caractères”.

Tout ce que vous récupérez de la ligne de commande n'est que tableaux de caractères; si vous voulez traiter un argument comment étant d'un autre type, vous avez la responsabilité de le convertir depuis votre programme. Pour faciliter la conversion en nombres, il existe des fonctions utilitaires de la librairie C standard, déclarées dans

<cstdlib>. Les plus simples à utiliser sont atoi( ), atol( ),et atof( )pour convertir un tableau de caractères ASCII en valeurs int, long,et double, respectivement. Voici un exemple d'utilisation de atoi( )(les deux autres fonctions sont appelées de la même manière) :

//: C03:ArgsToInts.cpp

// Convertir les paramètres de la ligne de commande en int

#include <iostream>

#include <cstdlib>

using namespace std;

int main(int argc, char* argv[]) { for(int i = 1; i < argc; i++)

cout << atoi(argv[i]) << endl;

} ///:~

Dans ce programme, vous pouvez saisir n'importe quel nombre de paramètres en ligne de commande. Vous noterez que la boucle forcommence à la valeur 1pour ignorer le nom du programme en argv[0]. Mais, si vous saisissez un nombre flottant contenant le point des décimales sur la ligne de commande, atoi( )ne prendra que les chiffres jusqu'au point. Si vous saisissez des caractères non numériques, atoi( )les retournera comme des zéros.

Exploration du format flottant

La fonction printBinary( )présentée précédemment dans ce chapitre est pratique pour scruter la structure interne de types de données divers. Le plus intéressant de ceux-ci est le format flottant qui permet au C et au C++

d'enregistrer des nombres très grands et très petits dans un espace mémoire limité. Bien que tous les détails ne puissent être exposés ici, les bits à l'intérieur d'un floatet d'un doublesont divisées en trois régions : l'exposant, la mantisse et le bit de signe; le nombre est stocké en utilisant la notation scientifique. Le programme suivant permet de jouer avec les modèles binaires de plusieurs flottants et de les imprimer à l'écran pour que vous vous rendiez compte par vous même du schéma utilisé par votre compilateur (généralement c'est le standard IEEE, mais votre compilateur peut ne pas respecter cela) :

//: C03:FloatingAsBinary.cpp //{L} printBinary

//{T} 3.14159

#include "printBinary.h"

#include <cstdlib>

#include <iostream>

using namespace std;

int main(int argc, char* argv[]) { if(argc != 2) {

cout << "Vous devez fournir un nombre" << endl;

exit(1);

}

double d = atof(argv[1]);

unsigned char* cp =

reinterpret_cast<unsigned char*>(&d);

for(int i = sizeof(double)-1; i >= 0 ; i -= 2) { printBinary(cp[i-1]);

printBinary(cp[i]);

} } ///:~

Tout d'abord, le programme garantit que le bon nombre de paramètres est fourni en vérifiant argc, qui vaut deux si un seul argument est fourni (il vaut un si aucun argument n'est fourni, puisque le nom du programme est toujours le premier élément de argv). Si ce test échoue, un message est affiché et la fonction de la bibliothèque standard du C exit( )est appelée pour terminer le programme.

Puis le programme récupère le paramètre de la ligne de commande et convertit les caractères en doublegrâce à atof( ). Ensuite le double est utilisé comme un tableau d'octets en prenant l'adresse et en la convertissant en unsigned char*. Chacun de ces octets est passé à printBinary( )pour affichage.

Cet exemple a été réalisé de façon à ce que que le bit de signe apparaisse d'abord sur ma machine. La vôtre peut être différente, donc vous pourriez avoir envie de réorganiser la manière dont sont affichées les données. Vous devriez savoir également que le format des nombres flottants n'est pas simple à comprendre ; par exemple, l'exposant et la mantisse ne sont généralement pas arrangés sur l'alignement des octets, mais au contraire un nombre de bits est réservé pour chacun d'eux, et ils sont empaquetés en mémoire de la façon la plus serrée possible. Pour vraiment voir ce qui se passe, vous devrez trouver la taille de chacune des parties (le bit de signe est toujours un seul bit, mais l'exposant et la mantisse ont des tailles différentes) et afficher les bits de chaque partie séparément.

Arithmétique des pointeurs

Si tout ce que l'on pouvait faire avec un pointeur qui pointe sur un tableau était de l'utiliser comme un alias pour le nom du tableau, les pointeurs ne seraient pas très intéressants. Cependant, ce sont des outils plus flexibles que cela, puisqu'ils peuvent être modifiés pour pointer n'importe où ailleurs (mais rappelez vous que l'identifiant d'un tableau ne peut jamais être modifié pour pointer ailleurs).

Arithmétique des pointeursfait référence à l'application de quelques opérateurs arithmétiques aux pointeurs. La raison pour laquelle l'arithmétique des pointeurs est un sujet séparé de l'arithmétique ordinaire est que les pointeurs doivent se conformer à des contraintes spéciales pour qu'ils se comportent correctement. Par exemple, un opérateur communément utilisé avec des pointeurs est le ++, qui “ajoute un au pointeur”. Cela veut dire en fait que le pointeur est changé pour se déplacer à “la valeur suivante”, quoi que cela signifie. Voici un exemple :

//: C03:PointerIncrement.cpp

#include <iostream>

using namespace std;

int main() { int i[10];

double d[10];

int* ip = i;

double* dp = d;

cout << "ip = " << (long)ip << endl;

ip++;

cout << "ip = " << (long)ip << endl;

cout << "dp = " << (long)dp << endl;

dp++;

cout << "dp = " << (long)dp << endl;

} ///:~

Pour une exécution sur ma machine, voici le résultat obtenu :

ip = 6684124 ip = 6684128

dp = 6684044 dp = 6684052

Ce qui est intéressant ici est que bien que l'opérateur ++paraisse être la même opération à la fois pour un int*et un double*, vous remarquerez que le pointeur a avancé de seulement 4 octets pour l' int*mais de 8 octets pour le double*. Ce n'est pas par coïncidence si ce sont les tailles de ces types sur ma machine. Et tout est là dans l'arithmétique des pointeurs : le compilateur détermine le bon déplacement à appliquer au pointeur pour qu'il pointe sur l'élément suivant dans le tableau (l'arithmétique des pointeurs n'a de sens qu'avec des tableaux). Cela fonctionne même avec des tableaux de structs:

//: C03:PointerIncrement2.cpp

#include <iostream>

using namespace std;

typedef struct { char c;

short s;

int i;

long l;

float f;

double d;

long double ld;

} Primitives;

int main() {

Primitives p[10];

Primitives* pp = p;

cout << "sizeof(Primitives) = "

<< sizeof(Primitives) << endl;

cout << "pp = " << (long)pp << endl;

pp++;

cout << "pp = " << (long)pp << endl;

} ///:~

L'affichage sur ma machine a donné :

sizeof(Primitives) = 40 pp = 6683764

pp = 6683804

Vous voyez ainsi que le compilateur fait aussi les choses correctement en ce qui concerne les pointeurs de structures (et de classes et d' unions).

L'arithmétique des pointeurs marche également avec les opérateurs --, +,et -, mais les deux derniers opérateurs sont limités : Vous ne pouvez pas additionner deux pointeurs, et si vous retranchez des pointeurs, le résultat est le nombre d'élément entre les deux pointeurs. Cependant, vous pouvez ajouter ou soustraire une valeur entière et un pointeur. Voici un exemple qui démontre l'utilisation d'une telle arithmétique :

//: C03:PointerArithmetic.cpp

#include <iostream>

using namespace std;

#define P(EX) cout << #EX << ": " << EX << endl;

int main() { int a[10];

for(int i = 0; i < 10; i++)

a[i] = i; // Attribue les valeurs de l?index int* ip = a;

P(*ip);

P(*++ip);

P(*(ip + 5));

int* ip2 = ip + 5;

P(*ip2);

P(*(ip2 - 4));

P(*--ip2);

P(ip2 - ip); // Renvoie le nombre d?éléments } ///:~

Il commence avec une nouvelle macro, mais celle-ci utilise une fonctionnalité du pré processeur appelée

Il commence avec une nouvelle macro, mais celle-ci utilise une fonctionnalité du pré processeur appelée