Programmation C/Bases du langage

Un livre de Wikilivres.

Un programme C dans son ensemble peut être décrit comme un ensemble de fonctions qui s'appellent les unes les autres. Une fonction parmi celles-ci est obligatoire : la fonction main ; C'est celle-ci qui est appelée par le système d'exploitation lorsque vous lui demandez d'amorcer votre programme. La notion de fonction en elle-même est un concept assez avancé, et sera expliquée plus loin dans le cours. Pour l'instant, abordez la notion de manière intuitive.

Bonjour ![modifier | modifier le wikicode]

L'un des plus petits programmes possible en langage C est :

#include <stdio.h> 

int main(void)
{
    printf("Bonjour !\n");
    return 0;
}

(Les numéros de lignes ont été ajoutés pour la lisibilité, mais ne font pas partie du programme.)

Ce programme a pour seul but d'afficher le texte « Bonjour ! » suivi d'un retour à la ligne. Voici la description de chaque ligne qui le compose :

#include <stdio.h>

Inclusion de l'en-tête nommé <stdio.h>. Il est lié à la gestion des entrées et sorties (STanDard Input Output, entrées/sorties standards) et contient, entre autres, la déclaration de la fonction printf permettant d'afficher du texte formaté.

int main(void)

Définition de la fonction principale du programme, main (principal en anglais). Il s'agit de la fonction appelée au démarrage du programme, tout programme en C doit donc la définir. La partie entre parenthèses spécifie les paramètres que reçoit le programme : ici, le programme n'utilisera aucun paramètre, ce qui est spécifié en C avec le mot-clé void (vide). La partie à gauche du nom de la fonction main spécifie le type renvoyé par la fonction : la fonction main est définie par la norme C comme renvoyant une valeur entière, de type int (pour integer, entier), au système d'exploitation.

{

Début de la définition de la fonction main.

    printf("Bonjour !\n");

Appel de la fonction printf (print formatted, affichage formaté, qui sera expliquée dans le chapitre dédié à cette fonction). La chaîne « Bonjour ! » va être affichée, elle sera suivie d'un retour à la ligne, représenté en C par \n.

    return 0;

La fonction main a été définie comme retournant (to return) une valeur de type int, on renvoie donc une telle valeur. Par convention, la valeur 0 indique au système d'exploitation que le programme s'est terminé normalement.

}

Fin de la définition de la fonction main.

Ce premier exemple ne détaille évidemment pas tout ce qui concerne la fonction main, ou printf, par exemple. La suite de l'ouvrage précisera les points nécessaires dans les chapitres appropriés (voir par exemple le paragraphe la fonction main du chapitre Fonctions et procédures, ou le paragraphe Sorties formatées du chapitre Entrées/sorties).

Compilation[modifier | modifier le wikicode]

Pour exécuter un programme C, il faut préalablement le compiler?.

Tapez le code source du programme Bonjour ! dans un éditeur de texte? et sauvegardez-le sous le nom bonjour.c (l'extension .c n'est pas obligatoire, mais est usuelle pour un fichier source en C). Ouvrez une fenêtre de commandes (terminal sous Unix, commandes MS-DOS sous Windows), et placez-vous dans le répertoire où est sauvegardé votre fichier. Pour compiler avec le compilateur standard de votre système, il faut taper la commande :

gcc bonjour.c

D'autres compilateurs pour le langage C peuvent être utilisés. La compilation du programme produira un fichier exécutable, appelé a.out sous Unix, et a.exe sous Windows. Il peut être exécuté en tapant ./a.out sous Unix, et a sous Windows.

Éléments de syntaxe[modifier | modifier le wikicode]

Identificateurs[modifier | modifier le wikicode]

Les identificateurs commencent par une lettre ou le caractère souligné ("_") et peuvent contenir des lettres, des chiffres et le caractère souligné (cependant, les identificateurs commençant par deux caractères soulignés, ou un caractère souligné suivi d'une majuscule sont réservés par l'implémentation, et ne doivent pas être utilisés dans un programme « ordinaire »). Tous les mots-clés ainsi que les symboles (variables, fonctions, champs, etc.) sont sensibles à la casse des lettres. Quelques exemples d'identificateurs valides :

toto
_coin_coin76

Mots réservés du langage[modifier | modifier le wikicode]

Le langage, dans la norme C90, possède 32 mots réservés (ou mots-clés) qui ne peuvent pas être utilisés comme identificateurs. La norme C99 en a ajouté cinq ; la norme C11 en a de nouveau apporté 7. Le tableau ci-dessous les liste tous, avec la mention « C99 » ou «  C11 » pour indiquer ceux définis uniquement dans les versions correspondantes ou ultérieures :

auto break case char const continue default do
double else enum extern float for goto if
inline (C99) int long register restrict (C99) return short signed
sizeof static struct switch typedef union unsigned void
volatile while _Alignas (C11) _Alignof (C11) _Atomic (C11) _Bool (C99) _Complex (C99) _Generic (C11)
_Imaginary (C99) _Noreturn (C11) _Static_assert (C11) _Thread_local (C11)

Un programme C90 peut par exemple définir inline comme un identifiant de variable ou de fonction mais cela est très déconseillé, car il sera invalide au sens de la norme C99. Une mise à jour du compilateur peut alors l'empêcher de compiler le code source en question. Pour éviter de telles situations, il est préférable de considérer tous les mots-clés du tableau précédent comme étant réservés, quels que soient le compilateur et la norme utilisés.

Commentaires[modifier | modifier le wikicode]

Les commentaires commencent par /* et se terminent par */, ils ne peuvent pas être imbriquées :

/* ceci est un commentaire */

/*

 ceci est aussi un commentaire

 */

/* /* ceci est encore un commentaire */
Avertissement Ce code contient une erreur volontaire !
/* /* */ ceci nest pas un commentaire */

Inclure des commentaires pertinents dans un programme est un art subtil. Cela nécessite un peu de pratique pour savoir guider les lecteurs et attirer leur attention sur certaines parties délicates du code. Pour faire court, on ne saurait trop que rappeler ce célèbre vers de Nicolas Boileau, qui disait que « ce que l'on conçoit bien, s'énonce clairement et les mots pour le dire arrivent aisément » (Art Poétique, 1674). Ou encore, pour adopter une approche plus pragmatique : si un programmeur a du mal à commenter (expliquer, spécifier) le fonctionnement d'un passage de code, c'est que le code est mal conçu et qu'il serait bénéfique de le réécrire plus lisiblement.

Attention! Les commentaires ne devraient pas être employés pour désactiver certaines parties du code. Cette technique dissuade un programmeur d'en inclure puisque le langage C ne permet pas d'imbriquer les commentaires. Pour cela, le préprocesseur dispose d'instructions dédiées, qui permettent heureusement de désactiver du code en laissant les commentaires.

Le // ajoute la possibilité de placer un commentaire d'une seule ligne, à partir du // jusqu'à la fin de la ligne :

// commentaire
instruction; // autre commentaire

Cependant, ces commentaires « à la C++ » sont apparus dans C99, et ne sont pas permis en C90.

Instructions[modifier | modifier le wikicode]

Les instructions se terminent par un point-virgule (;), on peut placer autant d'instructions que l'on veut sur une même ligne (même si ce n'est pas conseillé pour la lisibilité du code). Les blocs d'instructions commencent par une accolade ouvrante ({) et se terminent par une accolade fermante (}). Les instructions doivent obligatoirement être déclarées dans une fonction : il est impossible d'appeler une fonction pour initialiser une variable globale par exemple (contrairement au C++).

/* une instruction */
i = 1;

/* plusieurs instructions sur la même ligne */
i = 1; j = 2; printf("bonjour\n");

/* un bloc */
{
        int i;
        i = 5;
}

/* l'instruction vide */
;

Dans la mesure où le compilateur ne se soucie pas des blancs (espaces et retours à la ligne), vous pouvez formater votre code comme vous l'entendez. Il y a beaucoup de religions concernant les styles d'indentation, mais pour faire court et éviter les guerres saintes, on ne saurait trop conseiller que d'utiliser le même nombre de blancs par niveau d'imbrication de bloc.

À noter que l'instruction vide étant valide en C, on peut donc pratiquement rajouter autant de point-virgules que l'on veut. Le point-virgule ne sert pas uniquement à marquer la fin des instructions, les compilateurs l'utilisent généralement comme caractère de synchronisation, suite à une erreur dans un programme source. En fait, en général, lorsqu'une erreur est détectée, les compilateurs ignorent tout jusqu'au prochain point-virgule. Ce qui peut avoir des conséquences assez dramatiques, comme dans l'exemple suivant :

Avertissement Ce code contient une erreur volontaire !
int traite_arguments(int nb, char * argv[])
{
         /* ... */
         return 0
}

int main(int nb, char * argv[])
{
         int retour;
         retour = traite_arguments(nb, argv);
         /* ... */
}

On notera l'absence de point-virgule à la fin de l'instruction return à la fin de la fonction traite_arguments. Ce que la plupart des compilateurs feront dans ce cas sera d'ignorer tout jusqu'au prochain point-virgule. On se rend compte du problème : on a sauté une déclaration de fonction (avec ses deux paramètres) et une déclaration de variable. Ce qui veut dire qu'une cascade d'erreurs va suivre suite à l'oubli... d'un seul caractère (;) !

Déclarations de variables[modifier | modifier le wikicode]

T var1, var2, ..., varN;

Cette ligne déclare les variables var1, var2, ..., varN de type T. Une variable est dite locale, si elle est définie à l'intérieur d'une fonction et globale si définie en-dehors.

Par exemple :

int jour; /* 'jour' est une variable globale, de type entier */

int main(void)
{
    double prix; /* 'prix' est une variable locale à la fonction 'main', de type réel */
    return 0;
}

Variables locales[modifier | modifier le wikicode]

Les variables locales (aussi appelées automatiques) ne sont visibles que dans le bloc dans lequel elles sont définies, et n'existent que durant ce bloc. Par exemple :

Avertissement Ce code contient une erreur volontaire !
int fonction(int n)
{
    int i; /* i est visible dans toute la fonction */
    i = n + 1;
    {  /* début d'un nouveau bloc */
        int j; /* j est visible dans le nouveau bloc *seulement* */
        j = 2 * i; /* i est accessible ici */
    }
    i = j; /* ERREUR : j n'est plus accessible */
    return i;
}

Le code précédent accède à i depuis un bloc contenu dans le bloc où i a été défini, ce qui est normal. Puis il essaye d'accéder à la valeur de la variable j en-dehors du bloc où elle à été définie, ce qui est une erreur.

Les variables locales ne sont pas initialisées automatiquement et contiennent donc, après leur déclaration, une valeur aléatoire. Utiliser une telle valeur peut causer un comportement aléatoire du programme. Le programme suivant essaye d'afficher la valeur d'une telle variable, et on ne peut savoir à l'avance ce qu'il va afficher. Si on le lance plusieurs fois, il peut afficher plusieurs fois la même valeur aussi bien qu'il peut afficher une valeur différente à chaque exécution :

Avertissement Ce code contient une erreur volontaire !
#include <stdio.h>

int main(void)
{
    int n;
    printf("La variable n vaut %d\n", n);
    return 0;
}

Avant la normalisation ISO C99, les déclarations de variables locales devaient obligatoirement être placées juste au début d'un bloc et s'arrêtaient à la première instruction rencontrée. Si une déclaration est faite au-delà, une erreur sera retournée. Suivant la norme C99, les déclarations peuvent se trouver n'importe où dans un bloc. Par exemple, le code suivant est correct suivant la norme C99, mais pas suivant la norme C90, car la variable a est définie après l'instruction puts("Bonjour !"); :

int ma_fonction(int n)
{
    puts("Bonjour !");
    int a;  /* Valide en C99, invalide en C90 */
    /* autre chose... */
    return a;
}

Variables globales[modifier | modifier le wikicode]

Une attention particulière doit être portée aux variables globales, souvent source de confusion et d'erreur. Utilisez des noms explicites, longs si besoin et limitez leur usage au seul fichier où elles sont déclarées, en les déclarants statiques.

Les variables globales sont initialisées avant que la fonction main s'exécute. Si le programmeur ne fournit pas de valeur initiale explicitement, chaque variable reçoit une valeur par défaut suivant son type :

  • un nombre (entier, réel ou complexe) est initialisé à 0 ;
  • les pointeurs sont initialisés à NULL ;
  • pour une structure, l'initialisation se fait récursivement, chaque membre étant initialisé suivant les mêmes règles ;
  • pour une union, le premier membre est initialisé suivant ces règles.

Pour initialiser explicitement une variable globale, on ne peut utiliser que des constantes ; en particulier, on ne peut appeler de fonction, contrairement à d'autres langages.

Avertissement Ce code contient une erreur volontaire !
int jour_courant(void)
{
   /* retourne le n° du jour courant */
}

int jour = jour_courant(); /* ERREUR ! */

int main(void)
{
   /* ... */
   return 0;
}

Le code précédent ne pourra pas compiler, car on ne peut appeler directement jour_courant() pour initialiser jour. Si vous avez besoin d'un tel comportement, une solution est de faire l’initialisation explicite dans main :

int jour_courant(void)
{
   /* retourne le n° du jour courant */
}

int jour; /* 'jour' est initialisé à 0 *avant* le début de 'main' */

int main(void)
{
   jour = jour_courant();
   /* ... */
   return 0;
}

Traitements des ambiguïtés[modifier | modifier le wikicode]

Le C utilise un mécanisme élégant pour lever des constructions syntaxiques en apparence ambiguës. L'analyse des mots (lexèmes, token en anglais) se fait systématiquement de la gauche vers la droite. Si plusieurs lexèmes peuvent correspondre à une certaine position, le plus grand aura priorité. Considérez l'expression valide suivante :

a+++b;

Ce genre de construction hautement illisible et absconse est bien évidemment à éviter, mais illustre parfaitement bien ce mécanisme. Dans cet exemple, le premier lexème trouvé est bien sûr l'identificateur 'a' puis le compilateur a le choix entre l'opérateur unaire '++', ou l'opérateur binaire '+'. Le plus grand étant le premier, c'est celui-ci qui aura priorité. L'instruction se décompose donc en :

(a ++) + b;