Introduction à C. Message du siècle dernier

Préface


Dans mes commentaires, je me suis référé à plusieurs reprises au livre Operating Systems Design and Implementation d’Andrew Tanenbaum, à sa première édition, et à la façon dont C y est représenté. Et ces commentaires ont toujours été intéressants. J'ai décidé qu'il était temps de publier une traduction de cette introduction en C. C'est toujours d'actualité. Bien qu'il y ait certainement ceux qui n'ont pas entendu parler du langage de programmation PL / 1 , et peut-être même du système d'exploitation Minix .

Cette description est également intéressante d'un point de vue historique et pour comprendre jusqu'où le langage C est allé depuis sa naissance et l'industrie informatique dans son ensemble.

Je souhaite immédiatement réserver que ma deuxième langue soit le français:

image

Mais cela est compensé par 46 ans d' expérience en programmation .
Alors, commençons, c'est au tour d'Andrew Tanenbaum.

Introduction au langage C (pp. 350 - 362)




Le langage de programmation C a été créé par Dennis Ritchie d'AT & T Bell Laboratories en tant que langage de programmation de haut niveau pour le développement du système d'exploitation UNIX. Actuellement, la langue est largement utilisée dans divers domaines. C est particulièrement populaire auprès des programmeurs système car il vous permet d'écrire des programmes de manière simple et concise.

Le livre principal décrivant le langage C est le livre du langage de programmation C (1978) de Brian Kernigan et Dennis Ritchie. Des livres sur le langage C ont été écrits par Bolon (1986), Gehani (1984), Hancock et Krieger (1986), Harbison et Steele (1984) et bien d'autres.

Dans cette application, nous essaierons de donner une introduction assez complète au C, afin que ceux qui sont familiers avec les langages de haut niveau tels que Pascal, PL / 1 ou Modula 2 puissent comprendre la plupart du code MINIX donné dans ce livre. Les fonctionnalités C qui ne sont pas utilisées dans MINIX ne sont pas traitées ici. Nombreux points subtils omis. L'accent est mis sur la lecture de programmes C plutôt que sur l'écriture de code.

A.1. Bases du langage C


Un programme C se compose d'un ensemble de procédures (souvent appelées fonctions, même si elles ne renvoient pas de valeurs). Ces procédures contiennent des déclarations, des opérateurs et d'autres éléments qui, ensemble, indiquent à l'ordinateur quoi faire. La figure A-1 montre une petite procédure dans laquelle trois variables entières sont déclarées et affectées à des valeurs. Le nom de la procédure est principal. La procédure n'a pas de paramètres formels, comme l'indique l'absence d'identificateurs entre les crochets derrière le nom de la procédure. Le corps de la procédure est placé entre accolades ({}). Cet exemple montre que C a des variables et que ces variables doivent être déclarées avant utilisation. C a également des opérateurs, dans cet exemple ce sont des opérateurs d'affectation. Toutes les instructions doivent se terminer par un point-virgule (contrairement à Pascal, qui utilise des deux-points entre les instructions, pas après).

Les commentaires commencent par les caractères «/ *» et se terminent par les caractères «* /» et peuvent s'étendre sur plusieurs lignes.

main () /*   */ { int i, j, k; /*  3   */ i = 10; /*  i  10 ( ) */ j = i + 015; /*  j  i + 015 ( ) */ k = j * j + 0xFF; /*  k  j * j + 0xFF ( ) */ } . Al.    . 

La procédure contient trois constantes. Constante 10 dans la première affectation
c'est une constante décimale ordinaire. La constante 015 est une constante octale
(égal à 13 en décimal). Les constantes octales commencent toujours à zéro. La constante 0xFF est une constante hexadécimale (égale à 255 décimales). Les constantes hexadécimales commencent toujours par 0x. Les trois types sont utilisés en C.

A.2. Types de données de base


C a deux principaux types de données (variables): un entier et un caractère, déclarés respectivement int et char. Il n'y a pas de variable booléenne distincte. La variable int est utilisée comme variable booléenne. Si cette variable contient 0, cela signifie faux / faux et toute autre valeur signifie vrai / vrai. C a également des types à virgule flottante, mais MINIX ne les utilise pas.

Vous pouvez appliquer des «adjectifs» courts, longs ou non signés à un type int qui définit une plage de valeurs (dépendant du compilateur). La plupart des processeurs 8088 utilisent des entiers 16 bits pour int et short int et 32 ​​bits pour int long. Les entiers non signés (entier non signé) sur le processeur 8088 ont une plage de 0 à 65535, et non de -32768 à +32767, comme c'est le cas avec les entiers ordinaires (int). Un caractère prend 8 bits.

Le spécificateur de registre est également autorisé pour int et char, et indique au compilateur que la variable déclarée doit être placée dans le registre pour que le programme fonctionne plus rapidement.

Certaines annonces sont montrées sur la fig. A - 2.

 int i; /*    */ short int z1, z2; / *    */ char c; /*   */ unsigned short int k; /*      */ long flag_poll; /* 'int'    */ register int r; /*   */ . -2.  . 

La conversion entre les types est autorisée. Par exemple, l'opérateur

 flag_pole = i; 

autorisé même si i est de type int et que flag_pole est long. Dans de nombreux cas
il est nécessaire ou utile de forcer les conversions entre les types de données. Pour une conversion forcée, il suffit de mettre le type cible entre crochets devant l'expression à convertir. Par exemple:

  ( (long) i); 

indique de convertir l'entier i en long avant de le passer comme paramètre à la procédure p, qui attend le paramètre long.

Lors de la conversion entre les types, faites attention au signe.
Lors de la conversion d'un caractère en entier, certains compilateurs traitent les caractères comme signés, c'est-à-dire de - 128 à +127, tandis que d'autres les traitent comme
non signé, c'est-à-dire de 0 à 255. Dans MINIX, des expressions telles que

 i = c & 0377; 

qui convertit de (caractère) en entier, puis effectue un ET logique
(esperluette) avec la constante octale 0377. Le résultat est que les 8 bits élevés
sont mis à zéro, forçant en fait c à être considéré comme un nombre non signé de 8 bits, dans la plage de 0 à 255.

A.3. Types de composés et pointeurs


Dans cette section, nous examinerons quatre façons de créer des types de données plus complexes: les tableaux, les structures, les unions et les pointeurs. Un tableau est une collection / un ensemble d'éléments du même type. Tous les tableaux en C commencent par l'élément 0.

Annonce

 int a [10]; 

déclare un tableau a avec 10 entiers à stocker dans les éléments du tableau de [0] à a [9]. Deuxièmement, les tableaux peuvent avoir trois dimensions ou plus, mais ils ne sont pas utilisés dans MINIX.
Une structure est un ensemble de variables, généralement de différents types. La structure en C est similaire à celle de Pascal. Opératrice

 struct {int i; char c;} s; 

déclare s comme une structure contenant deux membres, l'entier i et le caractère c.

Pour affecter le membre i de la structure s à 6, écrivez l'expression suivante:

 si = 6; 

où l'opérateur point indique que l'élément i appartient à la structure s.
Un syndicat est également un ensemble de membres, semblable à une structure, sauf qu'à tout moment un seul d'entre eux peut être membre d'un syndicat. Annonce

 union {int i; char c;} u; 

signifie que vous pouvez avoir un entier ou un caractère, mais pas les deux. Le compilateur doit allouer suffisamment d'espace pour la combinaison afin qu'il puisse accueillir le plus grand élément de combinaison (du point de vue de la mémoire occupée). Les unions ne sont utilisées qu'à deux endroits dans MINIX (pour définir un message comme une union de plusieurs structures différentes, et pour définir un bloc de disque comme une union d'un bloc de données, d'un bloc i-node, d'un bloc catalogue, etc.).

Les pointeurs sont utilisés pour stocker les adresses des machines en C. Ils sont utilisés très, très souvent. Un astérisque (*) est utilisé pour indiquer un pointeur dans les annonces. Annonce

 int i, *pi, a [10], *b[10], **ppi; 

déclare un entier i, un pointeur sur un entier pi, un tableau a de 10 éléments, un tableau b de 10 pointeurs sur des entiers et un pointeur sur un pointeur ppi sur un entier.

Les règles de syntaxe exactes pour les déclarations complexes qui combinent des tableaux, des pointeurs et d'autres types sont quelque peu complexes. Heureusement, MINIX n'utilise que des déclarations simples.

La figure A-3 montre la déclaration d'un tableau z de structures de table struct, chacune ayant
trois membres, entier i, pointeur cp vers caractère et caractère c.

 struct table { /*      */ int i; / *  */ char *cp, c; /*      */ } z [20]; /*    20  */ .  - 3.  . 

Les tableaux de structures sont courants dans MINIX. De plus, la table de noms peut être déclarée comme une structure de table struct qui peut être utilisée dans les déclarations suivantes. Par exemple

 register struct table *p; 

déclare p un pointeur sur une structure de table struct et suggère de l'enregistrer
dans le registre. Pendant l'exécution du programme, p peut indiquer, par exemple, z [4] ou
à tout autre élément de z, dont les 20 éléments sont des structures de type struct table.

Pour faire de p un pointeur sur z [4], il suffit d'écrire

 p = &z[4]; 

où l'esperluette en tant qu'opérateur unaire (monadique) signifie "prendre l'adresse de ce qui la suit". Copiez la valeur du membre i dans la variable entière n
la structure pointée par p peut se faire comme suit:

 n = p->i; 

Notez que la flèche est utilisée pour accéder à un membre de la structure via un pointeur. Si nous utilisons la variable z, alors nous devons utiliser l'opérateur point:

 n = z [4] .i; 

La différence est que z [4] est une structure, et l'opérateur de point sélectionne les éléments
à partir de types composites (structures, tableaux) directement. À l'aide de pointeurs, nous ne sélectionnons pas directement un participant. Le pointeur vous demande de sélectionner d'abord une structure, puis de sélectionner ensuite un membre de cette structure.

Parfois, il est pratique de donner un nom à un type composite. Par exemple:

 typedef unsigned short int unshort; 

définit unshort comme non signé court (entier court non signé). Maintenant unshort peut être utilisé dans le programme comme type principal. Par exemple

 unshort ul, *u2, u3[5]; 

déclare un entier court non signé, un pointeur sur un entier court non signé et
un tableau d'entiers courts non signés.

A.4. Les opérateurs


Les procédures en C contiennent des déclarations et des déclarations. Nous avons déjà vu les déclarations, nous allons donc maintenant considérer les opérateurs. Le but des opérateurs conditionnels et de boucle est essentiellement le même que dans d'autres langues. La figure A-4 en montre plusieurs exemples. La seule chose à laquelle il faut faire attention est que les accolades sont utilisées pour grouper les opérateurs, et l'instruction while a deux formes, dont la seconde est similaire à l'instruction répétée de Pascal.

C a également une instruction for, mais elle ne ressemble à une instruction for dans aucune autre langue. L'instruction for a la forme suivante:

 for (<>; <>; <>) ; 

La même chose peut être exprimée à travers la déclaration while:

 <> while(<>) { <>; <> } 

À titre d'exemple, considérons l'énoncé suivant:

 for (i=0; i <n; i = i+l) a[i]=0; 

Cet opérateur met à zéro les n premiers éléments du tableau a. L'exécution de l'opérateur commence par mettre i à zéro (cela se fait en dehors de la boucle). Ensuite, l'opérateur est répété jusqu'à i <n, tout en effectuant l'affectation et l'augmentation de i. Bien sûr, au lieu de l'opérateur d'assigner une valeur à l'élément actuel d'un tableau zéro, il peut y avoir un opérateur composé (bloc) entre crochets.

 if (x < 0) k = 3; /*   if */ if (x > y) { /*   if */ i = 2; k = j + l, } if (x + 2 <y) { /*  if-else */ j = 2; k = j - 1; } else { m = 0; } while (n > 0) { /*  while */ k = k + k; n = n - l; } do { / *    while */ k = k + k; n = n - 1; } while (n > 0); . A-4.   if  while  C. 

C a également un opérateur similaire à l'opérateur case en Pascal. Il s'agit d'une instruction switch. Un exemple est illustré à la figure A-5. En fonction de la valeur de l'expression spécifiée dans switch, l'une ou l'autre instruction case est sélectionnée.

Si l'expression ne correspond à aucune des instructions case, l'instruction par défaut est sélectionnée.

Si l'expression n'est associée à aucune instruction case et que l'instruction par défaut est absente, l'exécution se poursuit à partir de l'instruction suivante après l'instruction switch.

Il convient de noter que pour quitter le bloc de cas, utilisez l'instruction break. S'il n'y a pas d'instruction break, le bloc de cas suivant sera exécuté.

 switch (k) { case 10: i = 6; break; /*   case 20, ..    switch */ case 20: i = 2; k = 4; break; / *   default* / default: j = 5; } . A-5.   switch 

L'instruction break agit également à l'intérieur des boucles for et while. Il ne faut pas oublier que si l'instruction break se trouve à l'intérieur d'une série de boucles imbriquées, la sortie n'est que d'un niveau supérieur.

Une instruction connexe est l'instruction continue, qui ne quitte pas la boucle,
mais provoque la fin de l'itération en cours et le début de l'itération suivante
immédiatement. Il s'agit essentiellement d'un retour en haut de la boucle.

C a des procédures qui peuvent être appelées avec ou sans paramètres.
Selon Kernigan et Ritchie (p. 121), il n'est pas autorisé de transférer des tableaux,
structures ou procédures en tant que paramètres, bien que passant des pointeurs à tout cela
autorisé. Existe-t-il un livre ou non (il apparaîtra dans ma mémoire: - "S'il y a de la vie sur Mars, s'il n'y en a pas sur Mars"), de nombreux compilateurs C autorisent les structures comme paramètres.
Le nom du tableau, s'il est écrit sans index, signifie un pointeur vers un tableau, ce qui simplifie le transfert d'un pointeur de tableau. Ainsi, si a est le nom d'un tableau de n'importe quel type, il peut être passé à g en écrivant

 g(); 

Cette règle s'applique uniquement aux tableaux; cette règle ne s'applique pas aux structures.
Les procédures peuvent renvoyer des valeurs en exécutant une instruction return. Cette instruction peut contenir une expression, dont le résultat sera renvoyé comme valeur de la procédure, mais l'appelant peut ignorer la valeur de retour en toute sécurité. Si la procédure renvoie une valeur, alors la valeur de type est écrite avant le nom de la procédure, comme illustré à la Fig. A-6. Comme les paramètres, les procédures ne peuvent pas renvoyer de tableaux, de structures ou de procédures, mais peuvent leur renvoyer des pointeurs. Cette règle est conçue pour une implémentation plus efficace - tous les paramètres et résultats correspondent toujours à un mot machine (dans lequel l'adresse est stockée). Les compilateurs qui autorisent l'utilisation de structures en tant que paramètres autorisent généralement leur utilisation en tant que valeurs de retour.

 int sum (i, j) /*      */ int i, j ; /*   */ { return (i + j); /*      */ } . -6.   ,   . 

C n'a pas d'E / S intégrées. L'entrée / sortie est implémentée en appelant des fonctions de bibliothèque, dont les plus courantes sont illustrées ci-dessous:

 printf («x=% dy = %oz = %x \n», x, y, z); 

Le premier paramètre est la chaîne de caractères entre guillemets (en fait, il s'agit d'un tableau de caractères).

Tout caractère qui n'est pas un pourcentage est simplement imprimé tel quel.

Lorsqu'un pourcentage se produit, le paramètre suivant est imprimé sous la forme définie par la lettre suivant le pourcentage:
d - imprimer sous forme d'entier décimal
o - imprimer comme un entier octal
u - imprimer comme un entier décimal non signé
x - affiche un entier hexadécimal
s - imprimer comme une chaîne de caractères
c - imprimer en un seul caractère
Les lettres D, 0 et X sont également autorisées pour l'impression décimale, octale et hexadécimale des nombres longs.

A.5. Expressions


Les expressions sont créées en combinant des opérandes et des opérateurs.

Opérateurs arithmétiques tels que + et - et opérateurs relationnels tels que <
et> similaires à leurs homologues dans d'autres langues. % Opérateur
utilisé modulo. Il convient de noter que l'opérateur d'égalité est ==, et l'opérateur d'inégalité est! =. Pour vérifier si a et b sont égaux, vous pouvez écrire comme ceci:

 if (a == b) <>; 

C vous permet également de combiner l'opérateur d'affectation avec d'autres opérateurs, donc

 a += 4; 

équivalent à l'enregistrement

  =  + 4; 

D'autres opérateurs peuvent également être combinés de cette manière.

C a des opérateurs pour manipuler les bits d'un mot. Les décalages et les opérations logiques au niveau du bit sont autorisés. Les opérateurs de décalage gauche et droit sont <<
et >> respectivement. Opérateurs logiques bit à bit &, | et ^, qui sont des ET logiques (ET), y compris OU (OU) et OU exclusif (XOP), respectivement. Si i a la valeur 035 (octal), alors l'expression i & 06 a la valeur 04 (octal). Un autre exemple, si i = 7, alors

 j = (i << 3) | 014; 

et obtenez 074 pour j.
Un autre groupe important d'opérateurs est les opérateurs unaires, dont chacun n'accepte qu'un seul opérande. En tant qu'opérateur unaire, esperluette & obtient l'adresse d'une variable.

Si p est un pointeur sur un entier et i est un entier, l'opérateur

 p = &i; 

calcule l'adresse i et la stocke dans la variable p.
L'opposé de la prise d'une adresse est un opérateur qui prend un pointeur en entrée et calcule la valeur à cette adresse. Si nous venons d'affecter l'adresse i au pointeur p, alors * p a la même signification que i.

En d'autres termes, en tant qu'opérateur unaire, un astérisque est suivi d'un pointeur (ou
expression donnant un pointeur) et renvoie la valeur de l'élément vers lequel il pointe. Si i a une valeur de 6, alors l'opérateur

 j = *; 

attribuera j le numéro 6.
L'opératrice! (le point d'exclamation est l'opérateur de négation) renvoie 0 si son opérande est différent de zéro et 1 si son opérateur est 0.

Il est principalement utilisé dans les instructions if, par exemple

 if (!x) k=0; 

vérifie la valeur de x. Si x est zéro (faux), alors k reçoit la valeur 0. En fait, l'opérateur! annule la condition qui la suit, tout comme l'opérateur not dans Pascal.

L'opérateur ~ est un opérateur complémentaire au niveau du bit. Chaque 0 dans son opérande
devient 1, et chaque 1 devient 0.

L'opérateur sizeof indique la taille de son opérande en octets. Par rapport à
un tableau de 20 entiers a sur un ordinateur avec des entiers de 2 octets, par exemple sizeof a aura une valeur de 40.

Le dernier groupe d'opérateurs est celui des opérateurs d'augmentation et de diminution.

Opératrice

 ++; 

signifie une augmentation de p. La quantité de p augmentera en fonction de son type.
Les nombres entiers ou caractères incrémentent de 1, mais les pointeurs incrémentent de
la taille de l'objet pointé de cette façon, si a est un tableau de structures, et p est un pointeur vers l'une de ces structures, et nous écrivons

 p = &a[3]; 

faire pointer p vers l'une des structures du tableau, puis après avoir augmenté p
pointera vers un [4] quelle que soit la taille des structures. Opératrice

 p--; 

similaire à l'opérateur p ++, sauf qu'il diminue plutôt qu'il n'augmente la valeur de l'opérande.

En déclaration

 n = k++; 

où les deux variables sont des entiers, la valeur d'origine de k est affectée à n et
ce n'est qu'alors que k augmente. En déclaration

 n = ++ k; 

k augmente d'abord, puis sa nouvelle valeur est stockée dans n.

Ainsi, un opérateur ++ (ou -) peut être écrit avant ou après son opérande, ce qui donne différentes valeurs.

La dernière déclaration est-ce? (point d'interrogation) qui sélectionne l'une des deux alternatives
séparés par deux points. Par exemple, un opérateur,

 i = (x < y ? 6 : k + 1); 

compare x à y. Si x est inférieur à y, alors i obtient la valeur 6; sinon, la variable i obtient la valeur k + 1. Les crochets sont facultatifs.

A.6. Structure du programme


Un programme C consiste en un ou plusieurs fichiers contenant des procédures et des déclarations.
Ces fichiers peuvent être compilés individuellement en fichiers objets, qui sont ensuite liés les uns aux autres (à l'aide de l'éditeur de liens) pour former un programme exécutable.
Contrairement à Pascal, les déclarations de procédure ne peuvent pas être imbriquées, elles sont donc toutes écrites au «niveau supérieur» dans le fichier programme.

Il est autorisé de déclarer des variables en dehors des procédures, par exemple au début du fichier avant la première déclaration de la procédure. Ces variables sont globales et peuvent être utilisées dans n'importe quelle procédure du programme, sauf si le mot clé statique précède la déclaration. Dans ce cas, ces variables ne peuvent pas être utilisées dans un autre fichier. Les mêmes règles s'appliquent aux procédures. Les variables déclarées à l'intérieur d'une procédure sont locales à la procédure.
La procédure peut accéder à la variable entière v déclarée dans un autre fichier (à condition que la variable ne soit pas statique), la déclarant externe:

 extern int v; 

Chaque variable globale doit être déclarée une seule fois sans l'attribut extern afin de lui allouer de la mémoire.

Les variables peuvent être initialisées lorsqu'elles sont déclarées:

 int size = 100; 

Les tableaux et les structures peuvent également être initialisés. Les variables globales qui ne sont pas explicitement initialisées reçoivent une valeur par défaut de zéro.

A.7. Préprocesseur C


Avant que le fichier source soit transféré vers le compilateur C, il est automatiquement traité
un programme appelé préprocesseur. C'est la sortie du préprocesseur, pas
Le programme d'origine est alimenté à l'entrée du compilateur. Le préprocesseur effectue
Trois conversions de base dans un fichier avant de le passer au compilateur:

1. Inclusion de fichiers.
2. Définition et remplacement des macros.
3. Compilation conditionnelle.

Toutes les directives du préprocesseur commencent par un signe numérique (#) dans la 1ère colonne.
Lorsqu'une directive view

 #include "prog.h" 

rencontré par le préprocesseur, il inclut le fichier prog.h, ligne par ligne, en
le programme à passer au compilateur. Lorsque la directive #include est écrite comme

 #include <prog.h> 

puis le fichier inclus est recherché dans le répertoire / usr / include au lieu du répertoire de travail. Il est courant en C de regrouper les déclarations utilisées par plusieurs fichiers dans un fichier d'en-tête (généralement avec le suffixe .h) et de les inclure si nécessaire.
Le préprocesseur permet également des définitions de macro. Par exemple

 #define BLOCK_SIZE 1024 

BLOCK_SIZE 1024.
10 «BLOCK_SIZE»
4- «1024» , . . , .

— . MINIX
, 8088, . :

 #ifdef i8088 <   8088> #endif 

i8088 , #ifdef i8088 #endif ; .

 cc -c -Di8088 prog.c 



 #define i8088 

i8088, 8088 . MINIX 68000s , .

, , . A-7 (a). prog.h, :

 int x; #define MAXAELEMENTS 100 

,

 cc -E -Di8088 prog.c 

, , , . A-7 (b).

, , C .

 #include prog.h int x; main () main (); { { int a[MAX_ELEMENTS]; int a [100];  = 4;  = 4; a[x] = 6; [] = 6; #ifdef i8088 printf("8088. a[x]:% d\n", a[x]); printf ("8088. a[x]:% d\n", a[x]); #endif } #ifdef m68000 printf ("68000. x=%d\n", x); #endif } () (b) . -7. (a)   prog.c. (b)  . 

, , #.

 cc -c -Dm68000 prog.c 

. :

 cc -c prog.c 

. ( , , -Dflags.)

.8.


, C, . :

 while (n--) *p++ = *q++; 

p q , n . n- , q, , . , 0, , .

:

 for (i = 0; i < N; i++) a[i] = 0; 

N 0. :

 for (p = &a[0]; p < &a[N]; p++) *p = 0; 

p , . , p N- . , , .

. Par exemple

 if (a = f (x)) <  >; 

f, a
, , () (). , . Opératrice

 if (a = b) <  >; 

b a, a, .

 if (a == b) <  >; 

, .

Postface


. , , . . , .

Source: https://habr.com/ru/post/fr464075/


All Articles