La programmation en C++

  1. Types élémentaires
  2. Types entiers à taille définie
  3. Types dérivés
    1. Les pointeurs
    2. Les tableaux
    3. Les structures
    4. Les unions
    5. Les champs de bits
  4. Variables
    1. Calcul d'adresse &
    2. Allocation déclarative
    3. Les variables static et extern
    4. Allocation dynamique
    5. Conversion de Type
  5. Fonctions
  6. Instructions
  7. Méthode de programmation
    1. Les modules en C++
    2. La classe canonique en C++
    3. Norme de programmation
  8. Les objets fondamentaux
    1. Le Ens32
    2. La Pile
    3. La File
    4. Le Land
    5. La Mem
  9. Les couches logiciels
  10. Les fondements du developpement informatique
    1. Le livre de bords
    2. La phase de test
    3. La documentation et le langage.
Peter PRINZ & Ulla KIRCH-PRINZ, "C précis & concis", O'REILLY, 2002
Claude DELANNOY, "Programmer en langage C++", Eyrolles, quatrième édition, 1998
Henri GARRETA, "C: Langage, bibliothèque, applications", InterEditions, 1992

Nous allons décrire le langage C++ un peu comme s'il était intérprété directement par la machine. Ce langage est resté proche de la machine.

Les langages de programmation classiques utilisent des variables typées comme paramètres et résultats de fonctions, les types servant à structurer les données, les variables servant à véhiculer les données, et les fonctions jouant le rôle de sous-programmes. Le programme se subdivise ainsi en plusieurs programmes plus simples exprimés dans le même langage, et ce sont ces trois entitées : le type, la variable et la fonction, qui sont la clef de cette dichotomie.

Mais il nous faut tout d'abord définir comment concrètement l'ordinateur procède pour accéder à la donnée contenue par une variables, en d'autre terme comment il procède pour se souvenir, et finalement comment il gère sa mémoire.

On définie ce qu'est un bloc mémoire, une donnée, une variable, et un type. Un bloc mémoire est un ensemble d'octets contigües. Une donnée est contenue dans un bloc mémoire. Une variable est une référence vers un bloc mémoire, et fait partie d'un programme. Une variable possède un type qui est précisé lors de sa déclaration. Son type décrit son comportement.

Lorsque une variable est déclarée, son bloc mémoire est alloué implicitement lorsque le bloc d'instruction où se trouve la déclaration est exécuté, et est libéré implicitement lorsque l'on a achevé l'exécution du bloc d'instruction. On parlera d'allocation déclarative.

On peut allouer explicitement un bloc mémoire à l'aide de la fonction malloc, on parlera alors d'allocation dynamique. Dans ce cas le bloc en question est lié à un descripteur qui contient la taille du bloc. On peut allouer explicitement un bloc qui contiendra un objet à l'aide de la fonction new, mais dans ce cas le descripteur du bloc contiendra une information supplémentaire, une référence à la classe de l'objet (qui contient toutes les méthodes qui lui sont applicables), c'est le typage dynamique, la variable peut alors contenir différent type d'objet.

1) Types élémentaires

char

Le type char représente un nombre entier occupant la plus petite cellule de mémoire adressable séparement. Cela correspond à la première quantification qui est généralement l'octet. Les types char se définissent alors ainsi :

signed char : -128...127
unsigned char : 0...255

Il sont soit signés ou non signés selon que le bit de poid fort de l'octet désigne le signe ou pas. Le type char représente un caractère de code correspondant à la valeur unsigned char, et il est affiché par défaut comme un caractère.

char *

Le type char * représente une adresse mémoire. Cela correspond à la seconde quantification, au mot d'adressage qui est généralement un mot de 4 octets. Le type char * pointe vers une chaine de caractères qui se termine selon la convention par \0, le caractère de code 0, et il est affiché par défaut comme une chaine de caractère. Une chaine de n caractères est simplement un bloc de n+1 octets.

int

Le type int correspond à la taille d'entier la plus efficace, c'est à dire la mieux adaptée à la machine sous-jacente. Il est définie généralement comme un mot de 4 octets. Il contient alors un entier compris entre -2 147 483 648 et 2 147 483 647.

intptr_t

Le type intptr_t, définie dans le fichier d'entête stdint.h, correspond à la taille d'entier signé assez grand pour contenir une adresse mémoire. Il est équivalent au type char * à part qu'il n'affiche pas par defaut la chaine de caractère pointée, mais l'adresse qu'il contient sous forme d'entier signé. Ce type est définie généralement comme un mot de 4 octets, et est alors identique au type int. Le type entier non signé équivalant se nomme uintptr_t

Note : Sur la plus part des systèmes, les adresses pour les mots d'adressage de 4 octets possèdent des contraintes d'alignement : Elles doivent être des adresses multiples de 4.

intmax_t

Le type intmax_t, définie dans le fichier d'entête stdint.h, correspond à la plus grande taille des entiers signés qui sont implémentés. Sur de nombreux compilateurs, le type intmax_t a une taille de 8 octets, et est identique au type long long. Le type entier non signé équivalant se nomme uintmax_t

2) Les types entiers à taille définie

Les types entiers à taille déterminée sont définis dans le fichier entête stdint.h :

type
taille
valeurs
int8_t 1 octet -128  à  127
int16_t 2 octets -32 768  à  32 767
int32_t 4 octets -2 147 483 648  à  2 147 483 647

int64_t

8 octets -9 223 372 036 854 775 808  à  9 223 372 036 854 775 807

type
taille
valeurs
uint8_t 1 octet 0  à  255
uint16_t 2 octets 0  à  65 535
uint32_t 4 octets 0  à  4 294 967 295

uint64_t

8 octets 0  à  18 446 744 073 709 551 615

3) Types dérivés

A partir de types quelconques T, R, S..., on construit des types dérivés grace à la définition de pointeur *, de tableau [ ], de structure struct{...}, et d'union union{...}.

3.1) Les pointeurs

Une donnée de type T* pointe sur un bloc de type T. La déclaration suivante crée une variable x de type T*.

T *x;

x est appelé un pointeur et contient l'adresse d'un bloc de type T.
x n'est pas initialisé et le bloc pointé par x n'est pas alloué.

Parcontre dans la déclaration suivante :

T y;

y contient une donné de type T
y est initialisé à zéro.

Des comportements arithmétiques particuliers sont associés à ce type. Lorsque on lui ajoute un entier n, cela calcule l'adresse augmentée de n fois la taille d'un bloc de type T, de tel sorte que x+1 est l'adresse du bloc consécutif suivant de type T, et x+n est l'adresse du nième bloc consécutif suivant de type T.

L'autre comportement particulier à un pointeur est l'opération d'adressage indirecte * et l'opération d'adressage indirecte indexé []. On note *x, l'adressage indirecte de x. (*x) est une variable référençant le bloc pointée par x c'est à dire le bloc mémoire dont l'adresse est contenue par x, c'est à dire le bloc mémoire dont l'adresse est contenue dans le bloc référencé par x. Ce bloc mémoire sera vu, selon la définition de x qui est de type T*, comme un bloc de type T. On note x[i], l'adressage indirecte indexé par i de x. x[i] est une variable référençant le bloc pointée par x+i.

Nous avons donc les identitées suivantes :

*x = x[0]
*(x+1) = x[1]
*(x+2) = x[2]
*(x+n) = x[n]

On réitére le processus pour définir des tableaux à double entrés, c'est à dire accessible après deux adressages indirectes successifs. La donnée de type T** désigne un pointeur de pointeur de blocs de type T. A partir d'une variable x de type T**, on peut effectuer deux adressages indirectes succécifs, et les mêmes identités apparaissent :

x[0][0] = **x = *x[0] = (*x)[0]
x[0][1] = *(*x+1) = *(x[0]+1) = (*x)[1]
x[1][0] = **(x+1) = *x[1] = (*(x+1))[0]
x[1][1] = *(*(x+1)+1) = *(x[1]+1) = (*(x+1))[1]

Noter que l'opérateur d'adressage indirecte indexée [ ] est prioritaire sur l'opérateur d'adressage indirecte *

On peut utiliser un nom tel que pT pour désigner le type T*. Les deux écritures suivantes sont équivalentes et définissent deux variables x et y de ce type :

T *x, *y; typedef T *pT;
pT x,y;

3.2) Les tableaux

Une donnée de type T[3] est un bloc composé de 3 blocs de type T consécutifs. La déclaration T x[3]; crée la variable x de type T[3], x est appelé tableau, et contient 3 blocs de type T. L'adresse du tableau x est identique à l'adresse du premier bloc de type T du tableau.

On peut utiliser un nom tel que T3 pour désigner le type T[3]. Les deux écritures suivantes sont équivalentes et définissent deux variables x et y de ce type :

T x[3],y[3]; typedef T T3[3];
T3 x,y;

Ce type se comporte comme un pointeur. Lorsque on lui ajoute un entier n, cela calcule l'adresse augmentée de n fois la taille d'un bloc de type T, de tel sorte que x+n est l'adresse du nième bloc consécutif de type T dans le tableau x.

x fait référence au tableau, il n'est pas nécessaire d'effectuer un adressage indirecte pour accéder au tableau. Cependant la même écriture sera utilisé. *x n'effectue pas un adressage indirecte car il n'y a pas de mémoire contenant l'adresse du tableau x. *x est une variable référençant le premier bloc de type T du tableau x. *x est un simple adressage. De même x[n] est une variable référençant le nième bloc de type T du tableau x. Nous avons de même x[n] = *(x+n). l'opération x[n] est un simple adressage indexé. Et si on désigne l'adresse du tableau par un pointeur de type T, la même notation s'appliquera et correspondra alors à un adressage indirecte * et à un adressage indirecte indexé [].

On peut combiner l'opérateur * de définition de pointeur et l'opérateur [] de définition de tableau pour créer des tableaux de pointeurs ou des pointeurs de tableaux.

T * x[3]; définie un tableau de trois pointeur de type T*.

T (* x)[3]; définie un pointeur de tableau de 3 blocs de type T.

On réitére le processus pour définir des tableaux à double entrés. Par exemple le type T[2][3] représente un bloc mémoires contiguë composé de 2 blocs consécutifs de type T[3] eux même composé de 3 blocs consécutifs de type T.

T * x[3]; typedef T * pT[3];
pT x[3];
T (*x)[3]; typedef T T3[3];
T3 *x;
T x[2][3]; typedef T T3[3];
T3 x[2];

3.3) Les structures

L'opérateur struct permet de construire des blocs composés de blocs de type divers consécutifs (sauf quelque fois en arrondissant en bloc de 4 octets).

Par exemple l'instruction struct {T x; R y; S z;} v; définie la variable v référençant un bloc composés d'un premier bloc de type T, d'un second bloc consécutif de type R, et d'un troisième bloc consécutif de type S. Le premier bloc possède la même adresse que le bloc structuré. L'adresse du second bloc est égale à l'adresse du bloc précédent augmenté de sa taille. Et de même pour les bloc suivants.

Des comportements particuliers sont associés à ce genre de type. On accède aux sous-données x de type T, y de type R, et z de type S, à l'aide de l'opérateur point : v.x, v.y, v.z

On peut utiliser un nom tel que Cell pour désigner le type struct {T x; R y; S z;}. Les deux écritures suivantes sont équivalentes et définissent deux variables v et w de ce type :

struct {T x; R y; S z;} v,w; struct Cell {T x; R y; S z;};
Cell v,w;

Dans un souci d'optimisation le compilateur peut parfois insérer des espaces de longueur variables entre les différents champs pour arrondire les tailles. C'est pourquoi on utilise toujours l'opérateur sizeof (type_structure) pour vérifier la taille mémoire d'une structure. En cas de doute, la macro offsetof(type_structure, membre) définie dans le fichier d'entête stddef.h permet d'obtenir l'emplacement d'un membre à l'interieur d'une structure en nombre d'octet entre l'adresse de début de la structure et celle du membre.

3.4) Les unions

L'opérateur union permet de construire des blocs, vues sous différents types.

Par exemple l'instruction union {T x; R y; S z;} v; définie la variable v référençant un bloc mémoire composés du bloc de taille égale à la plus grande taille des blocs de type T ou R ou S. On accède à la données sous les différente vue, selon le type T, ou R ou S, à l'aide de l'opérateur point. v.x, v.y, v.z

On peut utiliser un nom tel que Cell pour désigner le type union {T x; R y; S z;}. Les deux écritures suivantes sont équivalentes et définissent deux variables v et w de ce type :

union {T x; R y; S z;} v,w; union Cell {T x; R y; S z;};
Cell v,w;

Les structures et les unions se combinent librement, et peuvent être déclaré anonymement au sein d'une structure ou union de tel sorte que l'accès à leurs membres se fait directement sans passer par le nom de la structure intermedière. Par exemple voici la structure A :

struct A{
   T x;
   union{
      R y;
      S z;
      struct{
R u;S v;};
      R w;
   };
};

A q;

q est une variable de type A et on peut accéder à ses différents membres par : q.x, q.y, q.z, q.u, q.v, q.w

3.5) Les champs de bits

Au sein d'une structure ou d'une union, les champs de bits sont des entiers constitués d'un nombre donné de bits. Voici un exemple de déclaration d'une structure Cell ayant un membre x constitué des 3 premiers bits de poid faibles, et un espace de 12 bits non référencés, et un membre s constitué d'un bit suivant, et un membre r constitué des 16 bits suivants (type short), l'ensemble pouvant constituer un bloc de taille multiple de 4 octets, taille privilègiée par le compilateur.

struct Cell{
   signed int x : 3
   unsigned int : 12
   unsigned int s : 1
   short r;
};

Un élément w de type Cell accède à ces membres à l'aide de l'opérateur point : w.x, w.s, w.r

La taille du type Cell est vérifiée par sizeof(Cell) et vaut ici 4 octets. Il est préférable pour les structures que l'on manipule souvent, de choisire une taille de structure arrondie à un multiple de 4 octets. Car dans de nombreux compilateur, les fonctions en C++ sont implémentées de tel sorte quelle manipulent des blocs de 4 octets en entrée et en sortie.

4) Variables

La définition d'une variable est composée d'un type suivi du nom de la variable et éventuellement suivi d'une initialisation. Voici la liste des exemples :

int x;
   // Iinitialisé à 0 par défaut.
int x = 5;
int x[3];
   // Initialisé par un tableau de 3 zéros par défaut.
int x[3] = {6,4,8};
int x[36] = {1,5,2,7};
    // 0 pour les composantes manquantes.
int x[] = {9,7,8};
    // La dimenssion du tableau x est égale à 3.
int x*;
    // x n'est pas initialisé.
int x* = &u;
    // Initialisé à l'adresse de la variable u.
int * x[10] = {&u, &u};
   // NULL pour les composantes manquantes

int x[2][3] = {{7,5,6},{9,8,9}};
int x[10][36] = {{6,4},{9}};
    // 0 pour les composantes manquantes
int x[][10] = {{6,4},{9},{5,5,7},{8}};
    // Première dimenssion égale à 4, et 0 pour les composantes manquantes
struct {int x,y,z;} v = {5,2};
    // 0 pour les composantes manquantes
struct {int x, struct {int y,z;}} v = {5,{2,7}};
    // A chaque structure emboitée correspond un niveau de braquette {}
union {int x;struct{signed int b:3;signed int c:12;}} v = {9};

    // L'initialisation se fait selon la première composante de l'union.
union {int m[4];struct {int x,y,z}v;} u = {2,3,6,8};
    // L'initialisation se fait selon la première composante de l'union.

Plusieurs variables séparées par des virgules peuvent être crées et initialisées lors d'une déclaration. Par exemple, l'instruction suivante déclare x ,y comme entier et intialise x à zero et y à 5, et déclare un pointeur z de type int* sans l'initialiser, et déclare un tableau t de 4 int initialisé à t[0]=5, t[1]=7, t[2]=0, t[3]=0 :

int x, y=5 ,*z, t[4]={5,7};

4.1) Calcul d'adresse &

L'opération &expr calcule l'adresse d'une variable expr.
Si expr est de type T alors &expr est de type T*.

Par exemple :

int i;
int* p;
p = &i;

i et *p désigne la même variable.

Il y a deux façons d'allouer la mémoire, soit par déclaration, soit dynamiquement :

4.2) Allocation déclarative

La déclaration d'une variable va allouer un bloc mémoire pour la variable et l'initialiser. Les paramètres de cette allocation (tailles et initialisations) doivent être calculables initialement avant d'éxecuter le bloc d'instruction où figure la déclaration. L'allocation mémoire est faite à chaque fois que l'on entre dans le bloc d'instruction symbolisé par l'ouverture des accolades "{", et est libéré à chaque fois que l'on sort du bloc d'instruction symbolisé par la fermeture des accolades "}".

4.3) Les variables static et extern

Lors de la déclaration, si le type est précédé du mot clef static, il définie une variable pérenne durant l'execution du programme qui n'est allouée et initialisée qu'une seul fois au début du programme, puis modifiée au grès des instructions, et libéré à la fin du programme. Les paramètres (tailles et initialisations) doivent alors être calculables initialement à la compilation. L'initialisation ne se faisant qu'une seul fois, lorsque au cours de l'execution du programme on réexecute le bloc d'instruction ou se trouve la déclaration, celle-ci n'a aucun effet sauf de rappeler l'existance de cette variable perenne avec sa donnée existante résultat de toutes les modifications passées. Par contre la portée de cette variable est limitée au bloc où se trouve sa déclaration. Cela signifie que en dehors du bloc d'instruction, le même nom peut être utilisé pour une autre variable static qui désignera alrors une autres données variables et pérennes distinctes.

Lors de la déclaration, si le type est précédé du mot clef extern, cela a pour conséquence d'étendre la porté de la variable à tout le programme, mais cela ne fait ni l'allocation ni l'initialisation de la variable en question, il faut pour cela une seconde déclaration avec le mot clef static. Cela permet de définir des variables globales.

4.4) Allocation dynamique

Lorsque on alloue de la memoire avec les instructions malloc, calloc et realloc, il faut prévoir explicitement leur libération par l'instructions free.

Avec l'instructions malloc, les données ne sont pas initialisée. L''initialisation peut être faite à partir des fonctions rapides de recopie de blocs memcpy et memmove et de la fonction d'initialisation memset.

Lorsque l'on alloue dynamiquement un bloc mémoire par l'instruction p = (int*)malloc(10*sizeof(int)); le système réserve un bloc de mémoire un peu plus grand commençant par le déscripteur du bloc qui contient la taille du bloc, suivi par le bloc mémoire proprement dit qui se décompose ici en 10 blocs consécutifs de type int sans descripteur (Dans d'autre système le descripteur est placé ailleur). L'utilisateur n'a pas accès au descripteur de bloc. C'est une donnée encapsulée par le système d'exploitation.

Le bloc réservé dynamiquement peut être libéré sans avoir à préciser sa taille puisque celle-ci est décrite dans un Descripteur. Le pointeur du bloc peut être transmis à d'autres variables par simple affectation du genre x = p; et le bloc peut être libéré par l'instruction free(x) sans qu'il soit necessaire de préciser sa taille.

Il y a deux gestionnaires de mémoire que sont la pile (stack en anglais) et le tas (heap en anglais). Les deux écritures suivantes sont équivalentes mais n'utilise pas le même gestionnaire de mémoire :

Allocation déclarative
(Pile / Stack)
Allocation dynamique
(Tas / Heap)

{
  int v;
  ...
}

{
  static int
*p;
  p=(int*)calloc(1,sizeof(int));
  int &v =
*p;
  ...

  free(p);
}
{
  int v[2];
  ...
}
{
  static int
(*p)[2];
  p=(int(*)[2])calloc(2,sizeof(int));
  int &v =
*p;
  ...

  free(p);
}

{
  int v[2][3];
  ...
}

{
  static int
(*p)[2][3];
  p=(int(*)[2][3])calloc(2*3,sizeof(int));
  int &v =
*p;
  ...

  free(p);
}

L'allocation déclarative utilise la pile (stack). Elle alloue les mémoires nécessaires aux variables déclarées lors de l'éxecution d'un bloc d'instructions, et les libères sitôt que l'on quitte le bloc d'instructions. Ainsi les mémoires allouées sont toujours empilées succecivement formant une pile de blocs bien rangé en un seul morceau.

L'allocation dynamique utilise le tas (heap). Et comme la libération peut se faire selon un ordre quelconque, le tas devient vite un gruyère, les blocs mémoires alloués deviennent le plus souvent disjoints et répartis n'importe comment, limitant par cela la possibilitée de réserver des grands blocs. Pour remédier à cela, on doit implémenter un mécanisme de défragmentation du tas.

L'instruction x = (T*)malloc(sizeof(T[5])) alloue un bloc mémoire dans le tas (heap) de taille égale à la taille du type T[5] et retourne un pointeur de type T* sur le début du bloc mémoire. Le pointeur est mis dans la variable x qui doit être préalablement déclaré comme une variable de type T*

Voici des exemples d'allocations déclaratives et d'allocations dynamiques : Les cases mémoires oranges sont allouées dans le tas (heap) et les case mémoires blanches sont allouées dans la pile (stack).

Instructions
Type
de x
Taille
de x en octets
 Shéma de
la mémoire allouée
Identité
int x;
int
4
x = 0
int x = 3;
int
4
x = 3
int* x;
x =(int*)malloc(sizeof(int));
*x = 5;
int*
4
*x = x[0] = 5
int x[2];
int[2]
8
*x = x[0] = 0
*(x+1) = x[1] = 0
int* x;
x =(int*)malloc(sizeof(int[2]));
int*
4
*x = x[0] est non initialisé.
*(x+1) = x[1] est non initialisé.
int x[3] = {5,9,7};
int[3]
12
*x = x[0] = 5
*(x+1) = x[1] = 9
*(x+2) = x[2] = 7
int** x;
x =(int**)malloc(sizeof(int*));
*x = (int*)malloc(sizeof(int));
int*
4
**x = *(x[0]) = *x[0] = x[0][0] est non initialisé
int** x;
x = (int**)malloc(sizeof(int[2]));
x[0] =(int*)malloc(sizeof(int[3]));
x[1] =(int*)malloc(sizeof(int[3]));
int**
4

**x = *x[0] = (*x)[0] = x[0][0] est non initialisé
*(*x+1) = *(x[0]+1) = (*x)[1]= x[0][1] est non initialisé
*(*x+2) = *(x[0]+2) = (*x)[2]= x[0][2] est non initialisé
**(x+1) = *x[1] = (*(x+1))[0] = x[1][0] est non initialisé
*(*(x+1)+1) = *(x[1]+1) = (*(x+1))[1] = x[1][1] idem
*(*(x+1)+2) = *(x[1]+2) = (*(x+1))[2] = x[1][2] idem

int x[2][3] = {{6,9,8},{5,7,1}}
int[2][3]
24
**x = *x[0] = (*x)[0] = x[0][0] = 6
*(*x+1) = *(x[0]+1) = (*x)[1]= x[0][1] = 9
*(*x+2) = *(x[0]+2) = (*x)[2]= x[0][2] = 8
**(x+1) = *x[1] = (*(x+1))[0] = x[1][0] = 5
*(*(x+1)+1) = *(x[1]+1) = (*(x+1))[1] = x[1][1] =7
*(*(x+1)+2) = *(x[1]+2) = (*(x+1))[2] = x[1][2] =1

L'opérateur d'adressage indirecte indexé [] est prioritaire sur l'opérateur d'adressage indirecte * lui même prioritaire sur l'opération +.

4.5) Conversion de type

On passe d'un type à l'autre grace à l'opérateur de "cast" qui est le nom du type voulu entre parenthèse. Toutes les conversions ne sont pas possibles, mais entre pointeurs cela est possible. Par exemple, soit deux variables u et v l'une de type struct{T x,y,z;}*, l'autre de type T(*)[5]. Appelons A le type struct{T x,y,z;}et appelons B le type T[5]. Appelons pA le type A* et pB le type B*.

struct A {T x,y,z;};
typedef T B[5];
typedef A *pA;
typedef B *pB;
pA a;
pB b;

Les déclarations de pointeurs ne font pas d'initialisation ni d'allocation, c'est pourquoi il est prudent de le faire juste après la déclaration du pointeur (voir en même temps).

a = (pA)malloc(sizeof(A));
b = (pB)malloc(sizeof(B));

L'expression (pB)a est de type pB, et l'expression (pA)b est de type pA. Comme les deux types en question se superposent simplement nous remarquons les égalités suivantes :

(*(pB)a)[0] = a->x
(*(pB)a)[1] = a->y
(*(pB)a)[2] = a->z

(*(pA)b).x = (*b)[0]
(*(pA)b).y = (*b)[1]
(*(pA)b).z = (*b)[2]

L'opération a->x est par définition égale à (*a).x qui est équivalent à un adressage indirecte indexé.

L'opération (*b)[n] est un adressage indirecte indéxé lorsque *b est de type tableau. Noté que si *b n'est pas de type tableau T[], mais de type pointeur T* alors (*b)[n] est un adressage indirecte suivie d'un adressage indirecte indexé.

5) Fonctions

Le programme principale est la fonction main qui peut se présenter de cette façon :

int main( ){...}

Une fonction possède un type de retour et une liste de paramètres typés, pouvant être transmis par références ou par valeurs. Les paramètres, peuvent avoir une valeur par défaut. Les paramètres ayant une valeur par défaut sont nécessairement à la fin de la liste. Exemple :

int & f(int x, int &y, int z = 5, int &u = v){...}

La fonction f admet 4 paramètres : x est transmis par valeur, y est transmis par référence c'est à dire que toute modification de y dans la fonction va modifier la variable correspondante qui aura été transmise dans l'appel de la fonction f, z est transmis par valeur et s'il n'est pas présent c'est la valeur par defaut 5 qui est transmise à la fonction, u est transmis par référence et s'il n'est pas présent c'est la variable v qui est transmise par référence. Et la fonction retourne une référence à une variable entière.

Les noms de paramètres sont optionnel et le prototype de la fonction f peut s'écrire comme suit :

int& f(int, int &, int = 5, int & = v);

Les valeurs par défaut ne doivent être mentionnées que dans un seul prototype.

Le type de retours peut être un pointeur mais ne peut pas être un tableau sauf par référence. Les paramètres tableaux sont transmis par la seul valeur de leur adresse.

Si la fonction est public, elle doit être déclarée une première fois sous forme de prototype, c'est à dire sans son corps {...}, dans un fichier entête qui servira de référence aux autres fichiers voulant exploiter cette fonction.

6) Instructions

Une instruction doit se terminer par un point virgule ou bien doit constituer un bloc encadré par deux accolades. Un bloc contient des instructions. Les commentaires de fin de ligne commence par //. Les commentaires sur plusieurs lignes sont encadrés par /* et */ .

Instruction
Exemple
Déscription
if...else
if (x==5) {y=1; z=2;} else {y=0; z=1;} Si x est égale à 5 alors mettre 1 dans y et 2 dans z, sinon mettre 0 dans y et 1 dans z.
for
for(i=0;i<10;i++) {x = x+i;} Pour i variant de 0 à 9 et de 1 en 1, mettre x+i dans x.
while
while(x<10) {x++; y=y+x;} Tant que x est plus petit que 10 faire incrémenter x et mettre y+x dans y.
do...while
do {x=x+5; y=y*x} while (x<12) Faire ajouter 5 dans x et mettre y*x dans y tant que x plus petit que 12.
switch

switch (i)
{   case 1: x = 123; break;
    case 2: x = 250; break;
    case 5: x = 500; break;
    default: x = 0;
}

Si i égale 1 mettre 123 dans x et quitter l'instruction switch. Si i égale 2, mettre 250 dans x et quitter l'instruction switch. Si i égale 5, mettre 500 dans x et quitter l'instruction switch. Sinon (C'est à dire si x différent de 1,2 et 5) mettre 0 dans x.
break
break; Dans la portée d'une instruction for, while, do, et switch, l'instruction break provoque l'abandon de la structure et le passage à l'instruction écrite immédiatement après.
continue
continue; Dans la portée d'une instruction for, while, et do, l'instruction continue provoque l'abandon de l'itération courante et le démarrage de l'itération suivante.
return
return x+y; Dans la portée d'une fonction, l'instruction return expression, évalue l'expression, provoque l'abandon de la fonction et le retour à la fonction appelante avec comme valeur de retour la valeur de l'expression.

7) Méthode de programmation

Nous utilisons le langage C++. La programmation en C++ reste proche du langage machine, et permet au programmeur de se délester d'une charge de structuration assez pénible qui est prise en compte par le langage objet C++ et par la plateforme de développement. La normalisation du langage est nécessaire pour assurer la portabilité. Le C possède depuis longtemps cette qualité, et une qualité récente du C++ est sa proche normalisation dans le monde Linux. Plusieurs plateforme de développement C/C++ en open source, et gratuites, existes, tel que Code::Blocks et Eclipse, ainsi que des compilateur C/C++, open source, et gratuits, tel que cygwin et MinGW, tous deux alignés sur la proche normalisation du C++ dans le monde Linux. Nous utilisons la plateforme de développement "Eclipse platform version 3.3.0" avec "Eclipse C/C++ Development Tools version 4.0.1". Et nous utilisons le compilateur "MinGW", "gcc version 3.4.2 (mingw-special)".

Il nous faut une méthode de programmation. Prenons celle classiquement décrite par Bertrand Meyer, la programmation modulaire, c'est à dire vérifiant les 5 critères de modularité suivants :

Décomposabilité : Tout problème se subdivise en sous-problèmes qu'on peut résoudre séparément en des modules.
Composabilité : Les modules logiciels peuvent se combiner librement pour en produire d'autres.
Compréhensibilité : Chaque module peut être compris isolément par un lecteur humain.
Continuité : Une petite modification du problème entraîne une modification d'un petit nombre de modules.
Protection : L'effet d'une erreur se produisant dans un module reste localisé à un petit nombre de modules.

7.1) Les modules en C++

Le programme comprend plusieurs modules. Un module M est constitué de deux fichiers M.h et M.cpp. Le fichier M.h contient les définitions des types et des variables globales ainsi que les prototypes des fonctions. Le fichier M.cpp contient le corps des fonctions déclarées dans l'entête.

Le compilateur C++, précompile chaque module : M1.cpp, M2.cpp, M3.cpp..., contenant les sources des programmes en C++, en les transformant en des fichiers source écrit en C : M1.c, M2.c, M3.c.... Puis le compilateur C les compile et les transforme en des fichiers M1.o, M2.o, M3.o, puis effectue le linkage entre eux pour produire un fichier éxecutable Proj.exe qui porte le nom du projet. Le linkage établit les liens effectués par les appels de fonctions d'un module par un autre et par l'utilisation de variables communes à plusieurs modules.

Chaque module M1.cpp, M2.cpp, M3.cpp..., inclue par le biais de directives de compilation #include "...", les fichiers entêtes M1.h, M2.h, M3.h dont il a besoin ainsi que des fichiers entêtes système dont il a besoin qui sont de la forme #include<...>. Chaque module M.cpp contient à la première ligne une instruction #include "M.h", qui inclue son entête, à l'exception du module principale Proj.cpp qui contient la fonction main et qui n'a pas de fichier entête. Chaque fichier entête M.h contiennent dans les deux premières lignes, les directives #ifndef M_H_ et #define M_H_ et à la fin la directive #endif. Ces directives ont pour but d'éviter au compilateur de parcourir plusieurs fois le fichier entête ce qui causerait une erreur de compilation, les variables ne pouvant être déclarées plusieur fois.

La programmation modulaire consiste à créer des modules les plus autonomes possibles. Mais la définition du module est beaucoup trop lache pour permettre de garantir cette autonomie. On va donc la remplacer par la définition de classe qui possède des velléités d'autonomie beaucoup plus forte.

Une classe est un type dans lequel on a ajouté des méthodes spécifiques à ses objets. La classe peut créer à volonté des objets qui chacun éxecuteront les méthodes de la classe dans un environnement distinct décrit par la classe. Ainsi la classe est semblable à un mécanisme de création de processus éxecutés en série les uns après les autres. La classe est une généralisation des structures incluant dans celles-ci des méthodes.

Le programme est subdivisé en plusieurs classes qui elles même sont subdivisés en plusieurs méthodes. La classe est beaucoup plus autonome qu'une méthode ou une fonction. Ce concept permet de construire les fondations de notre informatique sous forme d'objets fondamentaux, les objets plus sophistiqués étant construit à partir de ces objets fondamentaux. C'est pourquoi nous définissons le module comme contenant exactement une classe, à l'exception du module principale contenant la fonction main.

La programmation objet sera néanmoins utilisée modestement. Les mathématiques ont des ambitions plus fortes concernant l'emboîtement des structures et leurs intéropérabilités. En particulier nous n'utiliserons pas le typage dynamique ni les fonctions d'allocation new et delete.

Pour chaque module (autre que principal) M, le fichier entête M.h contiendra la définition d'une classe, et le fichier M.cpp contiendra le corps des méthodes de la classe ainsi que les initialisations des variables statics de la classe.

7.2) La classe canonique en C++

La définission d'une classe consiste en la définition d'un type struct qui inclus en plus des méthodes. Les variables et méthodes peuvent être statics auquel cas elles sont communes à l'ensemble des objets de la classe, et sont utilisable par la classe elle-même sans qu'il y est d'objet. Des méthodes de prototype différent peuvent avoir le même nom, le type de paramètre passé à la méthode déterminant quel méthode devant être executé.
On ajoute des méthodes spécifiques appelées constructeurs portant le nom de la classe et des paramètres qui définissent l'initialisation. Ces méthodes sont automatiquement appelé lors de la déclaration de l'objet avec les paramètres de l'initialisation. On ajoute un constructeur particulier dit de recopie.
On ajoute une méthode spécifique appelées destructeur portant le nom de la classe préfixé par le caractère ~. Cette méthode est automatiquement appelé lors de la libération déclarative de l'objet, lorsque l'on quitte un bloc d'instruction ou figurait une déclaration de l'objet sans le prefixe static.
On ajoute une méthode pour l'affectation nommée operateur=.
On ajoute deux méthodes pour l'affichage et la lecture dans un flux de caractère nommé operateur<< et opérateur>>.
On ajoute des méthodes Rand(...) qui modifie aléatoirement l'objet. On ajoute la methode put(f) qui affiche l'objet dans le flux de caractère f. On ajoute la methode get(f) qui lit l'objet dans le flux de caractère f.
Une classe peut hériter d'une classe parente. Elle hérite des attributs et méthodes de la classe parente. Elle peut surdéfinir certaines méthodes héritées. On obtient ainsi une spécialisation de la classe parente.

Voici un modéle de classe que nous utiliserons. Il est placé dans un module M1 composé d'un fichier entête M1.h et d'un fichier corps M1.cpp :

M1.h :

#ifndef M1_H_
#define M1_H_
#include <iostream>
using namespace std;


struct
A {
int x,y;                    // Variables
static int s;               // Variable static
static void f();            // Méthode static

     A();                   // Constructeur sans argument
     A(int);                // Constructeur avec un argument de type int
     A(int,int);            // Constructeur avec deux arguments de type int
     A(const A &);          // Constructeur de recopie
     ~A();                  // Destructeur
A & operator=(const A &);   // Opérateur d'affectation
friend ostream & operator<<(ostream &f,A u){return u.put(f);}
friend istream & operator>>(istream &f,A &u){return u.get(f);}
ostream & put(ostream & f); // Affiche l'objet dans le flux de sortie f;
istream & get(istream & f); // Lit l'objet dans le flux d'entré f;
void A::Rand(int tmax);     // Modifie aléatoirement l'objet en une taille <= tmax.
void A::Rand();             // Modifie aléatoirement l'objet sans changer sa taille.
};
#endif

M1.cpp :

#include "M1.h"

int A::s = 5;
void A::f(){s=s+5;}
     A::A(){x=0;y=s;}
     A::A(int a){x=a;y=s;}
     A::A(int a,int b){x=a;y=b;}
     A::A(const A &u){x=u.x;y=u.y;}
     A::~A(){}
ostream & A::put(ostream &){}
istream & A::get(istream &){}
A & A::operator=(const A & u){x=u.x;y=u.y;}
void Rand(int){}
void Rand(){}

int main() {
int n(2);            // n est créé et initialise à 2.
A a(2);              // Le constructeur A(2) est éxecuté.
A b(5,8);            // Le constructeur A(5,8) est éxecuté.
A m[3] = {n,b,9};    // Les constructeurs A(n),A(a2),A(9) sont éxecutés.
A k[5] = {m[1],n,a}; // Les constructeurs A(m[1]),A(n),A(a),A(),A() sont éxecutés.

b.x = 10;            // l'objet b modifie sa variable x.
b.Rand();            // l'objet b execute la méthode Rand.

A::s = 12;           // La classe A modifie sa variable static s.
b.s = 10;            // La classe A modifie sa variable static s.
A::f();              // La classe A éxecute la méthode static f.
b.f();               // La classe A éxecute la méthode static f.
return 0;
}

Dans cette exemple de nombreuses méthodes non pas été programmées et ont un corps vide { }, et donc ne font rien.

7.3) Norme de programmation

La méthode de Bertrand Meyer n'est qu'une introduction posant les principes généraux, il nous faut l'appliquer, la préciser, la déployer en adoptant des normes. Et ceci afin de permettre un travail collectif. Même un travail personnel doit respecter des normes, car un travail personnel sur du long terme revient à un travail collectif.

Il y a deux divisions, l'une selon les modules, l'autre selon les classes. Par soucie de simplicité la division en modules correspondra exactement à la division en classes.

Voici quelques normes de programmation que nous utiliserons. Elles reprennent entre autre la définition de la classe canonique :

  1. Toutes les méthodes et classe que nous créons, ont des noms commençants par une majuscule.
  2. Une classe A ayant des composantes dynamiques, possède des méthodes particulières que sont les constructeur A( ), A(x),... et le destructeur ~A( ).
  3. Dans un bloc d'instruction, l'instruction A u(...); crée l'objet u et l'initialise à l'aide du constructeur A(...), et lorsque l'on quitte le bloc d'instruction, l'objet A est est désinitialisé par le destructeur ~A() puis libéré. L'instruction de recopie A u(v); crée l'objet u et éxecute un constructeur dit de recopie dont le prototype est A(const A &).
  4. Une classe A possède la methode Rand(...) qui fixe aléatoirement la valeur de l'objet.
  5. L'instruction d'affectation A=B; éxecute un opérateur dont le prototype est A & operator=(const A & ).
  6. Une classe A possède deux méthodes particulières que sont l'écriture put(fout) dans un flux de sortie de caractère fout, et la lecture get(fin) dans un flux d'entré de caractère fin. Pour un objet u de classe A, l'instruction fout<<u; éxecute l'instruction u.put(fout); qui écrit u sous forme de chaine de caractère dans le flux de sortie fout. Et l'instruction fin>>u; éxecute l'instruction u.get(fin); qui lit u sous forme de chaine de caractère dans le flux d'entré fin
  1. Un module M1, à l'exception du module principal, est constitué de deux fichiers M1.h et M1.cpp. Et chaque fichier M1.cpp contient à la première ligne une instruction #include "M1.h", qui inclue son entête, à l'exception du module principal.
  2. Le fichier M1.h contient la définition d'une classe
  3. Chaque fichier entête M1.h contient dans les deux premières lignes, les directives #ifndef M1_H_ et #define M1_H_ et à la fin la directive #endif.
  4. Le fichier M1.cpp contient le corps des méthodes et les initialisations des variables statics de la classe définie dans M1.h.
  5. Chaque fichier corps M1.cpp inclue par le biais de directives de compilation #include "...", les fichiers entêtes M1.h, M2.h, M3.h dont il a besoin ainssi que des fichiers entêtes système dont il a besoin qui sont de la forme #include<...>.
  6. Il existe un module principal composé du seul fichier Proj.cpp qui contient la fonction main.

8) Les objets fondamentaux

Il y a 4 types fondamentaux qui sont les entiers signés, d'un, de deux, de quatre, et de huit octets : int8_t, int16_t, int32_t, int64_t, et leur équivalent non signés : uint8_t, uint16_t, uint32_t, uint64_t.

8.1) Ens32

Le premier objet que nous utilisons est Ens32, le mot de 32 bits. Il représente à la fois :

On définie les conversions implicites entre int et Ens32. Voici toutes les opérations que l'on peut faire sur les objets Ens32 :

Ens32 A,B,C;
int n,x,i;
bool v;
ostrstream f;                // Flux de sortie de caractères.
char t[10];                  // Tampon de 10 charactères.
istrstream g(t, sizeof(t));  // Flux d'entré de caractères de tampon t

// Conversions implicites Ens32-->int et int-->Ens32
Ens32 D;       // Déclare un ensemble D vide.
Ens32 E=i;     // Déclare un ensemble E correspondant au nombre binaire i.
A = i;         // Affecte l'ensemble A selon le nombre binaire i.

A.Put1(i);     // A = A union {i}
A.Put0(i);     // A = A - {i}
A.PutX(i);     // A = (A - {i}) union ({i} - A)
A.Put(i,v);
   // Fixe le bit i de A à la valeur 0 si v=0, 1 si v=1, inversé si v=-1.
v = A.In(i);   // v = (i appartient à A)
v = A[i];      // v = (i appartient à A)
x = A.Next(n); // x = le plus petit élément >=n appartenant à A
A.put(f);      // Envoie l'ensemble A sur le flux de sortie f. Ex : {0,1,2,9,15,31}
A.get(g);      // Retire l'ensemble A du flux d'entré g
A = ~B;        // A = Complément de B
A = B|C;       // A = B Union C
A = B&C;       // A = B Inter C
A = B^C;       // A = (B Union C) - (B Inter C)
v = (A==B);    // v = (A==B)
v = (A<=B);    // v = (A inclus dans B)
v = (A<B);     // v = (A inclus strictement dans B)

v = (A>B);     // v = (A plus grand strictement que B)
v = (A<=B);    // v = (A contient B)

v = (A.b<B.b); // v = (A<B) Ordre lexicographique
A.Rand();      // A = un ensemble au hasard
A.Rand(B,C);   // A = un ensemble au hasard compris entre B et C compris.

Les fonctions s'appliquant à des variables de type int s'appliquerons aux objet Ens32 car il y a une conversion implicite. Et cette conversion n'allourdie pas le code produit par le compilateur. Car cette conversion impose un contrôle que lors de la compilation et non pendant l'éxecution.

8.2) Pile

Le second objet que nous utilisons est la pile de mots, le mot étant à la fois un int et un Ens32.

Les objets de taille variable contiennent une partie de taille variable qui est alloué dans le tas (heap), et une partie de taille fixe que est réservé dans la pile système (stack).

La pile possède 3 variables d'état : o, n, t.

o : Adresse de la pile, ou NIL si la pile est de taille zéro.
n : Nombre de mots utilisé par la piles, placés au début de la pile : o[0], o[2]..., o[n-1]
t : Taille de l'espace mémoire réservée. On peut ajouter des mots et augmenter n jusqu'à ce que n égale t sans devoir réallouer un espace mémoire.

Exemple de pile de taille 6 contenant les valeurs 5 et 7. La tête de la pile est o[n] :

Objet difficilement contournable, la pile de mots, avec les méthodes de trie rapide de complexité en O(n*ln(n)) et en O(Nmax) lorsque les entiers sont de norme inférieure à Nmax.

La pile se décompose en deux blocs disjoints, l'un non relocalisable, c'est à dire qui durant l'existence de l'objet restera au même endroit de la mémoire, l'autre relocalisable qui contient les données et qui est amené au cours de la vie de l'objet à changer de taille et de place. Le bloc non relocalisable comprend les trois valeurs t, n, o, désignant respectivement la taille mémoire du bloc relocalisable, le nombre de mots utilisés dedans, et son adresse mémoire ou NIL si la pile est de taille zéro.

Les méthodes fondamentales de l'objet pile sont décrite dans la synopsis suivante :

Instruction
Description
A.Empile(x); Empile x dans la pile A
x=A.Depile(); Dépile de la pile A une valeur et la met dans x.

Si on implémente un mécanisme de defragmentation du tas, c'est à dire un mécanisme de rangement des blocs relocalisables lorsqu'il vient à manquer de place mémoire, il faut prévoir un pointeurs supplémentaires dans le bloc relocalisable qui pointe vers la case mémoire de la variable o.

void Pile::Synopsis(){
int
t = 4;
int tmax = 10;
int i;
int x;
bool b;

Pile A(t);    // Creer une Pile A vide de taille t mots.
Pile B=t;     // Creer une Pile B vide de taille t mots.
Pile C;       // Creer une Pile C vide de taille nulle.
Pile D(B);    // Construction de la pile D par recopie de B.
Pile E=B;     // Construction de la pile E par recopie de B.

cout << A;    // Affiche la pile A, exemple : [126,5320,86]
cin >> A;     // Lit la pile A, exemple : [126,5320,86]
A.put(cout);  // Affiche la pile A, exemple : [126,5320,86]
A.get(cin);   // Lit la pile A, exemple : [126,5320,86]
A.Rand(tmax); // Initialise la Pile A au hasard de taille maximum tmax.
A.Rand();     // Initialise la Pile A au hasard sans changer n.
A.Muer(t);    // Change la taille de la Pile A en une pile de taille t.
A.Empile(x);  // Empile x dans la Pile A.
x=A.Depile(); // Depile x de A.

A=B;          // Affectation de A par B.
b=(A==B);     // Retourne vrai si A est égale à B.
i=A.Index(x); // Index de x dans la pile A, c'est le plus grand i tel que A.o[i]=x.
A.Supp(x);    // Supprime une occurence de x dans la pile A (celle de Index(x))
A.TriA();     // Trie la pile A.
A.TriAD();    // Trie la pile A en enlevant les doublons.
A.TriAP();    // Trie la pile A en enlevant les paires.

A.SuppDoublon(); // Supprime les doublons dans la pile triée A.
A.SuppPaire();   // Supprime les paires d'éléments égaux dans la pile triée A.

s=Pile::Cmp(a,b); // Compare deux entiers: Retourne -1,0,1 si *a<*b,*a==*b,*a>*b.
A.Qsort();        // Trie A selon l'ordre Pile::Cmp avec la procédure système qsort.
A.Bsearch(a);     // Recherche la valeur *a dans la pile A préalablement triée avec
                  //  la procédure système bsearch et retourne un pointeur le poitant
                  //  ou NULL s'il n'existe pas.

i = A.PointeurToIndex(p); // Calcule l'indexe i correspondant au pointeur p,
                          //  c'est à dire cherche i tel que A.o[i] = *p

p = &A.o[i];              // Calcule le pointeur correspondant à l'indexe i.
                          //  c'est à dire cherche p tel que A.o[i] = *p

u=A.t;    // Taille réservée pour la Pile A
u=A.n;    // Nombre d'éléments dand la Pile A
p=A.o;    // Pointeur vers le bloc pile A, (identique à p=A.o[0]).

u=A.o[i]; // Elément d'indexe i dans la Pile.
// Lorsque la pile n'est pas vide (A.n>0), l'élément de tête est A.o[A.n-1]

}

Une pile de mots peut contenir une proposition logique portant sur 32 variables booléennes, sous forme d'un polynôme appartenant à Z/2Z[x0,x1,x2…,x31], chaque mots de la pile étant un monôme, c'est à dire un sous-ensemble de {x0,x1,x2…,x31}. Elle peut contenir un entier de taille quelconque. Elle peut contenir un ensemble d'entier de taille quelconque. Elle peut contenir un nœud d'arité quelconque d'un graphe. Les différents rôles donnent lieu à la création de classes dérivées nommées :

8.3) File

Le troisième objet que nous utilisons est la file de mots, le mot étant à la fois un int et un Ens32.

Elle comprend 4 variables d'état o, i1, i2, t.

o : Adresse de la file, ou NIL si la file est de taille zéro.
i1 : indice du premier élément de la file, ou -1 si la file est vide. (0 <= i1 <= t-1).
i2 : indice du dernier élément de la file, ou -1 si la file est vide. (0 <= i2 <= t-1).
t : Taille de l'espace mémoire réservée.

Exemple de file de taille 9 contenant les valeurs 2,3,5 et 7. L'entré de la file est o[i1] et la sortie de la file est o[i2] :

Objet difficilement contournable, la file de mots représente un flux de donnée.

La file se décompose en deux blocs disjoints, l'un non relocalisable, l'autre relocalisable qui contient les données. Le bloc non relocalisable comprend les quatres valeurs t, o, i1, i2, désignant respectivement la taille mémoire du bloc relocalisable, son adresse, l'indice du premier mot de la file ou -1 si la file est de taille zéro, l'indice du dernier mot de la file ou -1 si la file est vide. Dans la file, l'élément suivant o[t-1], s'il existe, est par définition o[0].

Les méthodes fondamentales de l'objet file sont décrite dans la synopsis suivante :

Instruction
Déscription
B.EmPile1(x); Empile x dans la file B par la gauche.
x=B.Defile1(); Dépile de la file B par la gauche une valeur et le met dans x.
B.EmPile2(x); Empile x dans la file B par la droite.
x=B.Defile2(); Dépile de la file B par la droite une valeur et le met dans x.

L'objet file contient les fonctionalitées de l'objet pile. L'objet pile reste néanmoins utile puisque plus simple et plus efficace.

Si on implémente un mécanisme de defragmentation du tas, il faut prévoir un pointeur supplémentaire dans le bloc relocalisable qui pointe vers la case mémoire de la variable o.

8.4) Land

Le land se présente un peu comme une pile de blocs. Mais il utilise en plus un pointeur p de début de zone libre, ainsi qu'un filage des zones libres, c'est à dire que chaque bloc libre de la pile contient l'adresse d'un bloc libre suivant. Car à la différence des piles, les blocs, ne sont pas empilés consécutivement à partir du début, mais se libère petit à petit sans changer de place, formant un gruyère. Le land étant un gruyère, on peut facilement réunir plusieurs lands non contigüs, et former une pile de pointeur de lands, l'ensemble gérant l'allocation mémoire de bloc de taille fixe de la même façon qu'un unique land. L'ensemble est encore appelé un land.

Les méthodes fondamentales de l'objet land sont décrite dans la synopsis suivante :

Instruction
Déscription
p=C.Malloc(); Alloue dans le land C un bloc et retourne son adresse dans p.
C.Free(p); Libère dans C le bloc d'adresse p.
C.Defragmente(); Defragmente le Land.

Le Land reprogramme des fonctions propres au système d'exploitation gérant la mémoire, et permet d'aborder des questions fondamentales sur la manipulation des données. Une données est contenue dans une mémoire qu'il faut allouer soit de façon statique à l'initialisation du programme, soit de façon dynamique en utilisant un Land.

void Land::Synopsis(){
int nL = 10;
int nP = 10;
int t = 5;

Land K(t);          // Creer le Land K pour des bloc de t mots (K.t0=t).
K.AddLand(nL);      // Ajoute un bloc supplémentaire de nL éléments de taille K.t0 et
                    //  le lie a la pile de Land K.r en mettant dans sa dernière brique
                    //  l'adresse de la première brique libre de la pile de land, et
                    //  initialise le pointeur K.p sur sa première brique, Et si la pile
                    //  de Land a besoin d'être agrandie, elle l'augmente de 10 places

mot *p = K.New(nL); // Alloue un bloc de taille K.t0 mots dans le Land K, et si il n'y a
                    //  plus de place, alors ajoute un Land de nL blocs de taille K.t0
K.Delete(p);        // Libère le bloc d'adresse p dans le Land K.
K.Defragmente();    // Défragmente le Land et supprime les lands vides.
Land::Q;            // Quantité de mémoire utilisé par les Lands.
Land::Qi;           // Quantité de mémoire utilisé dans les lands.
}

 

8.5) Mem

Pour des raisons d'efficacités, nous n'utilisons pas les operateurs systèmes malloc et free d'allocation de mémoire de taille variable, car trop coûteuse en temps. Nous réservons de larges espaces mémoires au travers des Land dans lesquels nous gèrons l'allocation et la libération de blocs de taille déterminée. L'ensemble de ces procédures est regroupées dans la classe Mem. La vitesse de la gestion mémoire est 2 fois plus rapides en utilisant les Land.

La gestion de la mémoire est globale au programme, c'est pourquoi la classe Mem ne contient que des membres statiques et methodes statiques et ne crée aucun objet.

Il faut choisire une échelle pour la quantification des taille des blocs, car nous ne pouvons pas créer des Lands pour toutes les tailles possibles sans encombrer la mémoire. On choisie de considérer des tailles de blocs quelconques inférieur à N et une grande taille G et une très grande taille TG. Nous pourrions considérer d'autre type de quantification tel que par exemple des tailles égales à des puissances de 2.

Les méthodes fondamentales de la classe Mem sont décrites dans la synopsis suivante :

Instruction
Déscription
p = Mem::Malloc(t) Alloue un espace de taille >= t et retourne l'adresse de cette espace dans p, et ajuste t selon la quantification.
Mem::Free(p,t) Libère un espace de taille t pointé par p.
Mem::Defragmente(); Defragmente tous les lands.

Les type Pile et File sont reprogrammés pour utiliser la gestion de la mémoire faite par la classe Mem : On remplace simplement chaque appel malloc et free par des appels Mem::Malloc et Mem::Free.

void Mem::Synopsis(){
int t=1;
mot *p;

Mem::InitMem();    // Alloue et initialise les Lands K[1],K[2],..,K[N-1],K1,K2
                   //  Doit être executé une seul fois au début du programme
.
p = Mem::Malloc(t);// Alloue un bloc de taille au moins égale à t et retourne un
                   //  pointeur p, et retourne la valeur de sa nouvelle taille t
.
Mem::Free(p,t);    // Libère le bloc de taille t et d'adresse p
Mem::Defragmente();// Defragmente touts les lands.
Mem::TermineMem(); // Libère les Lands. Doit être executé une seul fois
                   //  au début du programme
.
Mem::N;    // Nombre constant de Pile de Land.
Mem::G;    // Taille constante des grands espace mémoire en nombre de int.
Mem::TG;   // Taille constante des trés grands espace mémoire en nombre de int.
Mem::K[t]; // Land pour les blocs de taille t (0<t<=N).
Mem::K1;   // Land pour les blocs de taille G.
Mem::K2;   // Land pour les blocs de taille TG.
}

 

9) Les couches logicielles

Le principe de cette subdivision est que chaque couche logicielle n'utilise que des types et méthodes déclarés à son niveau ou aux niveaux des couches inférieurs.

Nous regroupons dans le niveau 0, le module Commun.h contenant les fonction d'int, et le module Err.h contenant les procédures d'affichage des message d'erreur. Nous regroupons dans le niveau 1, la classe Land et la classe Mem qui assurent la gestion optimisée de la mémoire. Nous regroupons dans le niveau 2, la classe Ens32, la classe Pile et la classe File. Les types se présentent donc dans cette ordre :

Niveau 0

Niveau 1

Niveau 2

10) Les fondements du developpement informatique

Notre approche est trop lié au langage C++ et ne nous permet pas d'entrevoir les choix fondamentaux. Il faut donc à la fois s'imprégner des outils, qui sont mis à notre disposition pour nos constructions, et ouvrir une discution sur une base plus large que ces simples outils pour poser et résoudre des questions d'avantages fondamentales.

10.1) Le livre de bord

Tout développement important est controlé par des processus auto-adaptatifs. Sans ces processus, le développement est mort né à 99 chance pour 100. Le processus génératif inné est insuffisant pour exister, il doit entrer en symbiose avec le milieu ou il se développe par le biais de processus auto-adaptatif qui constitue en fait l'essentiel du travail de developpement. L'apprentissage qui découle de ce travail, et qui n'est qu'en partie transmissible, constitue l'acquit fétal en opposition à l'information innée.

Nous transcrivons dans un livre de bord (le présent ouvrage) les choix qui sont fait et les raisons de leur opportunités. Ce livre décrit non seulement les étapes de la construction introduisant un sens chronologique, une construction de normes successives, de concepts..., mais ouvre un dialogue sur l'opportunité de chacune de ces constructions et concepts. Il faut donc être à la fois dans le milieu pour construire avec les éléments du milieu, et être à l'exterieur du milieu pour acquerir un sense critique libre.

La confrontation entre le processus génératif inné (l'idée première), et le milieu (sa mise en oeuvre) se resoud au cours du développement et de son experimentation dans un rapport dialectique, une sorte de dialogue permanent entre la théorie et la réalité, entre l'intuition subjective et la création objective. Les concept qu'elle dévoile ordonne à de nouveaux buts. Ce dialogue est transcrit dans le livre de bord (le présent ouvrage) en analogie à la navigation, et permet d'adapter les buts aux possibilités qui apparaissent au cours du développement.

Comme pour toute construction, les fondations doivent être assurées. Selon le principe philosophique de concordance, le caractère véridique d'un calcul est renforcé lorsqu'il est le résultat concordant de plusieurs calculs basés sur des méthodes distinctes. Une preuve de bas niveau peut être ainsi obtenue pour chaque procédure en appliquant cette règle méthodologique. On éxecutera cette vérification lors d'une phase initiale du programme appellée phase de tests.

10.2) La phase de test

La seule façon de vérifier s'il n'y a pas d'erreur de programmation, est d'effectuer le même calcul par des voies différentes, où d'une façon plus légère, de vérifier le respect d'une propriété intrinsèque, comme la preuve par 9 pour vérifier une multiplication.Une tel vérification legère peut être faite avant l'éxecution du programme, au niveau du code source, et consiste à vérifier par exemple l'homogénéité des instructions relative au typage des variables, de la même façons que l'on vérifit l'homogénéité d'une formule physique relative aux unités des variables et constantes. Mais ce genre de vérification qui est déja en partie faite par le compilateur ne permet pas de construire une preuve qui soit à un niveau de probabilité arbitraire, et cela, d'autant moins que le calcul en question est d'avantage interopérables. C'est pourquoi nous privilègions l'établissement d'une preuve à l'éxecution, basé sur le respect de propriétés intrinseques.

A chaque démarrage de l'application, un test est effectué en prenant la date système comme graine du générateur de nombre aléatoire. Le test procéde statistiquement sur des données définies aux hasards. Les procédures sont testées par d'autres procédures soeurs dites de teste, séparant le code source en deux parties, la procédure et sa procédure de test effectuant un même calcul par des voies différentes, ou effectuant un calcule d'une propriétés intrinsèque devant être satisfaites.

Toute évolution d'une procédure sera automatiquement vérifiée par sa procédure test qui, elle, n'évoluent pas a priori. Lorsque les résultats diffères ou qu'une propriété intrinsèque n'est pas satisfaite, alors un message d'erreur est transmis ainsi que la graine génératrice des nombres aléatoires, ce qui permet de répéter l'erreur et de vérifier qu'il ne s'agit pas d'une erreur occasionnée par d'autre cause que le programme.

Si on change le compilateur ou le système d'exploitation. La présence des procédures tests permet de vérifier et de trouver rapidement les modifications de code à apporter pour corriger les éventuels défauts de portabilité du langage C++.

  1. A Chaque méthode Action( ) d'une classe, est associée une méthode TestAction( ) dans la même classe qui effectue le même calculs mais par des voie différentes, ou qui effectue un calcul d'une propriété intrinsèque devant être satisfaite.
  2. La méthode Test(n) teste n fois toutes les méthodes de la classe.
  3. Chaque module M comprend un fichier entête M.h et deux fichiers corps M.cpp et Test_M.cpp. Ce dernier regroupant toutes les methodes testes.

10.3) La documentation et le langage

Pour savoir quel doit être la mise en forme d'une documentation idéale, il convient d'en produire une. Le but d'une telle documentation et de transmettre une compétence, de permettre à une personne sans connaissance préalable de trouver facilement et rapidement une commande nécessaire pour résoudre une étape de programmation. La synopsis des fonctions basée sur les définitions est utiles pour vérifier une syntaxe précise, et le compilateur s'en charge très bien, mais n'est pas adaptée pour le développement. Les déclarations de procédure dans les entêtes qui jouent ce rôle synoptique sont en effet beaucoup moins lisibles que les exemples simples. Les exemples ont l'avantage d'être moteurs par rapport aux définitions qui elles, sont par essence inertes. Il convient donc de la remplacer par une nouvelle sorte de synopsis basée sur les exemples. Les exemples utilisent une notation implicite des variables selon leur type et leur sens, à la façon dont les physiciens notent les variables dans les formules. La formule résume la connaissance physique en sa plus brève expression. Bien qu'il n'y ai pas d'obligation formelle, les noms des variables ne sont pas pris au hasard, ils dépendent de leur type et de leur sens physique. A un moment donné, l'ensemble des règles implicites qui président à cette dénomination des variables est appelé environnement ou contexte.

Cette question du contexte est une problématique générale qui ne s'applique pas qu'au seul cas de la documentation synthétique, mais s'applique à toute tentative de définir un langage évolué. Nous ne pouvons pas noyer l'information haute dans le détail sans neutraliser notre capacité de raisonnement, d'où la nécessité d'informations implicites détenues dans un contexte.

Les exemples commentés ont trois avantages : Ils permettent d'utiliser une notation courte et intuitive des variables selon leurs types et leurs sens créant implicitement une nomenclature mémotechnique, ils respectent exactement la syntaxe de programmation de telle sorte qu'un simple copier-coller suivi du remplacement des variables permet d'ajouter l'appel de la procédure dans un programme en cours de développement, et ils constitueront la base d'une aide structurée compilable par un outil analogue à javadoc produisant une documentation structurée, au format html. Il est pratique d'intégrer cette synopsis au sein d'une procédure pour bénéficier de la vérification syntaxique du compilateur.

  1. Synopsis : Chaque classe Lambda contient une méthode Synopsis( ) mis dans un fichier à part dénommé Synopsis_Lambda.cpp, qui n'à pas vocation à être exécutée mais simplement à être compilée pour vérifier la syntaxe des exemples, à être imprimée pour donner un éventail d'action possible au développeur, et à être compilée en une documentation au format html. Elle contient des exemples et des commentaires comprenant une description de la classe, une description des variables et types utilisées, une description des exemples, et des liens....
  2. Les trois taches de construction d'une méthode Action(…) :
    Conception de la preuve : Trouver un calcule qui valide le résultat de Action(…) utilisant d'autres méthodes que celle utilisée dans Action(…), et à partir de paramètres aléatoires. La méthode ainsi construite s'appellera TestAction() et appartiendra à la même classe. Son code n'est pas optimisé. Un appel répétitif n fois de cette méthode sera ajouté dans la methode Test(...).
    Conception de l'interface : Optimiser le code de Action(…).
    Inscription aux livres de bord : Répertorier la méthode Action(…) dans la methode Synopsis( ) avec sa documentation.

 


Dominique Mabboux-Stromberg
mabboux@wanadoo.fr

Code source du projet : (sous licence libre GNU)

Version 0.1
Les sources : src.zip

Module principale :

MC.cpp

Les autres modules :

Entête
Corps
Teste
Synopsis
Niveau
Commun.h     Synopsis_Commun.cpp
0
Err.h Err.cpp   Synopsis_Err.cpp
0
Land.h Land.cpp Test_Land.cpp Synopsis_Land.cpp
1
Mem.h Mem.cpp Test_Mem.cpp Synopsis_Mem.cpp
1
Ens32.h Ens32.cpp Test_Ens32.cpp Synopsis_Ens32.cpp
2
Pile.h Pile.cpp Test_Pile.cpp Synopsis_Pile.cpp
2