Intéressé par des cours d'informatique en ligne ?
Visitez mon nouveau site https://www.yesik.it !

Dans cet article, nous allons voir deux notions fondamentales de la programmation impérative: la notion de variable et celle de boucle.

Nous verrons comment ces notions peuvent être appliquées pour raffiner successivement un programme C++. Vous aurez aussi à transposer ces connaissances dans d'autres langages: Java, PHP et JavaScript. Pour tirer le meilleur parti de cet article, il est donc indispensable d'avoir déjà un minimum d'expérience de ces langages (par exemple en ayant suivi le tutoriel Hello World).

Introduction théorique

Programmation impérative

Les langages impératifs reposent sur l'idée qu'un programme en cours d'exécution est l'association d'un état et d'instructions successives qui vont modifier cet état.

Dans ce paradigme de programmation, c'est le programmeur qui décide du quand et du quoi:


La notion d'ordre des instructions est assez intuitive. Bien entendu, s'ils n'offraient que la possibilité d'une exécution séquentielle des instructions les langages impératifs seraient par trop restrictifs. Ils proposent donc des structures de contrôle qui sont des constructions du langage permettant - par exemple - de répéter l'exécution de certains blocs de code.


Quand à l'état du programme, il est constitué par la mémoire (et accessoirement par les périphériques d'entrée/sortie). Si les langages primitifs comme l'assembleur modifient cet état en manipulant le contenu des cellules mémoires par leur adresse, les langages plus évolués introduisent la notion de variable.


Typage

Outre de permettre d'agir sur l'état du programme à un niveau d'abstraction plus élevé en donnant un nom symbolique aux données manipulées, les langages modernes introduisent la notion de type. Le type permet deux choses:

Certains langages (C, C++, Java, par ex.) associent l'information de type à la variable. On dit que ces langages sont typés statiquement: dans le code source (modèle statique du programme) on voit le type associé avec la variable symbolique. Et dans ces langages, une variable ne peut contenir que des données du type déclaré.

A l'inverse, d'autres langages (PHP, JavaScript, par ex.) associent l'information de type à la donnée. On dit que ces langages sont typés dynamiquement. Dans le code source aucune information ne restreint la nature des données qui peuvent être stockées dans une variable, et le type des données n'est connu que pendant l'exécution du programme (modèle dynamique du programme).

Voilà pour l'introduction théorique. Nous allons maintenant voir en pratique comment sont mises en œuvre ces notions dans un programme.

Programme initial

Historiquement, un des premiers rôle des ordinateurs a été de calculer des tables mathématiques (table des logarithmes, fonctions de Bessel, etc.) [1][2]. Pour cet article, nous allons cantonner l'ordinateur à ce rôle traditionnel de calculateur (computer). Mais rassurez-vous, les calculs que nous allons effectuer vont rester très abordables, puisqu'il s'agira de déterminer les premières puissances entières d'un nombre.

Considérons la classe C++ suivante:

class PowerCalculator
{
    public:
    void displayPowersOfSeven()
    {   
        std::cout << 1 << std::endl;
        std::cout << 1*7 << std::endl;
        std::cout << 1*7*7 << std::endl;
        std::cout << 1*7*7*7 << std::endl;
        std::cout << 1*7*7*7*7 << std::endl;
    }   
};

Sous réserve d'écrire le programme principal correspondant, un appel à la méthode displayPowersOfSeven produit le résultat suivant:

1
7
49
343
2401

À vous de jouer:

Ecrivez le même programme en Java, PHP et JavaScript.

Variables

Quand on observe le code source précédent, il est évident que nous effectuons plusieurs fois les mêmes calculs: si nous avions ces mêmes opérations à calculer à la main, nous noterions quelque part chaque résultat intermédiaire pour ne pas avoir à le recalculer. C'est exactement à ça que sert une variable: mémoriser une donnée pour un usage ultérieur.

Remarque:

Bien entendu, dans un cas aussi évident que celui-ci, les compilateurs modernes sont capables d'optimiser automatiquement le code. En fait, le compilateur remarquera même que nos calculs portent sur des constantes, et les résultats seront calculés à la compilation et non pas à l'exécution. Mais prétendons pour les besoins de notre démonstration que ce n'est pas le cas.

En C++ (comme en C ou Java), avant de pouvoir être utilisée, une variable doit être déclarée. La déclaration précise non seulement le nom de la variable, mais aussi le type des données qu'elle peut contenir. Enfin, cette variable peut être initialisée par la même occasion; c'est à dire qu'on lui donne une valeur initiale.

Piège:

Quel que soit le langage, c'est une erreur d'utiliser une variable sans l'initialiser auparavant!

Dans notre programme, nous utilisons des entiers. En C++, il existe plusieurs types possibles: short, int, long, unsigned short, etc. Ici nous allons utiliser le type entier int qui correspond à la taille d'un mot machine. Notre variable sera aussi initialisée avec pour valeur 1 (la première puissance de 7 à afficher).

La déclaration complète de notre variable sera:

int     power = 1;

Ensuite, après chaque affichage, la valeur de notre variable sera mise à jour pour contenir la puissance de 7 suivante. Cela s'écrira:

power = power*7;

Attention: ici le symbol = n'est pas une égalité au sens mathématique! C'est une affectation. C'est à dire qu' après cette instruction, la variable à gauche du symbole = contiendra la valeur à droite. On pourrait lire cette affectation "mettre dans power l'ancienne valeur de power multipliée par 7".

Au final, le code devient:

class PowerCalculator
{
    public:
    void displayPowersOfSeven()
    {   
        int     power = 1;
        std::cout << power << std::endl;
        power = power*7;
        std::cout << power << std::endl;
        power = power*7;
        std::cout << power << std::endl;
        power = power*7;
        std::cout << power << std::endl;
        power = power*7;
        std::cout << power << std::endl;
    }   
};

Si vous testez nos modifications, vous devriez obtenir le même résultat que précédemment:

1
7
49
343
2401

À vous de jouer - en Java:

Java déclare et utilise les variables entières de la même manière que C++. Ré-écrivez le programme ci-dessus en Java.

À vous de jouer - en PHP:

Les règles concernant les variables en PHP différent de celles de C++ ou Java. En effet, en PHP [3]:

  • les variables ne sont pas typées;
  • les variables n'ont pas à être déclarées: elles sont crées dès qu'on les initialise;
  • leur nom doit commencer par un $ (dollar)

Voici un exemple:

$qty = 10;
$price = 12;
print($qty * $price);

Muni de ces informations, ré-écrivez le programme précédent en PHP.

À vous de jouer - en JavaScript:

Les règles concernant les variables en JavaScript différent aussi de celles de C++ ou Java. En JavaScript:

  • les variables ne sont pas typées;
  • les variables sont déclarées par le mot-clé var.

Voici un exemple:

var qty = 10;
var price = 12;
document.write(qty * price);

Muni de ces informations, ré-écrivez le programme précédent en JavaScript.

Boucles

Arrivé à cette étape, on voit clairement que le code de la méthode displayPowersOfSeven est redondant. En effet, on voit apparaître 5 fois à l'identique les lignes:

/* ... */
        std::cout << power << std::endl;
        power = power*7;
/* ... */

Note:

En fait, ces deux lignes apparaissent 4 fois seulement dans le code. La dernière répétition est incomplète et ne contient que

std::cout << power << std::endl;

Néanmoins, même si elle n'apparaît pas vraiment dans le code, on peut faire comme si l'affectation de la variable power à la prochaine puissance de 7 avait également lieu après le dernier affichage: en effet, la variable n'étant plus utilisée après, changer sa valeur ne modifie en rien le programme. Et cela fait alors bel et bien apparaître 5 répétitions complètes de la même séquence de code.

Les langages impératifs proposent des structures de contrôle pour répéter un même fragment de code: les boucles: Ici, nous voulons répéter 5 fois la même chose. Il nous fout donc boucler en comptant les tours: 1, 2, 3, 4 et 5. Et arrivé au 5ème arrêter de répéter. Vous l'avez compris, nous utiliserons une variable (appelée variable de boucle) pour compter les répétitions:

class PowerCalculator
{
    public:
    void displayPowersOfSeven()
    {   
        int     power = 1;
        int     count = 0;
 
        while(count<5)
        {
                std::cout << power << std::endl;
                power = power*7;
 
                count=count+1;
        }
    }   
};

Ca marche toujours aussi bien:

sh$ g++ -Wall -Werror power.cc -o power && ./power
1
7
49
343
2401

À vous de jouer:

  1. On souhaite améliorer un peu la présentation du résultat. Modifiez donc le programme pour qu'il affiche:
     7^0 = 1
     7^1 = 7
     7^2 = 49
     7^3 = 343
     7^4 = 2401
    
  2. Java, PHP et JavaScript proposent exactement la même boucle while. Ré-écrivez le programme de la question précédente dans chacun de ces langages.

Paramètres

Ouf! Maintenant le code n'est plus redondant... Enfin, jusqu'à ce qu'on ait envie de calculer les puissances d'un autre nombre:

class PowerCalculator
{
    public:
    void displayPowersOfSeven()
    {   
        int     power = 1;
        int     count = 0;
 
        while(count<5)
        {
                std::cout << power << std::endl;
                power = power*7;
 
                count=count+1;
        }
    }   
 
    void displayPowersOfFive()
    {
        int     power = 1;
        int     count = 0;
 
        while(count<5)
        {
                std::cout << power << std::endl;
                power = power*5;
 
                count=count+1;
        }
    }   
};

Il ne vous semble pas que les méthodes displayPowersOfSeven et displayPowersOfFive se ressemblent? Elles ne diffèrent en fait que d'un chiffre: dans l'une on met à jour le contenu de la variable power en multipliant son ancienne valeur par 7, et dans l'autre par 5. C'est la seule différence! Et si l'on voulait afficher les puissances de 2, 3, 4, etc? Faudrait-il faire autant de méthodes? évidement non!

Ce qui serait bien, c'est de pouvoir indiquer à la méthode par combien on veut multiplier à chaque tour. C'est à dire communiquer une valeur entre le site d'appel de la méthode, et le site où le calcul est effectué. A nouveau, une variable va faire l'affaire. Une variable dont la valeur est transmise lors d'un appel de méthode est appelée paramètre. Les paramètres d'une méthode sont indiqués dans les parenthèses après le nom de la méthode:

/* ... */
    void displayPowers(int base)
    {
        int     power = 1;
        int     count = 0;
 
        while(count<5)
        {
                std::cout << power << std::endl;
                power = power*base;
 
                count=count+1;
        }
    }   
/* ... */

A elle toute seule, la méthode ci-dessus remplace displayPowersOfFive, displayPowersOfFiveOfSeven, etc. Bien entendu, lors de l'appel, il faudra maintenant préciser la base dont on veut afficher les puissances:

displayPowers(5);
displayPowers(7);

À vous de jouer:

  1. Tout comme C++, Java et PHP utilisent la même syntaxe pour déclarer des variables et pour déclarer des paramètres. Ré-écrivez le programme précédent en Java puis en PHP.
  2. En JavaScript, contrairement aux variables, les paramètres n'utilisent jamais le mot-clé var. Ré-écrivez le programme de la question précédente en JavaScript.

Syntaxes alternatives

Le C a été inventé à une époque ou la mémoire était une denrée rare et où les performances des ordinateurs étaient très limitées. Dans ces conditions, il n'est pas étonnant pour un langage qui se voulait efficace et proche du matériel de proposer différentes possibilités pour écrire des programmes les plus concis et les plus économes en ressources possibles.

Ceci a pour conséquence qu'il existe souvent plusieurs manières d'écrire la même chose en C. Aujourd'hui, beaucoup des limitations de l'époque ont disparu. Néanmoins, les multiples syntaxes équivalentes ont perduré, et, étant donné leur popularité, ont très souvent été reprises par les langages dont la syntaxe est inspirée du C. On ne peut donc raisonnablement se permettre de les ignorer.

Dans cette partie, nous allons donc voir comment il est possible de ré-écrire notre code pour tirer parti de certaines de ces syntaxes alternatives.

Incrément

Incrémenter, c'est ajouter une valeur à une variable. C'est une des opérations les plus fréquentes dans un programme informatique. Il n'est pas étonnant que les microprocesseurs possèdent tout un jeu d'instructions pour effectuer cette opération. Et le C propose donc une batterie d'opérateurs différents pour cette simple opération.

Ainsi, on peut utiliser 4 opérateurs différents pour incrémenter la variable count de notre exemple:


A l'origine, ce choix permettait au programmeur de prendre la meilleure instruction pour son microprocesseur, et d'écrire des programmes compacts et performants.

Aujourd'hui, il n'y a aucune justification de performance à utiliser ces instructions: la puissance et la capacité mémoire des ordinateurs modernes sont sans commune mesure. Et surtout les compilateurs sont beaucoup plus performants. Ainsi, ils sont capables de choisir bien mieux que le programmeur la meilleure instruction à mettre en œuvre.

Néanmoins, ces opérateurs sont toujours fréquemment utilisés, ne serait-ce que par économie de frappe. En outre, il est important d'en connaître les subtiles différences. D'autant plus que leur adoption est si grande par la communauté des développeurs, qu'on les retrouve dans la quasi-totalité des programmes C, C++, Java, PHP, JavaScript, ...

Affectation par addition

L'opérateur le plus proche de l'addition simple est l'opérateur d'affectation par addition +=. Il fait exactement la même chose que sa contre-partie avec affectation et addition séparée. Ainsi les deux lignes suivantes sont rigoureusement identiques:

count = count + 1;
count += 1;

Note:

Il existe bien d'autres opérateurs compacts du même style. On notera en particulier ceux relatifs aux autres opérations arithmétiques: *=, -=, /= et %=. Mais ce ne sont pas les seuls... wikipedia:Operators in C and C++

Post- et Pré- Incrément

L'opérateur += permet d'incrémenter d'une valeur quelconque le contenu d'une variable. Dans la pratique la valeur de l'incrément est souvent 1. Par conséquent, le C propose également deux opérateurs spécifiques pour incrémenter de 1. Il s'écrivent tous deux ++ mais diffèrent par leur position par rapport à la variable à modifier:

Si tous deux ajoutent 1 à la variable, ils diffèrent par le moment où la variable est modifiée. C'est particulièrement important si ces opérateurs sont utilisés dans une expression plus complexe:

Un exemple très simple pour illustrer. Considérons le fragment de code suivant (c'est du C++, mais c'est aussi vrai pour tous les langages qui proposent ces opérateurs):

/* ... */
    int       v = 0;
    std::cout << "Etape 1: " << v << std::endl;
    std::cout << "Etape 2: " << ++v << std::endl;
    std::cout << "Etape 3: " << v++ << std::endl;
    std::cout << "Etape 4: " << v << std::endl;
/* ... */

Ce fragment de code va afficher:

Etape 1: 0
Etape 2: 1
Etape 3: 1
Etape 4: 2


Note:

Il est parfois plus facile de comprendre les opérateurs de post- et pré- incrémentation en terme de construction équivalente. Ainsi, les deux fragments de code suivants sont équivalents:

std::cout << "Etape 2: " << ++v << std::endl;
v = v+1;
std::cout << "Etape 2: " << v << std::endl;

De la même manière, les deux fragments de code suivants sont aussi équivalents:

std::cout << "Etape 3: " << v++ << std::endl;
std::cout << "Etape 2: " << v << std::endl;
v = v+1;

Pour en revenir à notre programme, notre boucle while peut donc se ré-écrire en utilisant l'opérateur de pré-incrémentation:

/* ... */
        while(count<5)
        {
                std::cout << power << std::endl;
                power = power*base;
 
                ++count;
        }
/* ... */

À vous de jouer:

  1. Ré-écrivez la version Java de votre programme pour utiliser l'opérateur de pré-incrément;
  2. La boucle while ci-dessus ne peut-elle pas être écrite de façon encore plus compacte?

Boucle pour

Nous avons vu l'utilisation d'une boucle tant que (boucle while) pour répéter un bloc de code tant qu'une condition est vraie. Ici nous l'utilisons conjointement à une variable de boucle pour répéter un nombre de fois connu avant le début de la boucle.

C'est un cas assez fréquent. Et les langages de la famille du C proposent une construction spécifique: la boucle pour (boucle for).

Ainsi, une boucle de la forme:

initialisation;
while(condition)
{
    instruction 1;
    ...
    instruction n;

    mise à jour de la variable de boucle;
}

peut se ré-écrire:

for(initialisation; condition; mise à jour de la variable de boucle)
{
    instruction 1;
    ...
    instruction n;
}

Note:

Les deux constructions sont rigoureusement identiques!

Dans notre cas:

La méthode displayPowers peut donc se ré-écrire avec une boucle pour à la place de la boucle tant que:

void displayPowers(int base)
    {
        int     power = 1;
 
        for(int count = 0; count<5; ++count)
        {
                std::cout << power << std::endl;
                power = power*base;
        }
    }
};

Remarque:

En fait, dans la clause incrément de la boucle for, il est possible de modifier plusieurs variables. La boucle ci-dessus peut s'écrire de façon encore plus compacte:

/* ... */
        for(int count = 0; count<5; ++count, power*=base)
        {
                std::cout << power << std::endl;
        }
/* ... */

Mais attention: que ce soit possible ne signifie pas qu'il faut forcément le faire. Le choix de l'une ou l'autre syntaxe est une question de style. Et votre préoccupation devrait être plus d'écrire du code facilement compréhensible que du code le plus compact possible...

À vous de jouer:

Java, PHP et JavaScript possèdent une boucle for identique à celle du C++. Ré-écrivez le programme ci-dessus en utilisant chacun de ces langages.

Exploitation

Pour terminer cet article, nous allons utiliser les programmes réalisés afin de déterminer les limites des types entiers dans plusieurs des langages que vous avez utilisés:

  1. Avant toutes choses, un dernier changement: la méthode displayPowers affiche toujours les 5 premières puissances d'une base. Modifiez la donc pour permettre à l'appelant de choisir le nombre de puissances à afficher;
  2. Avec quelques connaissances élémentaires en architecture des ordinateurs - et si vous êtes malin - utilisez la méthode displayPowers pour déterminer le plus grand entier représentable dans une variable de type int en C++;
  3. Il existe d'autres types entiers en C++. Notamment short et long. En appliquant le même raisonnement que précédemment, déterminez le plus grand entier représentable avec chacun de ces types;
  4. Répondez aux deux dernières questions pour un programme Java;
  5. Les variables en PHP ne sont pas typées. Pourtant, il doit bien exister une limite au plus grand entier utilisable dans un script PHP. Essayez donc d'appliquer le même raisonnement pour déterminer cette limite...