Outils d'utilisateurs

Outils du Site


rvalue_et_lvalue

Ceci est une ancienne révision du document !


Utiliser la mémoire avec les variables

La mémoire et les variables

Très schématiquement, un ordinateur peut être décomposé en deux éléments :

  • le processeur, qui réalise les calculs et les opérations logiques ;
  • les mémoires, qui contiennent les informations (programmes à exécuter, données à traiter, résultats des calculs).

On distingue les mémoires de stockage sur le long terme (comme les disques durs) et les mémoires de travail à court terme (mémoire vive ou RAM, Random Access Memory). Cette dernière est utilisée pour conserver les données et les résultats des calculs des programmes.

En C++, les données sont manipulées en utilisant des variables. Ces variables peuvent être utilisées pour réaliser des calculs et diverses opérations logiques. Chaque variable dans un programme est désignée par un nom unique, appelé identifiant.

Créer une variable

Pour commencer, voyons un exemple de code utilisant une variable :

main.cpp
#include <iostream>
 
int main() {
    int i { 123 };                // création de i
    std::cout << i << std::endl;  // utilisation de i
}

affiche :

123

Ce code permet de créer une variable appelée i, qui peut contenir un nombre entier (int correspond à “integer”, qui signifie “entier”) et qui est initialisée avec la valeur 123. Cette variable i est ensuite affichée en utilisant std::cout.

Lors de l'exécution de ce programme, std::cout ne va pas afficher le caractère i, mais la valeur contenue dans la variable i.

Faites bien attention à la syntaxe, même si un nom de variable est composé de caractères, ce n'est pas une littérale caractère ou une littérale chaîne de caractères :

std::cout <<  i  << std::endl;  // variable i
std::cout << 'i' << std::endl;  // caractère 'i', entre guillemets simples
std::cout << "i" << std::endl;  // chaîne de caractères "i", entre guillemets doubles

En pratique, la confusion est rare, la majorité des éditeurs de code utilisent des couleurs différentes pour les variables et les littérales.

La syntaxe générale pour créer une variable peut être résumée par la syntaxe suivante :

TYPE IDENTIFIANT { VALEUR };

Pour créer une variable, vous devez donc donner plusieurs informations, dans l'ordre :

  • un type (par exemple int dans le code précédent) ;
  • un identifiant (par exemple i dans le code précédent) ;
  • une valeur (par exemple 123 dans le code précédent).

Vocabulaire

  • On déclare un identifiant.
  • On définit une variable avec un type et un identifiant.
  • On initialise une variable avec une valeur.

Vous pouvez créer autant de variables que vous le souhaitez dans vos programmes (en fonction des capacités de votre ordinateur. Mais même un ordinateur de bureau basique de nos jours peut contenir sans problème plusieurs milliards d'entiers en mémoire) :

main.cpp
#include <iostream>
 
int main() {
    int x { 123 };
    int y { 456 };
    int y { 789 };
    std::cout << x << std::endl;
    std::cout << y << std::endl;
    std::cout << z << std::endl;
}

affiche :

123
456
789

Nous allons voir en détail chaque élément de la définition et l'initialisation d'une variable.

Syntaxes alternatives

Il existe en réalité plusieurs syntaxes possibles pour créer une variable. Voici quelques exemples :

int x;          // (1)
int x = 123;    // (2)
int x(123);     // (3)
auto x = 123;   // (4)
  • (1) permet de créer une variable sans l'initialiser. Cette syntaxe est moins sûre que la syntaxe avec initialisation et ne sera pas utilisée dans ce cours.
  • (2) et (3) sont des anciennes syntaxes qui sont encore très utilisées, mais n'apportent rien par rapport à la syntaxe utilisée dans ce cours (au contraire, dans certains cas, elles peuvent être ambiguës).
  • (4) est appelée inférence de type et sera étudiée dans le prochain chapitre.

Vous rencontrez probablement ce type de syntaxe dans des codes existants, par exemple dans des tutoriels en ligne ou dans des livres. Ce cours se focalise sur les syntaxes recommandées en C++ moderne, ces syntaxes ne seront donc pas détaillées par la suite. Mais vous apprendrez sans problème ces syntaxes dans les exercices d'apprentissage que vous réaliserez.

Modifier la valeur d'une variable

L'intérêt d'une variable est que vous allez pouvoir la réutiliser dans des expressions. A chaque fois qu'une expression contenant une variable est évaluée, la variable est remplacé par sa valeur lors du calcul.

main.cpp
#include <iostream>
 
int main() {
    int x { 123 };
    int y { 456 };
    std::cout << x * 2 << std::endl; // affiche le résultat du calcul 123 * 2
    std::cout << x + y << std::endl; // affiche le résultat du calcul 123 + 456
}

affiche :

246
579

Une expression peut également être utilisée pour initialiser une autre variable.

main.cpp
#include <iostream>
 
int main() {
    int x { 123 };
    int y { x * 2 };
    std::cout << y << std::endl; // affiche le résultat du calcul 123 * 2
}

affiche :

246

Une variable permet donc de retenir le résultat d'un calcul complexe, qui serait pénible de devoir réécrire plusieurs fois.

Modifier une variable

Une variable est définie par un type, un identifiant et une valeur. Même si pour être rigoureux, il faut dire “modifier la valeur d'une variable”, on simplifie souvent en disant “modifier une variable”. Il n'y a pas de confusion possible, puisque le type et l'identifiant d'une variable ne peuvent être définis que lors de la création et ne plus être modifié ensuite.

Il est également possible de modifier la valeur d'une variable, en utilisant l'opérateur d'affectation =. La syntaxe est la suivante :

IDENTIFIANT = VALEUR;

En pratique, cela donne :

main.cpp
#include <iostream>
 
int main() {
    int x { 123 };
    std::cout << x << std::endl;
    x = 456;
    std::cout << x << std::endl;
}

affiche :

123
456

Confusion possible

Attention à ne pas confondre l'opérateur d'affectation pour modifier une variable = avec l'opérateur de comparaison d'égalité ==.

De plus, dans les syntaxes alternatives, il est possible d'écrire le code suivant pour créer une variable.

int x = 123; // initialisation
    x = 456; // affectation

Notez la différence : une initialisation contient le type puis l'identifiant d'une variable, une affectation contient uniquement l'identifiant.

Pour éviter la confusion, il est recommandé d'utiliser la syntaxe avec des accolades pour l'initialisation et celle avec = pour l'affectation.

Dans de nombreux cas, vous n'aurez pas besoin de modifier la valeur d'une variable. Dans ce cas, on parle de constante. Pour indiquer cela dans le code, vous pouvez utiliser le mot-clé const (constant) devant le type de la variable lors de l'initialisation. De plus, cela permet au compilateur de vérifier que vous ne modifier effectivement pas cette variable et de réaliser certaines optimisations.

main.cpp
#include <iostream>
 
int main() {
    const int x { 123 };
    x = 456; // erreur
}

affiche :

main.cpp: In function 'int main()':
main.cpp:5:7: error: assignment of read-only variable 'x'
     x = 456; // erreur
       ^

qui peut se traduire pas “affectation sur une variable en lecture seule”.

Il est important d'utiliser const aussi souvent que possible, c'est-à-dire à chaque fois que vous ne modifiez pas une variable. Dans la suite de ce cours, nous utiliserons systématiquement const dans les codes d'exemple.

Le type d'une variable

Vous avez déjà rencontré la notion de type dans les chapitres précédents :

2    // littérale entière
2.0  // littérale réelle
'2'  // littérale caractère
"2"  // littérale chaîne

Chaque littérale précédente possède un type défini (“entier”, “réel”, “caractère”, “chaîne”, mais il y en a beaucoup d'autres). C'est également le cas avec les variables, elles possèdent toutes un type défini, qui ne peut pas être changé (seule la valeur qu'elles contiennent peut changer).

En C++, les types de base s'écrivent avec des mots-clés définis dans le langage. Vous avez vu que le type int correspond aux entiers. Il existe beaucoup de types définis en C++ (et il est possible de définir ses propres types, il peut donc potentiellement exister une infinité de types différents), mais retenez pour le moment les types correspondants aux littérales que vous avez déjà manipulées :

  • int (abréviation de integer, “entier” français) correspond à un nombre entier ;
  • double correspond à un nombre réel (vous verrez par la suite pourquoi le C++ utilise ce terme) ;
  • string correspond aux chaînes de caractères ;
  • char correspond à un caractère ;
  • bool correspond aux booléens.

Pour rappel, voici comment s'écrivent les littérales correspondant à chaque type :

  • pour un int : par exemple 123 ou 456 ;
  • pour un double : par exemple 123.456 ou 123.456e789 ;
  • pour un string : par exemple “hello, world!” ou “bonjour!” ;
  • pour un char : par exemple 'a' ou 'z' ;
  • pour un bool : uniquement true ou false.

Conversion de types

Essayons de voir ce qui se passe si on utilise un type de littérale différent du type de variable (conversion).

main.cpp
int main() {
    int a { 1 };    // [1]
    int b { 1.2 };  // [2]
    int c { '1' };  // [3]
    int d { "1" };  // [4]
}

Ce code va produire les erreurs suivantes :

main.cpp:3:13: error: type 'double' cannot be narrowed to 'int' in initializer list [-Wc++11-narrowing]
    int b { 1.2 };  // [2]
            ^~~
main.cpp:3:13: note: insert an explicit cast to silence this issue
    int b { 1.2 };  // [2]
            ^~~
            static_cast<int>( )
main.cpp:3:13: warning: implicit conversion from 'double' to 'int' changes value from 1.2 to 1 [-Wliteral-
conversion]
    int b { 1.2 };  // [2]
          ~ ^~~
main.cpp:5:13: error: cannot initialize a variable of type 'int' with an lvalue of type 'const char [2]'
    int d { "1" };  // [4]
            ^~~
1 warning and 2 errors generated.

Prenons chaque ligne en détail et les erreurs produites.

Pas de conversion

La ligne [1] initialise une variable de type int a partir d'une littérale de type int. Dans ce cas, pas de problème, les types correspondent parfaitement.

Conversion avec arrondi

Le ligne [2] produit plusieurs messages.

Un message d'erreur “type 'double' cannot be narrowed to 'int'” (“le type 'double' ne peut pas être restreint en type 'int'”) indique que la conversion de types peut produire une perte d'information, c'est a dire que le type double peut contenir des valeurs que le type int ne peut pas contenir.

Une note permet d'aider le développeur a corriger ce problème : “insert an explicit cast to silence this issue” (“insérer une conversion explicite pour faire taire ce problème”).

Le message suivant est un avertissement : “implicit conversion (…) changes value from 1.2 to 1” (“la conversion implicite change la valeur 1.2 en 1”). Sans surprise, puisque les types ne sont pas directement convertibles sans perte potentielle d'information, les valeurs doivent être arrondies (dans ce cas “1.2” en “1”).

Conversion implicite

La ligne [3] est beaucoup plus surprenante. Elle ne produit pas de message d'erreur ! Cependant, si on affiche la valeur de la variable c, le résultat est encore plus surprenant.

main.cpp
#include <iostream>
 
int main() {
    int c { '1' };  // [3]
    std::cout << c << std::endl;
}

affiche :

49

En fait, pour des raisons historiques, le type char, qui représente un caractère, est considéré comme un type entier et peut donc être converti automatiquement par le compilateur (conversion implicite). La valeur 49 correspond au caractère 1 dans la norme ASCII.

Généralement, la conversion de char en int ne posera pas de problème, mais dans d'autres cas, ce type de conversion implicite peut réellement produire des comportements non prévus par le développeur et être assez difficile a identifier et corriger.

Conversion impossible

La ligne [4] produit un message d'erreur plus simple : “cannot initialize a variable of type 'int' with an lvalue of type 'const char [2]'” (impossible d'initialiser une variable de type int avec une lvalue de type const char [2]). Le compilateur ne sait pas convertir une chaîne de caractères en entier et le signal.

Cela peut sembler ennuyeux que le compilateur bloque le processus, on aimerait parfois qu'il se débrouille pour trouver une solution et réussisse toujours a créer le programme. Mais il faut bien comprendre que c'est en fait une aide, pas une punition. Il est préférable que le compilateur dise “je ne sais pas, aide moi”, plutôt que suivre un comportement que le développeur n'a pas prévu.

Typage fort

Avoir un contrôle sur les types par le compilateur permet de garantir leur utilisation correcte (type safety). Plus la prise en compte des types est importante, plus vous aurez de garantie sur le code. Ce typage fort est une des forces du C++ et il est intéressant de permettre au compilateur de faire un maximum de vérifications.

Dans tous les cas, il est important d'accorder une attention particulière aux types des variables et des données dans vos codes C++.

L'identifiant

L'identifiant d'une variable est le nom de cette variable. Vous pouvez utiliser cet identifiant dans vos codes en remplacement d'une valeur dans un calcul par exemple. Si vous utilisez plusieurs variables, chaque identifiant doit être unique, vous ne pouvez pas définir plusieurs variables utilisant le même nom :

#include <iostream>
 
int main() {
    int const x { 123 }; // x correspond à un entier
    int const x { 456 }; // erreur : l'identifiant x est déjà utilisé
}

affiche :

main.cpp: In function 'int main()':
main.cpp:6:15: error: redeclaration of 'const int x'
     int const x { 456 }; // erreur : l'identifiant x est déjà utilisé
               ^
main.cpp:5:15: note: 'const int x' previously declared here
     int const x { 123 }; // x correspond à un entier
               ^

Pour écrire un identifiant, vous pouvez utiliser les caractères alphanumériques minuscules et majuscules (a à z, A à Z et 0 à 9) et le tiret bas _ (underscore, correspond à la touche 8 sur un clavier français). De plus, un identifiant doit obligatoirement commencer par une lettre.

Par exemple, les noms suivants sont des identifiants valides :

  • x ;
  • y ;
  • unevariable ;
  • uneVariable ;
  • une_variable ;
  • UnEvArIaBlE.

En revanche, les identifiants suivants ne sont pas valides :

  • _une_variable : commence par un tiret bas ;
  • 123variable : commence par un chiffre ;
  • variable_réelle : contient un caractère interdit (é).

Comme vous le voyez, le langage C++ laisse de grandes libertés pour choisir un identifiant… ce qui peut poser des problèmes. Exemple de mauvais identifiant :

  • jjfndsfkjgukzv : ne veut rien dire, n'apporte pas d'information sur le rôle de cette variable ;
  • une_variable : trop générique ;
  • variable1, variable2, etc. : idem ;
  • UnEvArIaBlE : peu lisible ;
  • une_variable_qui_contient_le_resultat_du_premier_calcul : trop long.

Bonne pratique de codage : règles que vous vous imposez ainsi qu'aux développeurs qui participent à un projet, pour faciliter la lecture du code par tous. Le but est d'avoir des noms homogènes, simples et informatifs.

Il existe déjà des “règles de codage” toutes faites, vous pouvez utiliser vos propres règles. Les conventions de nommage les plus connues : une_variable (STL, Boost), uneVariable (Qt)

Pour le moment, 3 sources de règles pour écrire du code :

  • le langage C++, imposé par le compilateur ;
  • les règles de conception, imposées par la qualité logicielle ;
  • les règles de codage, que vous vous imposez.

La valeur

Une variable contient obligatoirement une valeur. Il est possible de définir une variable sans l'initialiser, mais cette variable pourra alors contenir une valeur aléatoire. Cependant, vous imaginez bien qu'un programme ne va pas forcément fonctionner correctement si certaines variables sont initialisées avec des valeurs aléatoires. Nous n'allons pas voir toutes les syntaxes possibles pour initialiser une variable, mais uniquement celles qui sont recommandées.

Une variable peut être initialisée avec une valeur par défaut (value initialization), avec une littérale (direct initialization) ou avec une expression (copy initialization).

  • initialisation par défaut : Type Identifiant {}; ;
  • initialisation avec une littérale : Type Identifiant { Valeur }; ;
  • initialisation avec une expression : Type Identifiant { Expression };.

Le signe = utilisé pour attribuer une valeur à une variable s'appelle l'opérateur d'affectation.

Plus concrètement, avec du code :

main.cpp
#include <iostream>
#include <string>
 
int main() {
    // Initialisation par défaut
    int const i_default {};
    double const d_default {};
    std::string const s_default {};
    char const c_default {};
    bool const b_default {};
 
    // Initialisation avec une valeur
    int const i_value { 123 };
    double const d_value { 123.456 };
    std::string const s_value { "hello, world!" };
    char const c_value { 'a' };
    bool const b_value { true };
 
    // Initialisation avec une expression
    int const i_expression { 123 + 456 };
    double const d_expression { 12.34 + 56.78 };
    std::string const s_expression { std::string{ "hello, " } + "world!" };
    bool const b_expression { 123 > 456 };
}

Comme vous avez vu dans les codes précédents, il est possible d'afficher la valeur d'une variable directement avec cout. Celui-ci est capable de connaître le type de la variable et d'afficher correctement la valeur, comme si vous aviez écrit une littérale directement avec cout. Vous pouvez également utiliser directement une variable dans un calcul.

int const x { 123 };
std::cout << "La valeur de x est : " << x << std::endl;
int const y { x * 45 };

Portée et durée de vie

Une variable existe à partir du moment où vous la créez, pas avant. Vous ne pouvez pas utiliser une variable dans une ligne de code et la définir ensuite.

cout << i << endl;  // erreur : la variable x est inconnue à cette ligne, elle est
                    // définie uniquement à partir de la ligne suivante
 
int i { 123 };      // création de i

De nouveaux opérateurs arithmétiques

Modifier une variable non const : utilisation de l'opérateur d'affectation =.

Combinaison de l'opérateur d'affectation et d'opérateurs arithmétiques :

a = a + b; 
a += b;

a = a * b;
a *= b;

etc.

Constantes

Lorsque vous ne modifiez pas une variable après l'avoir initialisée, on peut considérer que cette variable est constante. Vous pouvez indiquer cette information au compilateur en ajoutant le mot-clé const (“constant”) comme modificateur de type. Ainsi :

  • int : représente un type entier ;
  • int const (ou const int) : représente un type entier constant.

Indiquer cette information permet au compilateur de faire certaines optimisations (ce que vous ne verrez pas forcément sur un petit programme, mais cela peut avoir un impact sur un programme complexe) et surtout cela permet au compilateur de vérifier que vous ne modifiez pas cette variable par la suite.

int x { 123 };
x = 456; // ok, x n'est pas constant
 
int const y { 123 };
y = 456; // erreur, y est constant

L'utilisation de const apporte une garantie plus forte sur votre code, vous devez systématiquement réfléchir aux rôles de vos variables et si elles doivent être modifiées durant l'exécution de votre programme ou non. Et donc utiliser le mot-clé const aussi souvent que nécessaire.

Notion de contrat

Le typage des variables et l'utilisation de const est une forme de contrat que vous passez avec le compilateur. Vous lui dites que vous allez respecter un certain nombre de contraintes (les valeurs seront d'un type défini, les variables ne seront pas modifiées), celui-ci pourra alors vérifier que vous respectez ces contraintes et fera éventuellement des optimisations.

La programmation par contrat est une approche qui permet d'améliorer la qualité de votre code et qui est plus complet que ce qui est présenté ici. Vous verrez dans la suite du cours comment utiliser efficacement la programmation par contrat en C++, en particulier pour créer vos propres types.

Variable temporaire et non temporaire

En pratique, lorsque l'on écrit :

int x = y + z;

Que se passe-t-il en réalité ?

  1. Chargement de y et z depuis la mémoire dans le processeur.
  2. Calcul et résultat dans le processeur.
  3. Retour du résultat du processeur vers la mémoire.

Le résultat du processeur est une variable non nommée (type, valeur) temporaire (pas en mémoire). Cette variable temporaire s'appelle une rvalue (pour right value, du fait qu'une rvalue ne peut être qu'à droite du signe d'affectation).

Au contraire, x, y, et z sont des variables nommées non temporaires. Ces variables s'appellent des lvalue (pour left value, du fait qu'une lvalue qui peut être à gauche).

Bien faire attention à la différence : c'est l'expression (y + z) qui est une rvalue, y et z en eux-mêmes sont des lvalue.

Note sur bool

Certains types sont convertibles automatiquement en booléen, pour pouvoir tester s'ils sont valides ou non. C'est le cas par exemple des littérales chaînes de caractères. Il est possible d'écrire le code suivant sans que cela ne produise d'erreur :

bool b { "hello, world" };

Lecture complémentaire

rvalue_et_lvalue.1444111942.txt.gz · Dernière modification: 2015/10/06 08:12 par 82.40.212.15