Remarques 1

    Remarquer l'usage de const :
 
const char * const Destination

    Destination est un pointeur vers une chaîne de caractères (au sens C : NTCTS : Null Terminated Character Type Sequence) qui n'a aucune raison d'être modifiée par la fonction FileCopy(), puisque c'est le nom du fichier destination. Donc la chaîne est constante : premier const. Mais le pointeur n'a lui-même aucune raison dêtre modifié par la fonction : second const.


    Le nombre d'octets (bytes) NbBytes de chaque paquet est déclaré de type size_t plutôt que int, ou même unsigned int, comme on pourrait s'y attendre. La première raison est que ce paramètre est ensuite passé aux fonctions système read() et write(), dont les paramètres sont de type size_t. La seconde raison est que int et unsigned int sont des types des langages C/C++ (pas portables d'une implémentation à l'autre), alors que le type size_t concerne le système d'exploitation.


    Le type size_t est défini dans le fichier <sys/types.h> qui regroupe justement tous les types spécifiques du système Unix. Ne pas oublier la ligne :
 
#include <sys/types.h>     // size_t

en indiquant bien la raison de cette inclusion (cette indication est indispensable pour toute inclusion de tout fichier généraliste. Ne pas confondre le fichier "système" sys/types.h avec le fichier C <type.h> qui concerne les types et macros concernant les traitements de caractères (standard ISO), et son équivalent C++ <ctype>.


    Ne pas oublier que toutes les déclarations de variables, constantes, types, etc., directement incluses à partir de fichiers C (reconnaissables si le fichier inclus possède l'extension .h), sont globales, comme par exemple ::size_t.


    Ne pas oublier les clauses d'inclusion conditionnelles :
 
#if ! defined __NSUTIL_01A_H__
...


    En C/C++, la dernière constante d'un type énumératif peut être suivie d'une virgule : c'est une tolérance facilitant l'écriture de générateurs de code C/C++. Ainsi, les deux lignes ci-dessous sont équivalentes :
 
enum {CstNoError = 0, CstErrOpen = 1 };
enum {CstNoError = 0, CstErrOpen = 1, };

Remarques 2

    Pour des raisons de lisibilité, classer les fichiers inclus dans l'ordre suivant, en séparant chaque famille par une ligne vierge :


    Le type size_t vu plus haut est un type non signé. La fonction read() par exemple renvoie le nombre d'octets effectivement lus (donc non signé), mais peut aussi renvoyer -1 en cas d'erreur. D'où le type de retour de la fonction : ssize_t, signifiant signed size_t.


    Attention : les macros définies dans les fichiers .h (par exemple O_RDONLY) ne sont pas définies dans l'espace global. Il ne faut donc pas écrire ::O_RDONLY. En effet, elles sont directement remplacées par leur valeur par le pré-processeur avant la compilation et n'ont donc rien à voir avec le langage C++ et les espaces de noms.


    Lorsqu'une "variable" n'est pas censée être modifiée pendant toute l'exécution de la fonction dans laquelle elle est définie, il est normal et sain de la considérer comme une constante, et de la déclarer comme telle. Il faut rappeler qu'une constante doit être initialisée au moment même de sa déclaration. Ainsi, on écrira :
 
const int fdSource = ::open (Source, O_RDONLY);

    Cela n'est pas toujours possible, comme le montre ce petit programme :
 
int fdSource;
for (; -1 == (fdSource = ::open (Source, O_RDONLY)); ) 
{
    cout << "fichier inexistant" << endl;
    cin  >> Source;
}

    Déclarer
 
const int fdSource;

provoquerait une erreur de compilation car toute constante doit être initialisée à la déclaration. Une autre erreur de compilation apparaîtrait lors de la tentative d'affectation.

    C++ propose une solution originale grâce à la fonction générique const_cast<T>(v) qui renvoie la référence de la variable v (si le type T est une référence), débarrassée temporairement du qualifieur const.

    Ainsi, on peut écrire :
 
const int fdSource = n_importe_quoi;

for ( ; -1 == (const_cast <int &> (fdSource) = ::open (Source, O_RDONLY)); ) 
{
    cout << "fichier inexistant" << endl;
    cin  >> Source;
}

    Toute tentative ultérieure de modifier fdSource sans utiliser à nouveau const_cast provoquera une erreur de compilation.

Remarques 3

    Rappel : les fichiers .h et .hxx sont destinés à être directement ou indirectement inclus dans les fichiers sources de l'utilisateur. Il est donc nécessaire de ne jamais mettre de clause using namespace ... dans ces fichiers, car cette clause serait de fait introduite dans le programme de l'utilisateur, à son insu. En rendant visible la totalité des identificateurs de cet espace de noms, elle pourrait provoquer des conflits avec les identificateurs de l'utilisateur. On prendra donc bien soin de préfixer tous les identificateurs ne faisant pas partie de l'espace de noms actuel : std::string, ou nsUtil::CExcFct par exemple.


    Dans un fichier, il est commode de se créer des écritures abrégées, en particulier pour raccourcir des préfixes d'identificateurs. Cela se fait habituellement au moyen de macros, destinées au pré-processeur. De nouveau pour éviter d'éventuels conflits avec la même macro qui pourrait être redéfinie ailleurs (dans un autre fichiers inclus par exemple), il est nécessaire d'en supprimer la définition dès qu'elle n'est plus utile, comme par exemple :
 
#define SYST nsSysteme::CExcFctSyst

inline SYST::CExcFctSyst (
// ...

#undef SYST


    L'opérateur ternaire ?: est bien commode pour alléger une écriture qui serait inutilement lourde en utilisant un schéma alternatif. Sa syntaxe est :
 
expr1 ? expr2 : expr3

expr1 désigne une expression booléenne (vraie ou fausse) tandis que expr2 et expr3 doivent être de même type. Cela exclut par exemple :
 
int Nombre = ...;

cout << ((Nombre >= 0) ? Nombre : "Nombre négatif") << endl; 

expr2 est de type int et expr3 est de type char * (ou NTCTS).


    L'utilisation de la "variable" errno peut paraître surprenante : si elle est déclarée directement grâce à l'inclusion du fichier C <errno.h>, elle doit être écrite ::errno, comme tout identificateur global. Si elle est déclarée grâce au fichier C++ <cerrno>, elle doit être écrite std::errno.

    En fait, aucune de ces écritures n'est correcte car errno est (ici) une macro (voir remarque précédente), qui ne doit donc pas du tout être préfixée.

Remarques 4

    Toujours pas de using namespace ... dans les fichiers .h !!! En revanche, vous pouvez en utiliser autant que vous voulez dans les fichiers .cxx : ils sont compilés séparément et seul le fichier .o est utilisé ultérieurement.


    N'oubliez pas que, lors d'une comparaison d'égalité entre une constante et une variable, il est toujours préférable d'écrire en premier la constante (car on a vite fait de confondre l'affectation = et la comparaison ==). Par exemple :
 
if (-1 == NbLus)

au lieu de :
 
if (NbLus == -1)

qui pourrait être mal tapé en :
 
if (NbLus = -1)

aux conséquences désastreuses !!!

Remarques 5

    Les fonctions système read() et write() renvoient : d'où le type de retour des deux fonctions : ssize_t (signed size_t)

    Les wrappers Read() et Write() doivent lever une exception en cas d'échec des fonctions système correspondantes. Ils ne peuvent donc renvoyer normalement que des nombres positifs ou nuls. Le type de retour des deux wrappers doit donc être simplement size_t.


    La fonction système close() renvoie (voir man 2 close) :

    Le wrapper Close() doit lever une exception en cas d'échec de la fonction système. Il ne peut donc renvoyer normalement que 0, ce qui est sans intérêt. Le type de retour du wrapper doit donc être simplement void.


    La plupart des constantes symboliques représentant une option sont codées sous forme d'un entier dont un seul bit est positionné à 1. Ainsi, les constantes O_CREAT et O_EXCL sont définies dans le fichier /usr/include/bits/fcntl.h par :
 
#define O_CREAT           0100
#define O_EXCL            0200

    Ce sont des constantes octales dont les représentations en binaire sont  1 000 000 et 10 000 000, ou, codées par exemple sur les 4 octets d'entiers int :

0000 0000 0000 0000 0000 0000 0100 0000
et
0000 0000 0000 0000 0000 0000 1000 0000

    Si une variable, que nous appellerons oflag, contient les deux options, sa valeur en binaire est :

0000 0000 0000 0000 0000 0000 1100 0000

    Pour savoir si l'option O_CREAT (le flag) est positionnée dans cette variable, il ne suffit donc pas de faire la comparaison :
 
if (O_CREAT == oflag)

    Il faut isoler le bit correspondant (oflag & O_CREAT)et vérifier s'il est positionné == O_CREAT :
 
if ((oflag & O_CREAT) == O_CREAT)

    Comme le résultat de la première opération est soit nul (le bit isolé n'est pas positionné) soit non nul (le bit isolé est positionné), le test peut être simplifié en :
 
if (oflag & O_CREAT)

    Pour savoir si l'option n'est pas positionnée, il suffit d'écrire :
 
if (!(oflag & O_CREAT))

Attention, l'opérateur ! étant plus prioritaire que l'opérateur &, il faut parenthéser.


    La fonction FileCopy() est maintenant susceptible de lever deux types d'exceptions : CExcFct ou CExcFctSystFile. Cependant il est inutile de modifier la fonction main(). En effet, grâce au polymorphisme (fonction virtuelle _Edit() et passage de l'exception par référence), toute capture d'une exception de la classe CException permet de capturer une exception d'une classe dérivée : CExcFct ou CExcFctSystFile.

Remarques 6

    Du fait de l'utilisation de la fonction Open() à l'intérieur de la fonction FileCopy(), celle-ci est maintenant susceptible de lever non seulement une exception CExcFctSystFile, de l'espace de noms nsSysteme, mais aussi une exception CExcFct, de l'espace de noms nsUtil. Lorsqu'une fonction est susceptible de lever des exceptions différentes, mais appartenant à la même hiérarchie, il suffit d'indiquer dans le profil celle de plus haut niveau, ici CExcFct. Or cette exception fait partie du même espace de noms nsUtil que FileCopy(). Elle est donc immédiatement visible sans préfixage :
 
namespace nsUtil
{
    void FileCopy (......) throw (CExcFct);

} // nsUtil

Remarques 7

    Il faut remarque que, ppal() ayant été ajouté à l'espace de noms std, il devient inutile de préfixer les identificateurs de cet espace.


    La constante CstExcArg doit être redéfinie dans le fichier exo_02.cxx (par exemple directement dans la fonction ppal()).

Remarques 8

    La notation standardisée d'une option facultative est de la mettre entre []. Si de plus elle peut prendre plusieurs valeurs différentes, elles sont séparées par le symbole |. D'où la syntaxe suggérée de la commande :
 
exo_03 destination source [1 | g | b | B ]


Rappel : argv est un tableau de chaînes de caractères. Ainsi, argv[0] pointe sur la première chaîne (la commande elle-même), argv[1] pointe sur la deuxième chaîne (le nom du fichier destination), argv[2] pointe sur la troisième chaîne (le nom du fichier source). S'il n'est pas nul, argv[4] pointe sur la quatrième et dernière chaîne (l'option), qui est une suite de caractères terminée par le caractère nul '\0'.

    Il est donc impossible de comparer directement la chaîne de caractères avec les caractères 'B', 'b', 'g'. Une première idée serait de la comparer avec les chaînes de caractères correspondantes : "B", "b", "g", comme par exemple :
 
if ("B" == argv [3])

    Malheureusement, cette écriture ne fait que comparer les pointeurs :

argv[3] pointe-t-il bien sur la constante chaîne de caractères "B"

(en fait "B" représente le pointeur vers la chaîne de contenu B'\0'), et non :

argv[3] pointe-t-il bien sur une chaîne dont le contenu est identique à celui de la constante chaîne de caractères "B"

    Pour comparer deux chaînes de caractères au sens C (NTCTS), il faut uttiliser la fonction strcmp() (string compare) ce qui est trop lourd ici.

    Une autre idée serait d'utiliser l'option dans un schéma de choix, comme par exemple :
 
switch (argv [3])
{
    case "B" : // ...

    Malheureusement, il s'agit cette fois d'une erreur de syntaxe : l'expression qui suit l'instruction switch doit être de type scalaire entier (char, short, int, long, signés ou non). De plus, les constantes des instructions case doivent aussi être des constantes de type scalaire entier.

    La solution ici consiste à extraire le premier caractère (de rang 0) de la chaîne de caractères de rang 3, représentant l'option, et à le comparer à un caractère :
 
if ('B' == argv [3][0])

ou
 
switch (argv [3][0])
{
    case 'B' : // ...

Remarques 9

Rappel : lorsque les cas d'un schéma de choix ont des traitements communs, ceux-ci peuvent parfois être regroupés, comme dans l'exemple suivant :
 
case Cas1 :
    Traitement_1();
    break;

case Cas2 :
    Traitement_2();
    Traitement_1();
    break;

case Cas3 :
    Traitement_1();
    break;

    qui peut être simplifié en :
 
case Cas2 :
    Traitement_2();

case Cas1 :
case Cas3 :
    Traitement_1();
    break;

    On remarque que :

    Il n'est malheureusement pas possible d'indiquer un intervalle pour un case. Il faut donc soit énumérer explicitement toutes les valeurs, avec autant d'instructions case, soit utiliser un schéma alternatif.


    Dans la fonction ppal(), remarquer la façon dont l'imbrication des deux schémas de choix a été réduite à un seul schéma de choix.

Remarques 10

    Dans un fichier makefile, l'écriture $(VARINT) représente le contenu de la variable interne VARINT qui a été initialisée au lancement de la commande make ou par la ligne :
 
VARINT = suite_de_caractères_représentant_le_contenu_de_la_variable

    Il est possible de compléter localement le contenu de cette variable. Ainsi, dans le fichiers Makefile_05, au lieu d'écrire :
 
#
COMPILER = g++ -c -Wall $*.cxx
#
# ..............
#
nsUtil_05.o : nsUtil_05.cxx $(NSSYSTEME_H) $(NSUTIL_H) $(CEXCEPTION_H)
        g++ -c -D$(macro_destroy) nsUtil_05.cxx -Wall
#
# ..............
#

est-il plus simple d'écrire :
 
#
COMPILER = g++ -c -Wall $*.cxx
#
# ..............
#
nsUtil_05.o : nsUtil_05.cxx $(NSSYSTEME_H) $(NSUTIL_H) $(CEXCEPTION_H)
        $(COMPILER) -D$(macro_destroy)
#
# ..............
#

    On rappelle en effet que l'ordre des options n'a pas d'importance.

Remarques 11

    Les arguments d'une commande (argv) sont toujours passé sous forme de chaînes de caractères C (NTCTS).

Arguments numériques

    Lorsqu'ils représentent autre chose qu'une chaîne (en particulier un nombre), ils doivent être convertis (par exemple par les fonctions atoi(), ou strtol()). Si cette valeur n'est utilisée qu'une seule fois, la conversion peut être faite "dans la foulée", sans utiliser une variable intermédiaire. Par exemple, si argv[1] représente un délai initial en secondes, on écrira :
 
::sleep  (atoi (argv [1]));

    Si au contraire, cette valeur est fréquemment utilisée, il faut faire la conversion une seule fois et en conserver le résultat dans une variable intermédiaire, par exemple, si argv[2] représente un intervalle de temps, en secondes, entre deux opérations répétitives :
 
const unsigned int CstDelai = atoi (argv [2]);
...
for (...)
{
    ...
    ::sleep  (CstDelai); // et non ::sleep (atoi (argv [2]));
}

    Remarquer le choix de l'identificateur, préfixé par Cst, car cette donnée n'a aucune raison d'être modifiée par le programme.

    Enfin, si cette valeur peut être modifiée, la "variable" ne sera plus précédée du qualifiant const. Par exemple, si argv[3] représente un nombre de répétitions d'une boucle, la conversion en entier ne sera effectuée qu'une seule fois, avant l'entrée dans la boucle :
 
for (unsigned int CptBoucle = atoi (argv [3]); CptBoucle--; )
{
    ...
}

    L'écriture ci-dessous est particulièrement maladroite :
 
for (unsigned int CptBoucle = 0; CptBoucle < atoi (argv [3]); ++CptBoucle)
{
    ...
}

    dans laquelle la conversion en entier risque d'être effectuée à chaque boucle (sauf avec un compilateur particulièrement performant).

Arguments "textuels"

    Lorsqu'un argument représente une chaîne de caractères C (NTCTS), il n'est pas toujours nécessaire de la transformer en variable locale. Cela dépend essentiellement de l'usage qui doit en être fait. Par exemple, si argv[1] représente un nom de fichier, on pourra écrire :
 
open (argv[1], ...);

    En revanche, si la chaîne doit être "travaillée", on aura peut-être intérêt à la stocker sous forme d'un string, afin de bénéficier des opérations standard de cette classe :
 
string NomFich (Repertoire + '/' + argv [1] + '.' + Extension);

open ((NomFic.c_str(), ...);

Remarques 12

Rappel : le type char est un type numérique entier. Le passage d'un caractère au caractère suivant ou précédent peut se faire par incrémentation ou décrémentation directe de la variable de type caractère.

Remarques 13

    Une erreur classique consiste à lire des caractères sur un support (par exemple sur un fichier), à les ranger dans un tableau de caractères, et à essayer d'afficher dans le flux de sortie par exemple le tableau ainsi rempli sans précaution supplémentaire. Or l'injecteur << n'est surchargé que pour les chaînes de caractères (NTCTS), donc terminées par le caractère nul '\0' (qui fait office de drapeau) et pas pour les simples tableaux. Il n'est donc utilisable qu'après que ce caractère a été placé en fin du tableau préalablement rempli. Il faut donc aussi que le tableau ait une taille au moins supérieure d'une unité au nombre maximal de caractères à stocker.

    Il faut aussi rappeler que la fonction read() (et son wrapper Read()) renvoient le nombre de caractères lus. Comme l'indexation commence à 0 en C/C++, ce nombre représente aussi la position du prochain caractère (à lire par exemple, ou libre). C'est cette propriété qui est utilisée dans la séquence suivante :
 
char Tampon [6];    // pour ranger <= 5 caractères "efficaces"
                    // indexés de 0 à 4

Tampon [Read (fd, Tampon, 5)] = '\0'; // place '\0' au premier rang libre
cout << Tampon << endl;

Remarques 14

    Rappel : une constante littérale chaîne de caractères en C/C++ est en fait l'adresse de cette chaîne. Ainsi l'affectation suivante est-elle valide :
 
char * pChar = "coucou";

    Elle signifie qu'il y a quelque part la suite de caractères "coucou" (terminée par le septième caractère '\0'), dont l'adresse est transférée dans le pointeur pChar.

    De la même façon, dans l'instruction suivante :
 
Write (fd, "1234567", 7);

l'adresse de la chaîne de caractères "1234567" est transmise au paramètre formel buffer de la fonction Write(), de type void * (qui accepte donc tout type d'adresse pour le pointeur effectif). Cela n'implique en rien que la totalité de la chaîne soit écrite dans le fichier. C'est au contraire le dernier paramètre qui précise combien de caractères doivent être transmis à partir de cette adresse.

    A noter au passage que le caractère '\0' n'est, lui, pas écrit dans le fichier, ce qui est normal puisqu'un fichier texte n'est pas organisé en chaînes, mais en lignes.

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique

Créé le 07/07/2001 - Dernière mise à jour : 23/08/2001