Comparaison des langages

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.

  • Python utilise un interpréteur pour exécuter directement le code source

  • Un programme C peut être exécuté sans étape de compilation préalable

  • Pour exécuter un programme C, la première étape nécessaire est la

  • En C, un même code source doit être pour chaque cible matérielle différente

Structuration du code

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()

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).

Calcul du PGCD avec Python (sans IA)

Il est nécessaire que VS Code soit démarré sur la machine hôte, pointe vers le répertoire e3-programmation-labs-student et que le container Docker soit lancé.

  • Créer un fichier exercices/gcd.py et copier le code ci dessus ;

  • Exécuter le programme avec la commande python3 gcd.py ;

  • Vérifier que le résultat affiché est correct.

  • Lorsque c’est le cas :

    • git add .

    • git commit -m "Calcul du PGCD avec Python"

    • git push

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é.

Calcul du PGCD avec C (sans IA)

Il est nécessaire que VS Code soit démarré sur la machine hôte, pointe vers le répertoire e3-programmation-labs-student et que le container Docker soit lancé.

  • Créer le fichier exercices/gcd.c et copier le code ci dessus ;

  • Compiler le programme avec la commande gcc -std=c99 -Wall -Wextra gcd.c -o gcd ;

  • Exécuter le programme avec la commande ./gcd ;

  • Vérifier que le résultat affiché est correct.

  • Lorsque c’est le cas :

    • git add .

    • git commit -m "Calcul du PGCD avec C"

    • git push

  • La structure d’un programme Python et d’un programme C sont similaires

  • En Python, la fonction main est appelée automatiquement au démarrage du programme

  • En C, il est nécessaire d’importer stdio.h pour pouvoir utiliser printf()

  • Le compilateur utilisé dans les exemples C de ce cours est

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

Les variables sont des zones de mémoire nommées qui permettent de stocker des valeurs. Elles sont utilisées pour mémoriser des informations et les réutiliser dans le programme.

Les variables en C

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/c-03-programme-fig-01.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/c-03-programme-fig-02.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/c-03-programme-fig-03.png
  • Les 4 derniers digits hexadécimaux de l’adresse de x sont

  • Les 4 derniers digits hexadécimaux de l’adresse de y sont

  • 4 digits hexadécimaux représentent octets

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

  • En C, le type des variables doit être déclaré explicitement avant leur utilisation

  • C dispose d’un type booléen natif

  • En C, une même variable peut changer de type au cours de l’exécution d’un programme

  • Le type C pour stocker un entier sur 32 bits est

  • Le type C pour stocker un nombre réel sur 64 bits est

  • Dans un prédicat C, la valeur 0 est évaluée à

  • Une variable C doit être déclarée dans son domaine d’utilisation

Les variables en Python

C est un langage ancien et ne disposait pas des méthodes modernes de gestion automatique 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/c-03-programme-fig-04.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/c-03-programme-fig-05.png
  • En Python, l’affectation y = x crée un nouvel objet en mémoire

  • En Python, la gestion de la mémoire est automatique

  • La fonction id() en Python donne la position physique de l’objet en mémoire

  • Après x = 37 puis y = x en Python, l’expression x is y renvoie

  • En Python, une variable est une vers un objet en mémoire

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.

En Python, les variables locales sont créées automatiquement lors de leur première affectation.

  • En C, les variables locales sont déclarées au début de la fonction qui les utilise

  • En C, omettre la déclaration d’une variable est ignoré silencieusement par le compilateur

  • En Python, les variables locales doivent être déclarées explicitement avant leur première utilisation

  • En C, l’oubli de la déclaration d’une variable provoque une

Délimitation des blocs

Un bloc d’instructions est un ensemble d’instructions regroupées pour être exécutées ensemble. Il est souvent utilisé dans les structures de contrôle (conditions, boucles, fonctions, etc.) pour définir quelles instructions doivent être exécutées dans un certain contexte.

En Python, les espaces et les tabulations sont utilisés pour délimiter les blocs. Une même information est utilisée pour améliorer la lisibilité du code (pour l’humain) et pour structurer le code (pour la machine).

Avertissement

Un code mal structuré peut être valide syntaxiquement et s’exécuter correctement si l’indentation a un sens pour une autre structure de contrôle. Mais le comportement du programme sera alors différent de celui attendu.

En C, les blocs sont délimités par des accolades { } (pour la machine). Il faut ajouter des informations de lisibilité pour améliorer la compréhension du code. Les espaces et les tabulations sont utilisés pour cela. Ce qui signifie que le code ci dessous est parfaitement valide (pour la machine), quoique assez illisible (pour l’humain).

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.

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}
  • En Python, l’indentation seule délimite les blocs d’instructions

  • En C, les accolades { } sont toujours obligatoires, même si un bloc ne contient qu’une seule instruction

  • Un code mal indenté peut quand même s’exécuter correctement

  • En C, les blocs de code sont délimités par des

  • En Python, l’indentation sert à la fois à la lisibilité et à

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.

  • En Python, le caractère # débute un commentaire de fin de ligne

  • En Python, les commentaires de bloc s’écrivent entre /* et */

  • En C, un commentaire de ligne commence par les caractères

  • En C, un commentaire de bloc se termine par les caractères

  • Il est recommandé de commenter en expliquant pourquoi une instruction est nécessaire plutôt que comment elle fonctionne

L’affichage

printf() en C

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 que d’encapsuler tout le code dans des fonctions mais ce n’est pas requis par le langage Python. Pour C au contraire, cette structuration est indispensable.

Reprenons le programme permettant de calculer le PGCD utilisé en début de chapitre.

En langage C

La ligne 5 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(). Elle est ici définie à la ligne 17 et ne prend aucun paramètre. Elle appelle la fonction gcd() avec les paramètres 24 et 40, puis affiche le résultat.

La fonction main() est obligatoire dans un programme C. Elle peut prendre des paramètres, mais ce n’est pas le cas ici. Elle 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.

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

Pour le langage Python, la signature des fonctions n’impose pas de préciser le type des arguments ni celui de la valeur de retour. C’est une caractéristique du typage dynamique de Python, qui permet d’écrire des fonctions plus génériques. Ceci ne s’applique pas pour cet exemple car la fonction gcd() ne peut être utilisée qu’avec des entiers.

Une bonne pratique consiste cependant à utiliser le typing pour informer sur le type des paramètres et de la valeur de retour. Ainsi, la signature de la fonction gcd() pourrait être écrite comme suit :

def gcd(a: int, b: int) -> int:
    if b == 0:
        return a
    else:
        return gcd(b, a % b)

Cette information n’est pas utilisée par l’interpréteur Python mais peut être utilisée par des outils d’analyse statique du code pour détecter des incohérences dans le typage.

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

  • En C, la signature d’une fonction doit préciser le type des paramètres et de la valeur de retour

  • En Python, il est obligatoire de typer les paramètres dans la signature d’une fonction

  • En C, la fonction main est obligatoire dans tout programme

  • En Python, l’appel de la fonction main est automatique au démarrage du programme

  • Une fonction C qui ne retourne aucune valeur utilise le type

  • Par convention, la fonction main retourne si le programme s’est exécuté sans erreur

Les opérateurs

La plupart des opérateurs sont communs à Python et C. Voici les principaux.

Les opérateurs arithmétiques

Les opérateurs arithmétiques permettent le calcul mathématique.

Les opérateurs arithmétiques

Opérateur C

Description

Example

Python

+

Addition

a + b

+

-

Soustraction

a - b

-

*

Multiplication

a * b

*

/

Division

a / b

/

%

Reste de la division entière

a % b

%

++

Incrémentation

a++

--

Décrémentation

a--

Les opérateurs relationnels

Les opérateurs relationnels permettent de mettre en oeuvre les opérations de comparaison.

Les opérateurs relationnels

Opérateur C

Description

Example

Python

==

Egalité

(a == b)

==

!=

Différence

(a != b)

!=

>

Supérieur

(a > b)

>

<

Inférieur

(a < b)

<

>=

Supérieur ou égal

(a >= b)

>=

<=

Inférieur ou égal

(a <= b)

<=

Les opérateurs logiques

Les opérateurs logiques permettent de construire des prédicats.

Les opérateurs logiques

Opérateur C

Description

Example

Python

&&

Et logique

(a && b)

and

||

Ou logique

(a || b)

or

!

Négation

( !a )

not

Les opérateurs d’affectation

Les opérateurs d’affectation permettent d’affecter une valeur à une variable.

Les opérateurs d’affectation

Opérateur C

Description

Example

Python

=

Affectation

a = b

=

+=

Addition et affectation

a += 1

+=

-=

Soustraction et affectation

a -= 1

-=

*=

Multiplication et affectation

a *= 2

*=

/=

Division et affectation

a /= 2

/=

%=

Modulo et affectation

a %= 2

%=

Priorité des opérateurs

Lors de l’évaluation d’expressions complexes, des règles de priorité s’appliquent. En cas de doute, il sera sage d’utiliser les parenthèses pour contrôler l’ordre d’application des opérateurs.

Par exemple, l’expression x = 7 + 3 * 2 est évaluée à 13 parce que l’opérateur * a une priorité supérieure à +. Il sera cependant sage d’écrire x = 7 + (3 * 2).

Voici la liste des opérateurs classés par ordre de priorités décroissantes.

La priorité des opérateurs

Opérateurs C

Associativité

(), [], ++, --

gauche à droite

+, -, !

droite à gauche

*, /, %

gauche à droite

+, -

gauche à droite

<, <=, >, >=

gauche à droite

==, !=

gauche à droite

&&

gauche à droite

||

gauche à droite

=, +=, -=, *=, /=, %=

droite à gauche

La priorité des opérateurs Python

Opérateurs Python

Associativité

()

gauche à droite

**

gauche à droite

+x, -x, ~x

gauche à droite

*, /, //, %

gauche à droite

+, -

gauche à droite

<<, >>

gauche à droite

&

gauche à droite

^

gauche à droite

|

gauche à droite

==, !=, <, <=, >, >=, is, is not, in, not in

gauche à droite

not

gauche à droite

and

gauche à droite

or

gauche à droite

  • L’équivalent Python de l’opérateur C && est

  • L’équivalent Python de l’opérateur C || est

  • L’équivalent Python de l’opérateur C ! est

  • Python dispose d’un opérateur d’incrémentation ++

  • La valeur de l’expression 7 + 3 * 2 est

  • L’opérateur % calcule le de la division entière

  • Python dispose de l’opérateur ** (puissance) que C ne possède pas nativement