Looking for Computer Science & Information Technology online
courses ?
Check my new web site: https://www.yesik.it !
Sommaire
Dans un langage impératif, les variables servent à mémoriser des données pendant l'exécution d'un programme.
Dans l'article variables et boucles nous avons introduit ce concept en utilisant des variables locales.
Mais ce n'est pas la seule sorte de variable qui soit à la dispostion du programmeur. En effet, des langages impératifs orientés objets comme le C++ ou PHP proposent plusieurs sortes de variables. En particulier:
- les variables locales,
- les variables d'instance,
- les variables de classe,
- les variables globales.
Avertissement
Cet article est un article d'introduction. Les experts se rendront compte que je passe sous silence certains détails comme par exemple ceux liés à l'héritage et à l'accès aux variables d'instances d'une classe de base. Ou encore des spécificités de certains langages comme le modificateur friend du C++.
D'un point de vue pratique, cet article vise surtout à permettre à un débutant de savoir choisir quel sorte de variable utiliser.
Durée de vie et portée
Ces 4 types de variables se distinguent par leur portée et leur durée de vie.
La portée d'une variable correspond à la portion du programme où l'on peut accéder à la variable. La durée de vie définit quand la variable est créée et quand elle est détruite.
En jouant sur ces deux paramètres, les langages modernes proposent tout une palette de variables plus ou moins partagées par les différentes parties du programme – et donc plus ou moins protégées contre les modifications externes.

Note:
Pour être plus concret, posez-vous la question: dans quel cas est-il le plus facile de garantir le contenu d'une variable? Quand elle ne "sert" que dans 10 lignes groupées dans un seul fichier – ou si au contraire elle peut être utilisée par 100000 lignes réparties dans 100 fichiers?
Variables locales
Dans notre panorama des variables, les variables locales sont celles dont la portée et la durée de vie sont les plus réduites.
Par conséquent, ce sont aussi les variables dont il est le plus facile de contrôler le contenu.
Durée de vie
Une variable locale est créée à chaque fois que s'exécute la méthode (ou fonction) dans laquelle elle est déclarée. La variable est détruite dès la fin de cette méthode (ou fonction). Certains langages (C++, Java par ex.) sont encore plus restrictifs puisqu'une variable locale cesse d'exister dès la fin du bloc (entre accolades) où elle a été déclarée.
Portée
Une variable locale ne peut être utilisée que dans la méthode qui la déclare. Certains langages (C++, Java par ex.) sont encore plus restrictifs puisqu'une variable locale ne peut être utilisée que dans le bloc (entre accolades) où elle a été déclarée.
Exemple
#include <iostream> class Math { public: unsigned int factorielle(unsigned int n) { int resultat = 1; while(n > 0) { resultat *= n; --n; } return resultat; } }; int main() { Math math; int nbr = 3; std::cout << math.factorielle(nbr); std::cout << " est " << nbr << "!" << std::endl; nbr = 7; std::cout << math.factorielle(nbr); std::cout << " est " << nbr << "!" << std::endl; }
<?php class Math { public function factorielle($n) { $resultat = 1; while($n > 0) $resultat *= $n--; return $resultat; } } $math = new Math(); $nbr = 3; print $math->factorielle($nbr); print " est " . $nbr . "!\n"; $nbr = 7; print $math->factorielle($nbr); print " est " . $nbr . "!\n";
public class Math { public int factorielle(int n) { int resultat = 1; while(n > 0) resultat *= n--; return resultat; } static public void main(String[] args) { Math math = new Math(); int nbr = 3; System.out.print(math.factorielle(nbr)); System.out.println(" est " + nbr + "!"); nbr = 7; System.out.print(math.factorielle(nbr)); System.out.println(" est " + nbr + "!"); } }
Math = function() { var that = {}; that.factorielle = function(n) { var resultat = 1; while(n > 0) resultat *= n--; return resultat; } return that; }; (function() { var math = Math(); var nbr = 3; document.write(math.factorielle(nbr)); document.write(" est " + nbr + "!<br/>"); nbr = 7; document.write(math.factorielle(nbr)); document.write(" est " + nbr + "!<br/>"); })();

Note:
Les codes sources sont donnés en différents langages à titre de comparaison (C++, PHP, Java et JavaScript). Les explications quand à elles se rapportent à la version C++.
Le programme ci-dessus affiche les factorielle de 3 et 7. Il déclare plein de variables locales. Et uniquement des variables locales.

Remarque:
Ce programme utilise aussi deux variables non-locales: les variables pré-définies std::cout et std::endl.
-
math contient une instance de la classe Math. Cette variable ne peut être utilisée que dans le main.
Remarque:
Vous voyez là la différence entre portée et durée de vie: la variable math existe toujours pendant que la méthode factorielle s'exécute. Par contre, cette méthode ne peut tout simplement pas y accéder.
- nbr contient le nombre dont on veut calculer la factorielle. Notez que c'est la valeur stockée dans cette varaible qui est transmise lors de l'appel à la méthode factorielle. Pas la variable elle-même!
- resultat sert à mémoriser les résultats intermédiaires pendant le calcul.
- n est en réalité un paramètre formel. Mais un paramètre formel n'est rien d'autre qu'une variable locale initialisée par l'appelant (c'est à dire, le fragment de code qui appelle la méthode – ici le main). Un point important à noter est que n et nbr ne sont pas la même variable! n est modifiée pendant l'exécution de factorielle, mais pas nbr.

Récursivité
Il est possible qu'une méthode ou une fonction s'appelle elle-même. C'est ce que l'on appelle la récursivité. La récursivité peut être utile afin d'exprimer plus clairement un algorithme. Ainsi, on peut facilement modifier la méthode factorielle pour la rendre plus proche de la définition mathématique de la factorielle:
/* ... */ unsigned int factorielle(unsigned int n) { int resultat = 1; if (n > 0) resultat = n*factorielle(n-1); return resultat; } /* ... */
Pendant l'exécution factorielle est susceptible de s'appeler elle-même. Le point important à noter est que chaque exécution de la méthode factorielle possède son propre jeu de variables locales. Ou autrement dit, son propre exemplaire des variables n et resultat. Par conséquent, quand le contenu de la variable resultat est modifié, seule l'exemplaire de la variable resultat de la méthode en cours d'exécution est modifié.
Utilisation
Les variables locales sont les plus nombreuses dans un programme bien conçu. Elles servent pour mémoriser les données temporaires, les résultats intermédiaires, les compteurs de boucle, etc. Une bonne règle générale est de considérer qu'en cas de doute, il est préférable d'utiliser une variable locale.
Variables d'instance
Dans les langages impératifs orientés objets, la variable d'instance est une variable associée à un objet et accessible par les méthodes de cet objet.

Note:
Quand plusieurs objets coexistent en mémoire, chacun possède son propre jeu de variables d'instance.
Durée de vie
Les variables d'instance d'un objet sont crées lorsque cet objet est instancié. Elle sont détruites lorsque l'objet est détruit.
Portée
Une variable d'instance est accessible par toutes les méthodes appliquées sur un objet.

Remarque:
Selon les langages, il est possible de spécifier plus finement la portée des variables d'instance à l'aide de modificateur comme protected, private ou publique.
Exemple
#include <iostream> #include <string> class Book { public: Book(std::string title) : _title(title) { } std::string toString() { return "Title: " + _title; } private: std::string _title; }; int main() { Book book1("The Lord of the Ring"); Book book2("The Hobbit"); std::cout << book1.toString() << std::endl; std::cout << book2.toString() << std::endl; return 0; }
<?php class Book { public function __construct($title) { $this->title = $title; } public function __toString() { return "Title: " . $this->title; } private $title; } $book1 = new Book("The Lord of the Ring"); $book2 = new Book("The Hobbit"); print $book1 . "\n"; print $book2 . "\n";
class Book { public Book(String title) { this.title = title; } public String toString() { return "Title: " + title; } private String title; } public class BookStore { static public void main(String[] args) { Book book1 = new Book("The Lord of the Ring"); Book book2 = new Book("The Hobbit"); System.out.println(book1); System.out.println(book2); } }
var Book = function(title) { var _title = title; var that = {}; that.toString = function() { return "Title: " + _title; } return that; }; (function() { var book1 = Book("The Lord of the Ring"); var book2 = Book("The Hobbit"); document.write(book1 + "<br/>"); document.write(book2 + "<br/>"); })();
Ce programme crée deux objets (deux instances) de la classe Book puis demande à chacune de s'afficher.
Notez comme chaque objet possède son propre exemplaire de la variable d'instance _title: quand on crée le second livre, le titre du premier n'est pas écrasé. De la même manière, lors de l'affichage (toString), il n'y a aucune confusion possible entre le titre du premier livre et le titre du second: chaque objet possède bien son propre exemplaire de _title.
Utilisation
Les variables d'instances sont nécessaires quand une donnée doit survivre entre plusieurs utilisations du même objet: dans l'exemple ci-dessus, la donnée devait survivre entre la construction de l'objet et son affichage.
Variables de classe
Jusqu'à présent, nous avons parlé des variables locales et des variables d'instance. Toutes deux ont en commun de pouvoir exister simultanément en autant d'exemplaires que nécessaire. Par ailleurs, ces variables se caractérisent par une durée de vie limitée.
Avec les variables de classe, les choses changent radicalement...
Durée de vie
La durée de vie des variables de classe est des plus simples puisqu'elles existent du début à la fin du programme, et chacune en un seul exemplaire.

Remarque:
Pour être plus précis, avec les langages comme Java qui permettent de charger dynamiquement une classe pendant l'exécution du programme, les variables de classes existent à partir du moment où leur classe est chargée.
Portée
Les variables de classes sont accessibles par toutes les méthodes des objets d'une même classe.

Remarque:
Comme pour les variables d'instance, la visibilité des variables de classe peut être ajustée par les modificateurs private, protected ou public.
Exemple
#include <iostream> #include <sstream> #include <string> class Book { public: Book(std::string title) : _title(title), _id(gNextId++) { } std::string toString() { std::ostringstream os; os << "Title: " << _title << " id: " << _id; return os.str(); } private: std::string _title; int _id; static int gNextId; }; int Book::gNextId = 0; int main() { Book book1("The Lord of the Ring"); Book book2("The Hobbit"); std::cout << book1.toString() << std::endl; std::cout << book2.toString() << std::endl; return 0; }
<?php class Book { public function __construct($title) { $this->title = $title; $this->id = self::$nextId++; } public function __toString() { return "Title: " . $this->title . " id: " . $this->id; } private $title; private $id; private static $nextId = 0; } $book1 = new Book("The Lord of the Ring"); $book2 = new Book("The Hobbit"); print $book1 . "\n"; print $book2 . "\n";
class Book { public Book(String title) { this.title = title; this.id = nextId++; } public String toString() { return "Title: " + title + " id: " + id; } private String title; private int id; static private int nextId = 0; } public class BookStore { static public void main(String[] args) { Book book1 = new Book("The Lord of the Ring"); Book book2 = new Book("The Hobbit"); System.out.println(book1); System.out.println(book2); } }
var Book = (function() { var gNextId = 0; return function(title) { var _title = title; var _id = gNextId++; var that = {}; that.toString = function() { return "Title: " + _title + " id: " + _id; } return that; } }) (); (function() { var book1 = Book("The Lord of the Ring"); var book2 = Book("The Hobbit"); document.write(book1 + "<br/>"); document.write(book2 + "<br/>"); })();
Le programme ci-dessus utilise une variable de classe pour mémoriser le nombre de livres créé (et par conséquent l'ID du prochain livre).
En C++, la variable de classe se distingue d'une variable d'instance dans le code source par la présence du mot-clé static.

Remarque:
Remarquez également qu'en C++ la variable de classe est initialisée à l'extérieur de la classe. En effet, celle-ci existe (et donc doit être initialisée) avant même la création du premier objet. Chaque langage utilise ses propres conventions pour résoudre ce problème.
Utilisation
Les variables de classes sont utiles lorsqu'il est nécessaire de partager une information entre les différentes instances d'une classe.
Cela peut être une information de configuration, ou encore une information qui permet de communiquer entre les intances d'une même classe.
Un petit mot d'avertissement cependant: une variable de classe dont la visibilité est étendue par le mot-clé public n'est rien d'autre qu'une variable globale. Avec toutes les réserves que vous découvrirez ci-dessous...
Variables globales
De toutes les variables dont nous parlons ici, les globales sont celles qui sont les moins protégées contre les utilisations "abusives".
Durée de vie
Une variable globale existe du début à la fin d'un programme. Et en un seul exemplaire.
Portée
Une variable globale peut être utilisée par n'importe quelle méthode de n'importe quel objet de n'importe quelle classe. En fait, elle peut être utilisée n'importe où dans le programme. Il est clair dans ces conditions qu'il est très difficile de contrôler l'utilisation fait d'une telle variable dans un programme un tant soit peu complexe.
Exemple
Pour illustrer la dangerosité des variables globales, nous reprenons ici l'exemple précédent, mais en utilisant une variable globale à la place de la variable de classe:
#include <iostream> #include <sstream> #include <string> int gNextId = 0; class Book { public: Book(std::string title) : _title(title), _id(gNextId++) { } std::string toString() { std::ostringstream os; os << "Title: " << _title << " id: " << _id; return os.str(); } private: std::string _title; int _id; }; int main() { Book book1("The Lord of the Ring"); Book book2("The Hobbit"); std::cout << book1.toString() << std::endl; std::cout << book2.toString() << std::endl; return 0; }
Comme vous le voyez, très peu de choses ont changé. Et le programme fonctionne toujours. D'ailleurs, le code généré par le compilateur est quasiment identique.
Néanmoins, cette version est beaucoup plus fragile! En effet, imaginons une classe auteur qui utilise aussi un mécanisme d'ID:
class Author { public: Author(std::string name) : _name(name), _id(gNextId++) { } /* ... */
Comme vous le constatez, la classe auteur utilise le même compteur gNextId que la classe Book. Est-ce que ce comportement est désiré ou pas? Souhaitable ou non? En tous cas, ce code est susceptible d'introduire des comportements inattendus, voir des bugs, par exemple si l'auteur d'une de ces deux classes décide de "ré-initialiser" le compteur:
/* ... */ void reloadAuthorsFromDatabase() { gNextId = 0; while(_db.hasNextAuthor()) loadNextAuthor(); } /* ... */
En outre, au cas où un bug est découvert et qu'il est causé par une valeur erronée dans une variable globale, il devient très difficile de localiser le fragment de code responsable de cette erreur: en effet, la modification intempestive peut avoir eu lieu n'importe où dans le programme. Ca n'est pas si grave que ça si le programme fait 10 lignes. C'est un autre paire de manche s'il fait 100000 lignes...
Ces problèmes ne pourraient pas se produire avec une variable de classe (sous entendu non publique), puisqu'elle ne serait pas partagée entre Book et Author.
Utilisation
Les variables globales ne servent à rien. C'est un point de vue un peu extrême – et certainement un peu faux aussi. Néanmoins, dans la pratique il y a très peu de cas où elle sont réellement nécessaires. En outre, il est toujours possible d'utiliser une variable de classe à la place d'une variable globale. Par conséquent...