Bertrand Bordage de NoriPyt

  • Programmeur Python, Django, PostgreSQL
  • Membre de l’équipe de développement du CMS Wagtail
  • Créateur de nombreux projets open source plus ou moins célèbres

Le langage de programmation C

Démo de la puissance de C

Ce petit programme a été fait spécialement pour ce cours.

Derrière cette démonstration aux allures familières :

  • 420 lignes de code
  • 6 heures de travail
  • trois librairies externes : OpenGL, GLFW, GLU
  • une bonne dose de géométrie dont nous ne verrons pas le détail
  • aucun bug

Source de la démo


In [ ]:
//%cflags: -lm -lGL -lGLU -lglfw

#include "math.h"
#include "time.h"
#include "stdio.h"
#include "stdlib.h"

#include "GL/glu.h"
#include "GLFW/glfw3.h"


#define PI 3.14159265358979323846

#define WINDOW_WIDTH      800
#define WINDOW_HEIGHT     600
#define FOV                70  // Camera field of view, in degrees
#define MAP_WIDTH          64  // In number of cubes
#define MAP_DEPTH          64  // In number of cubes
#define MOVE_SPEED          4  // In world units per second
#define SIDE_LIGHTNESS    0.6  // Between 0 and 1
#define BOTTOM_LIGHTNESS  0.3  // Between 0 and 1


void check_gl_error() {
    GLenum error = glGetError();
    switch (error) {
        case GL_NO_ERROR:
            break;
        case GL_INVALID_ENUM:
            fprintf(stderr, "Invalid enum passed to last OpenGL call.\n");
            break;
        case GL_INVALID_VALUE:
            fprintf(stderr, "Invalid value passed to last OpenGL call.\n");
            break;
        case GL_INVALID_OPERATION:
            fprintf(stderr,
                    "Invalid operation passed to last OpenGL call.\n");
            break;
        case GL_INVALID_FRAMEBUFFER_OPERATION:
            fprintf(stderr, "Invalid OpenGL operation, the currently bound "
                            "framebuffer was not complete.\n");
            break;
        case GL_OUT_OF_MEMORY:
            fprintf(stderr, "Not enough memory for the last OpenGL call.\n");
            break;
        default:
            fprintf(stderr, "Unknown OpenGL error (code %i)\n", error);
            break;
        
    }
}


void error_callback(int error, const char* description) {
    fprintf(stderr, "Error: %s\n", description);
}


typedef struct {
    float x;
    float y;
    float z;
    float dx;
    float dy;
    float dz;
    float ax;
    float ay;
    double mouse_x;
    double mouse_y;
    char mouse_grabbed;
} Camera;


typedef struct {
    float x;
    float y;
    float z;
    int red;
    int green;
    int blue;
} Cube;


typedef struct {
    GLFWwindow* window;
    unsigned int window_width;
    unsigned int window_height;
    Camera camera;
    unsigned int n_cubes;
    Cube* cubes;
    double last_frame_time;
    double dt;
} Game;


Game game;


static void set_framebuffer_size(GLFWwindow* window, int width, int height) {
    glViewport(0, 0, width, height);
    game.window_width = width;
    game.window_height = height;
}


static void key_callback(GLFWwindow* window, int key, int scancode,
                         int action, int mods) {
    if (action == GLFW_PRESS) {
        switch (key) {
            case GLFW_KEY_W:
                game.camera.dz -= MOVE_SPEED;
                break;
            case GLFW_KEY_A:
                game.camera.dx -= MOVE_SPEED;
                break;
            case GLFW_KEY_S:
                game.camera.dz += MOVE_SPEED;
                break;
            case GLFW_KEY_D:
                game.camera.dx += MOVE_SPEED;
                break;
            case GLFW_KEY_SPACE:
                game.camera.dy += MOVE_SPEED;
                break;
            case GLFW_KEY_LEFT_CONTROL:
                game.camera.dy -= MOVE_SPEED;
                break;
            case GLFW_KEY_ESCAPE:
                glfwSetInputMode(game.window,
                                 GLFW_CURSOR, GLFW_CURSOR_NORMAL);
                game.camera.mouse_grabbed = 0;
                break;
        }
    } else if (action == GLFW_RELEASE) {
        switch (key) {
            case GLFW_KEY_W:
                game.camera.dz += MOVE_SPEED;
                break;
            case GLFW_KEY_A:
                game.camera.dx += MOVE_SPEED;
                break;
            case GLFW_KEY_S:
                game.camera.dz -= MOVE_SPEED;
                break;
            case GLFW_KEY_D:
                game.camera.dx -= MOVE_SPEED;
                break;
            case GLFW_KEY_SPACE:
                game.camera.dy -= MOVE_SPEED;
                break;
            case GLFW_KEY_LEFT_CONTROL:
                game.camera.dy += MOVE_SPEED;
                break;
        }
    }
}


static void mouse_button_callback(GLFWwindow* window,
                                  int button, int action, int mods) {
    if (action == GLFW_PRESS) {
        if (button == GLFW_MOUSE_BUTTON_LEFT) {
            glfwSetInputMode(game.window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
            glfwGetCursorPos(game.window,
                             &game.camera.mouse_x, &game.camera.mouse_y);
            game.camera.mouse_grabbed = 1;
        }
    }
}

static void mouse_move_callback(GLFWwindow* window,
                                double mouse_x, double mouse_y) {
    if (game.camera.mouse_grabbed) {
        game.camera.ay += (mouse_x - game.camera.mouse_x) / 300;
        game.camera.ax += (mouse_y - game.camera.mouse_y) / 300;
        if (game.camera.ax < -PI / 2) {
            game.camera.ax = -PI / 2;
        } else if (game.camera.ax > PI / 2) {
            game.camera.ax = PI / 2;
        }
        game.camera.mouse_x = mouse_x;
        game.camera.mouse_y = mouse_y;
    }
}


void vertexA(Cube cube) {
    glVertex3f(cube.x+.5, cube.y+.5, cube.z+.5);
}

void vertexB(Cube cube) {
    glVertex3f(cube.x-.5, cube.y+.5, cube.z+.5);
}

void vertexC(Cube cube) {
    glVertex3f(cube.x-.5, cube.y-.5, cube.z+.5);
}

void vertexD(Cube cube) {
    glVertex3f(cube.x+.5, cube.y-.5, cube.z+.5);
}

void vertexE(Cube cube) {
    glVertex3f(cube.x-.5, cube.y+.5, cube.z-.5);
}

void vertexF(Cube cube) {
    glVertex3f(cube.x+.5, cube.y+.5, cube.z-.5);
}

void vertexG(Cube cube) {
    glVertex3f(cube.x+.5, cube.y-.5, cube.z-.5);
}

void vertexH(Cube cube) {
    glVertex3f(cube.x-.5, cube.y-.5, cube.z-.5);
}


void render_cube(Cube cube) {
    // Makes the color darker to simulate lighting.
    glColor3ub(cube.red * SIDE_LIGHTNESS,
               cube.green * SIDE_LIGHTNESS,
               cube.blue * SIDE_LIGHTNESS);
    check_gl_error();
    glBegin(GL_QUADS);

    //
    // Front
    //
    vertexA(cube);
    vertexB(cube);
    vertexC(cube);
    vertexD(cube);
    //
    // Back
    //
    vertexE(cube);
    vertexF(cube);
    vertexG(cube);
    vertexH(cube);
    //
    // Left
    //
    vertexF(cube);
    vertexA(cube);
    vertexD(cube);
    vertexG(cube);
    //
    // Right
    //
    vertexB(cube);
    vertexE(cube);
    vertexH(cube);
    vertexC(cube);
    //
    // Top
    //
    // Makes the color fully lit to simulate lighting.
    glColor3ub(cube.red, cube.green, cube.blue);
    vertexF(cube);
    vertexE(cube);
    vertexB(cube);
    vertexA(cube);
    //
    // Bottom
    //
    // Makes the color very dark to simulate lighting.
    glColor3ub(cube.red * BOTTOM_LIGHTNESS,
               cube.green * BOTTOM_LIGHTNESS,
               cube.blue * BOTTOM_LIGHTNESS);
    vertexD(cube);
    vertexC(cube);
    vertexH(cube);
    vertexG(cube);

    glEnd();
    check_gl_error();
}


float rad2deg(float angle) {
    return 180 * angle / PI;
}


void render() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    check_gl_error();

    // Sets the projection matrix to a perspective.
    glMatrixMode(GL_PROJECTION);
    check_gl_error();
    glLoadIdentity();
    check_gl_error();
    gluPerspective(FOV, (float)game.window_width / game.window_height,
                   1, 1000);

    // Moves and rotates the world so the camera is its origin.
    glMatrixMode(GL_MODELVIEW);
    check_gl_error();
    glLoadIdentity();
    check_gl_error();
    Camera camera = game.camera;
    glRotatef(rad2deg(camera.ax), 1, 0, 0);
    glRotatef(rad2deg(camera.ay), 0, 1, 0);
    glTranslatef(-camera.x, -camera.y, -camera.z);

    for (unsigned int i = 0; i < game.n_cubes; i++) {
        render_cube(game.cubes[i]);
    }
}


void create_window() {
    glfwWindowHint(GLFW_SAMPLES, 4);
    game.window_width = WINDOW_WIDTH;
    game.window_height = WINDOW_HEIGHT;
    game.window = glfwCreateWindow(
        game.window_width, game.window_height,
        "OpenGL is hard, but rewarding", NULL, NULL);
    glfwMakeContextCurrent(game.window);
    // Sets game rendering to the same frequency as the screen frequency.
    glfwSwapInterval(1);
}


void update_camera() {
    game.camera.x += (
        game.camera.dx * cos(game.camera.ay)
        + game.camera.dz * cos(game.camera.ay + PI / 2)) * game.dt;
    game.camera.y += game.camera.dy * game.dt;
    game.camera.z += (
        game.camera.dx * sin(game.camera.ay)
        + game.camera.dz * sin(game.camera.ay + PI / 2)) * game.dt;
}


void loop() {
    while (!glfwWindowShouldClose(game.window)) {
        update_camera();
        render();
        // Puts on the screen the frame rendered to a temporary buffer.
        glfwSwapBuffers(game.window);
        // Runs all pending events.
        glfwPollEvents();

        double now = glfwGetTime();
        game.dt = now - game.last_frame_time;
        game.last_frame_time = now;

        printf("FPS: %i Position: [%f %f %f] Direction: [%f %f]\r",
               (int)round(1 / game.dt),
               game.camera.x, game.camera.y, game.camera.z,
               game.camera.ax, game.camera.ay);
    }
}


unsigned char rand_char() {
    return 255 * (float)rand() / RAND_MAX;
}


void create_map(unsigned int width, unsigned int depth) {
    game.n_cubes = width * depth;
    game.cubes = (Cube*)malloc(game.n_cubes * sizeof(Cube));
    for (int z = 0; z < depth; z++) {
        for (int x = 0; x < width; x++) {
            game.cubes[x + z*width] = (Cube){
                x - width * .5, rand_char() / 120 - 5, z - depth * .5,
                rand_char(), rand_char(), rand_char()};
        }
    }
}


int main() {
    // Sets the pseudo-random generator seed using the current time.
    srand(time(NULL));
    
    glfwSetErrorCallback(error_callback);

    if (!glfwInit()) {
        fprintf(stderr, "Error while initializing GLFW.\n");
        return 1;
    }

    create_window();
    if (game.window == NULL) {
        fprintf(stderr, "Cannot create a window.\n");
        return 1;
    }

    glfwSetFramebufferSizeCallback(game.window, set_framebuffer_size);
    glfwSetKeyCallback(game.window, key_callback);
    glfwSetMouseButtonCallback(game.window, mouse_button_callback);
    glfwSetCursorPosCallback(game.window, mouse_move_callback);

    game.camera = (Camera){x: 0, y: 0, z: 0, dx: 0, dy: 0, dz: 0, 
                           ax: 0, ay: 0, mouse_grabbed: 0};
    create_map(MAP_WIDTH, MAP_DEPTH);

    glEnable(GL_DEPTH_TEST);
    check_gl_error();

    glEnable(GL_CULL_FACE);
    check_gl_error();
    glCullFace(GL_BACK);
    check_gl_error();

    loop();

    free(game.cubes);
    glfwDestroyWindow(game.window);
    glfwTerminate();
    // Adds a new line to avoid getting a weird line due to previous \r.
    puts("");
    return 0;
}

Quelqu’un a compris quelque chose ?

Au fur et à mesure qu’on découvrira C, on reviendra sur cette démo pour comprendre de mieux en mieux comment elle marche.

Projet pour ceux se sentant à l’aise

Dès que vous avez fini les exercices ou pris un peu d’avance, améliorez cette démo !

Vous pouvez vous y mettre à plusieurs, mais ne parlez pas : chuchotez ou utilisez Slack.

Vous pouvez améliorer ce que vous souhaitez, mais en cas de manque d’inspiration, ce serait super de :

  • [facile] mettre le fond en bleu ciel au lieu de noir
  • [facile] ajouter un mode « course » en appuyant sur la touche majuscule gauche
  • [moyen] mettre en plein écran quand on appuie sur F11
  • [moyen] poser un nouveau bloc quand on clique près d’un bloc déjà existant
  • [moyen] faire en sorte que notre position suive le relief et qu’on ne vole plus
  • [difficile] faire en sorte qu’on puisse sauter
  • [difficile] écrire un algorithme diamond-square pour générer de manière plus intéressante la hauteur des cubes
  • [moyen] réutiliser l’algorithme diamond-square pour définir les couleurs de blocs, un peu comme des biomes
  • [moyen] permettre de saisir la taille de la carte en argument du programme, en faisant ./demo 64x128
  • [très difficile] générer le monde au fur et à mesure qu’on avance dans le jeu, de sorte qu’il n’y ait toujours un bloc sous nos pieds
  • [très difficile] réécrire l’envoi des cubes à la carte graphiques en utilisant des VAO (Vertex Array Object) et VBO (Vertex Buffer Object) pour décupler les performances, et afficher bien plus de cubes

Autre idée de projet

Créer un résolveur de sudoku. Il lira un fichier contenant le sudoku à résoudre.

En sortie, le programme donnera le sudoku résolu.

Au niveau algorithmique, c’est relativement brutal : il suffit de passer en revue les cases vides. Pour chaque case vide, il faut passer en revue les chiffres de 1 à 9 et ainsi trouver les chiffres possibles pour cette case. Pour chaque chiffre possible, il faut « ouvrir » une nouvelle branche d’arborescence permettant de tester des possibilités pour chacune des autres cases.

L’algorithme passe donc en revue les solutions sous forme d’arborescence, et dès qu’il trouve une solution, il s’arrête et l’affiche.

Oui, c’est le retour de la programmation récursive. Il est possible de réaliser ce programme sans récursivité, mais c’est nettement plus difficile.

Pour ceux qui aiment les défis, généraliser cet algorithme à n’importe quelle taille de sudoku rectangulaire régulier : 6×6, 12×12, 12×6, etc.

Documentations

Difficile de trouver une bonne documentation pour C : il n’y en a pas vraiment d’officielle.

Toutefois, quelques sites contenant tout ce dont on a besoin :

Et concernant la démo, elle utilise GLFW, OpenGL et GLU (dont la documentation est incluse dans celle d’OpenGL 2.1)

Et si vous n’avez pas la foi : Google + StackOverflow !

Un langage à part

C est le langage entre l’assembleur et tous les logiciels, y compris les autres langages de programmation.

L’assembleur est la forme textuelle du langage machine. L’assembleur est directement transposé en langage machine compréhensible par le processeur.

La quasi totalité des langages de programmation actuels sont écrits en C, particulièrement les langages compilés : Python, PHP, Java, JavaScript…

C est un langage dit bas niveau parce qu’il est proche du langage machine. Par opposition, les langages interprétés plus récents comme Python, PHP, Java, etc sont dit haut niveau.

Pourquoi « encore » utiliser C ?

C peut sembler archaïque, langage compilé d’il y a 45 ans.

Pourtant, tous les développeurs sont amenés à en faire dans leur vie pour ces raisons :

  • performances optimales (indispensable pour des jeux, traitement d’images, etc)
  • quasiment toutes les librairies bas niveau sont écrites en C (pour les échanges réseau, le traitement d’images, les connexions à des bases de données, etc)
  • le cœur des systèmes d’exploitation est écrit en C
  • les langages haut niveau sont écrits en C

Quand ne pas utiliser C ?

Bien qu’universel et très puissant, C est très exigeant et contient beaucoup de différences d’un système d’exploitation à un autre.

Pour ces raisons, il est conseillé d’utiliser à la place un langage interprété comme Python quand :

  • les performances sont peu importantes
  • on n’a pas besoin d’utiliser directement une librairie C
  • on souhaite obtenir rapidement un résultat
  • on souhaite maintenir moins de code
  • on ne souhaite pas galérer avec de la compilation sous Windows et MacOS

En pratique, dans la plupart des métiers informatiques, on fait peu de C.

Toutefois, ne pas connaître C risque de vous poursuivre toute votre vie professionnelle. Un bon développeur est toujours capable de lire du C, que ce soit pour juste compiler un programme déjà existant, ou pour améliorer une fonctionnalité de son propre langage de programmation.

Évitons les confusions : C ≠ C++ ≠ C#

Pour faire court :

Nom Naissance Lien avec les autres Vu lors de ce cours
C 1972 Utilisé par tous les langages modernes Oui
C++ 1983 Étend C en ajoutant de nouvelles fonctionnalités Non
C# 2000 Aucun rapport, se veut comme un successeur à C et C++ Non

Quelques compilateurs

Il existe des dizaines de compilateurs C.

Voici les plus utilisés aujourd’hui :

Nom Licence Support Linux Support MacOS Support Windows
GCC Open source Oui Oui Oui
Clang Open source Oui Oui Oui
Microsoft Visual C Gratuit mais fermé Non Non Oui

Clang ≈ le futur de GCC

En attendant, on utilisera plutôt GCC.

Installation du compilateur

On utilise uniquement GNU Compiler Collection, alias GCC.

Sous Ubuntu : sudo apt install gcc

Sous Windows :

  • installer MinGW (mingw-get-setup.exe sur https://sourceforge.net/projects/mingw/files/Installer/)
  • lancer le gestionnaire de paquets de MinGW et installer mingw32-gcc
  • ajouter à votre variable %PATH% le chemin des binaires de MinGW (C:\MinGW\bin dans beaucoup de cas)
  • relancer votre session ou redémarrer

Malgré cette installation plus complexe, il est toujours possible que vous ne puissiez pas exécuter gcc directement dans votre invite de commandes Windows.

Windows : enfer pour développer correctement, privilégier n’importe quelle distribution de Linux.

Pour ceux qui galèrent, installer VirtualBox avec une machine virtuelle Ubuntu 17.10.

Premier programme

Voici le plus simple programme C. Il contient juste une fonction main :


In [1]:
int main() {}

Pour le compiler :

  • enregistrer ce code dans un fichier, par exemple first.c
  • ouvrir un terminal dans le dossier du fichier
  • lancer gcc first.c -o first

Pour le lancer : ./first

Salut tout le monde !

Bon, le programme précédent ne faisait absolument rien.

Faisons maintenant le traditionnel “Hello World!”.


In [2]:
#include "stdio.h"

int main() {
    puts("Hello World!");
}


Hello World!

Ici, on a utilisé la fonction puts permettant d’afficher du texte.

Cette fonction est contenue dans la librairie standard stdio, d’où la première ligne.

La plupart des librairies standard de C commencent par std. Ici, le io signifie “Input/Output”.

Exercice 1

Écrire, compiler et lancer un programme affichant Je m’appelle [votre nom]., puis J’habite à [votre ville]. dans une seconde ligne, en remplaçant vos nom et ville.

L’implicite main

Que se passe-t-il si j’essaie de renommer main ?


In [3]:
#include "stdio.h"

int hello() {
    puts("Hello World!");
}


/tmp/tmp0wjofvkj.out: /tmp/tmpbr5b7rcf.out: undefined symbol: main
[C kernel] Executable exited with code 1

La compilation a fonctionné.

Par contre quand on lance le programme, on a une erreur et rien ne s’affiche !

Tous les programmes C qu’on souhaite lancer directement doivent contenir une fonction main, c’est uniquement cette fonction qui sera exécutée au lancement du programme.

Bien entendu, les librairies qui ne sont pas faites pour être lancées directement peuvent ne pas avoir cette fonction.

Variables

Pour définir une variable en C, on a deux possibilités.

Définir la variable sans l’attribuer :


In [ ]:
int age;

Il s’agit donc du type, ici int, suivi du nom de la variable, fini par un ;.

Toutes les instructions C doivent impérativement finir par ;.

Définir la variable en l’attribuant :


In [ ]:
int age = 28;

On peut également définir plusieurs variables à la fois :


In [ ]:
int birth_year = 1989, this_year = 2017;

Exercice 2

Écrire, compiler et lancer un programme où vous définissez votre âge.

Types élémentaires

Le nombre d’octets d’un type varie d’un type de processeur à un autre. Des processeurs 32 bits et 64 bits auront donc certains types de longueur différente. Dans le tableau ci-dessous, il s’agit des tailles pour un processeur moderne 64 bits.

Notation Nom français Exemple Nombre d’octets
char Octet, ou caractère ASCII 'A' ou 65 1
short Petit nombre entier 3725 2
int Nombre entier 35816642 4
long Long nombre entier 654987546216454298 8
float Nombre à virgule 1.618033 4
double Nombre à virgule plus précis 1.6180339887498 8

Types élémentaires (suite)

Quelques spécificités sont à noter :

  • Pas de type booléen en C, on utilise un type numérique à la place.
  • Les types char, short, int, long peuvent être signed ou unsigned. float et double sont toujours signés. Par défaut, les types short, int et long sont signed, mais char n’est pas explicitement signé ou non par défaut.
  • Le minimum d’un type entier unsigned est $0$ et le maximum est $2^{8n}-1$. Par exemple, unsigned short va de $0$ à $65535$.
  • Le minimum d’un type entier signed est $-2^{8n-1}$ et le maximum est $2^{8n-1}-1$, où $n$ est le nombre d’octets. Par exemple, signed short va de $-32768$ à $32767$.
  • Il existe un type spécial, void. Il sert à indiquer qu’une fonction ne renvoie rien, mais on ne peut pas définir une variable comme void.

Exercice 3

Écrire, compiler et lancer un programme contenant une variable char, une unsigned short, une long et une float. Chaque variable doit avoir une valeur attribuée.

Opérateurs élémentaires

Notation Nom Types compatibles Exemple
+, -, *, / Opérations usuelles Types numériques -65 * (3 + 5.7)
% Modulo Types numériques 12 % 5
== Égalité Tous types 13 == 5
!= Différence Tous types 5 != 3.5
< > Inférieur/supérieur Types numériques 2 < 8
<= >= Inférieur/supérieur ou égal Types numériques 8700 >= -32
&& « Et » booléen Tous types 0 && 1
|| « Ou » booléen Tous types 3 < 2 || 15 > 8

Opérations modifiant directement les objets (on dit qu’ils sont modifiés en place ou inplace) :

Notation Nom Types compatibles Exemple
++ Incrémentation Types numériques age++
-- Décrémentation Types numériques age--

Exercice 4

Écrire, compiler et lancer un programme faisant ceci :

  • définir une variable i à -172 en choisissant le type le plus adapté
  • décrémenter cette valeur
  • effectuer un modulo 16 sur i
  • définir une variable is_even vérifiant si la variable i est paire.

Fonctions d’entrée/sortie

Toutes les fonctions permettant d’écrire à l’écran ou de demander à l’utilisateur de taper sont dans la librairie standard stdio.

Pour se servir de ces fonctions, il faut donc toujours mettre #include "stdio.h" au début du fichier.

Quelques caractères échappés à connaître pour tous les langages :

Notation Nom
\n Retour à la ligne
\t Tabulation
\r « Retour chariot » : revient au début de la ligne en cours

Fonctions d’entrée/sortie (suite)

Sortie

On a déjà vu puts(texte), qui permet d’afficher du texte à l’écran suivi d’un retour à la ligne.

Juste avant, on a vu printf(format, valeur1, valeur2, …) qui permet d’afficher une ou plusieurs variables suivant un motif. C’est la fonction d’affichage qui vous sera la plus utile, elle peut même s’utiliser à la place de puts. Exemples d’utilisation :


In [4]:
#include "stdio.h"

int main () {
    printf("Bonjour");
    printf(" tout le monde\n");
    printf("J’ai %i ans\n", 28);
    printf("Il fait %f °C.\n", 21.37);
    printf("Il fait %.2f °C.\n", 21.37);
}


Bonjour tout le monde
J’ai 28 ans
Il fait 21.370000 °C.
Il fait 21.37 °C.

Exercice 5

Afficher la variable age créée dans l’exercice 2.

Exercice 6

Afficher le résultat de la multiplication de 6 par l’addition de 12 à 9 modulo 7.

Exercice 7

Afficher le tableau suivant. Il doit être le plus beau possible, avec des bordures continues, sans « trous ». Pour bien faire, il faut utiliser des box-drawing characters.

Language Year GitHub popularity
C 1972 #10
Python 1990 #2
PHP 1994 #5
Java 1995 #3
JavaScript 1995 #1

Pour info, la popularité sur GitHub est mesurée en nombre de pull request ouvertes en 2017. Voir https://octoverse.github.com/.

Exercice 8

Définir une variable de type signed short à sa plus grande valeur possible. Afficher cette variable, puis l’incrémenter et l’afficher à nouveau. Conclure.


In [ ]:
// Exercice 7

#include "stdio.h"

int main() {
    printf("┏━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┓\n");
    printf("\033[1m");  // Changes the text to bold.
    printf("┃ %10s ┃ %4s ┃ %17s ┃\n", "Language", "Year", "GitHub popularity");
    printf("\033[0m");  // Resets text to normal weight.
    printf("┡━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━┩\n");
    printf("│ %10s │ %4s │ %17s │\n", "C", "1972", "#10");
    printf("│ %10s │ %4s │ %17s │\n", "C++", "1984", "#6");
    printf("│ %10s │ %4s │ %17s │\n", "Python", "1990", "#2");
    printf("│ %10s │ %4s │ %17s │\n", "PHP", "1994", "#5");
    printf("│ %10s │ %4s │ %17s │\n", "Java", "1995", "#3");
    printf("│ %10s │ %4s │ %17s │\n", "JavaScript", "1995", "#1");
    printf("└────────────┴──────┴───────────────────┘\n");
}

In [ ]:
// Exercice 8

#include "stdio.h"

int main () {
    short i = 32767;
    printf("%i\n", i);
    i++;
    printf("%i\n", i);
}

Fonctions d’entrée/sortie (suite)

Entrée (caractère simple)

L’entrée la plus simple se fait avec getchar :


In [ ]:
#include "stdio.h"

int main() {
    printf("Tape une lettre : ");
    char letter = getchar();
    printf("Tu as tapé %c !\n", letter);
}

Malheureusement, c’est insuffisant pour plus d’un caractère, ou pour un autre type.

Exercice 9

Demander à l’utilisateur s’il veut faire quelque chose, du genre “Would you like to see more? [yN]” ou « Voulez-vous en voir plus ? [oN] ».

Fonctions d’entrée/sortie (suite)

Entrée (tous cas)

La vraie réciproque de printf est scanf. Voilà comment s’en servir :


In [ ]:
#include "stdio.h"

int main() {
    unsigned int age;
    printf("Quel âge as-tu ? ");
    scanf("%u", &age);
    printf("Oh, tu as %u ans !\n", age);
}

Contrairement à printf, le format ne contient que le format de la valeur.

Autre problème, on ne peut pas passer directement des valeurs. À la place, on crée des variables et on les référence avec &. On reviendra sur le concept de référence.

Exercice 10

Écrire un outil simple de multiplication demandant deux nombres entiers à l’utilisateur et affichant après saisie la multiplication des deux.

Fonctions d’entrée/sortie (suite)

Formats

Se référer aux documentations pour l’ensemble des possibilités de printf et scanf.

Formats les plus importants communs à printf et scanf :

Notation Type Exemple d’entrée/sortie
%i short, int -8530
%u unsigned short/int 12
%li long -546413432864
%u unsigned long 165413179
%f float, double 7.1325
%.1f float, double 7.1
%c char a
%s char* (on verra ça) abcdef

Exercice 11

Réécrire l’exercice précédent, mais en faisant des multiplications de float à la place.


In [ ]:
// Exercice 11

#include "stdio.h"

int main() {
    float a, b;
    printf("We’re going to do a multiplication.\n");
    printf("Enter both terms separated by spaces or newlines.\n");
    scanf("%f%f", &a, &b);
    printf("%f × %f = %f\n", a, b, a * b);
}

Créer une fonction

Comme vu rapidement avec main, une fonction C a obligatoirement un type de retour.

Chacun de ses arguments doit également être typé.

Par exemple, une fonction calculant la longueur d’hypothénuse d’un triangle rectangle :


In [5]:
//%cflags: -lm

#include "math.h"
#include "stdio.h"

float hypotenuse(float adjacent, float opposite) {
    return sqrt(adjacent*adjacent + opposite*opposite);
}

int main() {
    printf("%f\n", hypotenuse(50, 75));
}


90.138779

Lier des librairies compilées

Dans l’exemple précédent, le //%cflags: -lm me permet d’ajouter des arguments à gcc. Ne le recopiez pas dans la source, mais servez-vous en pour compiler.

Lors de ce cours, //%cflags: sera utilisé uniquement pour ajouter lier des librairies au programme, de sorte que cela fonctionne. Ici, on lie la librairie math dont la version compilée est m en faisant -lm. Pour lier OpenGL, on utilise -lGL.

Dans cet exemple, on compile en faisant :

gcc hypotenuse.c -lm -o hypotenuse

Attention ! L’ordre des arguments est important, il faut toujours mettre après la source les arguments -l….

Puis on se sert du programme normalement :

./hypotenuse

Exercice 12

Créer une fonction calculant pour tout $n$, $\sqrt[\leftroot{-2}\uproot{2}5]{n}$.

Pour rappel, $\sqrt[\leftroot{-2}\uproot{2}i]{n} ⇔ n^\frac{1}{i}$.

Se servir de cette fonction pour calculer $\sqrt[\leftroot{-2}\uproot{2}5]{5}$, $\sqrt[\leftroot{-2}\uproot{2}5]{32}$ et $\sqrt[\leftroot{-2}\uproot{2}5]{243}$.

Retour sur la fonction main

La fonction main a toujours eu le type de retour int jusqu’à présent. Étrange étant donné qu’on en renvoie rien ! On devrait plutôt avoir ceci, non ?


In [ ]:
void main() {}

Pourtant on constate que le programme renvoie une erreur 202, bizarre, non ?

En fait, main renvoie le code de retour (=return code) du programme.

Le return code permet de préciser une erreur, qui va interrompre un script par exemple.

Le code pour une absence d’erreur est 0. C’est le code par défaut.

Exemple d’erreur :


In [6]:
int main() {
    return 1;
}


[C kernel] Executable exited with code 1

Exercice 13

Demander à l’utilisateur quel return code renvoyer.

Attention, c’est juste pour l’exercice. Il est inadapté de demander explicitement un code de retour à l’utilisateur dans la vie de tous les jours.

Conditions

Comme dans tous les autres langages, sans surprise, C propose des ifs et else :


In [7]:
int main() {
    int i = 3, return_code = 0;
    if (i == 2) {
        return_code = 1;
    } else if (i == 5) {
        return_code = 2;
    } else {
        return_code = 3;
    }
    return return_code;
}


[C kernel] Executable exited with code 3

Exercice 14

Écrire une petite calculatrice gérant les opérations +, -, × et ÷.

L’utilisateur doit d’abord saisir un premier nombre à virgule, puis un opérateur, puis un autre nombre.

Selon ces trois paramètres, le programme doit correctement renvoyer le résultat de l’opération, sinon afficher un message d’erreur et quitter avec le return code adapté lorsque les données sont invalides.

Exemple d’entrée/sortie :

28
*
2
= 56

ou

7+13
= 20

In [ ]:
// Exercice 14

#include "stdio.h"

#include "stdio.h"

int main() {
    int n;
    float a, b, result;
    char operator;

    n = scanf("%f %c %f", &a, &operator, &b);
    // Consumes extra input characters
    while (getchar() != '\n') {}

    if (n < 3) {
        fprintf(stderr, "Invalid first operand (%f), operator (%c)"
                        " or second operand (%f).\n", a, operator, b);
        return 1;
    }

    if (operator == '+') {
        result = a + b;
    } else if (operator == '-') {
        result = a - b;
    } else if (operator == '*') {
        result = a * b;
    } else if (operator == '/') {
        result = a / b;
    } else {
        fprintf(stderr, "Invalid operator %c.\n", operator);
        return 1;
    }

    printf("= %f\n", result);
}

Boucle while

La boucle while exécute le même ensemble d’instructions tant que sa condition est vraie (while en anglais).


In [ ]:
#include "stdio.h"

int main() {
    char answer;
    while (answer != 'y') {
        puts("Processing…");
        puts("Would you like to quit? [yN]");
        answer = getchar();
    }
}

Une variante de la boucle while existe, le dowhile. Il vérifie la condition à la fin de la boucle au lieu du début :


In [ ]:
#include "stdio.h"

int main() {
    do {
        puts("Processing…");
        puts("Would you like to quit? [yN]");
    } while (getchar() != 'y');
}

Exercice 15

Améliorer la calculatrice pour qu’elle demande en boucle des calculs à l’utilisateur.

Le résultat précédent doit toujours être utilisé comme premier opérande de l’opération suivante.

Créer une fonction contenant la logique d’opération pour simplifier la lisibilité.

Exemple d’entrée/sortie :

7
+
23
= 30.000000
/3
= 10.000000
- 8
= 2.000000
/ 4
= 0.500000

In [ ]:
// Exercice 15

#include "stdio.h"
#include "math.h"

float calculate(float a) {
    int n;
    float b;
    char operator;

    // Consumes extra input characters
    while (getchar() != ' ') {}
    n = scanf("%c%f", &operator, &b);
    // Consumes extra input characters
    while (getchar() != '\n') {}

    if (n < 2) {
        fprintf(stderr, "Invalid operator (%c) or second operand (%f).\n",
                operator, b);
        return NAN;
    }

    if (operator == '+') {
        return a + b;
    } else if (operator == '-') {
        return a - b;
    } else if (operator == '*') {
        return a * b;
    } else if (operator == '/') {
        return a / b;
    }
    fprintf(stderr, "Invalid operator %c.\n", operator);
    return NAN;
}

int main() {
    float a, result;
    int n = scanf("%f", &a);
    if (n == 0) {
        fprintf(stderr, "Invalid first operand.\n");
        return 1;
    }
    while (1) {
        result = calculate(a);
        if (isnan(result)) {
            return 1;
        }
        printf("= %f\n", result);
        a = result;
    }
}

Boucle for

La boucle for est adaptée pour itérer sur des objets :


In [8]:
#include "stdio.h"

int main() {
    for (int i = 1; i <= 6; i++) {
        printf("%i ", i);
    }
}


1 2 3 4 5 6 

Elle contient une condition, comme while, mais également deux parties supplémentaires :

  • une instruction lancée avant la première itération, ici int i = 1
  • une instruction lancée à la fin de chaque itération, ici i++

Exercice 16

Ajouter à la calculatrice la fonction puissance (notée ^) sans utiliser la fonction pow de math.h.


In [ ]:
// Exercice 16

#include "stdio.h"
#include "math.h"

float calculate(float a) {
    int n;
    float b, result;
    char operator;

    // Consumes extra input characters
    while (getchar() != ' ') {}
    n = scanf("%c %f", &operator, &b);
    // Consumes extra input characters
    while (getchar() != '\n') {}

    if (n < 2) {
        fprintf(stderr, "Invalid operator (%c) or second operand (%f).\n",
                operator, b);
        return NAN;
    }

    if (operator == '+') {
        return a + b;
    } else if (operator == '-') {
        return a - b;
    } else if (operator == '*') {
        return a * b;
    } else if (operator == '/') {
        return a / b;
    } else if (operator == '^') {
        result = a;
        for (int i = 1; i < b; i++) {
            result *= a;
        }
        return result;
    }
    fprintf(stderr, "Invalid operator %c.\n", operator);
    return NAN;
}

int main() {
    float a, result;
    int n = scanf("%f", &a);
    if (n == 0) {
        fprintf(stderr, "Invalid first operand.\n");
        return 1;
    }
    while (1) {
        result = calculate(a);
        if (isnan(result)) {
            return 1;
        }
        printf("= %f\n", result);
        a = result;
    }
}

Interrompre une boucle

Parfois, on a besoin d’arrêter une boucle avant qu’elle ait fini.

Pour cela, on utilise le mot-clé break.

Il est souvent plus facile de faire appel à break que d’utiliser dowhile. De même, il est souvent peu adapté de mettre une condition à while, car on va avoir besoin de tester une condition non pas au début ou la fin d’une boucle, mais au milieu :


In [ ]:
#include "stdio.h"

int main() {
    while (1) {
        puts("Would you like to quit? [yN]");
        if (getchar() == 'y') {
            break;
        }
        puts("Processing…");
    }
}

De même, on peut utiliser break au cours d’une boucle for.

Exercice 17

Remplacer la puissance « écrite à la main » par la fonction pow de math.h

Arrêter la calculatrice lorsque le résultat d’une opération est un nombre spécial secret.

Par exemple, arrêter la calculatrice lorsqu’on atteint 666, 7, un nombre proche de 3.14, 1.618, etc.


In [ ]:
//%cflags: -lm
// Exercice 17

#include "stdio.h"
#include "math.h"

float calculate(float a) {
    int n;
    float b;
    char operator;

    // Consumes extra input characters
    while (getchar() != ' ') {}
    n = scanf("%c%f", &operator, &b);
    // Consumes extra input characters
    while (getchar() != '\n') {}

    if (n < 2) {
        fprintf(stderr, "Invalid operator (%c) or second operand (%f).\n",
                operator, b);
        return NAN;
    }

    if (operator == '+') {
        return a + b;
    } else if (operator == '-') {
        return a - b;
    } else if (operator == '*') {
        return a * b;
    } else if (operator == '/') {
        return a / b;
    } else if (operator == '^') {
        return pow(a, b);
    }
    fprintf(stderr, "Invalid operator %c.\n", operator);
    return NAN;
}

int main() {
    float a, result;
    int n = scanf("%f", &a);
    if (n == 0) {
        fprintf(stderr, "Invalid first operand.\n");
        return 1;
    }
    while (1) {
        result = calculate(a);
        if (result == 666 || result == 7
                || (result >= 3.14 && result < 3.15)
                || (result >= 1.618 && result < 1.619)) {
            printf("Secret goal reached. Exiting.\n");
            return 0;
        } else if (isnan(result)) {
            return 1;
        }
        printf("= %f\n", result);
        a = result;
    }
}

Switchcase

C possède également une manière d’éviter des redondances dans les if :


In [9]:
int main () {
    int i = 3, return_code = 0;
    switch (i) {
        case 2:
            return_code = 1;
            break;
        case 5:
            return_code = 2;
            break;
        default:
            return_code = 3;
    }
    return return_code;
}


[C kernel] Executable exited with code 3

On précise break sinon les case et default suivant un case vrai sont également exécutés. Cette fonctionnalité un peu spéciale est parfois utile, mais la plupart du temps il est préférable de « casser » à la fin de chaque case.

Malheureusement, comme on le voit, le switchcase est souvent finalement plus long qu’un ensemble de ifs.

On privilégie souvent les ifs, mais on utilise des switchcase lorsqu’on a besoin de l’optimisation de vitesse qu’ils apporteent par rapport aux ifs.

Exercice 18

Réécrire la calculatrice de sorte que les conditions soient remplacées par des switchcase lorsque que c’est adapté.


In [ ]:
//%cflags: -lm
// Exercice 18

#include "stdio.h"
#include "math.h"

float calculate(float a) {
    int n;
    float b;
    char operator;

    // Consumes extra input characters
    while (getchar() != ' ') {}
    n = scanf("%c%f", &operator, &b);
    // Consumes extra input characters
    while (getchar() != '\n') {}

    if (n < 2) {
        fprintf(stderr, "Invalid operator (%c) or second operand (%f).\n",
                operator, b);
        return NAN;
    }

    switch (operator) {
        case '+':
            return a + b;
        case '-':
            return a - b;
        case '*':
            return a * b;
        case '/':
            return a / b;
        case '^':
            return pow(a, b);
        default:
            fprintf(stderr, "Invalid operator %c.\n", operator);
            return NAN;
    }
}

int main() {
    float a, result;
    int n = scanf("%f", &a);
    if (n == 0) {
        fprintf(stderr, "Invalid first operand.\n");
        return 1;
    }
    while (1) {
        result = calculate(a);
        if (result == 666 || result == 7
                || (result >= 3.14 && result < 3.15)
                || (result >= 1.618 && result < 1.619)) {
            printf("Secret goal reached. Exiting.\n");
            return 0;
        } else if (isnan(result)) {
            return 1;
        }
        printf("= %f\n", result);
        a = result;
    }
}

Tableaux

En C, un tableau est un succession de données du même type.

Un tableau a toujours une taille fixe qui doit être écrite explicitement dans le code.

Pour définir un tableau de 20 nombres entiers :


In [ ]:
int ages[20];

Pour assigner directement un tableau :


In [ ]:
int ages[6] = {8, 57, 32, 12, 26, 22};

Pour obtenir un élément à un index particulier (noter qu’on commence à 0 et non à 1) :


In [ ]:
ages[0];

Pour réassigner un élément :


In [ ]:
ages[0] = 9;

Exercice 19

Écrire un programme permettant de faire les comptes.

L’utilisateur saisira jusqu’à 100 nombres à virgule jusqu’à ce qu’il ne saississe plus qu’une phrase avec des espaces, comme “End of month”.

Le programme affichera alors un tableau listant toutes les opérations avec le solde du compte après chaque opération.

Exemple d’entrée/sortie :

857.32
1500
-500
-3
-20
End of month
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃  Amount ┃ Account balance ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│  857.32 │          857.32 │
│ 1500.00 │         2357.32 │
│ -500.00 │         1857.32 │
│   -3.00 │         1854.32 │
│  -20.00 │         1834.32 │
└─────────┴─────────────────┘

In [ ]:
// Exercice 19

#include "stdio.h"
#include "string.h"

void main() {
    int size;
    float operations_amounts[100];
    float amount;
    for (size = 0; size < 100; size++) {
        int n_parsed = scanf("%f", &amount);
        if (n_parsed < 1) {
            break;
        }
        operations_amounts[size] = amount;
    }
    printf("┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓\n");

    printf("\033[1m");  // Changes the text to bold.
    printf("┃ %10s ┃ %15s ┃\n",
           "Amount", "Account balance");
    printf("\033[0m");  // Resets text to normal weight.
    printf("┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩\n");
    float total = 0;
    for (int i = 0; i < size; i++) {
        amount = operations_amounts[i];
        total += amount;
        printf("│ %10.2f │ %15.2f │\n", amount, total);
    }
    printf("└────────────┴─────────────────┘\n");
}

Chaîne de caractères

En C, la chaîne de caractères n’existe pas vraiment en tant que telle.

Il s’agit en réalité d’un tableau contenant un caractère par case.

À la fin du tableau, une case additionelle contient le caractère spécial \0, marquant la fin de la chaîne de caractères.


In [ ]:
char name[60] = "Bonjour !";

Ici, le tableau contient donc :

0 1 2 3 4 5 6 7 8 9
B o n j o u r   ! \0

De la 11e à la 60e case, le tableau contient bien des valeurs, mais elles sont semi-aléatoires.

Dans cette variable name, on pourra donc définir à nouveau une valeur, tant que sa longueur n’excède pas 60 octets.

À noter : la librairie string.h contient de nombreuses fonctionalités utiles pour nous faciliter la vie. Les plus intéressantes pour tout de suite sont strlen et strcpy.

Exercice 20

Améliorer le programme de comptes pour pouvoir gérer la description de l’opération.

Désormais, l’utilisateur saisira des descriptions d’opérations bancaires suivis de nombres à virgule jusqu’à ce qu’il ne saississe plus qu’une phrase avec des espaces, comme “End of month”.

Exemple d’entrée/sortie :

Solde 857.32
Salaire 1500
Loyer -500
Chips -3
Disque -20
End of month
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃          Description ┃  Amount ┃ Account balance ┃
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│                Solde │  857.32 │          857.32 │
│              Salaire │ 1500.00 │         2357.32 │
│                Loyer │ -500.00 │         1857.32 │
│                Chips │   -3.00 │         1854.32 │
│               Disque │  -20.00 │         1834.32 │
└──────────────────────┴─────────┴─────────────────┘

In [ ]:
// Exercice 20

#include "stdio.h"
#include "string.h"

void main() {
    int size;
    char operations_names[100][20];
    float operations_amounts[100];
    char name[20];
    float amount;
    for (size = 0; size < 100; size++) {
        int n_parsed = scanf("%s %f", name, &amount);
        if (n_parsed < 2) {
            break;
        }
        strcpy(operations_names[size], name);
        operations_amounts[size] = amount;
    }
    printf("┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓\n");

    printf("\033[1m");  // Changes the text to bold.
    printf("┃ %20s ┃ %10s ┃ %15s ┃\n",
           "Description", "Amount", "Account balance");
    printf("\033[0m");  // Resets text to normal weight.
    printf("┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩\n");
    float total = 0;
    for (int i = 0; i < size; i++) {
        strcpy(name, operations_names[i]);
        amount = operations_amounts[i];
        total += amount;
        printf("│ %20s │ %10.2f │ %15.2f │\n", name, amount, total);
    }
    printf("└──────────────────────┴────────────┴─────────────────┘\n");
}

Tableaux à 2, 3, 4, … dimensions

Jusqu’à présent, on a fait des tableaux à une dimension.

Pour ajouter des dimensions, il suffit d’utiliser d’ajouter de nouvelles paires de crochets :


In [ ]:
unsigned short goban[19][19];

Ici, on a créé un plateau de jeu de go (dit goban) de 19 cases de côté, soit 361 cases.

Pour dire que le joueur 1 pose une pierre en C12, on fait :


In [ ]:
goban[2][11] = 1;

Pour savoir s’il y a une pierre posée en H4, on fait appel à :


In [ ]:
goban[7][3];

Exercice 21

Écrire un programme de tic-tac-toe. La plateau de jeu est affiché à nouveau à chaque tour. On demande tour à tour aux joueurs les coordonnées de là où ils veulent jouer. Bien entendu, le jeu doit s’arrêter dès qu’un joueur a gagné, et on doit empêcher les joueurs de tricher.

Exemple d’entrée/sortie :

    A   B   C
  ┌───┬───┬───┐
1 │   │   │   │
  ├───┼───┼───┤
2 │   │   │   │
  ├───┼───┼───┤
3 │   │   │   │
  └───┴───┴───┘

Player X? B2

    A   B   C
  ┌───┬───┬───┐
1 │   │   │   │
  ├───┼───┼───┤
2 │   │ X │   │
  ├───┼───┼───┤
3 │   │   │   │
  └───┴───┴───┘

Player O? C1

    A   B   C
  ┌───┬───┬───┐
1 │   │   │ O │
  ├───┼───┼───┤
2 │   │ X │   │
  ├───┼───┼───┤
3 │   │   │   │
  └───┴───┴───┘

In [ ]:
// Exercice 21

#include "stdio.h"

char board[3][3] = {{' ', ' ', ' '},
                    {' ', ' ', ' '},
                    {' ', ' ', ' '}};

void display() {
    printf("    A   B   C\n");
    printf("  ┌───┬───┬───┐\n");
    printf("1 │ %c │ %c │ %c │\n",
           board[0][0], board[0][1], board[0][2]);
    printf("  ├───┼───┼───┤\n");
    printf("2 │ %c │ %c │ %c │\n",
           board[1][0], board[1][1], board[1][2]);
    printf("  ├───┼───┼───┤\n");
    printf("3 │ %c │ %c │ %c │\n",
           board[2][0], board[2][1], board[2][2]);
    printf("  └───┴───┴───┘\n");
}


char get_winner() {
    char player;
    // Checks rows.
    for (int row = 0; row < 3; row++) {
        player = board[row][0];
        if (player != ' ' && board[row][1] == player && board[row][2] == player) {
            return player;
        }
    }
    // Checks columns.
    for (int col = 0; col < 3; col++) {
        player = board[0][col];
        if (player != ' ' && board[1][col] == player && board[2][col] == player) {
            return player;
        }
    }
    // Checks diagonals.
    player = board[1][1];
    if (player != ' ') {
        if ((board[0][0] == player && board[2][2] == player)
                || (board[0][2] == player && board[2][0] == player)) {
            return player;
        }
    }
    return ' ';
}


void ask_for_input(char player) {
    int n_parsed;
    unsigned char col, row;
    while (1) {
        printf("Player %c? ", player);
        n_parsed = scanf("%c%hhu", &col, &row);
        // Consumes extra input characters
        while (getchar() != '\n') {}

        if (n_parsed == 2) {
            col -= 65;  // Converts to a number between 0 and 2.
            row -= 1;   // Converts to a number between 0 and 2.
            if (col < 3 && row < 3 && board[row][col] == ' ') {
                break;
            }
        }
        printf("Invalid choice\n");
    }
    board[row][col] = player;
}


int main() {
    char player = 'X';
    while (1) {
        display();
        ask_for_input(player);
        char winner = get_winner();
        if (winner != ' ') {
            display();
            printf("%c won!\n", winner);
            break;
        }
        // Ternary operator, equivalent to `if … else`.
        player = player == 'X' ? 'O': 'X';
    }
}

Type casting

Pour passer d’un type à un autre, on utilise du type casting. Pour cela, on précise avant une valeur le type souhaité entre parenthèses :


In [10]:
#include "stdio.h"

int main() {
    int age = -106;
    printf("%i %i %li %f",
           (char)age, (unsigned char)age, (long)age, (float)age);
}


-106 150 -106 -106.000000

Notez que cela ne transforme pas les données stockées dans la RAM, mais bien la manière dont elles sont interprétées.

Ainsi, -106 en int est représenté FFFFFF96. En castant en char, on conserve juste le dernier octet, 96. En signed char, 96 correspond à -106, mais en unsigned char il correspond à 150.

Attention donc, le type casting n’est pas exactement une conversion, mais plutôt une interprétation différente de la même donnée.

Exercice 22

Écrire un programme affichant le résultat de la division de deux nombres entiers demandés à l’utilisateur. Constater le problème et corriger sans demander des float à l’utilisateur.

Pointeurs

Problème majeur avec les tableaux : ils ont une taille fixe. Au quotidien, on a sans arrêt besoin de lister des quantités indéterminées de données. Les pointeurs permettent de faire des tableaux de taille dynamique.

Les pointeurs sont un peu complexes à appréhender, mais il est primordial de les comprendre, ils sont omniprésents en C.

Pointer vers une variable

Un pointeur est en réalité une adresse de la RAM. à cette adresse seront stockées les données qui nous intéressent. Le & vu lors du scanf permet d’aller chercher l’adresse mémoire d’une variable, et ainsi de créer un pointeur. Par exemple :


In [11]:
#include "stdio.h"

int main() {
    int age = 28;
    int* age_pointer = &age;
    printf("Mon âge, à l’adresse mémoire %lu, est %i.\n",
           (unsigned long)age_pointer, age_pointer[0]);
    age_pointer[0] = 29;
    printf("Joyeux anniversaire ! %i ans déjà.\n", age_pointer[0]);
}


Mon âge, à l’adresse mémoire 140731546071708, est 28.
Joyeux anniversaire ! 29 ans déjà.

Pointeurs (suite)

Pointer vers une variable : explications

Beaucoup de choses dans l’exemple :

  • &age permet d’obtenir l’adresse en RAM de la variable age
  • int* est le type d’une adresse en RAM de variable int
  • age_pointer contient donc l’adresse en RAM de age : comme cela « pointe » vers age, on dit que cette variable est un pointeur
  • on caste age_pointer en unsigned long pour pouvoir afficher quelle est l’adresse mémoire : en effet, un pointeur est lui-même stocké sous forme d’une adresse de 8 octets, permettant ainsi de le caster directement en unsigned long
  • age_pointer[0] permet d’obtenir la donnée stockée à l’adresse mémoire stockée dans age_pointer : ici, c’est donc la valeur de age puisqu’age_pointer pointe à l’adresse de age.

Exercice 23

Créer une fonction prenant en paramètre un pointeur de nombre à virgule et renvoyant ce nombre divisé par 3.

Dans main, utiliser cette fonction sur un nombre à virgule saisi par l’utilisateur.


In [ ]:
// Exercice 23

#include <stdio.h>

float divide(float* number) {
    return number[0] / 3;
}

int main() {
    float number;
    scanf("%f", &number);
    printf("%f\n", divide(&number));
}

Pointeurs (suite)

Pointer vers un tableau

Pointer vers un tableau existant fonctionne comme précédemment, mais on peut demander des indices différents de 0 :


In [12]:
#include "stdio.h"

int main() {
    char name[50] = "Bertrand Bordage";
    char* name_pointer = name;
    printf("%c%c %s", name_pointer[0], name_pointer[7], &name_pointer[9]);
}


Bd Bordage

Quelques explications s’imposent encore.

Pointeurs (suite)

Pointer vers un tableau : explications

Dans l’exemple précédent, on a utilisé à nouveau les mêmes notations, mais elles peuvent paraître incohérentes par rapport à précédemment. Pourtant tout est logique, voilà les raisons :

  • On créé name_pointer sans utiliser name : c’est parce que name n’est pas un char mais un tableau de char, ce qui est un concept proche du pointeur, mais avec une taille fixe. Cette proximité permet de créer un pointeur directement à partir d’un tableau, sans &.
  • name_pointer[0] correspond à la première lettre du tableau, et non au tableau lui-même : le pointeur est en fait une autre façon d’utiliser le tableau. L’adresse du pointeur est l’adresse de la première case du tableau, c’est pourquoi on ne fait pas name_pointer[0][0] pour obtenir la première lettre.
  • &name_pointer[9] permet d’aller chercher la référence de la dixième case du tableau, et donc de créer un pointeur commençant à Bordage. Le %s du format permet d’afficher une chaîne de caractères, donc il affiche les caractères commençant à l’index 9 jusqu’à rencontrer \0.

Exercice 24

Écrire une fonction string_length renvoyant la longueur d’une chaîne de caractères passée en argument sous la forme de pointeur.

Interdiction bien sûr d’utiliser strlen de string.h.

Utiliser cette fonction pour afficher la longueur d’une chaîne saisie par un utilisateur, pouvant aller jusqu’à 10000 caractères.


In [ ]:
// Exercice 24

#include "stdio.h"

unsigned int string_length(char* data) {
    unsigned int i = 0;
    while (data[i] != '\0') {
        i++;
    }
    return i;
}

int main() {
    char data[10000];
    scanf("%s", data);
    printf("%i\n", string_length(data));
}

Pointeurs (suite)

Chaîne de caractères

Comme vu précédemment, on peut utiliser des pointeurs pour les chaînes de caractères. Dans tous les cas où on souhaite utiliser une chaîne de caractères de taille indéterminée, on préfère utiliser un pointeur. De plus, c’est simple à utiliser :


In [13]:
#include "stdio.h"

int main() {
    char* name = "Bertrand Bordage";
    printf("%s", name);
}


Bertrand Bordage

À partir de maintenant, on privilégiera char* pour les chaînes de caractères.

Pointeurs (suite)

Tableau de taille dynamique

On arrive au cœur de l’intérêt du pointeur : faire de grand tableaux dont la taille varie. Par exemple :


In [14]:
#include "stdio.h"
#include "stdlib.h"

int* get_range(int n) {
    int* numbers = (int*)malloc(n * sizeof(int));
    while (n > 0) {
        n--;
        numbers[n] = n;
    }
    return numbers;
}

int main() {
    int* range = get_range(6);
    printf("%i, %i", range[0], range[5]);
    free(range);
}


0, 5

Pointeurs (suite)

Tableau de taille dynamique : explications

Dans l’exemple précédent :

  • La gestion de mémoire de ce pointeur ne peut être faite automatiquement, on doit donc la faire manuellement avec malloc et free, permettant de réserver et libérer de la RAM.
  • stdlib.h contient les fonctions malloc et free utilisées ensuite.
  • malloc permet de réserver un emplacement mémoire, ici de la taille de n int.
  • sizeof permet d’obtenir le nombre d’octets d’un type, ici int, donc cela vaut 4.
  • On caste le pointeur renvoyé par malloc car malloc renvoie toujours le type void*.
  • free(range) libère l’espace mémoire réservé dès qu’on n’en a plus besoin.

Attention ! Tout malloc doit toujours avoir un free qui correspond, sans quoi le programme risque des fuites de mémoire, et ainsi utiliser de plus en plus de RAM inutilement.

Autre fonction utile : realloc, qui permet d’agrandir un espace mémoire déjà réservé.

Exercice 25

Réécrire le programme de l’exercice 24 de sorte que l’utilisateur puisse mettre en entrée une chaîne de caractères potentiellement illimitée.


In [ ]:
// Exercice 25

#include "stdio.h"
#include "stdlib.h"

unsigned int string_length(char* data) {
    unsigned int i = 0;
    while (data[i] != '\0') {
        i++;
    }
    return i;
}

char is_end_of_input(char* buffer) {
    for (int i = 0; i < 10; i++) {
        if (buffer[i] == '\0') {
            return 1;
        }
    }
    return 0;
}

int main() {
    unsigned int size = 10;
    char* data = (char*)malloc(size * sizeof(char));
    char* current_buffer = data;
    while (1) {
        scanf("%10[^\n]", current_buffer);
        if (is_end_of_input(current_buffer)) {
            break;
        }
        size += 10;
        data = (char*)realloc(data, size * sizeof(char));
        current_buffer = data + size - 10;
    }
    printf("%i\n", string_length(data));
    free(data);
}

Pointeurs (suite)

Pointeurs vs tableaux

Dernier avantage des pointeurs : ils peuvent être renvoyés par une fonction, ce qui est impossible pour un tableau.

Tout ceci dit, on peut se demander quel est l’intérêt des tableaux, quand les pointeurs sont si puissants.

Et effectivement, dans la plupart des cas, on utilise des pointeurs et non des tableaux, car il est finalement rare d’avoir des listes de données de taille fixe.

Mais il existe des cas où il est utile de donner une taille fixe à un tableau. Comme vu précédemment, cela convient par exemple pour des gobans, échiquiers, etc, dont la taille est toujours fixe.

Comme nous le verrons, cela peut également être utile pour les structures.

Création de type

On peut créer un nouveau type en se basant sur des types déjà existants :


In [ ]:
typedef unsigned long int uint128;
typedef char chessboard[8][8];

int main() {
    uint128 i = 68714234687197654;
    chessboard board;
}

Ici, uint128 n’est finalement qu’un raccourci d’un type déjà existant, ce qui est peu utile.

Le second cas, est nettement plus utile. Lors d’un programme d’échecs, on n’a pas à se préoccuper du nombre de cases dans un échiquier, ou du type de chaque case. On dit juste qu’on créé un échiquier, tout simplement.

Le cas précédent est déjà utile, mais les structures sont un type composé encore plus intéressant.

Exercice 26

Créer un nouveau type pour le « plateau » du tic-tac-toe.

Initialiser un plateau avec la partie suivante :

┌───┬───┬───┐
│ X │ X │   │
├───┼───┼───┤
│ O │ O │ X │
├───┼───┼───┤
│ X │   │ O │
└───┴───┴───┘

Lire/écrire des fichiers

stdio contient également de quoi traiter des fichiers. Malheureusement, comme c’est plus proche de ce qu’il se passe vraiment dans la machine, c’est assez compliqué.

Outils indispensables

  • FILE : type d’un fichier, utilisé avec un pointeur
  • fopen : fonction permettant d’ouvrir le fichier en choisissant son mode (lecture, écriture, etc)
  • fwrite : fonction permettant d’écrire un groupe d’octets dans le fichier
  • fread : fonction permettant de lire un groupe d’octets dans le fichier
  • feof : fonction permettant de savoir si on est à la fin du fichier
  • fclose : fonction permettant de fermer et donc enregistrer le fichier

Lire/écrire des fichiers

Écrire

Voici comment écrire le contenu d’un tableau de caractères content dans un fichier example.txt :


In [ ]:
#include "stdio.h"
#include "stdlib.h"

int main() {
    FILE* file = fopen("example.txt", "w");
    char content[] = "Lorem ipsum\n";
    fwrite(content, sizeof(char), sizeof(content) - 1, file);
    fclose(file);
}

Exercice 27

Modifier le tic-tac-toe de l’exercice 21 pour que chaque tour soit enregistré dans un même fichier les contenant tous à la suite.

Lire/écrire des fichiers

Lire

Paradoxalement, la lecture d’un fichier est nettement plus complexe. En effet, la taille du contenu est inconnue, et on doit gérer le cas où le fichier n’existe pas. Cela donne :


In [ ]:
#include "stdio.h"
#include "stdlib.h"

int main() {
    FILE* file = fopen("example.txt", "r");
    if (file == NULL) {
        return 1;
    }
    long index = 0;
    long size = 0;
    char* content = NULL;
    while (!feof(file)) {
        index = size;
        size += BUFSIZ;
        content = (char*)realloc(content, size * sizeof(char));
        fread(&content[index], sizeof(char), BUFSIZ, file);
    }
    printf("%s", content);
    free(content);
    fclose(file);
}

Exercice 28

Modifier le tic-tac-toe pour reprendre automatiquement une partie en cours dans le fichier sauvegardé à l’épisode précédent. Si la partie était finie, on recommence une nouvelle partie.

Structures

La structure est un type composé de plusieurs autres types groupés ensembles et utilisables très facilement. C’est l’ancêtre du concept de classe, pour ceux qui connaissent.

Ici, on crée un type Book contenant trois attributs : title, author et pages.


In [15]:
#include "stdio.h"
#include "string.h"

typedef struct {
    char title[100];
    char author[60];
    int pages;
} Book;

void show_book(Book book) {
    printf("Book “%s” by %s contains %i pages\n",
           book.title, book.author, book.pages);
}

int main() {
    Book book = {"Les Piliers de la Terre", "Ken Follet", pages: 1050};
    show_book(book);
    book.pages = 1076;
    strcpy(book.title, "Pillars of the Earth");
    show_book(book);
}


Book “Les Piliers de la Terre” by Ken Follet contains 1050 pages
Book “Pillars of the Earth” by Ken Follet contains 1076 pages

Exercice 29

Créer une structure Language permettant de contenir les informations de l’exercice 7.

Écrire une fonction permettant d’afficher la ligne de tableau correspondant à un langage.

Créer chacun des langages et les afficher sous forme de tableau à l’aide de la fonction ainsi créée.

Modules

Jusqu’à présent, on a créé uniquement des programmes exécutables directement. Souvent, on réunit des fonctionnalités à part dans un même module qui pourra être utilisable dans plusieurs programmes.

À chaque utilisation de module, on faisait un #include "module.h". Jusqu’à présent on n’a défini que des fichiers .c, qui contiennent le fonctionnement du code. Les fichiers .h contiennent les définitions de types et les signatures des fonctions, c’est-à-dire une version de la fonction spécifiant uniquement son nom, son type de retour et ses arguments.

Modules

Exemple

Fichier triangle.c :


In [ ]:
#include "math.h"

typedef struct {
    float x;
    float y;
} Point;

typedef struct {
    Point a;
    Point b;
    Point c;
} Triangle;

float distance(Point a, Point b) {
    float dx = (b.x - a.x);
    float dy = (b.y - a.y);
    return sqrt(dx * dx + dy * dy);
}

float perimeter(Triangle t) {
    return distance(t.a, t.b) + distance(t.b, t.c) + distance(t.a, t.c);
}

Modules

Exemple (suite)

Fichier triangle.h :


In [ ]:
typedef struct {
    float x;
    float y;
} Point;

typedef struct {
    Point a;
    Point b;
    Point c;
} Triangle;

float distance(Point a, Point b);

float perimeter(Triangle t);

Modules

Exemple (suite)

Fichier example.c :


In [ ]:
#include "stdio.h"
#include "triangle.h"

int main() {
    Triangle t = {(Point){30, 5}, (Point){10, 16}, (Point){3, 8}};
    printf("%f\n", perimeter(t));
}

Modules

Compilation

Malheureusement, ce n’est pas de tout repos :

  • gcc -c -fPIC triangle.c -o triangle.o
  • gcc -shared triangle.o -o libtriangle.so
  • gcc example.c -o example -L. -ltriangle -lm

Puis on peut lancer le programme ainsi :

  • LD_LIBRARY_PATH=. ./example (LD_LIBRARY_PATH est utile ici car la librairie triangle n’est pas installée globalement)

Cette tâche laborieuse est souvent automatisée par Make ou autres outils.

Noter ici qu’on lie les librairies triangle et m à la troisième ligne de compilation. C’est à cet endroit qu’on ajoute les éventuelles autres librairies à lier.

Exercice 30

Copier et compiler la librairie comme indiqué.

Directives du préprocesseur

Avant l’étape de compilation, un préprocesseur entre en jeu et permet de modifier votre code avant qu’il soit compilé.

Les instructions données au préprocesseur sont appellées directives et commencent par un #, sans ; à la fin.

Oui, les #include – dont la syntaxe était un peu à part – étaient bien des directives de préprocesseur.

#define

Elle permet de créer une variable globale qui sera remplacée partout juste avant la compilation. Ainsi, le préprocesseur va transformer ce code en la cellule suivante :


In [ ]:
#define SUDOKU_SIZE 8
char sudoku[SUDOKU_SIZE][SUDOKU_SIZE];

In [ ]:
char sudoku[8][8];

Directives du préprocesseur (suite)

if, elif, else, endif

On peut également définir du code en fonction d’un paramètre de préprocesseur.

C’est considéré comme une mauvaise pratique de s’en servir, mais souvent on n’a pas le choix. En effet, le nom des modules et leurs possibilités dépendent des systèmes d’exploitation. Ainsi, la plupart des conditions visibles dans des librairies en C permettent d’écrire de la compatibilité entre systèmes d’exploitation :


In [ ]:
#if defined __unix__  // For Linux and MacOS
    #include "unistd.h"
#elif defined _WIN32  // For Windows (even with 64 bits…)
    #include "windows.h"
#endif

Exercice 31

Réécrire l’exercice 25 en utilisant un paramètre de préprocesseur pour définir la taille du buffer.

Exercice final

Écrire un programme simulant un jeu d’échecs. À chaque coup y compris le premier, on affichera l’échiquier avec les pièces dedans et des coordonnées. On demandera tour à tour aux deux joueurs quel déplacement effectuer. La partie s’arrête quand il n’y a plus de pièces blanches ou noires dans l’échiquier. Pour commencer, on autorise tous les déplacements et on exclut les règles complexes comme les roques.

Idéalement, le programme sauvegardera automatiquement une partie au fur et à mesure qu’elle avance. La sauvegarde contiendra tous les états successifs du plateau tel qu’affichés dans le terminal.

De même, idéalement il faudrait structurer le programme de manière modulaire, de sorte d’éviter un énorme fichier contenant toute la logique.

Exemple d’entrée/sortie :

    A   B   C   D   E   F   G   H
  ┌───┬───┬───┬───┬───┬───┬───┬───┐
1 │ ♜ │ ♞ │ ♝ │ ♛ │ ♚ │ ♝ │ ♞ │ ♜ │
  ├───┼───┼───┼───┼───┼───┼───┼───┤
2 │ ♟ │ ♟ │ ♟ │ ♟ │ ♟ │ ♟ │ ♟ │ ♟ │
  ├───┼───┼───┼───┼───┼───┼───┼───┤
3 │   │   │   │   │   │   │   │   │
  ├───┼───┼───┼───┼───┼───┼───┼───┤
4 │   │   │   │   │   │   │   │   │
  ├───┼───┼───┼───┼───┼───┼───┼───┤
5 │   │   │   │   │   │   │   │   │
  ├───┼───┼───┼───┼───┼───┼───┼───┤
6 │   │   │   │   │   │   │   │   │
  ├───┼───┼───┼───┼───┼───┼───┼───┤
7 │ ♙ │ ♙ │ ♙ │ ♙ │ ♙ │ ♙ │ ♙ │ ♙ │
  ├───┼───┼───┼───┼───┼───┼───┼───┤
8 │ ♖ │ ♘ │ ♗ │ ♕ │ ♔ │ ♗ │ ♘ │ ♖ │
  └───┴───┴───┴───┴───┴───┴───┴───┘

White player? B8 C6

    A   B   C   D   E   F   G   H
  ┌───┬───┬───┬───┬───┬───┬───┬───┐
1 │ ♜ │ ♞ │ ♝ │ ♛ │ ♚ │ ♝ │ ♞ │ ♜ │
  ├───┼───┼───┼───┼───┼───┼───┼───┤
2 │ ♟ │ ♟ │ ♟ │ ♟ │ ♟ │ ♟ │ ♟ │ ♟ │
  ├───┼───┼───┼───┼───┼───┼───┼───┤
3 │   │   │   │   │   │   │   │   │
  ├───┼───┼───┼───┼───┼───┼───┼───┤
4 │   │   │   │   │   │   │   │   │
  ├───┼───┼───┼───┼───┼───┼───┼───┤
5 │   │   │   │   │   │   │   │   │
  ├───┼───┼───┼───┼───┼───┼───┼───┤
6 │   │   │ ♘ │   │   │   │   │   │
  ├───┼───┼───┼───┼───┼───┼───┼───┤
7 │ ♙ │ ♙ │ ♙ │ ♙ │ ♙ │ ♙ │ ♙ │ ♙ │
  ├───┼───┼───┼───┼───┼───┼───┼───┤
8 │ ♖ │   │ ♗ │ ♕ │ ♔ │ ♗ │ ♘ │ ♖ │
  └───┴───┴───┴───┴───┴───┴───┴───┘

Black player?

Merci et bonne continuation !

Et tenez-moi au courant si vous continuez à améliorer la démo.

Twitter : @NoriPytCom - GitHub : @BertrandBordage