wiki:InsiaProgCSyntaxe1

Sommaire

Syntaxe - Fonctions, variables et types simples

1. Syntaxe de base

Un programme en C est constitué d'une suite d'instructions. Une instruction se termine toujours avec un point-virgule. En général, on écrit une instruction par ligne bien que ceci ne soit pas obligatoire:

age = 0;
printf("Quel est votre âge ?");
fscanf("%d", &age);
printf("Vous êtes né en %d", 2006-age);

Blocs

Le langage C est dit structuré: le programme est découpé en petites sous-parties appelées blocs, et nous allons pouvoir décider comment les blocs s'articulent à l'aide de découpage arbitraires (les fonctions) et logiques (les boucles et les branchements). Un bloc est équivalent à une instruction, se délimite par des accolades, et ne finit pas par un point virgule:

void ma_fonction() {
  age = 0;
  printf("Quel est votre âge ?");
  fscanf("%d", &age);
  if (age < 18) {
    printf("Vous êtes mineur");
    printf("Vous serez majeur dans %d année(s)", 18 - age);
  }
}

Remarquez l'indentation des blocs afin de rendre immédiatement visible la structure du programme. Il existe plusieurs styles d'indentation: le plus important est d'en adopter un de manière consistante tout au long de votre projet.

Nous verrons au cours des chapitres suivants (fonctions, variables, boucles, branchements) dans quel cas nous utiliserons les blocs et de quelle manière.

Commentaires

Les commentaires sont totalement ignorés par le compilateur. Ils peuvent être insérés à n'importe quel endroit du programme, et éventuellement faire plusieurs lignes:

age = 0; /* Initialise la variable */
/* Ci-dessous, nous allons appeler
   la fonction de calcul de l'âge du capitaine */
/*ici:*/ age_du_capitaine(/*pas de paramètre à passer*/);

Attention: les commentaires de la forme // commentaire ne sont autorisés qu'en C++ et C99.

Directives du préprocesseur

Elles ne font pas partie du langage C lui-même, mais elles sont normalisées. Elles commencent toujours par un dièse (#):

#include <stdio.h>
#define MACRO_CARRE(x) (x*x)
#ifndef MACRO_CARRE
#warning la macro MACRO_CARRE n'est pas définie !
#endif

2. Les fonctions

La fonction constitue la structure logique de plus haut niveau du langage C. Un programme en C est une séquence de fonctions qui s'appellent les unes les autres. En particulier, il n'est pas possible d'écrire des instructions en C en dehors du corps des fonctions. C'est pour cette raison que tout programme minimaliste en C comporte au moins la fonction main, qui a la particularité d'être le point d'entrée du programme:

int main(int argc, char** argv) {
  ...
}

Un nom de fonction peut contenir des chiffres (0-9), des lettres en minuscule et majuscule (a-z, A-Z) et le souligné (_) et ne peut pas commencer par un chiffre.

Note: en C, on préfère l'utilisation des minuscules et du souligné (_) pour nommer les fonctions (à comparer au "CamelCase" populaire en C++ et Java). Les noms en majuscules sont en général réservés aux macros.

Attention: le langage C est sensible à la casse, les fonctions fait_qqchose et Fait_Qqchose sont distinctes.

Déclaration

La syntaxe générale de déclaration d'une fonction est la suivante:

type_de_retour nom_de_la_fonction(type_arg1 arg1, type_arg2, arg2, ...) {
  (corps de la fonction)
}

La fonction peut accepter un nombre quelconque d'arguments ou aucun. Elle peut optionnellement renvoyer une valeur. Exemples:

void fonction_simple() {
  printf("Très simple !");
}

int produit_entiers(int x, int y) {
  return x*y;
}

Il est souvent utile de pouvoir déclarer séparément la forme de la fonction - appelée prototype - et les instructions qui la compose - son implémentation. Ce que nous avions vu ci-dessus sont des déclarations de fonction accompagnées de leur implémentation. Il est possible de déclarer (en plus, dans le même programme) leur prototype:

void fonction_simple();
int produit_entiers(int x, int y);

La déclarations de prototypes sert en particulier dans le cas des bibliothèques de fonctions (afin de constituer leur interface), et dans le cas d'appels croisés de fonctions, par exemple:

int calcul2(int x); /* Nous pouvons ainsi utiliser calcul2() _avant_ de fournir son implémentation */

int calcul1(int x, int y) {
  return x + calcul2(1, y);
}

it calcul2(int x) {
  return x * calcul1(x);
}

Utilisation

Pour appeler une fonction, il suffit d'écrire son nom dans une instruction suivi du nombre exact de paramètres requis entre parenthèses:

int p;
fonction_simple();
p = produit_entiers(5, 9);

Dans le cas où la fonction renvoie une valeur, celle-ci peut être directement récupérée dans une variable, ou utilisée dans un test de boucle, de branchement, etc.

Passage par copie

Les paramètres d'une fonction ainsi que sa valeur retour sont passées par copie. Cela signifie que lorsque vous manipulez dans le corps d'une fonction les paramètres reçus, ceci n'a pas d'effet sur les variables utilisées par l'appelant:

void mise_a_zero(x) {
  x = 0;
}

int main(int argc, char** argv) {
  int x = 5;
  mise_a_zero(x);
  printf("x vaut %d", x); /* Affiche "5" */
}

Ceci signifie qu'en règle général, l'effet d'une fonction sur l'appelant reste cantonné à sa valeur de retour. Ce principe d'isolation n'est vrai que dans la mesure où les pointeurs ne sont pas utilisés dans les paramètres de la fonction.

Corolaire: chaque appel de fonction consomme de la mémoire pour recopier les paramètres, ce qui va restreindre le nombre d'appels imbriqués possible. Ceci est particulièrement vrai pour les récursions.

Attention: le passage par référence de la forme fonction(type& variable) est spécifique au C++.

3. Les variables

Une variable en C doit toujours être déclarée avant d'être utilisée. Un nom de variable, comme un nom de fonction, peut contenir des chiffres (0-9), des lettres en minuscule et majuscule (a-z, A-Z) et le souligné (_) et ne peut pas commencer par un chiffre. Exemple de variables valides: compteur, _somme, test2.

Attention: le langage C est sensible à la casse, les variables compteur et Compteur sont distinctes.

Déclaration

Une variable est déclarée en faisant précéder son nom d'un type, et optionnellement d'une initialisation. On peut déclarer plusieurs variables du même type en une seule instruction en séparant les noms des variables par des virgules.

Elle peut être déclarée de manière globale, c'est-à-dire en dehors de tout bloc (et des fonctions en particulier):

int compteur = 0, un_entier;

int main(int argc, char** argv) {
...

Une variable peut également être déclarée en début de bloc (début de fonction, début de boucle, etc) :

int main(int argc, char** argv) {
  int compteur = 0;
  ...
}

Enfin, les paramètres d'une fonction sont équivalents à des variables déclarées:

int main(int argc, char** argv) {
  /* argc et argv sont des variables de type int */
  ...

Les deux derniers modes de déclaration (début de bloc et paramètres de fonctions) créent des variables dites locales.

Attention: il est autorisé de déclarer une variable n'importe où dans un bloc (et pas nécessairement au début) seulement en C99 ou C++.

Utilisation

Une variable s'utilise naturellement, comme l'abstraction mathématique classique. On peut en particulier assigner une valeur constante, le résultat d'une opération ou d'une fonction à une variable:

int a, b;
a = 42;
a = (10 * b) + 5;
a = age_du_capitaine(b);

Il est tout à fait possible d'utiliser la même variable à droite et à gauche d'un assignement: l'évaluation de la partie gauche est d'abord effectuée (avec la valeur courante de la variable donc), puis l'assignation est effectuée et remplace la valeur de cette variable. Par exemple pour incrémenter une variable entière:

int a;
a = a + 1;

Portée

Une variable déclarée n'est utilisable que dans une portion bien déterminée du programme.

Pour une variable globale, la règle est simple: elle est utilisable à tout endroit du programme. Par exemple:

int compteur;

void initialise_compteur() {
  compteur = 0;
}

int main(int argc, char** argv) {
  initialise_compteur();
  return compteur;  
}

Une variable locale n'est par contre utilisable que dans le bloc (et ses sous-blocs) où elle a été déclarée. Si il existe à la fois une variable globale et locale du même nom, c'est la variable locale qui est utilisée. Exemple:

int argc = 42;

int main(int argc, char** argv) {
  int i;

  for (i = 0; i < argc; i++) {
    printf("%d/%d", i, argc);
  }
}

4. Types simples

Le langage C est dit typé: chaque variable a un type précis, et chaque fonction ou opérateur exige également un type précis. La vérification de la compatibilité des types et de leur bon usage est effectuée par le compilateur.

Le langage C fournit des types élémentaires qui représentent des données telles qu'elles peuvent être manipulées par le processeur: des entiers, des nombres à virgule flottante (approximation de réels) et des pointeurs (adresses mémoires).

Les types simples ont une propriété importante pour le langage C: ce sont les seuls types qui peuvent être utilisés pour les paramètres d'une fonction. Ceci aura une incidence importante sur la gestion des types composés, où la notion de pointeur devient fondamentale.

La manipulation de types simples peut en général être compilée de manière très efficace. C'est une des raisons pour laquelle le C est apprécié pour la programmation de traitements numériques optimisés.

On peut récupérer lors de la compilation la taille d'un type de donnée en octets à l'aide de l'opérande sizeof (qui fonctionne à la fois sur un type et une variable):

int x;
printf("La taille en mémoire d'une variable de type 'int' est %d octet(s)", sizeof(x));

Les entiers

Il existe quatre types d'entiers principaux, 8 au total si l'on compte les variantes signées/non-signées:

Nom Capacité officielle Capacité réelle gcc/i386
char 8 bits minimum 8 bits
short plus que char, moins que int 16 bits
int plus que short, moins que long 32 bits
long plus ou autant que int 32 bits
long long 64 bits minimum (C99 seulement) 64 bits

Chaque type est signé par défaut: un bit est réservé pour représenter le signe de l'entier, les bits restants pour sa valeur. Voici les plages de valeurs représentables pour chaque taille d'entier:

Taille Signature Plage de valeurs
8 bits signé -128 .. 127
8 bits non signé 0 .. 255
16 bits signé -32768 .. 32767
16 bits non signé 0 .. 65535
32 bits signé -2147483648 .. 2147483647
32 bits non signé 0 .. 4294967295

Exemples de déclarations de variables entières:

int x;
signed short y;
unsigned char b;
unsigned long long un_gros_chiffre;

L'arithmétique des entiers sur un ordinateur est toujours de type modulo: l'espace des valeurs représentables par les entiers est limité (au sens mathématique) et cyclique. En pratique, on parle de "débordement" (overflow) quand le résultat d'une opération dépasse la capacité d'un type, et ces cas doivent être prévus et traités avec soin.

Enumération

Il s'agit d'un type comparable à un entier, mais qui ne peut représenter qu'un jeu de valeurs (symboliques) prédéfinies. Exemple:

enum { numero_un, numero_deux, numero_trois } rang;
rang = numero_un;
if (rang == numero_deux) {
  printf("Nous avons le deuxième numéro");
}

Le compilateur refusera de mélanger des entiers et des enums (du moins sans warning insistant), ce qui permet de garantir la restriction de l'usage de variables à des valeurs connues et prédéfinies.

Les flottants

Les types suivants sont disponibles, par ordre de capacité croissante: float, double et long double.

Il s'agit plus précisément de nombres à virgule flottante: il s'agit d'une technique pour approximer les nombres réels. La représentation d'un nombre à virgule flottante utilise un nombre fixe de bits pour la mantisse, et le reste pour l'exposant. Ceci fait référence à l'écriture scientifique d'un réel:

2.23855 * 10^-15

On ne peut pas donc représenter plus de nombres qu'avec un type entier de même taille (le nombre de bits reste limité), mais:

  • on peut effectuer des opérations sur les réels de façon "naturelle" (opérations linéaires, trigonométrie, etc)
  • on peut représenter des valeurs sur une amplitude très élevée (grâce à l'exposant)

Les pointeurs

Les pointeurs sont des adresses mémoire, et servent à accéder à des structures de données complexes de manière dynamique (par exemple l'adressage d'une entrée d'un tableau). Bien qu'on puisse effectuer des opérations arithmétiques limitées sur les pointeurs, il est prudent de les considérer comme des types opaques (dont on ignore les détails internes). En pratique, il s'agit d'un type équivalent à un entier dont la largeur est de 32 bits (i386) ou 64 bits (ia64, alpha, sparc64, amd64, etc.).

Un pointeur s'obtient en dérivant un type: en pratique on désire pointer un espace mémoire dont on connaît le type (c.a.d la représentation) afin d'être en mesure de le manipuler. Exemple:

int a;
int * adresse_de_a = &a;

a = 42;
*adresse_de_a = 42; /* Opération équivalente */

On désigne un pointeur par un type suivi d'une étoile. Pour l'initialiser, on peut facilement récupérer l'adresse d'une variable du même type à l'aide de l'opérateur &. Enfin il est possible de manipuler l'espace mémoire pointé comme si c'était une variable du même type en précédant le nom du pointeur par l'opérateur *.

Last modified 14 years ago Last modified on Oct 23, 2006, 10:39:01 AM