Ce cours suppose que vous savez déjà programmer dans un autre langage et que les concepts de base, présent dans du pseudo-code, sont connus (fonction, variables, boucles, etc). Il s'agit simplement d'un cours sur la syntaxe et les outils.

Nous allons voir un sous-ensemble de ce qu'on a le droit de faire en C, mais ce n'est pas du tout exhaustif. En particulier, on ne parlera pas de ce qui n'est pas permis en C. Le but est de permettre de commencer le projet plus doucement. Cela dit, sauf pour la syntaxe pour la lecture d'entrée (scanf ou getc ou fscanf, etc), ce sous-ensemble devrait aussi suffire pour compléter le projet. Mais vous êtes encouragés à découvrir le reste du C.

Hello world

Écrire dans un fichier texte hello.c.

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

et lancer dans le répertoire où se trouve hello.c

$ gcc hello.c

Ce répertoire contient maintenant un fichier a.out qu'on peut lancer avec

$ ./a.out

Si on veut donner un nom différent de sortie, disons hello.

$ gcc hello.c -o hello
$ ./hello

Plan

Nous allons voir ce que ces lignes veulent dire aujourd'hui.

Mais pas tous en même temps. Donc les premiers programmes auront des lignes non-expliqués.

Pour ceux et celles qui connaissent déjà un peu le C, certains sujets sont omis par choix.

Fonctions

Comme en pseudo-code, hello.c est lu ligne par ligne, de haut en bas. Le premier morceaux qu'on rencontre après #include <stdio.h> est int main(){.

Cette ligne déclare une fonction nommée main, sans arguments avec retour une valeur de type int. { marque le début d'un ensemble de lignes décrivant la fonction.

printf("Hello world");

Appel la fonction printf (définit ailleurs) avec un paramètre "Hello world" qui est une constante de type char[].

return 0;

Retourne une constante de type int. 0 a comme convention de vouloir dire « pas d'erreur ».

}

Fin de l'ensemble de lignes pour la fonction main.

Attention: Toutes les lignes doivent se terminer par un ; (c'est une des premières sources d'erreurs!)

Types

Les types que nous allons voir aujourd'hui sont:

Plus de détails pour ces derniers dans quelques transparents.

Variables

Toute variable en C doit être déclarée. Un deuxième exemple

#include <stdio.h>
int main(){
    int x;
    x = 3;
    printf("%d\n", x);
    return 0;
}

int x déclare que la variable de nom x a type int. (= est l'opération d'assignation donc x = 3 assigne 3 à x.)

Cependant, l'information de type est parfois séparé en deux endroit!

int x[100];
int y[];
int *z;

L'idée est que C déclare le type de sortie avec une expression exemple. I.e., si plus tard on écrit x[100], on sait que c'est un int.

Attention: Les variables ne sont pas initialisées à la déclaration et ont une valeur arbitraire avant la première assignation. (Enlever la ligne x = 3; pour voir ce que ça donne. Essayer de compiler avec -Wall.)

Arguments de fonctions

Le type (ainsi que le nom) des arguments (ou paramètres) d'une fonction doivent aussi être déclarés. Un troisième exemple:

#include <stdio.h>

int successeur(int i){
    return i + 1;
}

int main(){
    int x;
    x = successeur(3);
    printf("%d\n", x);
    return 0;
}

Les arguments sont séparés de virgules

int addition(int i, int j){
    return i + j;
}

Prototype de fonctions

Les appels aux fonctions qui se trouvent plus tard dans le fichier doivent être précédés d'une déclaration de la fonction appelée (ou une valeur par défaut va être utilisée)!

int successeur(int i);
int fouille_binaire(int i, int t[]);

Ces déclarations, indiquant le types d'entrée et de sortie de la fonction, sont normalement tous placés près du début du fichier.

Syntaxe de base

/* Commentaire
qui peut être sur plusieurs
lignes */
// Commentaire jusqu'à fin de ligne.
if (x == 3) {
    printf("x vaut 3\n");
} else {
    printf("x ne vaut pas 3\n", x);
}

Opération de comparaison: ==, >, <

Le résultat d'une comparaison est un int de valeur 0 ou 1. La syntaxe if (condition) test si condition est 0 (« faux ») ou pas (« vrai »).

Combinaison de conditions: && (et), || (ou). E.g., if ((x == 3) && (y == 4))

Syntaxe de base: Boucles

while(condition){
    //code interne
}

Exécute le code interne tant que la condition est vraie.

int i;
for (i = 0; i < 5; i = i + 1) {
    //code interne
}

est équivalent à

int i;
i = 0;
while (i < 5) {
    //code interne
    i = i + 1;
}

En résumé, la syntaxe est for (initialisation; condition; changement).

Chaînes de caractères et printf

Un tableau de char est considéré comme un chaîne de caractères (« string »). Pour un tableau de taille fixe, c'est le nombre maximum de caractères que la variable peut stocker.

Par convention, une caractère spécial '\0' indique la fin de la chaîne (la syntaxe "chaine" ajoute ce caractère automatiquement).

char c[10] = {'C', 'h', 'a', 'i', 'n', 'e', '\0'};

est équivalent à

char c[10] = "Chaine";

Caractères spéciaux (difficilement affichable sur l'écran et ont donc une notation spéciale à l'intérieur du code): '\0', '\n', '\t', '\\', ...

La fonction printf, qui sera sans doute beaucoup utilisée, prends comme premier argument une chaîne de caractères où certain caractère sont interprétés de façon particulière

printf("int:%d float:%f char:%c char[]:%s pourcent:%%\n", 1234, 0.25, 'a', "Chaine");
// Sortie: int:1234 float:0.250000 char[]:Chaine

Les autres arguments sont lu dans l'ordre et remplacent les %d, %f et %s.

Erreurs classiques

Quand cela arrive, le compilateur se plaint

prog.c:7: error: expected declaration specifiers or ‘...’ before string constant

Nom du fichier, suivi du numéro de ligne, suivi d'un message.

Essayez quelques exercices maintenant.

Déboggage

Si le programme compile mais ne fonctionne pas de manière voulue, on peut utiliser un déboggeur comme gdb pour examiner les états intermédiaires du programme.

Ajouter -g lors de la compilation

$ gcc var.c -o var -g

Lancer le programme avec gdb

$ gdb ./var
(gdb) run

Le programme tourne jusqu'à sa fin ou la première erreur rencontré. On peut aussi demander d'arrêter le programme à un endroit.

Déboggage: Un exemple

(gdb) break main
Breakpoint 1 at 0x80483d8: file var.c, line 9.
(gdb) run
Starting program: /tmpfs/var 

Breakpoint 1, main () at var.c:9
9                x = successeur(3);
(gdb) print x
$1 = 2654196
(gdb) step
successeur (i=3) at var.c:4
4                return i + 1;
(gdb) print i
$2 = 3
(gdb) step
5            }
(gdb) step
main () at var.c:10
10                printf("%d\n", x);
(gdb) list
5            }
6        
7            int main(){
8                int x;
9                x = successeur(3);
10                printf("%d\n", x);
11                return 0;
12            }
(gdb) step
4
11                return 0;
(gdb) step
12            }
(gdb) step
0x00145ce7 in __libc_start_main () from /lib/libc.so.6

Avant de lancer run, break main indique que le programme devrait s'arrêter à l'entrée de la fonction main. break peut aussi être suivi d'un numéro de ligne.

Déboggage: Commandes utiles

Quelques autres commandes utiles dans gdb:

Les commandes ont tous une forme courte correspondante (e.g., r pour run, s pour step).

Pointeurs et gestion de la mémoire

En C, la mémoire est simplement un gigantesque tableau (pas tout à fait mais réfléchir ainsi suffit normalement). Tout stockage de valeur a besoin de mémoire. Une déclaration comme int x[100] réserve des entrées contiguës de ce tableau et donne à x le premier (plus petit) indice de cette région réservée.

Cependant, si la taille 100 n'est pas connue d'avance (par exemple si c'est le résultat d'un calcul), on peut faire une demande de mémoire lors du lancement du programme.

#include <stdlib.h>

int *x;
x = malloc(100 * sizeof(int));
x[10] = 3;

x contient l'indice de la mémoire (« l'adresse ») où nous pouvons stocker un tableau int de taille 100.

printf("%d\n", x[10]);
printf("%p\n", x);
printf("%d\n", x);  // Donne un avertissement

La valeur de x est elle-même stockée quelque part d'autre dans la mémoire. Mais pour les déclarations d'entier, nous avons pas besoin d'allouer cette mémoire nous même.

Gestion de la mémoire: sizeof et free

sizeof nous donne le nombre d'entrée du tableau mémoire que chaque int occupe. Attention: x est un simple entier contenant un adresse et donc sizeof(x) donne le même résultat que sizeof(int) (au lieu de la taille du bloc de mémoire qui lui est alloué).

Pour libérer la mémoire allouée et permettre de stocker d'autre valeurs à ces indices:

free(x);

Gestion de la mémoire: Tableau 2D

Pour un tableau à deux dimensions, il suffit simplement d'avoir un premier tableau d'adresses et que chacune de ces adresses correspondent au (début d'un) autre tableau.

int **z;
int i;
z = malloc(100 * sizeof(int*));
for (i = 0; i < 100; i = i + 1){
    z[i] = malloc(90 * sizeof(int));
}
z[10][20] = 5;
printf("%d\n", z[10][20]);
printf("%p\n", z);
printf("%p\n", z[0]);

L'écriture à une adresse de mémoire qui ne vous est pas attribué (par exemple, quand un pointeur a sa valeur initiale arbitraire) peut causer un « Segmentation fault ». Utiliser gdb pour trouver où est le problème.

Opération * et &

Création de nouvelles structures avec struct

Nous pouvons tout faire avec des tableau mais cela rends l'interprétation du code difficile. Qu'en est-il des listes, arbres, etc? Pour cela, nous pouvons utiliser struct

#include <stdio.h>
#include <stdlib.h>

struct liste {
    int valeur;
    struct liste *prochain;
};

int main(){
    struct liste *liste1;
    liste1 = malloc(sizeof(struct liste));
    liste1->valeur = 10;
    return 0;
}

Attention au ; à la fin de la déclaration de struct. Nous avons déclaré un type liste (en fait struct liste) avec deux membres nommés valeur et prochain. On utilise la syntaxe -> pour accéder aux membres. En fait, liste1->valeur est un raccourci pour (*liste1).valeur.

struct statique

Nous pouvons avoir des struct liste qui ne sont pas des pointeurs

    struct liste liste2;
    liste2.valeur = 1;

C'est un peu bizarre que les structs (statiques) ne sont pas des pointeurs alors que les tableaux (e.g., int x[10]) le sont. Faire attention au passage de struct comme argument de fonction pour cette raison.

Entête et utilisation de plusieurs fichiers

Jusqu'à présent, tout était dans un seul fichier. Avec plusieurs fichiers, on peut faire appel aux fonctions de deux.c à partir de un.c. Pour ce faire, il faut déclare les prototypes des fonctions de deux.c dans un fichier deux.h et les inclure dans un.c avec

#include "deux.h"

et ensuite lancer

$ gcc un.c deux.c -o un

Entête et utilisation de plusieurs fichiers: Un exemple

/* deux.h */
int fdeux();

/* deux.c */
int fdeux(){
    return 2;
}

/* un.c */
#include <stdio.h>
#include "deux.h"

int main(){
    printf("%d\n", fdeux());
    return 0;
}

La différence entre #include "deux.h" et #include <deux.h> est le chemin dans lequel gcc va chercher le fichier deux.h.

Non abordés

Entre autre, les sujets suivants ne seront pas abordés: