.. _c-03-programme: Comparaison avec Python ======================= On vient de voir qu'une différence majeure entre Python et C est la manière dont on exécute un programme dans chacun des deux langages : - Python utilise un interpréteur qui lit le code source et l'exécute directement sur toute cible matérielle sur laquelle l'interpréteur Python est installé ; - C nécessite une étape de compilation spécifique pour produire un exécutable sur chaque cible matérielle. Par exemple un même code doit être compilé une fois pour Windows et une autre fois pour Linux. Pour une première comparaison de la façon d'écrire et d'exécuter du code dans chacun des deux langages, considérons le calcul du `PGCD `_ de deux entiers selon l'algorithme d'Euclide :: pgcd(a, b) = pgcd( b, reste(a, b) ) pour b ≠ 0 pgcd(a, 0) = a PGCD : code Python ------------------ Le code Python correspondant au calcul du PGCD selon l'algorithme d'Euclide est donné ci dessous: .. code-block:: python :linenos: # gcd.py def gcd(a, b): if b == 0: return a else: return gcd(b, a % b) def main(): print("GCD: " + str(gcd(24, 40))) if __name__ == "__main__": main() Pour rappel, en Python le code est organisé en quatre parties : #. l'importation de bibliothèques externes. Il n'y en a pas besoin ici, et cette partie est vide ; #. la définition des variables globales. Il n'y en a pas besoin ici, et cette partie est vide ; #. la définition des fonctions secondaires. Ici une seule fonction secondaire, :func:`gcd` qui prend en argument deux nombres, implémente l'algorithme d'Euclide, et retourne le PGCD (lignes 3-7) ; #. la définition de la fonction principale :func:`main` qui appelle les fonctions précédentes. Ici :func:`gcd` est appelée avec les arguments 24 et 40, puis le résultat est affiché (lignes 9-10) ; #. l'appel de la fonction :func:`main` déclenché lors de l'exécution du programme (lignes 12-13). Le programme est exécuté avec:: $ python gcd.py L'affichage correspondant:: GCD: 8 PGCD : code C ------------- Voici le même programme écrit en C : .. code-block:: c :linenos: // gcd.c #include int gcd(int a, int b) { if (b == 0) { return a; } else { return gcd(b, a % b); } } int main() { printf("GCD: %d\n", gcd(24, 40)); return 0; } L'organisation du code est quasi identique : #. l'importation des bibliothèques externes (ligne 3) ; #. la définition des variables globales. Il n'y en a pas besoin ici, et cette partie est vide ; #. la définition des fonctions secondaires (lignes 5-15) ; #. la définition de la fonction principale :func:`main` (lignes 17-21) ; #. il n'y a pas d'appel explicite de :func:`main`, celui ci est déclenché par l'exécution du programme. Il y a cependant des différences dans le code proprement dit : - pour utiliser :func:`printf`, il faut "importer" une bibliothèque (ligne 3), alors que la fonction :func:`print` fait partie du langage en Python ; - les variables doivent être déclarées explicitement alors que Python pratique l'inférence de type. Ici ça concerne les paramètres de la fonction ainsi que sa valeur de retour (ligne 5) ; - la structuration du code est définie par une paire d'accolades alors qu'en Python elle est définie par l'indentation ; - toujours pour la structuration, chaque instruction C se termine par un ``;`` ; - le prédicat d'un test (ligne 7) doit être encapsulé dans une paire de parenthèses ``( )`` ; - il est courant que la fonction :func:`main` retourne 0 (ligne 20) pour signifier au système d'exploitation que tout s'est bien passé. Pour exécuter ce code, il faut d'abord compiler le programme pour produire un exécutable:: $ gcc -std=c99 -Wall -Wextra gcd.c -o gcd Les options utilisées pour la compilation : - ``-std=c99`` : version du langage C ; - ``-Wall`` : affiche tous les warnings ; - ``-Wextra`` : donne des informations additionnelles sur la cohérence du code ; - ``-o`` : nom de l'exécutable. :file:`gcd.c` contient le code source. :file:`gcd` est l'exécutable généré par le processus de compilation et d'édition de liens. Il faut ensuite lancer le programme exécutable:: $ ./gcd GCD: 8 Le résultat est sans surprise identique à celui obtenu avec le programme Python. On vient de constater qu'il y a une grande similitude entre la structuration des deux langages. Observons maintenant les différences de code mises en évidence ici de manière approfondie. Les variables ------------- Le langage C nécessite de déclarer explicitement le type des variables utilisées. Cette connaissance est utilisée par le compilateur et permet d'optimiser la mémoire et le temps d'exécution. Les principaux types sont : - ``int`` pour les entiers (32 bits) ; - ``double`` pour les nombres rééls (64 bits) ; - ``char`` pour les caractères (8 bits). .. note:: C dispose en fait de nombreux autres types de données qu'on n'abordera pas dans le cadre de ce cours. La table ci dessous en donne la liste, les bornes des valeurs qu'ils permettent de représenter sans erreur, et le spécificateur à utiliser avec ``printf()``. Les valeurs ci dessous sont dépendantes de la plateforme matérielle (CPU) et logicielle (compilateur). Elles sont données ici pour une architecture Intel(R) Core(TM) i7-6700 CPU, une distribution Linux Ubuntu 20.04 et le compilateur gcc 9.4.0. .. csv-table:: Types de données en C :file: files/c/c-03-programme-data-types.csv :header-rows: 1 :delim: ; :align: center Contrairement à Python : - C n'a pas de type booléen. La *vérité* est stockée dans un ``int`` (ce qui n'est pas très efficace d'un point de vue encombrement mémoire). Dans un prédicat, ``0`` est évalué à "False" et tout autre nombre entier est évalué à "True" ; - le typage en C est statique, c'est à dire qu'une même variable ne peut pas changer de type au cours de la vie d'un programme. Pour approfondir la notion de *vérité* en C, répondez aux questions suivantes. .. quiz:: quizz-01 :title: La vérité en C - Quel est le nombre d'octets utilisé pour stocker un entier ? :quiz:`{"type":"FB","answer":"4", "size":5}` - Quel est le nombre de bits utilisé pour stocker un entier ? :quiz:`{"type":"FB","answer":"32", "size":5}` - Quel est le nombre minimal théorique de bits nécessaire pour stocker un booléen ? :quiz:`{"type":"FB","answer":"1", "size":5}` Déclaration et affectation .......................... L'instruction ci dessous déclare une variable ``x`` de type ``int``: .. code-block:: c int x; Ceci réserve un espace nommé dans la mémoire, à une adresse définie par le compilateur, de taille définie par le type. La valeur est indéterminée. .. image:: images/c-03-programme-fig-01.png :width: 20% :align: center On lui affecte ensuite une valeur avec l'instruction : .. code-block:: c x = 37; On peut également effectuer les 2 opérations avec une seule instruction : .. code-block:: c int x = 37; .. image:: images/c-03-programme-fig-02.png :width: 20% :align: center On a une bijection entre la zone mémoire réservée et le nom qu'on lui attribue. C'est une différence majeure avec Python, on aura l'occasion de le voir plus loin. .. important:: Une variable doit être déclarée **une fois et une seule** dans le domaine dans lequel elle est utilisée, avec deux possibilités : - déclaration et initialisation à deux endroits distincts du code ; - déclaration et initialisation sur la même ligne. Si on tente de lui affecter un type différent, le compilateur effectue la conversion nécessaire si elle est possible/pertinente. Ci dessous, conversion d'un nombre décimal en entier : .. code-block:: c int x = 37.0; // x = 37 Déclarer une autre variable et lui affecter le contenu d'une autre réserve **un autre** espace mémoire. Ainsi le code : .. code-block:: c int x = 37; int y = x; va réserver un nouvel espace mémoire qui différera du premier : - par son adresse ; - et par le nom qui permet de le référencer. .. image:: images/c-03-programme-fig-03.png :width: 20% :align: center .. quiz:: quizz-01 :title: L'encombrement mémoire - Quel sont les 4 derniers digits hexadécimaux de l'adresse de ``x`` ? :quiz:`{"type":"FB","answer":"ca00", "size":5}` - Quel sont les 4 derniers digits hexadécimaux de l'adresse de ``y`` ? :quiz:`{"type":"FB","answer":"ca04", "size":5}` - Combien d'octets représentent 4 digits hexadécimaux ? :quiz:`{"type":"FB","answer":"2", "size":5}` - Sachant que ``x`` est un ``int``, le compilateur a t-il réservé une adresse mémoire contigüe pour ``y`` ? :quiz:`{"type":"TF","answer":"T"}` Comparaison avec Python ......................... C est un langage ancien et ne disposait pas des méthodes modernes de gestion de la mémoire. Pour garantir une exécution rapide, il était **nécessaire** d'interagir directement avec la mémoire. Ce mécanisme est implémenté à très bas niveau dans le langage et perdure aujourd'hui, ce qui lui assure toujours une grande vitesse d'exécution, au détriment d'une certaine complexité de programmation. En revanche Python, dispose d'un mécanisme automatisé de gestion de mémoire qui permet : - de réserver automatiquement un emplacement en mémoire en fonction de l'objet manipulé ; - de faire croître dynamiquement la taille réservée tout au long de la vie de l'objet ; - de libérer automatiquement la mémoire lorsque l'objet n'est plus utilisé. Pour illustrer ce fonctionnement différent, observons l'affectation d'une variable en Python. .. code-block:: python >>> x = 37 >>> id(x) 1658682502576 Un objet est créé en mémoire, et **une référence** lui est affectée. La fonction :func:`id` peut être vue comme une fonction de localisation en mémoire mais, contrairement au C, ne véhicule pas d'information sur la position **physique** en mémoire. .. image:: images/c-03-programme-fig-04.png :width: 40% :align: center Contrairement à C, l'affectation de ``x`` à une nouvelle variable **ne créée pas de nouvel objet** en mémoire, comme le montre le code suivant .. code-block:: python >>> y = x >>> id(y) 1658682502576 >>> x is y True qui peut s'illustrer avec le schéma suivant : .. image:: images/c-03-programme-fig-05.png :width: 40% :align: center Variables locales ................. En C, si des variables locales sont nécessaires, la déclaration de ces variables est placée au début de la fonction qui les utilise. L'algorithme d'Euclide utilisé précédemment n'avait pas besoin de variables additionnelles. Les seuls paramétres ``a`` et ``b`` suffisaient. Si l'on omet de déclarer une variable, le compilateur produit une erreur avec un message indicatif. La lecture approfondie de ce type de message est une étape nécessaire dans la conception d'un programme. Espaces, tabulations et accolades --------------------------------- En Python, les espaces et les tabulations sont utilisés pour structurer le programme et ont donc un impact sur la façon dont le code sera exécuté. En C, les espaces et les tabulations sont utilisés uniquement pour séparer les instructions, variables, etc... Ce qui signifie que le code ci dessous est parfaitement valide, quoique assez illisible. .. code-block:: c :linenos: // gcd2.c #include int gcd(int a, int b){if (b == 0){return a;}else{return gcd(b, a % b);}}int main(){printf("GCD: %d\n", gcd(24, 40));return 0;} Python force le programmeur à écrire un programme lisible. Ce n'est pas le cas de C mais la plupart des environnements de développements modernes, `VS Code `_ par exemple, utilisent des formatteurs qui indentent le code de façon efficace (:kbd:`Shift+Alt+F` sous Windows). En C, les accolades ``{ }`` sont utilisées pour structurer le code. Elles sont optionnelles si le bloc ne contient qu'une seule instruction. Ainsi, la fonction ci dessous est du code C valide .. code-block:: c :linenos: int gcd(int a, int b) { if (b == 0) return a; else return gcd(b, a % b); } que l'on peut, sans perte de lisibilité, également écrire : .. code-block:: c :linenos: int gcd(int a, int b) { if (b == 0) return a; else return gcd(b, a % b); } Les commentaires ---------------- Le code source d'un programme contient des instructions écrites par un humain à destination d'une machine dans un langage déterminé (C ou Python par exemple). Il doit également contenir des informations facilitant la compréhension, la relecture et la maintenance, pour celui qui l'a écrit mais également pour ceux amenés à intervenir sur le code à un moment ou un autre de la vie du programme. On signale à la machine que ces instructions ne sont pas a exécuter avec une syntaxe particulière : - en Python, tous les caractères à droite du caractère ``#`` seront ignorés ; - en C : - tous les caractères à droite des caractères ``//`` seront ignorés (commentaires de ligne) ; - tous les caractères compris entre ``/*`` et ``*/`` seront ignorés (commentaires de bloc). Exemples de commentaires en Python: .. code-block:: python # gcd.py # Calcul du Greatest Common Divisor def gcd(a, b): # Algorithme d'Euclide Exemples de commentaires en C: .. code-block:: c /* gcd.c Calcul du Greatest Common Divisor */ int gcd(int a, int b) // Algorithme d'Euclide Une bonne pratique est de commenter son code en expliquant **pourquoi** on écrit une instruction et éventuellement **comment** si l'implémentation est complexe. La fonction :func:`printf` -------------------------- En C, l'affichage est contrôlé par la fonction :func:`printf`. Cette fonction comporte ``n`` paramètres (au moins 1) : - le premier paramètre est une chaîne de formatage : - contenant un texte fixe ; - et éventuellement ``n-1`` places réservées pour l'affichage de ``n-1`` variables ; - les ``n-1`` variables éventuelles à afficher. Ainsi l'instruction .. code-block:: c printf("GCD: %d\n", gcd(24, 40)) se décompose en : - la chaîne de formatage : ``"GCD: %d\n"`` dans laquelle ``%d`` est un espace réservé pour la valeur à afficher. Les autres caractères sont imprimés tels quels ; - et la valeur à afficher : ``gcd(24, 40)`` qui sera insérée dans l'espace réservé. Dans la chaine de formatage, on utilise le caractère spécial ``\n`` qui insère un saut de ligne. Voici les caractères spéciaux les plus courants: - ``\n`` : saut de ligne ; - ``\t`` : tabulation ; - ``\\`` : le caractère ``\`` ; - ``\"`` : le caractère ``"``. Les espaces réservés suivants sont les plus courants : - ``%d`` : nombre entier. Par exemple 1000 ; - ``%f`` : nombre réél en notation décimale. Par exemple 1000.0 ; - ``%e`` : nombre réél en notation scientifique. Par exemple 1.000000e3 ; - ``%c`` : caractère alphanumérique ; - ``%s`` : chaîne de caractères. Il y a beaucoup d'autres paramètres permettant de contrôler l'affichage. Une référence complète `ici `_. .. quiz:: quizz-02 :title: La chaine de formatage On souhaite écrire un programme qui affiche le résultat de l'opération de multiplication de deux entiers, de telle sorte qu'un exemple d'affichage dans le terminal puisse être:: $ ./mult 3 x 5 = 15 $ - Quelle est le contenu de la chaine de formatage correspondante ? :quiz:`{"type":"FB","answer":"\"%d x %d = %d\\n\"", "size":20}` Les fonctions ------------- C'est une bonne pratique en Python que d'encapsuler tout le code dans des fonctions mais ce n'est pas requis par le langage. Pour C au contraire, cette structuration est indispensable. Reprenons le code de la fonction :func:`gcd` étudiée précédemment. .. code-block:: c :linenos: int gcd(int a, int b) { if (b == 0) { return a; } else { return gcd(b, a % b); } } La ligne 1 représente la signature de la fonction, que l'on peut décomposer en trois parties : - le type de la valeur de retour : ``int`` ; - le nom de la fonction : :func:`gcd` ; - la liste de ses paramètres formels typés placés entre parenthèses : ``int a, int b``. Le corps de la fonction est placé entre accolades ``{ }`` et doit retourner une valeur dont le type est cohérent avec la signature. Si une fonction ne retourne rien, le type ``void`` est utilisé. Dans un programme C découpé en fonctions, il doit exister une fonction principale :func:`main`. La fonction :func:`main` retourne un entier dont la valeur est utilisée par le système d'exploitation pour connaitre le statut du programme après son exécution. Par convention, la valeur 0 est retournée si tout s'est bien passé, et un entier différent de 0 en cas de problème dans l'exécution. La fonction :func:`main` est le point d'entrée du programme, en ce sens que c'est celle qui va être appelée automatiquement lorsqu'on va exécuter le programme. Les autres fonctions, que l'on placera au début du programme, sont appelées fonctions secondaires. .. note:: Rigoureusement on peut placer ces fonctions à la suite de la fonction :func:`main` mais il faut alors que le prototype des fonctions en question soit placé avant :func:`main`, le plus souvent dans un fichier ``.h``. A titre d'exemple, la fonction :func:`printf` est utilisable dans :func:`main` car elle est déclarée dans ``stdio.h``. En Python, l'appel de :func:`main` n'est pas automatique, il faut le déclencher.