Section courante

A propos

Section administrative du site

Les premiers pas

Le B est un langage informatique conçu et implémenté à Murray Hill en 1969. Il a fonctionné et a été activement soutenu et documenté sur le système TSS H6070 de Murray Hill.

Le B est particulièrement adapté aux calculs non numériques, typifiés par la programmation système. Ceux-ci impliquent généralement de nombreuses décisions logiques complexes, des calculs sur des entiers et des champs de mots, en particulier des caractères et des chaînes de bits, et aucun point flottant. Les programmes B pour de telles opérations sont sensiblement plus faciles à écrire et à comprendre que les programmes GMAP. Le code généré est assez bon. L'implémentation de sous-systèmes TSS simples est une utilisation particulièrement intéressante de B.

B rappelle BCPL, pour ceux qui s'en souviennent. La conception et l'implémentation originales sont l'oeuvre de K. L. Thompson et D. M. Ritchie ; leur version 6070 originale a été considérablement améliorée par S. C. Johnson, ayant également écrit la bibliothèque d'exécution.

Il est supposer que l'utilisateur est familier avec les mystères de TSS, tels que la création de fichiers, l'édition de texte,..., et qu'il a déjà programmé dans un langage quelconque.

Présentation générale des programmes

main( ) {
-- instructions --
}

nouvfonc(arg1, arg2) {
-- instructions --
}

fon3(arg) {
-- plus d'instructions --
}

Tous les programmes B sont constitués d'une ou plusieurs «fonctions», étant similaires aux fonctions et sous-routines d'un programme Fortran, ou aux procédures de PL/I. main est une telle fonction, et en fait tous les programmes B doivent avoir une fonction main. L'exécution du programme commence à la première instruction de main, et se termine généralement à la dernière. main invoquera généralement d'autres fonctions pour effectuer son travail, certaines provenant du même programme, et d'autres des bibliothèques. Comme en Fortran, une méthode de communication de données entre fonctions est par des paramètres. Les parenthèses suivant le nom de la fonction entourent la liste des paramètres ; ici main est une fonction sans paramètre, indiquée par (). Les {} entourent les instructions de la fonction.

Les fonctions sont totalement indépendantes en ce qui concerne le compilateur, et main n'a pas besoin d'être la première, bien que l'exécution commence par elle. Ce programme a deux autres fonctions : nouvfonc a deux paramètres, et fon3 en a un. Chaque fonction est constituée d'une ou plusieurs instructions exprimant des opérations sur des variables et le transfert de contrôle. Les fonctions peuvent être utilisées de manière récursive à peu de frais en temps ou en espace.

La plupart des instructions contiennent des expressions, étant à leur tour constituées d'opérateurs, de noms et de constantes, avec des parenthèses pour modifier l'ordre d'évaluation. B possède un ensemble particulièrement riche d'opérateurs et d'expressions, et relativement peu de types d'instructions.

Le format des programmes B est assez libre ; ils se sont efforcer d'avoir un style uniforme comme bonne pratique de programmation. Les instructions peuvent être divisées en plusieurs lignes à tout moment raisonnable, c'est-à-dire entre les noms, les opérateurs et les constantes. Inversement, plusieurs instructions peuvent apparaître sur une ligne. Les instructions sont séparées par des points-virgules. Un groupe d'instructions peut être regroupé et traité comme une seule instruction en les enfermant dans {} ; dans ce cas, aucun point-virgule n'est utilisé après le '}'. Cette convention est utilisée pour regrouper les instructions d'une fonction.

Compilation et exécution de programmes B

(a) et (b) ne doivent être effectués qu'une seule fois ; ils créent un espace de fichier pour le programme source et le programme chargé. (d) compile le programme source à partir de bsource et, après de nombreuses manipulations, crée un programme dans bhs. (e) exécute ce programme. ./bj est décrit plus en détail dans [1].

Vous pouvez de temps en temps recevoir des messages d'erreur des compilateurs B. Ceux-ci ressemblent à l'un des suivants :

ee n
ee name n

ee est un message d'erreur à deux caractères et n est le numéro approximatif de la ligne source de l'erreur.

Un programme B fonctionnel ; variables

  1. main( ) {
  2.   auto a, b, c, sum;
  3.  
  4.   a = 1; b = 2; c = 3;
  5.   sum = a+b+c;
  6.   putnumb(sum);
  7. }

Ce programme simple additionne trois nombres et affiche la somme. Il illustre l'utilisation de variables et de constantes, de déclarations, d'un peu d'arithmétique et d'un appel de fonction. Discutons-en dans l'ordre inverse.

putnumb est une fonction de bibliothèque à un paramètre, affichant un nombre sur le terminal (à moins qu'une autre destination ne soit spécifiée). Notez qu'une fonction est appelée en la nommant, suivie du ou des paramètres entre parenthèses. Il n'y a pas d'instruction CALL comme en Fortran.

Les instructions arithmétiques et d'affectation sont à peu près les mêmes qu'en Fortran, à l'exception des points-virgules. Notez que nous pouvons en placer plusieurs sur une ligne si nous le souhaitons. Inversement, nous pouvons diviser une instruction en plusieurs lignes si cela semble souhaitable - la division peut se faire entre l'un des opérateurs ou des variables, mais pas au milieu d'un nom de variable.

Une différence majeure entre B et Fortran est que B est un langage sans type : il n'y a pas d'analogue en B de la convention Fortran IJKLMN. Ainsi, a, b, c et sum sont toutes des quantités de 36 bits, et les opérations arithmétiques sont des entiers.

Les noms de variables ont de un à huit caractères ASCII, choisis parmi A à Z, a à z, ., _, 0 à 9, et commencent par un non-chiffre. Stylistiquement, il est bien mieux d'utiliser une seule casse (majuscule ou minuscule) et de donner aux fonctions et aux variables externes des noms étant uniques dans les six premiers caractères. (Les noms de fonctions et de variables externes sont utilisés par le GMAP par lots, étant limité à des identificateurs à casse unique de six caractères.)

L'instruction "auto ..." est une déclaration, c'est-à-dire qu'elle définit les variables à utiliser dans la fonction. auto en particulier est utilisé pour déclarer des variables locales, des variables n'existant que dans leur propre fonction (main dans ce cas). Les variables peuvent être réparties arbitrairement entre les déclarations automatiques, mais toutes les déclarations doivent précéder les instructions exécutables. Toutes les variables doivent être déclarées, soit comme auto, extrn, soit implicitement comme paramètres de fonction.

Les variables automatiques diffèrent des variables locales Fortran sur un point important : elles apparaissent lorsque la fonction est saisie et disparaissent lorsqu'elle est quittée. Elles ne peuvent pas être initialisées au moment de la compilation et elles n'ont pas de mémoire d'un appel à l'autre.

Arithmétique ; nombres octaux

Toutes les opérations arithmétiques en B sont des nombres entiers, à moins que des fonctions spéciales ne soient écrites. Il n'existe pas d'équivalent de la convention Fortran IJKLMN, pas de virgule flottante, pas de types de données, pas de conversions de type et pas de vérification de type. Les utilisateurs de nombres complexes à double précision devront se débrouiller seuls.

Les opérateurs arithmétiques sont les habituels '+', '-', '*' et '/' (division entière). De plus, on a l'opérateur de reste '%' :

  1. x = a%b

définit x comme le reste après que a est divisé par b (les deux doivent être positifs).

Étant donné que B est souvent utilisé pour la programmation système et la manipulation de bits, les nombres octaux sont une partie importante du langage. La syntaxe de B dit que tout nombre commençant par 0 est un nombre octal (et ne peut donc pas contenir de 8 ou de 9). Ainsi, 0777 est une constante octale, avec une valeur décimale de 511.

Caractères; putchar; Nouvelle ligne

  1. main( ) {
  2.   auto a;
  3.   a= 'allo';
  4.   putchar(a);
  5.   putchar('*n' );
  6. }

Ce programme affiche "allo" sur le terminal. Il illustre l'utilisation de constantes et de variables de type caractère. Un "caractère" est un à quatre caractères ASCII, entourés de guillemets simples. Les caractères sont entreposés dans un seul mot machine, justifié à droite et rempli de zéros. Nous avons utilisé une variable pour contenir "allo", mais nous aurions également pu utiliser directement une constante.

La séquence "*n" est le jargon B pour "caractère de nouvelle ligne", qui, une fois imprimé, fait passer le terminal au début de la ligne suivante. Aucune sortie ne se produit jusqu'à ce que putchar rencontre un "*n" ou que le programme se termine. L'omission du "*n", une erreur courante, fait apparaître toute la sortie sur une seule ligne, ce qui n'est généralement pas le résultat souhaité. Il existe plusieurs autres échappements comme "*n" pour représenter des caractères difficiles à obtenir. Notez que "*n" est un seul caractère : putchar('Bon*n') affiche quatre caractères (le maximum avec un seul appel).

Comme B est un langage sans type, l'arithmétique sur les caractères est tout à fait légale, et a même parfois du sens :

  1. c = c+'A'-'a';

convertit un seul caractère entreposé en c en majuscule (en utilisant le fait que les lettres ASCII correspondantes sont à une distance fixe les unes des autres).

Variables externes

  1. main( ) {
  2.  extrn a, b, c;
  3.  putchar(a); putchar(b); putchar(c); putchar('!*n');
  4. }
  5.  
  6. a 'bonj';
  7. b 'our!';
  8. c ' ! !';

Cet exemple illustre les variables externes, variables ressemblant un peu à Fortran COMMON, en ce sens qu'elles existent en dehors de toutes les fonctions et sont (potentiellement) disponibles pour toutes les fonctions. Toute fonction qui souhaite accéder à une variable externe doit contenir une déclaration extrn pour celle-ci. De plus, nous devons définir toutes les variables externes en dehors de toute fonction%. Pour nos variables d'exemple a, b et c, cela se fait dans les trois dernières lignes.

L'entreposage externe est statique et reste toujours existant, il peut donc être initialisé. (Inversement, l'entreposage automatique apparaît chaque fois que la fonction est invoquée et disparaît lorsque la fonction est quittée. Les variables auto du même nom dans différentes fonctions ne sont pas liées.) L'initialisation se fait comme indiqué : la valeur initiale apparaît après le nom, sous forme de constante. Nous avons montré des constantes de type caractère ; d'autres formes sont également possibles. Les variables externes sont initialisées à zéro si elles ne sont pas définies explicitement.

Les fonctions

  1. main( ) {
  2.   extrn a,b,c,d;
  3.   put2char(a,b) ;
  4.   put2char(c,d) ;
  5. }
  6.  
  7. put2char(x,y) {
  8.   putchar(x);
  9.   putchar(y);
  10. }
  11.  
  12. a 'bonj'; b 'our!'; c ' ! !'; d '!*n';

Cet exemple fait la même chose que le précédent, mais utilise une fonction séparée (et assez artificielle) de deux paramètres, put2char. Les noms factices x et y font référence aux paramètres dans putchar ; ils ne sont pas déclarés dans une instruction auto. Nous aurions tout aussi bien pu créer des variables automatiques a, b, c, d, ou appeler put2char comme :

  1. put2char( 'bonj','our!');
  2.   /* ... */

L'exécution de put2char se termine lorsqu'elle rencontre le '}' de fermeture, comme dans main ; le contrôle revient à l'instruction suivante du programme appelant.

Entrée; getchar

  1. main( ) {
  2.   auto c;
  3.   c= getchar();
  4.   putchar(c);
  5. }

getchar est une autre fonction d'entrée/sortie de la bibliothèque. Elle récupère un caractère de la ligne d'entrée à chaque fois qu'elle est appelée et renvoie ce caractère, ajusté à droite et rempli de zéros, comme valeur de la fonction. L'implémentation réelle de getchar consiste à lire une ligne entière, puis à distribuer les caractères un par un. Une fois que le '*n' à la fin de la ligne a été renvoyé, le prochain appel à getchar entraînera la lecture d'une nouvelle ligne entière et le renvoi de son premier caractère. L'entrée se fait généralement depuis le terminal.

Opérateurs relationnels

Opérateur Description
== égal à (".EQ." pour les programmeurs de Fortran)
!= Pas égal à
> Plus grand que
< Moins que
>= Supérieur ou égal à
<= Inférieur ou égal à

Ce sont les opérateurs relationnels ; nous utiliserons '!=' ("différent de) dans l'exemple suivant. N'oubliez pas que le test d'égalité est '==' ; l'utilisation d'un seul '=' conduit à un désastre d'une sorte ou d'une autre. Remarque : toutes les comparaisons sont arithmétiques et non logiques.

if; goto; Étiquettes; Commentaires

  1. main( ) {
  2.   auto c;
  3. read:
  4.   c= getchar();
  5.   putchar(c);
  6.   if(c != '*n') goto read;
  7. }               /* boucle si ce n'est pas une nouvelle ligne */

Cette fonction lit et écrit une ligne. Elle illustre plusieurs nouveautés. La première et la plus simple est le commentaire, étant tout ce qui est inclus dans /*...*/, comme dans PL/I.

read est un étiquette - un nom suivi de deux points ':', également comme dans PL/I. Une instruction peut avoir plusieurs étiquettes si on le souhaite. (Les bonnes pratiques de programmation dictent l'utilisation de peu d'étiquettes, donc les exemples ultérieurs s'efforceront de s'en débarrasser.) Le goto fait référence à une étiquette dans la même fonction.

L'instruction if est l'une des quatre conditions de B. (while, switch et "?:" sont les autres.) Son format générale est :

if(expression) statement

La condition à tester est toute expression entre parenthèses. Elle est suivie d'une instruction. L'expression est évaluée et si sa valeur est différente de zéro, l'instruction est exécutée. L'un des avantages de B est que l'instruction peut être rendue arbitrairement complexe (même si nous en avons utilisé des très simples ici) en enfermant des instructions simples entre {}.

Instructions composés

  1. if(a<b) {
  2.  t = a;
  3.  a = b;
  4.  b = t;
  5. }

Prenons un exemple simple d'instructions composées : supposons que nous voulions nous assurer que a dépasse b, dans le cadre d'une routine de tri. L'échange de a et b nécessite trois instructions dans B ; elles sont regroupées par {} dans l'exemple ci-dessus.

En règle générale, dans B, partout où vous pouvez utiliser une instruction simple, vous pouvez utiliser n'importe quelle instruction composée, n'étant qu'un certain nombre d'instructions simples ou composées entourées de {}. Les instructions composées ne sont pas suivies de points-virgules.

La possibilité de remplacer à volonté des instructions simples par des instructions complexes est l'une des choses rendant B beaucoup plus agréable à utiliser que Fortran. La logique nécessitant plusieurs GOTO et étiquettes en Fortran peut être réalisée de manière simple, claire et naturelle en utilisant les instructions composées de B.

Imbrication de fonctions

  1. read:
  2.  c = putchar(getchar());
  3.  if (c != '*n') goto read;

Les fonctions renvoyant des valeurs (comme toutes le font) peuvent être utilisées partout où vous utiliseriez normalement une expression. Ainsi, nous pouvons utiliser getchar comme paramètre de putchar, en imbriquant les fonctions. getchar renvoie exactement le caractère dont putchar a besoin comme paramètre. putchar renvoie également son paramètre comme valeur, nous l'assignons donc à c pour référence ultérieure.

Encore mieux, si c n'est pas nécessaire plus tard, nous pouvons dire :

  1. read:
  2.  if (putchar(getchar()) != '*n') goto read;    

Instruction while; Affectation dans une expression

  1. while ((c=getchar()) != '*n') putchar(c);

Éviter les goto, comme l'exemple le précédent. La différence ? Celui-ci n'affiche pas le '*n' de fin.) L'instruction while est fondamentalement une instruction de boucle, dont le format générale est :

while (expression) statement

Comme dans l'instruction if, l'expression et l'instruction peuvent toutes deux être arbitrairement compliquées, bien que nous ne l'ayons pas encore vu. L'action de while est :

Notre exemple récupère le caractère et l'affecte à c, puis teste pour voir s'il s'agit d'un '*n'. Si ce n'est pas le cas, il exécute la partie instruction du while, un putchar, puis répète.

Notez que nous avons utilisé une instruction d'affectation "c=getchar()'" dans une expression. Il s'agit d'un raccourci de notation pratique, produisant généralement un meilleur code et étant parfois nécessaire si une instruction doit être compressée. Cela fonctionne car une instruction d'affectation a une valeur, tout comme toute autre expression. Cela implique également que nous pouvons utiliser avec succès plusieurs affectations comme :

  1. x = y = f(a)+g(b)

En passant, la paire de parenthèses supplémentaire dans l'instruction d'affectation au sein du conditionnel était vraiment nécessaire : si nous avions dit :

  1. c = getchar() != '*n'

c serait défini sur 0 ou 1 selon que le caractère récupéré était une nouvelle ligne ou non. Cela est dû au fait que la force de liaison de l'opérateur d'affectation '=' est inférieure à celle de l'opérateur relationnel '!='.

Plus d'informations sur les While ; Instruction Null ; exit

  1. main( ) {
  2.  auto c;
  3.  while (1) {
  4.   while ( (c=getchar()) != ' ')
  5.    if (putchar(c) == '*n') exit();
  6.   putchar( '*n' );
  7.   while ( (c=getchar()) == ' '); /* sauter les blancs */
  8.   if (putchar(c)=='*n') exit(); /* terminé lorsque la nouvelle ligne */
  9.  }
  10. }

Cet exemple concis lit une ligne du terminal et affiche chaque chaîne de caractères non vide sur une ligne distincte : un ou plusieurs espaces sont convertis en une seule nouvelle ligne.

La ligne quatre récupère un caractère et, s'il n'est pas vide, l'affiche et utilise la fonction de bibliothèque exit pour terminer l'exécution s'il s'agit d'une nouvelle ligne. Notez l'utilisation de l'affectation dans une expression pour enregistrer la valeur de c.

La ligne sept ignore plusieurs espaces. Elle récupère un caractère et, s'il est vide, exécute l'instruction ';'. C'est ce qu'on appelle une instruction null - elle ne fait vraiment rien. La ligne sept n'est qu'une boucle très serrée, avec tout le travail effectué dans la partie expression de la clause while.

La construction légèrement étrange :

  1. while(1){
  2.   /* ... */
  3. }

est un moyen d'obtenir une boucle infinie. "1" est toujours différent de zéro ; donc la partie instruction (composée ici !) sera toujours effectuée. La seule façon de sortir de la boucle est d'exécuter un goto ou un break, ou de revenir de la fonction avec une instruction return, ou de terminer l'exécution en quittant.

Clause else ; Expressions conditionnelles

if(expression) statement else statement2

est la forme la plus générale du if - la partie else est parfois utile. L'exemple canonique définit x au minimum de a et b :

  1. if (a<b) x=a; else x=b;

Les if et else peuvent être utilisés pour construire une logique se ramifiant de plusieurs manières, puis rejoint une structure de programmation commune, de cette manière :

if(...)
{ .....
  ..... }
else if(...)
{ .....
  ..... }
else if(....)
{ .....
  ..... }

Les conditions sont testées dans l'ordre, et un seul bloc est exécuté - le premier dont le if est satisfait. Lorsque ce bloc est terminé, l'instruction suivante exécutée est celle qui suit le dernier bloc "else if".

B fournit une forme alternative de conditionnelle étant souvent plus concise et efficace. Elle est appelée "expression conditionnelle" car c'est une conditionnelle ayant réellement une valeur et pouvant être utilisée partout où une expression peut l'être. Le dernier exemple peut être mieux exprimé ainsi :

  1. x = a<b ? a : b;    

La signification est la suivante : évaluer l'expression à gauche de «?». Si elle est vraie, évaluer l'expression entre «?» et «:» et attribuer cette valeur à x. Si elle est fausse, évaluer l'expression à droite de « : » et attribuer sa valeur à x.

Opérateurs unaires

  1. auto k;
  2. k = 0;
  3. while (getchar() != '*n') ++k;

B possède plusieurs opérateurs unaires intéressants en plus du traditionnel '-'. Le programme ci-dessus utilise l'opérateur d'incrémentation '++' pour compter le nombre de caractères dans une ligne d'entrée. "++k" est équivalent à "k=k+1" mais génère un meilleur code. '++' peut également être utilisé après une variable, comme dans :

  1. k++

La seule différence est la suivante : supposons que k soit 5. Alors :

  1. x = ++k

fixe k à 6 puis fixe x à k, c'est-à-dire à 6. Mais :

  1. x = k++

définit x sur 5, puis k sur 6.

De même, l'opérateur de décrémentation unaire '--' peut être utilisé avant ou après une variable pour la décrémenter de un.

Opérateurs de bits

  1. c = o;
  2. i = 36;
  3. while ((i = i-9) >= 0) c = c | getchar()<<i;

B possède plusieurs opérateurs pour la manipulation logique des bits. L'exemple ci-dessus en illustre deux, le OU '|' et le décalage à gauche <<. Ce code regroupe les quatre caractères de 9 bits suivants de l'entrée dans le mot unique de 36 bits c, en décalant successivement le caractère vers la gauche de i positions de bits jusqu'à la position correcte et en l'intégrant dans le mot par OU.

Les autres opérateurs logiques sont ET '&', le décalage logique à droite '>>', le OU exclusif '^' et le complément à 1 '~'.

  1. x = ~y&0777

définit x sur les 9 derniers bits du complément à 1 de y, et :

  1. x = x&077

remplace x par ses six derniers bits.

L'ordre d'évaluation des expressions non parenthésées est déterminé par les forces de liaison relatives des opérateurs impliqués.

Opérateurs d'affectation

Une caractéristique inhabituelle de B est que les opérateurs binaires normaux comme «+», «-», ... peuvent être combinés avec l'opérateur d'affectation = pour former de nouveaux opérateurs d'affectation. Par exemple :

  1. x =- 10

signifie «x=x-10». '=-' est un opérateur d'affectation. Cette convention est un raccourci de notation utile et permet généralement à B de générer un meilleur code. Mais les espaces autour de l'opérateur sont essentiels ! Par exemple :

  1. x = -10

définit x à -10, tandis que :

  1. x =- 10

soustrait 10 de x. Lorsqu'il n'y a pas d'espace :

  1. x=-10

diminue également x de 10. Ceci est tout à fait contraire à l'expérience de la plupart des programmeurs.

Étant donné que les opérateurs d'affectation ont une force de liaison inférieure à celle de tout autre opérateur, l'ordre d'évaluation doit être surveillé attentivement :

  1. x = x<<y|z

signifie « décaler x vers la gauche de y places, puis OU en z, et entreposer en x ». Mais :

  1. x =<< y|z

signifie «décaler x vers la gauche de y|z positions», ce qui est assez différent.

L'exemple de compression de caractères peut maintenant être mieux écrit comme :

  1. c= 0;
  2. i = 36;
  3. while ((i =- 9) >= 0) c =| getchar()<<i;

Vecteurs

Un vecteur est un ensemble de mots consécutifs, un peu comme un tableau unidimensionnel en Fortran. La déclaration :

  1. auto v[10];

alloue 11 mots consécutifs ? v[0], v[1], ..., v[10] pour v. La référence au i-ème élément est faite par v[i] ; les crochets [] indiquent l'indice. (Il n'y a pas d'ambiguïté de type Fortran entre les indices et l'appel de fonction.)

  1. sum = o;
  2. i = 0;
  3. while (i<=n) sum =+ V[i++];

Ce code additionne les éléments du vecteur v. Notez l'incrément dans l'indice - c'est un usage courant en B. De même, pour trouver le premier élément nul de v :

  1. i = -1;
  2. while (v[++i]);

Vecteurs externes

Les vecteurs peuvent bien sûr être des variables externes plutôt que des variables automatiques. Nous déclarons v externe dans la fonction (ne mentionnez pas de taille) en :

  1. extrn v;

et puis en dehors de toutes les fonctions écrivez :

  1. v[10];

et puis en dehors de toutes les fonctions écrivez :

  1. v[10]  'hi!', 1, 2, 3, 0777;

définit v[0] sur la constante de caractère «hi!» et V[1] à v[4] sur divers nombres. v[5] à v[10] ne sont pas initialisés.

Adressage

Pour comprendre toutes les ramifications des vecteurs, une digression dans l'adressage est nécessaire. L'implémentation réelle d'un vecteur consiste en un mot v contenant dans sa moitié droite l'adresse du 0-ème mot du vecteur ; c'est-à-dire que v est en réalité un pointeur vers le vecteur réel.

Cette implémentation peut conduire à un piège délicat pour les imprudents. Si nous disons :

  1. auto u[10], v[10];
  2. /* ... */
  3. u = v;
  4. v[0] = ...
  5. v[1] = ...
  6. /* ... */

les personnes peu méfiantes pourraient croire que u[0] et u[1] contiennent les anciennes valeurs de v[0] et v[1] ; en fait, puisque u n'est qu'un pointeur vers v[0], u fait en réalité référence au nouveau contenu. L'instruction "u=v" ne provoque aucune copie d'informations dans les éléments de u ; ils peuvent en effet être perdus car u ne pointe plus vers eux.

Le fait que les éléments vectoriels soient référencés par un pointeur vers la zéroième entrée rend l'abonnement trivial - l'adresse de v[i] est simplement v+i. De plus, des constructions comme v[i][j] sont tout à fait légales : v[i] est simplement un pointeur vers un vecteur, et v[i][j] est sa j-ième entrée. Vous devez cependant configurer votre propre entreposage.

Pour faciliter la manipulation des adresses lorsque cela semble conseillé, B fournit deux opérateurs d'adresse unaires, '*' et '&'. '&' est l'opérateur d'adresse donc &x est l'adresse de x, en supposant qu'il en ait une. '*' est l'opérateur d'indirection ; «*x» signifie «utiliser le contenu de x comme adresse».

Les opérations d'abonnement peuvent désormais être exprimées différemment :

Chaînes de caractères

Une chaîne de caractères dans B est par essence un vecteur de caractères. Elle s'écrit comme :

  1. "C'est une chaîne de caractères"

et, en représentation interne, est un vecteur avec des caractères regroupés par quatre dans un mot, justifiés à gauche et terminés par un caractère mystérieux appelé «*e» (ascii EOT). Ainsi, la chaîne de caractères :

  1. "allo"

comporte cinq caractères, dont le dernier est '*e'. Les caractères sont numérotés à partir de zéro. Notez la différence entre les chaînes de caractères et les constantes de caractères. Les chaînes de caractères sont entre guillemets doubles et contiennent zéro ou plusieurs caractères ; elles sont justifiées à gauche et terminées par un délimiteur explicite, '*e'. Les constantes de caractères sont entre guillemets simples, contiennent de un à quatre caractères, sont justifiées à droite et remplies de zéros.

Certains caractères ne peuvent être insérés dans des chaînes de caractères que par des «séquences d'échappement». Par exemple, pour insérer un guillemet double dans une chaîne de caractères, utilisez '*'', comme dans :

  1. "Il a dit : *"Allons-y.*""

Les variables externes peuvent être initialisées avec des valeurs de chaîne de caractères simplement en faisant suivre leurs noms par des chaînes de caractères, exactement comme pour les autres constantes. Ces définitions initialisent a, b et trois éléments de v :

  1. a "bonjour"; b "le monde";
  2. v[2] "c'est le moment", "pour tous les bons hommes", "venir en aide au parti";

B fournit plusieurs fonctions de bibliothèque pour manipuler des chaînes de caractères et accéder aux caractères qu'elles contiennent d'une manière relativement indépendante de la machine. Les deux plus couramment utilisées sont char et lchar. char(s,n) renvoie le n-ième caractère de s, justifié à droite et rempli de zéros. lchar(s,n,c) remplace le n-ième caractère de s par c, et renvoie c.

Étant donné que les chaînes sont des vecteurs, l'écriture de "s1=s2" ne copie pas les caractères de s2 dans s1, mais fait simplement pointer s1 vers s2. Pour copier s2 dans s1, nous pouvons utiliser char et lchar dans une fonction comme strcopy :

  1. strcopy(s1 ,s2){
  2.  auto i;
  3.  i = 0;
  4.  while (lchar(s1,i,char(s2,i)) != '*e') i++;
  5. }

Instruction return

Comment pouvons-nous faire en sorte qu'une fonction comme char renvoie une valeur à son appelant ? Voici char :

  1. char(s, n){
  2.  auto y,sh,cpos;
  3.  y = s[n/4];        /* mot contenant le n-ième caractère */
  4.  cpos = n%4;        /* position du caractère dans le mot */
  5.  sh = 27-9*cpos;    /* positions de bits à décaler */
  6.  y =  (y>>sh)&0777; /* décaler et masquer tous les bits sauf 9 */
  7.  return(y);         /* renvoyer le caractère à l'appelant */
  8. }

Un retour dans une fonction provoque un retour immédiat au programme appelant. Si le retour est suivi d'une expression entre parenthèses, cette expression est évaluée et la valeur est renvoyée à l'appelant. L'expression peut être arbitrairement complexe, bien sûr : char est en fait défini comme :

  1. char(s,n) return((s[n/4]>>(27-9*(n%4)))&0777);

Notez que char est écrit sans {}, car il peut être exprimé comme une instruction simple.

Autres fonctions de chaîne de caractères

concat(s,a1,a2,...,a10) concatène les zéro ou plusieurs chaînes de caractères en une seule grande chaîne de caractères s, et renvoie s (ce qui signifie un pointeur vers S[0]). s doit être déclaré suffisamment grand pour contenir le résultat (y compris le '*e' de fin).

getstr(s) récupère la ligne suivante entière de l'unité d'entrée dans s, en remplaçant le '*n' de fin par '*e'. En B, c'est :

  1. getstr(s){
  2.  auto c,i;
  3.  while ((c=getchar()) != '*n') lchar(s,i++,c);
  4.  lchar(s,i,'*e');
  5.  return(s);
  6. }

De même, putstr(s) place la chaîne de caractères s sur l'unité de sortie, à l'exception du «*e» final. Ainsi :

  1. auto s[20];
  2. putstr(getstr(s)); putchar('*n');

copie une ligne d'entrée vers la sortie.

Instruction switch ; break

  1. loop:
  2. switch (c=getchar()){
  3.  
  4. pr:
  5.       case 'p':  /* Affiche */
  6.       case 'P':
  7.         print();
  8.         goto loop;
  9.  
  10.       case 's':  /* Sous-routine */
  11.       case 'S':
  12.         subs() ;
  13.         goto pr;
  14.  
  15.       case 'd':  /* Efface */
  16.       case 'D':
  17.         delete();
  18.         goto loop;
  19.  
  20.       case '*n': /* Quitte dans une nouvelle-ligne */
  21.         break;
  22.  
  23.       default:
  24.         error();  /* Erreur if échec de sortie */
  25.         goto loop;
  26.  
  27. }

Ce fragment d'un programme d'édition de texte illustre l'instruction switch, étant une branche à plusieurs voies. Ici, la branche est contrôlée par un caractère lu dans c. Si c est 'p' ou 'P', la fonction print est appelée. Si c est un 's' ou 'S', subs et print sont tous deux appelés. Des actions similaires ont lieu pour d'autres valeurs. Si c ne mentionne aucune des valeurs mentionnées dans les instructions case, l'instruction default (dans ce cas, une fonction d'erreur) est invoquée.

Le format général d'un switch est :

switch (expression) statement

L'énoncé est toujours complexe et contient des énoncés dans le format suivant :

case constant:

Les constantes de notre exemple sont des caractères, mais elles pourraient aussi être des nombres. L'expression est évaluée et sa valeur comparée aux cas possibles. Si une correspondance est trouvée, l'exécution continue à ce cas. (L'exécution peut bien sûr échouer d'un cas à l'autre et peut se transférer à l'intérieur du commutateur ou à l'extérieur de celui-ci.) Notez que nous mettons plusieurs cas sur plusieurs instructions.

Si aucune correspondance n'est trouvée, l'exécution continue à l'instruction étiquetée default : si elle existe ; s'il n'y a pas de correspondance et pas de défaut, toute la partie instruction du commutateur est ignorée.

L'instruction break force un transfert immédiat vers l'instruction suivante après l'instruction switch, c'est-à-dire l'instruction après le {} qui entoure la partie instruction du commutateur. break peut également être utilisé dans les instructions while de manière analogue. break est un moyen utile d'éviter les étiquettes inutiles.

Entrée/sortie formatées

Dans cette section, nous attirons l'attention sur la fonction printf, permettant de produire des entrées/sorties formatées.

Beaucoup plus sur les entrés/sorties

Cette discussion ne traite que des choses faciles.

En gros, toutes les entrées/sorties de B sont orientées caractères et basées sur getchar et putchar. Par défaut, ces fonctions font référence au terminal et les entrées/sorties y vont à moins que des mesures explicites ne soient prises pour les détourner. La déviation est facile et systématique, donc le changement d'unité d'entrées/sorties est tout à fait réalisable.

À tout moment, il y a une unité d'entrée et une unité de sortie ; getchar et putchar les utilisent implicitement pour leur entrée et leur sortie. L'unité est un nombre compris entre -1 et 10 (un peu comme les numéros de fichier Fortran). Les valeurs actuelles sont stockées dans les variables externes rd.unit et wr.unit afin qu'elles puissent être modifiées facilement, bien que la plupart des programmes n'aient pas besoin de les utiliser directement. Lorsque l'exécution d'un programme B commence, rd.unit est 0, ce qui signifie « lire depuis le terminal », et wr.unit est -1, ce qui signifie «écrire sur le terminal».

Pour effectuer une entrée à partir d'un fichier, nous définissons rd.unit sur une valeur et attribuons un nom de fichier à cette unité. Ensuite, jusqu'à ce que l'affectation ou la valeur soit modifiée, getchar lit ses caractères à partir de ce fichier. Ainsi, pour ouvrir en lecture un fichier permanent ASCII appelé "cat/file", comme unité d'entrée 5 :

  1. openr(5, "cat/file")

Le premier paramètre est le numéro de l'unité et le second est une description de cat/file. (Une chaîne de caractères vide signifie le terminal.) rd.unit est défini à 5 par openr, donc les appels successifs à getchar, getstr,..., récupéreront les caractères de ce fichier. Si le fichier est inexistant ou si nous lisons jusqu'à la fin du fichier, tous les appels successifs produisent '*e'.

L'écriture est à peu près la même :

  1. openw(u,s)

ouvre le fichier ASCII s comme unité u pour l'écriture. Les appels successifs à putchar, putstr, printf,..., placeront la sortie sur le fichier s.

Si l'unité d'entrée/sortie mentionnée dans openr ou openw est déjà ouverte, elle est fermée avant la prochaine utilisation. C'est-à-dire que toute sortie pour les fichiers de sortie est vidée et la fin du fichier est écrite ; toute entrée des fichiers d'entrée est rejetée. Les fichiers sont également fermés de cette manière lors de la fin normale.

Les programmes n'ayant qu'une seule unité d'entrée et une seule unité de sortie ouvertes simultanément n'ont pas besoin de référencer explicitement rd.unit et wr.unit, car les routines d'ouverture le feront automatiquement. Sinon, n'oubliez pas de les déclarer dans une instruction extrn.

La fonction flush ferme l'unité de sortie actuelle sans écrire de caractère de nouvelle ligne de fin. Si ce fichier est le terminal, voici comment forcer une question de prompt avant de lire une réponse sur la même ligne :

  1. putstr("ancien ou nouveau ?");
  2. flush();
  3. getstr(s);

affichera :

ancien ou nouveau ?

et lit la réponse à partir de la même ligne.

Les noms de fichiers ne peuvent être que "cat/file", "/file" ou "file" : aucune permission, mot de passe, sous-catalogue ou nom alternatif n'est autorisé. Les noms de fichiers contenant un '/' sont supposés être des fichiers permanents ; si vous essayez d'ouvrir un fichier permanent qui est déjà consulté, il est libéré et consulté à nouveau. S'il ne peut pas être trouvé, l'accès échoue. Un nom de fichier sans '/' peut ou non être un fichier permanent, donc si un fichier de ce nom est déjà consulté, il est utilisé sans poser de question. S'il n'est pas déjà consulté, une recherche dans le catalogue de l'utilisateur est effectuée pour lui. S'il ne peut pas non plus y être trouvé et que l'écriture est souhaitée, un fichier temporaire est créé.

La fonction reread est utilisée pour relire une ligne. Sa fonction principale est que si elle est appelée avant le premier appel à getchar, elle récupère la ligne de commande avec laquelle le programme a été appelé.

  1. reread();
  2. getstr(s);

récupère la ligne de commande et la place dans la chaîne de caractères s.

Appel système depuis TSS

Les utilisateurs de TSS peuvent facilement exécuter d'autres sous-systèmes TSS à partir d'un programme B en cours d'exécution, en utilisant la fonction de bibliothèque system. Par exemple :

  1. system("./rj (w) ident;file");

agit exactement comme si nous avions tapé :

./rj (w) ident;file

en réponse à "SYSTEM?". Le paramètre de system est une chaîne de caractères, étant exécutée comme si elle était tapée au niveau "SYSTEM?". (Aucun '*n' n'est nécessaire à la fin de la chaîne de caractères.) Le retour se fait au programme B à moins qu'un désastre ne survienne. Cette fonctionnalité permet aux programmes B d'utiliser les sous-systèmes TSS et d'autres programmes pour effectuer des fonctions majeures sans comptabilité ni surcharge de base.

Nous pouvons également utiliser printf pour écrire sur l'unité d'entrée/sortie "system", étant prédéfinie comme -1. Toutes les sorties écrites sur l'unité -1 agissent comme si elles étaient tapées au niveau "SYSTEM?". Ainsi :

  1. extrn wr.unit;
  2. /* ... */
  3. wr.unit = -1;
  4. opts = "(w)";
  5. fname = "file";
  6. printf("./rj %s ident;%s*n", opts,fname);

définira l'unité de sortie actuelle sur -1, le système, puis affichera dessus en utilisant printf pour formater la chaîne de caractères. Cet exemple fait la même chose que le dernier, mais les options RUNJOB et les noms de fichiers sont des variables, pouvant être modifiées à volonté. Notez que cette fois, un '*n' est nécessaire pour terminer la ligne.

En raison de graves défauts de conception dans TSS, il n'existe généralement aucun moyen décent d'envoyer plus que la ligne de commande en entrée à un programme appelé de cette façon, et aucun moyen d'en récupérer la sortie.

Récursivité

  1. putnumb(n) {
  2.  auto a;
  3.  if(a=n/10)      /* test d'affectation et non d'égalité */
  4.     putnumb(a);  /* récursif */
  5.     putchar(n%10 + '0');
  6. }

Cette version simplifiée de la fonction de bibliothèque putnum illustre l'utilisation de la récursivité. (Elle ne fonctionne que pour n>0.) "a" est défini sur le quotient de n/10 ; si ce dernier est différent de zéro, un appel récursif sur putnumb est effectué pour afficher le quotient. Finalement, le chiffre de poids fort de n sera affiché, puis à chaque invocation de putnumb, le chiffre suivant sera ajouté.

Passage d'arguments pour les fonctions

Il existe une subtilité dans l'utilisation des fonctions pouvant piéger le programmeur Fortran sans méfiance. Le passage de paramètres est un appel par valeur", ce qui signifie que la fonction appelée reçoit les valeurs réelles de ses paramètres et ne connaît pas leurs adresses. Cela rend très difficile la modification de la valeur de l'un des paramètres d'entrée !

Par exemple, considérons une fonction flip(x,y) consistant à échanger ses arguments. Nous pourrions naïvement écrire :

  1. flip(x,y){
  2.  auto t;
  3.  t = x;
  4.  x = y;
  5.  y = t;
  6. }

mais cela n'a aucun effet. (Non-croyants : essayez !) Ce que nous devons faire, c'est passer les paramètres sous forme d'adresses explicites et les utiliser comme adresses. Ainsi, nous appelons flip comme :

  1. flip(&a,&b);

et la définition de flip est :

  1. flip(x,y){
  2.  auto t;
  3.  t = *y;
  4.  *x = *y;
  5.  *y = t;
  6. }

(Et n'oubliez pas les espaces à droite des signes égaux.) Ce problème ne se pose pas si les paramètres sont des vecteurs, puisqu'un vecteur est représenté par son adresse.

Appel de sous-routines Fortran et GMAP à partir de programmes B

Comme nous l'avons mentionné, B n'a pas de virgule flottante, de tableaux multidimensionnels et d'autres choses similaires que Fortran fait mieux. Un programme B, callf, permet aux programmes B d'appeler des sous-routines Fortran et GMAP, comme ceci :

  1. callf(name, a1,a2, ...,a10)

Ici, le nom est la fonction Fortran ou la sous-routine à utiliser, et les a sont les zéro ou plusieurs paramètres de la sous-routine.

Il existe certaines restrictions sur callf, liées au problème de «call by value» mentionné ci-dessus. Tout d'abord, les variables n'étant pas des vecteurs ou des chaînes de caractères doivent être référencées par adresse : utilisez «&x» au lieu de «x» comme paramètre. Deuxièmement, aucune constante n'est autorisée parmi les paramètres, car la construction «&constante» est illégale dans B. Nous ne pouvons pas dire :

  1. tod = callf(itimez,&0)

pour obtenir l'heure du jour ; à la place, nous devons utiliser :

  1. t=0;
  2. tod = callf(itimez,&t);

Troisièmement, il n'est pas possible de renvoyer des valeurs de fonctions à virgule flottante à partir de programmes Fortran, car les résultats sont dans le mauvais registre. Ils doivent être explicitement renvoyés comme arguments. Nous ne pouvons donc pas dire :

  1. s  = callf(sin,&x)

mais plutôt :

  1. callf(sin2,&x,&s)

sin2 est défini comme :

  1. subroutine sin2(x,y)
  2. y = sin(x)
  3. return
  4. end

Quatrièmement, callf n'est pas d'une efficacité aveuglante, car la conversion des séquences d'appel entraîne une surcharge importante. (De très courtes routines GMAP pour s'interfacer avec des programmes B peuvent souvent être codées de manière assez efficace).



Dernière mise à jour : Mercredi, le 20 août 2014