Introduction au C

From Deimos.fr / Bloc Notes Informatique
Jump to: navigation, search

Contents

1 Introduction

L'Histoire du langage C est intimement liée à celle du système d'exploitation UNIX. En 1965, Ken Thompson, de Bell Labs, développait un système d'exploitation qu'il baptisa MULTICS (Multiplexed Information and Computing System) afin de faire tourner un jeu qu'il avait créé, et qui donna naissance en 1970 au système d'exploitation UNICS (Uniplexed Information and Computing System) rapidement rebaptisé UNIX.

A l'époque, le seul langage qui permettait de développer un système d'exploitation était le langage d'assemblage. Ken Thompson développa alors un langage de plus haut niveau, le langage B (dont le nom provient de BCPL, un sous-ensemble du langage CPL, lui-même dérivé de l'Algol, un langage qui fut populaire à l'époque), pour faciliter l'écriture des systèmes d'exploitations. C'était un langage faiblement typé (un langage non typé, par opposition à un langage typé, est un langage qui manipule les objets sous leur forme binaire, sans notion de type (caractère, entier, réel, ...)) et trop dépendant du PDP-7 (la machine sur laquelle a été développé UNIX) pour permettre de porter UNIX sur d'autres machines. Alors Denis Ritchie (qui fut, avec Ken Thompson, l'un des créateurs d'UNIX) et Brian Kernighan améliorèrent le langage B pour donner naissance au langage C. En 1973, UNIX fut réécrit entièrement en langage C. Pendant 5 ans, le langage C fut limité à l'usage interne de Bell jusqu'au jour ou Brian Kernighan et Denis Ritchie publièrent une première définition du langage dans un ouvrage intitulé The C Programming Language. Ce fut le début d'une révolution dans le monde de l'informatique.

Grâce à sa puissance, le langage C devint rapidement très populaire et en 1983, l'ANSI (American National Standards Institute) décida de le normaliser en ajoutant également quelques modifications et améliorations, ce qui donna naissance en 1989 au langage tel que nous le connaissons aujourd'hui.

Les caractéristiques du langage C sont les suivants :

  • Universalité : langage de programmation par excellence, le C n'est pas confiné à un domaine particulier d'applications. Il peut être utilisé aussi bien pour l'écriture de systèmes d'exploitations que de programmes scientifiques ou de gestion, de logiciels modernes, de bases de données, de compilateurs, assembleurs ou intérpréteurs, ...
  • Souplesse : c'est un langage concis, très expressif, et les programmes écrits dans ce langage sont très compacts grâce à un jeu d'opérateurs puissant. Votre seule limite est votre imagination !
  • Puissance : le C est un langage de haut niveau mais qui permet d'effectuer des opérations de bas niveau et d'accéder aux fonctionnalités du système, ce qui est la plupart du temps impossible dans les autres langages de haut niveau.
  • Portabilité : c'est un langage qui ne dépend d'aucune plateforme matérielle ou logicielle. Le C permet en outre d'écrire des programmes portables, c'est-à-dire qui pourront être compilés pour n'importe quelle plateforme sans aucune modification.

De plus, sa popularité mais surtout l'élégance des programmes écrits en C est telle que sa syntaxe a influencé de nombreux langages dont C++ (qui est considéré comme un sur ensemble du C), JavaScript, Java, PHP et C#.

2 Programmes et fonctions

2.1 Premier Programme

Nous allons écrire un simple programme "Hello World" :

#include <stdio.h>
 
int main()
{
    printf("Hello, world\n");
    return 0;
}

main est la fonction principale du code que vous allez écrire. C'est par elle que tout va commencer quand vous allez appeler votre programme.

Selon la norme officielle du langage C, main est une fonction qui doit retourner un entier (int). Chez de nombreux systèmes (dont Windows et UNIX), cet entier est appelé le code d'erreur de l'application. En langage C, bien que cela ne soit pas forcément le cas pour le système d'exploitation, on retourne 0 pour dire que tout s'est bien passé.

Comme dans beaucoup de languages, il nous faut déclarer chaque variable que nous allons utiliser. Ici c'est simple, c'est le chargement préprocesseur stdio.h qui s'en occupera à notre place.

Ensuite, nous allons le compiler pour voir les éventuelles erreurs et pour pouvoir lancer notre premier programme :-) :

Command gcc
gcc -Wall hello_world.c -o hello_world

  • gcc : la commande qui correspond au compilateur utilisé.
  • -Wall : activation du mode warning pour les éventuelles erreurs. Très pratique pour débugger
  • hello_world.c : fichier source
  • hello_world : binaire de destination

2.1.1 Commentaires

Pour ce qui est des commentaires, voici les solutions :

// Commentaire à la façon C++ mais qui devrait fonctionner avec tous les compilos récents

ou

/* On peut commencer un paragraphe
de commentaire sans ce soucier
de quoi que ce soit jusqu'à la fin */

2.2 Fonctions

En mathématiques, on définit une fonction comme suit :

f(x) = x2 - 3

Cela signifie que f est une fonction qui reçoit en argument un réel x et qui retourne un réel : x2 - 3.

Ecrivons une fonction C que nous appellerons f, qui reçoit en argument un entier x et qui retourne également un entier : x2 - 3 :

int f(int x)
{
    return x*x - 3;
}

Maintenant, utilisons cette fonction :

#include <stdio.h>
 
int f(int); /* declaration de la fonction f */
 
int main()
{
    int x = 4;
    printf("f(%d) = %d\n", x, f(x));
    return 0;
}
 
int f(int x)
{
    return x*x - 3;
}

int f(int x) dit : f est une fonction qui nécessite en argument un int et qui retourne un int.
Le %d dans la fonction printf est ce qu'on appelle un spécificateur de format. Elle renseigne sur la manière dont nous souhaitons afficher le texte. Ici, on veut afficher les nombres 4 et 13 (f(4)). Nous disons donc à printf d'utiliser le format « nombre entier » (%d) pour les afficher. Le premier %d correspond au format que nous voulons utiliser pour afficher x et le deuxième pour f(x).

Sachez également que la variable x dans la fonction main n'a absolument rien à voir avec la variable x en paramètre de la fonction f. Chaque fonction peut avoir ses propres variables et ignore complètement ce qui se passe chez les autres fonctions.

Note : Dans une déclaration, on peut mettre le nom des arguments de la fonction (bon uniquement pour la déco et rien d'autre) :

int Surface(int Longueur, int largeur);

De même il est fortement conseillé de déclarer quel type d'argument de fonction vous allez utiliser. Utilisez plutôt :

int Surface(void); /* void : 'vide', ou 'rien' si vous préférez */

à la place de ne rien mettre comme :

int Surface(); /* Surface est une fonction. Point. */

Dans une définition (implémentation) :

  • On peut ne pas préciser le type de retour d'une fonction. Dans ce cas celle-ci est supposée retourner un int.
  • Une paire de parenthèses vide signifie que la fonction n'accepte aucun argument.

Autres remarques :

  • La déclaration d'une fonction n'est nécessaire que lorsque son utilisation précède sa définition. Cependant, il est toujours conseillé de ne définir une fonction qu'après son utilisation (ce qui requiert donc une déclaration) ne serait-ce que pour la lisibilité du programme (en effet c'est le programme qu'on veut voir à première vue, pas les petits détails).
  • Une fonction peut ne pas retourner de valeur. Son type de retour est alors void.

3 Les macros

3.1 Le préprocesseur

Avant d'être effectivement compilés, les fichiers sources C sont traités par un préprocesseur qui résout certaines directives qui lui sont données comme l'inclusion de fichier par exemple. Le préprocesseur, quoi qu'étant un programme à priori indépendant du compilateur, est un élément indispensable du langage.

Une directive donnée au préprocesseur commence toujours par #. Nous avons déjà rencontré la directive include qui permet d'inclure un fichier. La directive define permet de définir des macros.

Une macro, dans sa forme la plus simple, se définit de la manière suivante :

#define <macro> <le texte de remplacement>

Pour remplacer par exemple toutes les occurences PLUS par + :

#define PLUS +

De même, cela peut même remplacer parfois des fonctions :

#define carre(x) x * x

Dans ce cas, carre(3) sera remplacer par 3 * 3. Et pour terminer :

#define PI 3.14

Le compilateur remplace ici chaque occurrence de PI par 3.14. On peut également faire une constante symbolique :

#define USER " Toto "

3.2 Variables globales multi fichiers

Par défaut, une variable globale n’est accessible que dans le fichier source dans lequel elle est déclarée, car chaque fichier source va être compilé dans un fichier objet indépendant. Cependant, il est possible de rendre une variable globale accessible dans tous les fichiers source d’un programme grâce au mot clé extern. Cette pratique, est toutefois à éviter :

extern my_extern_var

4 Expressions et instructions

4.1 Les types de données

Type C Type correspondant
char caractère (entier de petite taille)
int entier
float nombre flottant (réel) en simple précision
double nombre flottant (réel) en double précision

Par exemple :

char ch;
unsigned char c;
unsigned int n; /* ou tout simplement : unsigned n */

La plus petite valeur possible que l'on puisse affecter à une variable de type entier non signé est 0 alors que les entiers signés acceptent les valeurs négatives.

4.1.1 int

Devant int, on peut mettre également short ou long auxquels cas on obtiendrait un entier court (short int ou tout simplement short) respectivement un entier long (long int ou tout simplement long). Voici des exemples de déclarations valides :

int n = 10, m = 5;
short a, b, c;
long x, y, z = -1;
unsigned long p = 2;

long peut être également mis devant double, le type résultant est alors long double (quadruple précision).


Devant char ou int, on peut mettre le modificateur signed ou unsigned selon que l'on veut avoir un entier signé (par défaut) ou non signé. signed int est la valeur signée (négative ou positive). unsigned int représente des valeurs positive.

Nom de type Autre Nom Intervalle de valeur Octets
int signed, unsigned int -32 768 à 32 767 2
short short_int, signed short, signed short_int -32 768 à 32 767 2
long long_int, signed long, signed long_int -2 147 493 648 à 2 147 483 647 4
unsigned unsigned int 0 à 65 535 2
unsigned short unsigned short_int 0 à 65 535 2
unsigned long unsigned long_int 0 à 4 294 967 295 4

4.1.2 Nombres entiers

  • Toute constante littérale « pure » de type entier (ex : 1, -3, 60, 40, -20, ...) est considérée par le langage comme étant de type int.
  • Pour expliciter qu'une constante littérale de type entier est de type unsigned, il suffit d'ajouter à la constante le suffixe u ou U. Par exemple : 2u, 30u, 40U, 50U, ...
  • De même, il suffit d'ajouter le suffixe l ou L pour expliciter qu'une constante littérale de type entier est de type long (on pourra utiliser le suffixe UL par exemple pour unsigned long).
  • Une constante littérale de type entier peut également s'écrire en octal (base 8) ou en hexadécimal (base 16). L'écriture en hexa est évidement beaucoup plus utilisée.
  • Une constante littérale écrite en octal doit être précédée de 0 (zéro). Par exemple : 012, 020, 030UL, etc.
  • Une constante littérale écrite en hexadécimal doit commencer par 0x (zéro x). Par exemple 0x30, 0x41, 0x61, 0xFFL, etc.

4.1.3 Nombres Flottants (floats)

Il désigne les nombres décimaux réels (positifs ou négatifs) (à virgule flottante). Toute constante littérale « pure » de type flottant (ex : 0.5, -1.2, ...) est considérée comme étant de type double.

Le suffixe f ou F permet d'expliciter un float. Attention, 1f n'est pas valide car 1 est une constante entière. Par contre 1.0f est tout à fait correcte. Le suffixe l ou L permet d'expliciter un long double.

Une constante littérale de type flottant est constituée, dans cet ordre :

  • d'un signe (+ ou -)
  • d'une suite de chiffres décimaux : la partie entière
  • d'un point : le séparateur décimal
  • d'une suite de chiffres décimaux : la partie décimale
  • d'une des deux lettres e ou E : symbole de la puissance de 10 (notation scientifique)
  • d'un signe (+ ou -)
  • d'une suite de chiffres décimaux : la puissance de 10

Par exemple, les constantes littérales suivantes représentent bien des nombres flottants : 1.0, -1.1f, 1.6E-19, 6.02e23L, 0.5 3e8

Nom de type Autre Nom Intervalle
Floats - -3.4E38 à +3.4E38
double -1.7E308 à +1.7E308
long double -1.7E308 à +1.7E308

4.1.4 Type char

char est utilisé pour désigner des caractères :

Nom de type Autre Nom Intervalle Octets
char signed char -128 à 127 1
unsigned char -0 à 255 1
  • Les types de données de base ne contiennent qu’une seule valeur.
  • Les types dérivés ou agrégats peuvent contenir plus d’une valeur.
    • Exemple: chaînes, tableaux, structures, énumérations, unions, pointeurs.

4.2 Spécification de format dans sprintf

Voici la liste des codes format que nous utiliserons les plus souvent par la suite :

Code format Utilisation
c Afficher un caractère
d Afficher un int
u Afficher un unsigned int
x, X Afficher un entier dans le format hexadécimal
f Afficher un float ou un double en notation décimale
e Afficher un float ou un double en notation scientifique avec un petit e
E Afficher un float ou un double en notation scientifique avec un grand E
g, G Afficher un float ou un double (utilise le format le plus adapté)
% Afficher le caractère '%'

De plus :

  • h devant d ou u indique que l'argument est un short
  • l devant d, u, x ou X indique que l'argument est de type long
  • L devant f, e, E ou g indique que l'argument est de type long double

Il y a bien plus de possibilités (qu'il faut connaître !) que je ne détaillerais pas ici (mais nous utiliserons certaines d'entre elles le moment venu). N'hésitez donc surtout pas à consulter la doc.

4.3 Les variables et les constantes

4.3.1 Les constantes

En C il existe 4 types de base:

  • Les constantes entières.
  • Les constantes en virgule flottante (constante réelle).
  • Les constantes caractères.
  • Les constantes chaînes de caractère.

Les 2 premières constantes sont des constantes numériques.

  • Les valeurs numériques entières sont : décimal, octal et hexadécimal.
  • En base 10 : un point décimal, un exposant (Note: la précision dépends des compilateurs (min : 6 chiffres, max : 18 chiffres))
  • Caractères unique entre apostrophes (128 caractères) :
    • Caractère : 0...9 A...Z a...z
    • Code ASCII : 48...57 65...90 97...122
  • Suite de caractères entre guillemets.
    • Le compilateur place automatiquement le caractère nul '\0' en fin de chaîne (invisible). "bonjour" est en fait "bonjour\0" ce qui est utile dans les programmes pour marquer une fin de chaîne. 'A' est différent de "A" car 'A' est un caractère avec la valeur ASCII 48 alors que "A" est une chaîne ("A\0") sans valeur numérique. "A" occupe plus de place que 'A'.

Voici comment créer des variables :

int a, b, c;
int i = 0, j, n = 10;
double x, y, z = 0.5;

Une déclaration avec initialisation donne :

int n = 0;

Alors que voici une déclaration, suivit d'une affectation :

int n;
n = 0;

Une constante est une variable dont la particularité est d'être en lecture seule (la valeur d'une constante ne peut pas être modifiée) :

const int n = 10;

N'oubliez pas de déclarer toutes vos variables ou constantes sous peine d'erreurs.

4.3.2 Les variables

Une variable est une valeur d'un type définit qui peut être modifiée :

int a;
a = 10;
int a = 9;

4.4 Définition de nouveaux types

Le C dispose d'un mécanisme très puissant permettant au programmeur de créer de nouveaux types de données en utilisant le mot clé typedef :

typedef int ENTIER;

Définit le type ENTIER comme n'étant autre que le type int. Rien ne nous empêche donc désormais d'écrire :

ENTIER a, b;

Bien que dans ce cas, un simple #define aurait pu suffire, il est toujours recommandé d'utiliser typedef qui est beaucoup plus sûr.

Pour déclarer les fonctions sans les définir, on utilise les prototypes. Les structures peuvent être déclarées de la manière suivante :

class myclass;

4.5 Les pointeurs

Comme nous le savons très bien, l'endroit où s'exécute un programme est la mémoire donc toutes les données du programme (les variables, les fonctions, ...) se trouvent en mémoire. Le langage C dispose d'un opérateur & permettant de récupérer l'adresse en mémoire d'une variable ou d'une fonction quelconque. Par exemple, si n est une variable, &n désigne l'adresse de n.

Le C dispose également d'un opérateur * permettant d'accéder au contenu de la mémoire dont l'adresse est donnée. Par exemple, supposons qu'on ait :

int n;

Alors les instructions suivantes sont strictement identiques :

n = 10;
*( &n ) = 10;

Un pointeur (ou une variable de type pointeur) est une variable destinée à recevoir une adresse. On dit alors qu'elle pointe sur un emplacement mémoire. L'accès au contenu de la mémoire se fait par l'intermédiaire de l'opérateur *.

Voici comment déclarer une variable p destinée à recevoir l'adresse d'une variable de type int :

int *p;
int * o;

J'en ai mis une autre avec o, juste pour montrer qu'en C on peut mettre autant d'espaces que l'on veut. Et maintenant, voici d'autres exemples :

int * p1, p2, p3; /* Seul p1 est de type int *. Les autres sont simplement des int.*/
int *p1, *p2, *p3; /* Là évidement, ils sont tous des int*/
/*Utilisation simplifiée avec un typedef :*/
typedef int * PINT;
PINT p1, p2, p3;

Par contre cela n'aurait pas marché si on avait défini PINT à l'aide d'un #define car cela nous amènerait au premier exemple.

5 Entrées et sorties

5.1 Saisir des données tapées au clavier avec la fonction scanf

Il faut bien comprendre 2 choses avant de commencer :

  • Si nous voulons afficher un entier (avec printf), nous devons afficher l'entier (la valeur de la variable).
  • Si nous voulons demander à l'utilisateur (celui qui utilise notre programme) de taper un nombre puis ranger le nombre ainsi tapé dans une variable, nous devons fournir l'adresse de la variable dans laquelle nous souhaitons ranger le nombre entré.

Un exemple aidera la compréhension :

#include <stdio.h>
 
int main()
{
    int a, b, c;
 
    printf("Ce programme calcule la somme de 2 nombres.\n");
 
    printf("Entrez la valeur de a : ");
    scanf("%d", &a);
 
    printf("Entrez la valeur de b : ");
    scanf("%d", &b);
 
    c = a + b;
    printf("%d + %d = %d\n", a, b, c);
 
    return 0;
}

Attention aux espaces et ce que vous demandez de taper avec scanf. Par exemple, si vous souhaitez que la personne tapes 'ans' en + d'un nombre :

scanf("%d ans", &a);

L'utilisateur qui tapera son age sera obligé de taper "x ans". Attention donc à ce qu'il faut taper avec scanf.

C'est ce qu'on appelle une saisie formatée. Les fonctions telles que scanf sont plutôt destinées à être utilisées pour lire des données provenant d'un programme sûr (par l'intermédiaire d'un fichier par exemple), pas celles provenant d'un humain, qui sont sujettes à l'erreur. Les codes format utilisés dans scanf sont à peu près les mêmes que dans printf, sauf pour les flottants notamment.

Code format Utilisation
f, e, g float
lf, le, lg double
Lf, Le, Lg long double

Voici un programme qui permet de calculer le volume d'un cône droit à base circulaire selon la formule : V = 1/3 * (B * h) où B est la surface de base soit pour une base circulaire : B = PI*R2, où R est le rayon de la base.

#include <stdio.h>
 
double Volume(double r_base, double hauteur);
 
int main()
{
    double R, h, V;
 
    printf("Ce programme calcule le volume d'un cone.\n");
 
    printf("Entrez le rayon de la base : ");
    scanf("%lf", &R);
 
    printf("Entrez la hauteur du cone : ");
    scanf("%lf", &h);
 
    V = Volume(R, h);
    printf("Le volume du cone est : %f", V);
 
    return 0;
}
 
double Volume(double r_base, double hauteur)
{
    return (3.14 * r_base * r_base * hauteur) / 3;
}

5.2 Exemple de permutation des contenus de deux variables

Cette fonction doit donc pouvoir localiser les variables en mémoire autrement dit nous devons passer à cette fonction les adresses des variables dont on veut permuter le contenu :

#include <stdio.h>
 
void permuter(int * addr_a, int * addr_b);
 
int main()
{
    int a = 10, b = 20;
 
    permuter(&a, &b);
    printf("a = %d\nb = %d\n", a, b);
 
    return 0;
}
 
void permuter(int * addr_a , int * addr_b)
/***************\
* addr_a <-- &a *
* addr_b <-- &b *
\***************/
{
    int c;
 
    c = *addr_a;
    *addr_a = *addr_b;
    *addr_b = c;
}

5.3 Opérateurs arithmétiques courants

Les opérateurs arithmétiques courants +, -, * et / existent en langage C. Toutefois, la division entière est un tout petit peu délicate. En effet, si a et b sont des entiers, a / b vaut le quotient de a et b c'est-à-dire par exemple, 29 / 5 vaut 5. Le reste d'une division entière s'obtient avec l'opérateur modulo %, c'est-à-dire, en reprenant l'exemple précédent, 29 % 5 vaut 4.

5.4 Opérateurs de comparaison

Opérateur Rôle
< Inférieur à
> Supérieur à
== Egal à
<= Inférieur ou égal à
>= Supérieur ou égal à
!= Différent de

5.5 Opérateurs logiques

Opérateur Rôle
&& ET
|| OU
! NON
int prop1, prop2, prop_ou, prop_et, prop_vrai;
prop1 = (1 < 1000);
prop2 = (2 == -6);
prop_ou = prop1 || prop2; /* VRAI, car prop1 est VRAI */
prop_et = prop1 && prop2; /* FAUX, car prop2 est FAUX */
prop_vrai = prop1 && !prop_2 /* VRAI car prop1 et !prop2 sont VRAI */

5.6 Sizeof : Taille des données

La taille d'une donnée désigne la taille, en octets, que celle-ci occupe en mémoire. Par extension de cette définition, la taille d'un type de données désigne la taille d'une donnée de ce type. Attention ! octet désigne ici, par abus de langage, la taille d'un élément de mémoire sur la machine cible (la machine abstraite), c'est-à-dire la taille d'une case mémoire (qui vaut 8 bits dans la plupart des architectures actuelles), et non un groupe de 8 bits. En langage C, un octet (une case mémoire) est représenté par un char. La taille d'un char n'est donc pas forcément 8 bits, même si c'est le cas dans de nombreuses architectures, mais dépendante de la machine. La norme requiert toutefois qu'un char doit faire au moins 8 bits et que la macro CHAR_BIT, déclarée dans limits.h, indique la taille exacte d'un char sur la machine cible.

Le C dispose d'un opérateur, sizeof, permettant de connaître la taille, en octets, d'une donnée ou d'un type de données. La taille d'un char vaut donc évidemment 1 puisqu'un char représente un octet. Par ailleurs, il ne peut y avoir de type dont la taille n'est pas multiple de celle d'un char. Le type de la valeur retournée par l'opérateur sizeof est size_t, déclaré dans stddef.h, qui est inclus par de nombreux fichiers d'en-tête dont stdio.h.

Comme nous l'avons déjà dit plus haut, la taille des données est dépendante de la machine cible. En langage C, la taille des données n'est donc pas fixée. Néanmoins la norme stipule qu'on doit avoir :

sizeof (char) <= sizeof (short) <= sizeof (int) <= sizeofd(long)

Sur un processeur Intel (x86) 32 bits par exemple, un char fait 8 bits, un short 16 bits, et les int et les long 32 bits.

5.7 Les opérateurs d'incrémentation et de décrémentation

Tout comme dans beaucoups de languages :

#include <stdio.h>
 
int main()
{
    int i = 1, j;
    j = ++i; /* j = 1+1 => j=2 */
    printf ("j (++i) = %d\n", j); 
    j = --i; /* j = 2-1 => j=1 */
    printf ("j (--i) = %d\n", j); 
    return 0;
}

Que l'on note ++i ou i++ peut importe, l'effet est le même.

5.8 Expression conditionnelle

Une expression conditionnelle est une expression dont la valeur dépend d'une condition. L'expression :

p ? a : b

vaut a si p est vrai et b si p est faux.

Pour des opérations d'affectation, ce sont les opérateurs : +=, -=, *=, /=, ...

x += a;

par exemple est équivalent à : </syntaxhighlight> x = x + a; </syntaxhighlight> Les opérateurs sont classés par ordre de priorité. Voici les opérateurs que nous avons étudiés jusqu'ici classés dans cet ordre. Opérateur Associativité

Opérateur Associativité
Parenthèses de gauche à droite
! ++ -- - (signe) sizeof de gauche à droite
* / % de gauche à droite
+ - de gauche à droite
< <= > >= de gauche à droite
== != de gauche à droite
& (adresse de) de gauche à droite
&& de gauche à droite
|| de gauche à droite
Opérateurs d'affectation (= += ...) de droite à gauche
, de gauche à droite

Ce n'est pas parce que cet ordre existe qu'il faut le retenir par coeur. Pour du code lisible, il est même conseillé de ne pas trop en tenir compte et d'utiliser des parenthèses dans les situations ambiguës.

6 Les caractères

La représentation numérique des caractères définit ce qu'on appelle un jeu de caractères. Par exemple, dans le jeu de caractères ASCII (American Standard Code for Information Interchange) qui est un jeu de caractères qui n'utilise que 7 bits et qui est à la base de nombreux codes populaires de nos jours, le caractère 'A' est représenté par le code 65, le caractère 'a' par 97 et '0' par 48. Hélas, même ASCII ne définit pas le langage C. En effet si le C dépendait d'un jeu de caractères particulier, il ne serait alors pas totalement portable. Le standard définit néanmoins un certain nombre de caractères que tout environnement compatible avec le C doit posséder parmi lesquels les 26 lettres de l'alphabet latin (donc en fait 52 puisqu'on différencie les majuscules et les minuscules), les 10 chiffres décimaux, les caractères # < > ( ) etc. Le programmeur (mais pas le compilateur) n'a pas besoin de connaître comment ces caractères sont représentés dans le jeu de caractères de l'environnement. Le standard ne définit donc pas un jeu de caractères mais seulement un ensemble de caractères que chaque environnement compatible est libre d'implémenter à sa façon (plus éventuellement les caractères spécifiques à cet environnement). La seule contrainte imposée est que leur valeur doit pouvoir tenir dans un char.

Concernant la technique d'échappement, sachez également qu'on peut insérer du code octal (commençant par 0) ou hexadécimal (commençant par x) après le caractère d'échappement \ pour obtenir un caractère dont le code dans le jeu de caractères est donné. L'hexadécimal est de loin le plus utilisé. Par exemple : '\x30', '\x41', '\x61', ... Et enfin pour les caractères de code 0, 1, ... jusqu'à 7, on peut utiliser les raccourcis '\0', '\1', ... '\7'.

Le dépassement de capacité a lieu lorsqu'on tente d'affecter à une lvalue une valeur plus grande que ce qu'elle peut contenir. Par exemple, en affectant une valeur sur 32 bits à une variable ne pouvant contenir que 16 bits.

6.1 Conversions

6.1.1 Implicites

En langage C, des règles de conversion dite implicite s'appliquent aux données qui composent une expression complexe lorsqu'ils ne sont pas de même type (entier avec un flottant, entier court avec entier long, entier signé avec un entier non signé, etc.). Par exemple, dans l'expression :

'A' + 2

'A' est de type char et 2 de type int. Dans ce cas, 'A' est tout d'abord converti en int avant que l'expression ne soit évaluée. Le résultat de l'opération est de type int (car un int + un int donne un int). Ici, il vaut 67 (65 + 2). En fait, les char et les short sont toujours systématiquement convertis en int c'est-à-dire que dans l'addition de deux char par exemple, tous deux sont tout d'abord convertis en int avant d'être additionnés, et le résultat est un int (pas un char). Un unsigned char sera converti en unsigned int, et ainsi de suite.

En règle générale : le type le plus « faible » est convertit dans le type le plus « fort ». Par exemple, les entiers sont plus faibles que les flottants donc 1 mélangé à un flottant par exemple sera tout d'abord converti en 1.0 avant que l'opération n'ait effectivement lieu.

Le compilateur convertit le rang inférieur vers le rang supérieur (= promotion) :

char < short < int < long < float < double

6.1.2 Explicites (cast)

Il suffit de préciser le type de destination entre parenthèses devant l'expression à convertir. Par exemple :

float f;
f = (float)3.1416;

Dans cet exemple, on a converti explicitement 3.1416, qui est de type double, en float. Lorsqu'on affecte un flottant à un entier, seule la partie entière, si elle peut être représentée, est retenue.

7 Les instructions

7.1 if

Permet d'effectuer des choix conditionnels. La syntaxe de l'instruction est la suivante :

if ( <expression> )
    <une et une seule instruction>
else
    <une et une seule instruction>

Une instruction if peut ne pas comporter de else. Lorsqu'on a plusieurs instructions if imbriquées, un else se rapporte toujours au dernier if suivi d'une et une seule instruction. Par exemple : écrivons un programme qui compare deux nombres.

#include <stdio.h>
 
int main()
{
    int a, b;
 
    printf("Ce programme compare deux nombres.\n");
 
    printf("Entrez la valeur de a : ");
    scanf("%d", &a);
 
    printf("Entrez la valeur de b : ");
    scanf("%d", &b);
 
    if (a < b)
        printf("a est plus petit que b.\n");
    else
        if (a > b)
            printf("a est plus grand que b.\n");
        else
            printf("a est egal a b.\n");
 
    return 0;
}

7.2 do

Permet d'effectuer une boucle. La syntaxe de l'instruction est la suivante :

do
    <une et une seule instruction>
while ( <expression> );

L'instruction do permet d'exécuter une instruction tant que <expression> est vraie. Le test est fait après chaque exécution de l'instruction. Voici un programme qui affiche 10 fois Bonjour.

#include <stdio.h>
 
int main()
{
    int nb_lignes_affichees = 0;
 
    do
        {
            printf("Bonjour.\n");
            nb_lignes_affichees++;
        }
    while (nb_lignes_affichees < 10);
 
    return 0;
}

7.3 while

Permet d'effectuer une boucle. La syntaxe de l'instruction est la suivante :

while ( <expression> )
    <une et une seule instruction>

L'instruction while permet d'exécuter une instruction tant que <expression> est vraie. Le test est fait avant chaque exécution de l'instruction. Donc si la condition (<expression>) est fausse dès le départ, la boucle ne sera pas exécutée.

7.4 for

Permet d'effectuer une boucle. La syntaxe de l'instruction est la suivante :

for ( <init> ; <condition> ; <step>)
    <instruction>

Elle est pratiquement identique à :

<init>;
while ( <condition> )
{
    <instruction>
    <step>
}

Par exemple, écrivons un programme qui affiche la table de multiplication par 5.

#include <stdio.h>
 
int main()
{
    int n;
 
    for(n = 0; n <= 10; n++)
        printf("5 x %2d %2d\n", n, 5 * n);
 
    return 0;
}

Le format %2d permet d'afficher un entier avec 2 caractères au minimum (l'espace restant sera rempli par des espaces).

7.5 break

Permet de sortir immédiatement d'une boucle ou d'un switch. La syntaxe de cette instruction est :

break;

7.6 Switch et case

Ces instructions permettent d'éviter des instructions if trop imbriquées comme illustré par l'exemple suivant :

#include <stdio.h>
 
int main()
{
    int n;
 
    printf("Entrez un nombre entier : ");
    scanf("%d", &n);
 
    switch(n)
    {
    case 0:
        printf("Cas de 0.\n");
        break;
 
    case 1:
        printf("Cas de 1.\n");
        break;
 
    case 2: case 3:
        printf("Cas de 2 ou 3.\n");
        break;
 
    case 4:
        printf("Cas de 4.\n");
        break;
 
    default:
        printf("Cas inconnu.\n");
    }
 
    return 0;
}

7.7 Continue

Dans une boucle, permet de passer immédiatement à l'itération suivante. Par exemple, modifions le programme table de multiplication de telle sorte qu'on affiche rien pour n = 4 ou n = 6.

#include <stdio.h>
 
int main()
{
    int n;
 
    for(n = 0; n <= 10; n++)
    {
        if ((n == 4) || (n == 6))
            continue;
 
        printf("5 x %2d %2d\n", n, 5 * n);
    }
 
    return 0;
}

7.8 Return

Permet de terminer une fonction. La syntaxe de cette instruction est la suivante :

return <expression>; /* termine la fonction et retourne <expression> */

ou :

return; /* termine la fonction sans spécifier de valeur de retour */

8 Tableaux, pointeurs et chaînes de caractères

Un tableau est une variable qui regroupe une ou plusieurs données de même type. L'accès à un élément du tableau se fait par un système d'indice, l'indice du premier élément étant 0. Par exemple :

int t[10];

déclare un tableau de 10 éléments (de type int) dont le nom est t. Les éléments du tableau vont donc de t[0], t[1], t[2] ... à t[9]. t est une variable de type tableau, plus précisément (dans notre cas), une variable de type tableau de 10 int (int [10]). Les éléments du tableau sont des int. Toutes les règles s'appliquant aux variables s'appliquent également aux éléments d'un tableau.

char msg[ ] = "bonjour";
char msg[8] = "bonjour"; // le 8 correspond au nombre de lettres du mot et bonjour plus le '\0'.

8.1 Initialisation

On peut initialiser un tableau à l'aide des accolades. Par exemple :

int t[10] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90};

Bien évidemment, on n'est pas obligé d'initialiser tous les éléments, on aurait donc pu par exemple nous arrêter après le 5ème élément, et dans ce cas les autres éléments du tableau seront automatiquement initialisés à 0. Attention ! une variable locale non initialisée contient « n'importe quoi », pas 0 !

Lorsqu'on déclare un tableau avec initialisation, on peut ne pas spécifier le nombre d'éléments car le compilateur le calculera automatiquement. Ainsi, la déclaration :

int t[] = {0, 10, 20, 30};

est strictement identique à :

int t[4] = {0, 10, 20, 30};

8.1.1 Calculer la taille d'un tableau

La taille d'un tableau est évidemment le nombre d'éléments du tableau multiplié par la taille de chaque élément. Ainsi, le nombre d'éléments dans un tableau est égal à sa taille divisée par la taille d'un élément. On utilise alors généralement la formule sizeof(t) / sizeof(t[0]) pour connaître le nombre d'éléments d'un tableau t. La macro définie ci-dessous permet de calculer la taille d'un tableau :

#define COUNT(t) (sizeof(t) / sizeof(t[0]))

8.2 Multidimentions

On peut également créer un tableau à plusieurs dimensions. Par exemple :

int t[10][3];

Un tableau à plusieurs dimensions n'est en fait rien d'autre qu'un tableau (tableau à une dimension) dont les éléments sont des tableaux. Comme dans le cas des tableaux à une dimension, le type des éléments du tableau doit être parfaitement connu. Ainsi dans notre exemple, t est un tableau de 10 tableaux de 3 int, ou pour vous aider à y voir plus clair :

typedef int TRIPLET[3];
TRIPLET t[10];

Les éléments de t vont de t[0] à t[9], chacun étant un tableau de 3 int.

On peut bien entendu créer des tableaux à 3 dimensions, 4, 5, 6, ...

On peut également initialiser un tableau à plusieurs dimensions. Par exemple :

int t[3][4] = { {0, 1, 2, 3},
                {4, 5, 6, 7},
                {8, 9, 10, 11} };

Qu'on aurait également pu tout simplement écrire :

int t[][4] = { {0, 1, 2, 3},
               {4, 5, 6, 7},
               {8, 9, 10, 11} };

9 Arithmétique des pointeurs

9.1 Introduction au calcul d'adresses

Voici un exemple de pointeur :

t[5] = '*';

Dans la pratique, on utilise un pointeur sur un élément du tableau, généralement le premier. Cela permet d'accéder à n'importe quel élément du tableau par simple calcul d'adresse. Comme nous l'avons dit plus haut : t + 1 est équivalent à &(t[1]), t + 2 à &(t[2]), etc.

Voici un exemple qui montre une manière de parcourir un tableau :

#include <stdio.h>
 
#define COUNT(t) (sizeof(t) / sizeof(t[0]))
 
void Affiche(int * p, size_t nbElements);
 
int main()
{
    int t[10] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90};
 
    Affiche(t, COUNT(t));
 
    return 0;
}
 
void Affiche(int * p, size_t nbElements)
{
    size_t i;
 
    for(i = 0; i < nbElements; i++)
        printf("%d\n", p[i]);
}

9.2 L'arithmétique des pointeurs

L'arithmétique des pointeurs est née des faits que nous avons établis précédemment. En effet si p pointe sur un élément d'un tableau, p + 1 doit pointer sur l'élément suivant. Donc si la taille de chaque élément du tableau est par exemple de 4, p + 1 déplace le pointeur de 4 octets (où se trouve l'élément suivant) et non de un.

De même, puisque l'on devrait avoir (p + 1) - p = 1 et non 4, la différence entre deux adresses donne le nombre d'éléments entre ces adresses et non le nombre d'octets entre ces adresses. Le type d'une telle expression est ptrdiff_t, qui est défini dans le fichier stddef.h.

Et enfin, l'écriture p[i] est strictement équivalente à *(p + i).

Cela montre à quel point le typage des pointeurs est important. Cependant, il existe des pointeurs dits génériques capables de pointer sur n'importe quoi. Ainsi, la conversion d'un pointeur générique en un pointeur d'un autre type par exemple ne requiert aucun cast et vice versa.

9.3 Pointeurs génériques

Le type des pointeurs génériques est void *. Comme ces pointeurs sont génériques, la taille des données pointées est inconnue et l'arithmétique des pointeurs ne s'applique donc pas à eux. De même, puisque la taille des données pointées est inconnue, l'opérateur d'indirection * ne peut être utilisé avec ces pointeurs, un cast est alors obligatoire. Par exemple :

int n;
void * p;
 
p = &n;
*((int *)p) = 10; /* p etant desormais vu comme un int *, on peut alors lui appliquer l'operateur *. */

Etant donné que la taille de toute donnée est multiple de celle d'un char, le type char * peut être également utilisé en tant que pointeur universel. En effet, une variable de type char * est un pointeur sur octet autrement dit peut pointer n'importe quoi. Cela s'avère pratique des fois (lorsqu'on veut lire le contenu d'une mémoire octet par octet par exemple) mais dans la plupart des cas, il vaut mieux toujours utiliser les pointeurs génériques. Par exemple, la conversion d'une adresse de type différent en char * et vice versa nécessite toujours un cast, ce qui n'est pas le cas avec les pointeurs génériques.

Dans printf, le spécificateur de format %p permet d'imprimer une adresse (void *) dans le format utilisé par le système.

Et pour terminer, il existe une macro à savoir NULL, définie dans stddef.h, permettant d'indiquer q'un pointeur ne pointe nulle part. Son intérêt est donc de permettre de tester la validité d'un pointeur et il est conseillé de toujours initialiser un pointeur à NULL.

9.4 Exemple avec un tableau à plusieurs dimensions

Soit :

int t[10][3];

Définissons le type TRIPLET par :

typedef int TRIPLET[3];

De façon à avoir :

TRIPLET t[10];

Vu d'un pointeur, t représente l'adresse de t[0] (qui est un TRIPLET) donc l'adresse d'un TRIPLET. En faisant t + 1, on se déplace donc d'un TRIPLET soit de 3 int.

D'autre part, t peut être vu comme un tableau de 30 int (3 * 10 = 30), on peut donc accéder à n'importe quel élément de t à l'aide d'un pointeur sur int.

Soit p un pointeur sur int et faisons :

p = (int *)t;

On a alors, numériquement, les équivalences suivantes :

t 	p
t + 1 	p + 3
t + 2 	p + 6
... 	 
t + 9 	p + 27

Prenons alors à présent, le 3ème TRIPLET de t soit t[2].

Puisque le premier élément de t[2] se trouve à l'adresse t + 2 soit p + 6, deuxième se trouve en p + 6 +1 et le troisième et dernier en p + 6 + 2. A près cet entier, on se retrouve au premier élément de t[3], en p + 9.

En conclusion, pour un tableau déclaré :

<type> t[N][M];

on a la formule :

t[i][j] = *(p + N*i + j) /* ou encore p[N*i + j] */

Où évidemment : p = (int *)t.

Et on peut bien sur étendre cette formule pour n'importe quelle dimension.

10 Les chaînes de caractères

Par définition, une chaîne de caractères, ou tout simplement : chaîne, est une suite finie de caractères. Par exemple, "Bonjour", "3000", "Salut !", "EN 4", ... sont des chaînes de caractères. En langage C, une constante de type chaîne de caractères s'écrit entre double quottes, exactement comme dans les exemples donnés ci-dessus.

La longueur d'une chaîne est le nombre de caractères qu'elle comporte. Par exemple, la chaîne "Bonjour" comporte 7 caractères ('B', 'o', 'n', 'j', 'o', 'u' et 'r'). Sa longueur est donc 7. En langage C, la fonction strlen, déclarée dans le fichier string.h, permet d'obtenir la longueur d'une chaîne passée en argument. Ainsi, strlen("Bonjour") vaut 7.

10.1 Représentation des chaînes de caractères en langage C

Comme nous l'avons déjà mentionné plus haut, les constantes de type chaîne de caractères s'écrit en langage C entre double quottes. En fait, le langage C ne dispose pas vraiment de type chaîne de caractères. Une chaîne est tout simplement représentée à l'aide d'un tableau de caractères.

Cependant, les fonctions manipulant des chaînes doivent être capables de détecter la fin d'une chaîne donnée. Autrement dit, toute chaîne de caractères doit se terminer par un caractère indiquant la fin de la chaîne. Ce caractère est le caractère '\0' et est appelé le caractère nul ou encore caractère de fin de chaîne. Son code ASCII est 0. Ainsi la chaîne "Bonjour" est en fait un tableau de caractères dont les éléments sont 'B', 'o', 'n', 'j', 'o', 'u', 'r', '\0', autrement dit un tableau de 8 caractères et on a donc "Bonjour"[0] = 'B', "Bonjour"[1] = 'o', "Bonjour"[2] = 'n', ... "Bonjour"[7] = '\0'. Toutefois, comme il s'agit d'une constante (constante chaîne de caractères), le contenu de la mémoire allouée pour la chaîne "Bonjour" ne peut être modifié.

Les fonctions de manipulation de chaîne de la bibliothèque standard du langage C sont principalement déclarées dans le fichier string.h. Voici un exemple d'utilisation d'une de ces fonctions :

#include <stdio.h>
#include <string.h>
 
int main()
{
    char t[50];
 
    strcpy(t, "Hello, world!");
    printf("%s\n", t);
 
    return 0;
}

Dans cet exemple, la chaîne t ne peut contenir tout au plus que 50 caractères, caractère de fin de chaîne inclus. Autrement dit t ne peut que contenir 49 caractères « normaux » car il faut toujours réserver une place pour le caractère de fin de chaîne : '\0'. On peut aussi bien sûr initialiser une chaîne au moment de sa déclaration, par exemple :

char s[50] = "Bonjour";

Qui est strictement équivalente à :

char s[50] = { 'B', 'o', 'n', 'j', 'o', 'u', 'r', '\0'};

Puisque, vu d'un pointeur, la valeur d'une expression littérale de type chaîne n'est autre que l'adresse de son premier élément, on peut utiliser un simple pointeur pour manipuler une chaîne. Par exemple :

char * p = "Bonjour";

Dans ce cas, p pointe sur le premier élément de la chaîne "Bonjour". Or, comme nous l'avons déjà dit plus haut, la mémoire allouée pour la chaîne "Bonjour" est en lecture seule donc on ne peut pas écrire par exemple :

p[2] = '*'; /* Interdit */

Avec un tableau, ce n'est pas l'adresse en mémoire de la chaîne qui est stockée, mais les caractères de la chaîne, copiés caractère par caractère. La mémoire utilisée par le tableau étant indépendante de celle utilisée par la chaîne source, on peut faire ce qu'on veut de notre tableau. La fonction strcpy permet de copier une chaîne vers un autre emplacement mémoire.

Le paragraphe suivant discute des fonctions de manipulation de chaînes en langage C.

10.2 strcpy, strncpy

#include <stdio.h>
#include <string.h>
 
int main()
{
    char t1[50], t2[50];
 
    strcpy(t1, "Hello, world!");
    strcpy(t2, "*************");
    strncpy(t1, t2, 3);
    printf("%s\n", t1);
 
    return 0;
}

Attention ! Si t1 n'est pas assez grand pour pouvoir contenir la chaîne à copier, vous aurez un débordement de tampon (buffer overflow). Un tampon (ou buffer) est tout simplement une zone de la mémoire utilisée par un programme pour stocker temporairement des données. Par exemple, t1 est un buffer de 50 octets. Il est donc de la responsabilité du programmeur de ne pas lui passer n'importe quoi ! En effet en C, le compilateur suppose que le programmeur sait ce qu'il fait !

La fonction strncpy s'utilise de la même manière que strcpy. Le troisième argument indique le nombre de caractères à copier. Aucun caractère de fin de chaîne n'est automatiquement ajouté.

10.3 strcat, strncat

Ceci va donc rajouter à la suite d'un tableau des caractères :

#include <stdio.h>
#include <string.h>
 
int main()
{
    char t[50];
 
    strcpy(t, "Hello, world");
    strcat(t, " from");
    strcat(t, " strcpy");
    strcat(t, " and strcat");
    printf("%s\n", t);
 
    return 0;
}

Ce qui donnera :

Hello, world from strcpy and strcat

10.4 strlen

Retourne le nombre de caractères d'une chaîne.

10.5 strcmp, strncmp

On n'utilise pas l'opérateur == pour comparer des chaînes car ce n'est pas les adresses qu'on veut comparer mais le contenu mémoire. La fonction strcmp compare deux chaînes de caractères et retourne :

  • zéro si les chaînes sont identiques
  • un nombre négatif si la première est "inférieure" (du point de vue lexicographique) à la seconde
  • et un nombre positif si la première est "supérieure" (du même point de vue ...) à la seconde

Ainsi, à titre d'exemple, dans l'expression

strcmp("clandestin", "clavier")

La fonction retourne un nombre négatif car, 'n' étant plus petit que 'v' (dans le jeu de caractères ASCII, ça n'a rien à voir avec le langage C), "clandestin" est plus petit que "clavier".

10.6 Implémentation de quelques fonctions de manipulation de chaîne

Nous allons donc implémenter deux fonctions de manipulation de chaîne à savoir str_len et str_cpy, qui s'utiliseront de la même manière que leurs bessons strlen et strcpy.

size_t str_len(char * t)
{
    size_t len;
 
    for(len = 0; t[len] != '\0'; len++)
        /* On ne fait rien, on laisse seulement boucler */ ;
 
    return len;
}
char * str_cpy(char * dest, char * source)
{
    int i;
 
    for(i = 0; source[i] != '\0'; i++)
        dest[i] = source[i];
 
    dest[i] = '\0';
 
    return dest;
}

Remarquez bien la manière dont nous avons implémenté la fonction str_cpy. Vous vous attendiez peut-être à ce que cette fonction retourne void et non un char *. Et bien non ! De nombreuses fonctions de la bibliothèque standard utilisent également cette « convention », ce qui permet d'écrire du code du genre :

char s[50] = "Bonjour";
printf("%s\n", strcat(s, " tout le monde !"));

Les entrées/sorties (E/S) ne font pas vraiment partie du langage C car ces opérations sont dépendantes du système. Donc pour réaliser des opérations d'entrée/sortie en langage C il faut en principe passer par les fonctionnalités de bas niveau du système. Néanmoins sa bibliothèque standard est fournie avec des fonctions permettant d'effectuer de telles opérations afin de faciliter l'écriture de code portable. Les fonctions et types de données liées aux entrées/sorties sont principalement déclarés dans le fichier stdio.h (standard input/output).

Les entrées/sorties en langage C se font par l'intermédiaire d'entités logiques, appelés flux, qui représentent des objets externes au programme, appelés fichiers. Un fichier peut être ouvert en lecture, auquel cas il est censé nous fournir des données (c'est-à-dire être lu) ou ouvert en écriture, auquel cas il est destiné à recevoir des données provenant du programme. Un fichier peut être à la fois ouvert en lecture et en écriture. Une fois qu'un fichier est ouvert, un flux lui est associé. Un flux d'entrée est un flux associé à un fichier ouvert en lecture et un flux de sortie un flux associé à un fichier ouvert un écriture.

Lorsque les données échangées entre le programme et le fichier sont de type texte, la nécessité de définir ce qu'on appelle une ligne est primordiale. En langage C, une ligne est une suite de caractères terminée par le caractère de fin de ligne (inclus) : '\n'. Par exemple, lorsqu'on effectue des saisies au clavier, une ligne correspond à une suite de caractères terminée par ENTREE. Puisque la touche ENTREE termine une ligne, le caractère généré par l'appui de cette touche est donc, en C standard, le caractère de fin de ligne soit '\n'.

Lorsque le système exécute un programme, trois fichiers sont automatiquement ouverts :

  • l'entrée standard par défaut le clavier
  • la sortie standard, par défaut l'écran (ou la console)
  • et l'erreur standard, par défaut associé à l'écran (ou la console)

Vous connaissez la suite, pour les redirections dans des fichiers (< >).

10.7 lire un caractère, puis l'afficher

La macro getc permet de lire un caractère sur un flux d'entrée. La macro putc permet d'écrire un caractère sur un flux de sortie. Voici un programme simple qui montre comment utiliser les macros getc et putc :

#include <stdio.h>
 
int main()
{
    int c; /* le caractere */
 
    printf("Veuillez taper un caractere : ");
    c = getc(stdin);
 
    printf("Vous avez tape : ");
    putc(c, stdout);
 
    return 0;
}

Vous vous demandez certainement la raison pour laquelle on a utilisé int plutôt que char dans la déclaration de c. Et bien tout simplement parce que getc retourne un int (de même putc attend un argument de type int). Mais justement : Pourquoi ? Et bien parce que getc doit pouvoir non seulement retourner le caractère lu (un char) mais aussi une valeur qui ne doit pas être un char pour signaler qu'aucun caractère n'a pu être lu. Cette valeur est EOF. Elle est définie dans le fichier stdio.h. Dans ces conditions, il est clair qu'on peut utiliser tout sauf un char comme type de retour de getc.

Un des cas les plus fréquents où getc retourne EOF est lorsqu'on a rencontré la fin du fichier. La fin d'un fichier est un point situé au-delà du dernier caractère de ce fichier (si le fichier est vide, le début et la fin du fichier sont donc confondus). On dit qu'on a rencontré la fin d'un fichier après avoir encore tenté de lire dans ce fichier alors qu'on se trouve déjà à la fin, pas juste après avoir lu le dernier caractère. Lorsque stdin est associé au clavier, la notion de fin de fichier perd à priori son sens car l'utilisateur peut très bien taper n'importe quoi à n'importe quel moment. Cependant l'environnement d'exécution (le système d'exploitation) offre généralement un moyen de spécifier qu'on n'a plus aucun caractère à fournir (concrètement, pour nous, cela signifie que getc va retourner EOF). Sous Windows par exemple, il suffit de taper en début de ligne la combinaison de touches Ctrl + Z (héritée du DOS) puis de valider par ENTREE. Evidemment, tout recommence à zéro à la prochaine opération de lecture.

Les macros getchar et putchar s'utilisent comme getc et putc sauf qu'elles n'opèrent que sur stdin, respectivement stdout. Elles sont définies dans stdio.h comme suit :

#define getchar() getc(stdin)
#define putchar(c) putc(c, stdout)

Et enfin fgetc est une fonction qui fait la même chose que getc (qui peut être en fait une fonction ou une macro ...). De même fputc est une fonction qui fait la même chose que putc.

10.8 Saisir une chaîne de caractères

Il suffit de lire les caractères présents sur le flux d'entrée (dans notre cas : stdin) jusqu'à ce que l'on ait atteint la fin du fichier ou le caractère de fin de ligne. Nous devrons fournir en arguments de la fonction l'adresse du tampon destiné à contenir la chaîne de caractère saisie et la taille de ce tampon pour supprimer le risque de débordement de tampon.

#include <stdio.h>
 
char * saisir_chaine(char * lpBuffer, size_t nBufSize);
 
int main()
{
    char lpBuffer[20];
 
    printf("Entrez une chaine de caracteres : ");
    saisir_chaine(lpBuffer, sizeof(lpBuffer));
 
    printf("Vous avez tape : %s\n", lpBuffer);
 
    return 0;
}
 
char * saisir_chaine(char * lpBuffer, size_t nBufSize)
{
    size_t nbCar = 0;    
    int c;
 
    c = getchar();
    while ((nbCar < nBufSize - 1) && (c != EOF) && (c != '\n'))
    {
        lpBuffer[nbCar] = (char)c;
        nbCar++;
        c = getchar();
    }
 
    lpBuffer[nbCar] = '\0';
 
    return lpBuffer;
}

La fonction scanf permet également de saisir une chaîne de caractères ne comportant aucun espace (espace, tabulation, etc.) grâce au spécificateur de format %s. Elle va donc arrêter la lecture à la rencontre d'un espace (mais avant d'effectuer la lecture, elle va d'abord avancer jusqu'au premier caractère qui n'est pas un espace). scanf ajoute enfin le caractère de fin de chaîne. Le gabarit permet d'indiquer le nombre maximum de caractères à lire (caractère de fin de chaîne non compris). Lorsqu'on utilise scanf avec le spécificateur %s (qui demande de lire une chaîne sans espace), il ne faut jamais oublier de spécifier également le nombre maximum de caractères à lire (à mettre juste devant le s) sinon le programme sera ouvert aux attaques par débordement de tampon. Voici un exemple qui montre l'utilisation de scanf avec le spécicateur de format %s :

#include <stdio.h>
 
int main()
{
    char lpBuffer[20];
 
    printf("Entrez une chaine de caracteres : ");
    scanf("%19s", lpBuffer);
 
    printf("Vous avez tape : %s\n", lpBuffer);
 
    return 0;
}

Et enfin, il existe également une fonction, gets, déclarée dans stdio.h, qui permet de lire une chaîne de caractères sur stdin. Cependant cette fonction est à proscrire car elle ne permet pas de spécifier la taille du tampon qui va recevoir la chaîne lue.

10.9 Lire une ligne avec fgets

La fonction fgets permet de lire une ligne (c'est-à-dire y compris le '\n') sur un flux d'entrée et de placer les caractères lus dans un buffer. Cette fonction ajoute ensuite le caractère '\0'. Exemple :

#include <stdio.h>
 
int main()
{
    char lpBuffer[20];
 
    printf("Entrez une chaine de caracteres : ");
    fgets(lpBuffer, sizeof(lpBuffer), stdin);
 
    printf("Vous avez tape : %s", lpBuffer);
 
    return 0;
}

Dans cet exemple, deux cas peuvent se présenter :

  • l'utilisateur entre une chaîne comportant 18 caractères tout au plus puis valide le tout par ENTREE, alors tous les caractères de la ligne, y compris le caractère de fin de ligne, sont copiés dans lpBuffer puis le caractère de fin de chaîne est ajouté
  • l'utilisateur entre une chaîne comportant plus de 18 caractères (c'est-à-dire >= 19) puis valide le tout par ENTREE, alors seuls les 19 premiers caractères sont copiés vers lpBuffer puis le caractère de fin de chaîne est ajouté

10.10 Mécanisme des entrées/sorties en langage C

Les entrées/sorties en langage C sont bufférisées, c'est-à-dire que les données à lire (respectivement à écrire) ne sont pas directement lues (respectivement écrites) mais sont tout d'abord placées dans un buffer associé au fichier. La preuve, vous avez certainement remarqué par exemple que lorsque vous entrez des données pour la première fois à l'aide du clavier, ces données ne seront lues qu'une fois que vous aurez appuyé sur la touche ENTREE. Ensuite, toutes les opérations de lecture qui suivent se feront immédiatement tant que le caractère '\n' est encore présent dans le tampon de lecture, c'est-à-dire tant qu'il n'a pas été encore lu. Lorsque le caractère '\n' n'est plus présent dans le buffer, vous devrez à nouveau appuyer sur ENTREE pour valider la saisie, et ainsi de suite.

Les opérations d'écriture sont moins compliquées, mais il y a quand même quelque chose dont il serait totalement injuste de ne pas en parler. Comme nous l'avons déjà dit plus haut, les entrées/sorties en langage C sont bufferisées c'est-à-dire passent par un tampon. Dans le cas d'une opération d'écriture, il peut arriver que l'on souhaite à un certain moment forcer l'écriture physique des données présentes dans le tampon associé au fichier sans attendre que le système se décide enfin de le faire. Dans ce cas, on utilisera la fonction fflush :
Dans le cas d'un flot de sortie, cette fonction provoque l'écriture physique immédiate du tampon en cours de remplissage. Elle rend EOF en cas d'erreur, zéro dans les autres cas. D'après la norme officielle du langage C, l'effet de fflush sur un flot qui n'est pas un flot de sortie est indéfini. Mais pour la plupart des bibliothèques actuelles, l'appel de cette fonction sur un flot d'entrée supprime les caractères disponibles dans le tampon. Par exemple, dans le cas fréquent où l'entrée standard correspond au clavier, l'appel fflush(stdin) fait disparaître tous les caractères déjà tapés mais pas encore lus par le programme.

Remarque. Si le fichier physique qui correspond au flot indiqué est un organe interactif, par exemple l'écran d'un poste de travail, alors la fonction fflush est implicitement appelée dans deux circonstances très fréquentes :

  • l'écriture du caractère '\n' qui produit l'émission d'une marque de fin de ligne et la vidange effective du tampon,
  • le début d'une opération de lecture sur l'unité d'entrée associée (les organes d'entrée-sortie interactifs forment généralement des couples) ; ainsi, par exemple, une lecture au clavier provoque la vidange du tampon d'écriture à l'écran. Cela permet qu'une question soit effectivement affichée avant que l'utilisateur ne doive taper la réponse correspondante.

10.10.1 Lire de manière sûre des données sur l'entrée standard

Tout d'abord, analysons le tout petit programme suivant :

#include <stdio.h>
 
int main()
{
    char nom[12], prenom[12];
 
    printf("Entrez votre nom : ");
    fgets(nom, sizeof(nom), stdin);
 
    printf("Entrez votre prenom : ");
    fgets(prenom, sizeof(prenom), stdin);
 
    printf("Votre nom est : %s", nom);
    printf("Et votre prenom : %s", prenom);
 
    return 0;
}

Dans ce programme, si l'utilisateur entre un nom comportant moins de 10 caractères puis valide par ENTREE, alors tous les caractères rentrent dans nom et le programme se déroule bien comme prévu. Par contre si l'utilisateur entre un nom comportant plus de 10 caractères, seuls les 11 premiers caractères seront copiés dans nom et des caractères sont donc encore présents dans le buffer du clavier. Donc, à la lecture du prénom, les caractères encore présents dans le buffer seront immédiatement lus sans que l'utilisateur n'ait pu entrer quoi que ce soit. Voici un deuxième exemple :

#include <stdio.h>
 
int main()
{
    int n;
    char c;
 
    printf("Entrez un nombre (entier) : ");
    scanf("%d", &n);
 
    printf("Entrez un caractere : ");
    scanf("%c", &c);
 
    printf("Le nombre que vous ave entre est : %d\n", n);
    printf("Le caractere que vous ave entre est : %c\n", c);
 
    return 0;
}

Lorsqu'on demande à scanf de lire un nombre, elle va déplacer le pointeur jusqu'au premier caractère non blanc, lire tant qu'elle doit lire les caractères pouvant figurer dans l'expression d'un nombre, puis s'arrêter à la rencontre d'un caractère invalide (espace ou lettre par exemple).

Donc dans l'exemple ci-dessus, la lecture du caractère se fera sans l'intervention de l'utilisateur à cause de la présence du caractère '\n' (qui sera alors le caractère lu) due à la touche ENTREE frappée pendant la saisie du nombre.

Ces exemples nous montrent bien que d'une manière générale, il faut toujours vider le buffer du clavier après chaque saisie, sauf si celui-ci est déjà vide bien sûr. Pour vider le buffer du clavier, il suffit de manger tous les caractères présents dans le buffer jusqu'à ce qu'on ait rencontré le caractère de fin de ligne ou atteint la fin du fichier. A titre d'exemple, voici une version améliorée (avec vidage du tampon d'entrée après lecture) de notre fonction saisir_chaine :

char * saisir_chaine(char * lpBuffer, int nBufSize)
{
    char * p;
 
    fgets(lpBuffer, nBufSize, stdin);
 
    p = lpBuffer + strlen(lpBuffer) - 1;
    if (*p == '\n')
        *p = '\0'; /* on écrase le \n */
    else
    {
        /* On vide le tampon de lecture du flux stdin */
        int c;
 
        c = getchar();
        while ((c != EOF) && (c != '\n'))
           c = getchar();
    }
 
    return lpBuffer;
}

11 L'allocation dynamique de mémoire

L'intérêt d'allouer dynamiquement de la mémoire se ressent lorsqu'on veut créer un tableau dont la taille dont nous avons besoin n'est connue qu'à l'exécution par exemple. Autrement dit :

int t[10];
...
/* FIN */

Peut être remplacé par :

int * p;
 
p = malloc(10 * sizeof(int));
...
free(p); /* libérer la mémoire lorsqu'on n'en a plus besoin */
/* FIN */

Les fonctions malloc et free sont déclarées dans le fichier stdlib.h. malloc retourne NULL en cas d'échec. Voici un exemple qui illustre une bonne manière de les utiliser :

p = malloc(10 * sizeof(int));
if (p != NULL)
{
    ...
    free(p);
}
else
    /* ECHEC */

La fonction realloc :

void * realloc(void * memblock, size_t newsize);

permet de « redimensionner » une mémoire allouée dynamiquement (par malloc par exemple). Si memblock vaut NULL, realloc se comporte comme malloc. En cas de réussite, cette fonction retourne alors l'adresse de la nouvelle mémoire, sinon la valeur NULL est retournée et la mémoire pointée par memblock reste inchangée.

int * p, * q; /* q : pour tester le retour de realloc */
 
p = malloc(10 * sizeof(int));
if (p != NULL)
{
    ...
 
    q = realloc(p, 20 * sizeof(int));
 
    if (q != NULL)
    {
        p = q;
 
        ...
    }
 
    free(p);
}
 
...

12 Ressources

http://c.developpez.com/cours
C en Action - O'Reilly