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:

 1# gcd.py
 2
 3def gcd(a, b):
 4    if b == 0:
 5        return a
 6    else:
 7        return gcd(b, a % b)
 8
 9def main():
10    print("GCD: " + str(gcd(24, 40)))
11
12if __name__ == "__main__":
13    main()

Pour rappel, en Python le code est organisé en quatre parties :

  1. l’importation de bibliothèques externes. Il n’y en a pas besoin ici, et cette partie est vide ;

  2. la définition des variables globales. Il n’y en a pas besoin ici, et cette partie est vide ;

  3. la définition des fonctions secondaires. Ici une seule fonction secondaire, gcd() qui prend en argument deux nombres, implémente l’algorithme d’Euclide, et retourne le PGCD (lignes 3-7) ;

  4. la définition de la fonction principale main() qui appelle les fonctions précédentes. Ici gcd() est appelée avec les arguments 24 et 40, puis le résultat est affiché (lignes 9-10) ;

  5. l’appel de la fonction 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 :

 1// gcd.c
 2
 3#include <stdio.h>
 4
 5int gcd(int a, int b)
 6{
 7    if (b == 0)
 8    {
 9        return a;
10    }
11    else
12    {
13        return gcd(b, a % b);
14    }
15}
16
17int main()
18{
19printf("GCD: %d\n", gcd(24, 40));
20return 0;
21}

L’organisation du code est quasi identique :

  1. l’importation des bibliothèques externes (ligne 3) ;

  2. la définition des variables globales. Il n’y en a pas besoin ici, et cette partie est vide ;

  3. la définition des fonctions secondaires (lignes 5-15) ;

  4. la définition de la fonction principale main() (lignes 17-21) ;

  5. il n’y a pas d’appel explicite de 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 printf(), il faut « importer » une bibliothèque (ligne 3), alors que la fonction 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 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.

gcd.c contient le code source. 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.

Types de données en C

Data Type

Memory (bytes)

min

max

Format Specifier

short int

2

-32,768

32,767

%hd

unsigned short int

2

0

65,535

%hu

unsigned int

4

0

4,294,967,295

%u

int

4

-2,147,483,648

2,147,483,647

%d

long int

8

-2,147,483,648

2,147,483,647

%ld

unsigned long int

4

0

4,294,967,295

%lu

long long int

8

-9,223,372,036,854,775,808

9,223,372,036,854,775,807

%lld

unsigned long long int

8

0

18,446,744,073,709,551,615

%llu

char

1

-128

127

%c

signed char

1

-128

127

%c

unsigned char

1

0

255

%c

float

4

1.2E-38

3.4E+38

%f

double

8

2.3E-308

1.7E+308

%lf

long double

12

3.4E-4932

1.1E+4932

%Lf

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.

  • Quel est le nombre d’octets utilisé pour stocker un entier ?

  • Quel est le nombre de bits utilisé pour stocker un entier ?

  • Quel est le nombre minimal théorique de bits nécessaire pour stocker un booléen ?

Déclaration et affectation

L’instruction ci dessous déclare une variable x de type int:

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.

../_images/x.png

On lui affecte ensuite une valeur avec l’instruction :

x = 37;

On peut également effectuer les 2 opérations avec une seule instruction :

int x = 37;
../_images/x37.png

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 :

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 :

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.

../_images/y37.png
  • Quel sont les 4 derniers digits hexadécimaux de l’adresse de x ?

  • Quel sont les 4 derniers digits hexadécimaux de l’adresse de y ?

  • Combien d’octets représentent 4 digits hexadécimaux ?

  • Sachant que x est un int, le compilateur a t-il réservé une adresse mémoire contigüe pour y ?

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.

>>> x = 37
>>> id(x)
1658682502576

Un objet est créé en mémoire, et une référence lui est affectée. La fonction 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.

../_images/pyobj_x.png

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

>>> y = x
>>> id(y)
1658682502576
>>> x is y
True

qui peut s’illustrer avec le schéma suivant :

../_images/pyobj_xy.png

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.

1// gcd2.c
2#include <stdio.h>
3int 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 (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

1int gcd(int a, int b)
2{
3if (b == 0)
4    return a;
5else
6    return gcd(b, a % b);
7}

que l’on peut, sans perte de lisibilité, également écrire :

1int gcd(int a, int b)
2{
3    if (b == 0) return a;
4    else return gcd(b, a % b);
5}

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:

# gcd.py
# Calcul du Greatest Common Divisor
def gcd(a, b): # Algorithme d'Euclide

Exemples de commentaires en 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 printf()

En C, l’affichage est contrôlé par la fonction 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

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.

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 ?

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 gcd() étudiée précédemment.

 1int gcd(int a, int b)
 2{
 3    if (b == 0)
 4    {
 5        return a;
 6    }
 7    else
 8    {
 9        return gcd(b, a % b);
10    }
11}

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 : 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 main().

La fonction 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 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 main() mais il faut alors que le prototype des fonctions en question soit placé avant main(), le plus souvent dans un fichier .h. A titre d’exemple, la fonction printf() est utilisable dans main() car elle est déclarée dans stdio.h.

En Python, l’appel de main() n’est pas automatique, il faut le déclencher.