Les premiers pas
Cette page présente de manière informelle la notation du C++, le modèle de mémoire et de calcul du C++, ainsi que les mécanismes de base permettant d'organiser le code dans un programme. Elle aborde également les différentes particularités du langage facilitant l'écriture de programmes structurés, en mettant l'accent sur les éléments qui facilitent la gestion de la mémoire, ainsi que la manipulation des données et des variables. Il s'agit des fonctionnalités du langage prenant en charge les styles les plus souvent rencontrés en C, incluant les structures de contrôle classiques, ainsi que les mécanismes permettant de travailler avec des pointeurs et de gérer les allocations dynamiques de mémoire. Ces concepts sont parfois appelés programmation procédurale et sont essentiels pour comprendre les bases du C++ avant d'aborder des paradigmes plus avancés comme la programmation orientée objet. En somme, cette présentation vise à offrir une vue d'ensemble de ces mécanismes fondamentaux, tout en expliquant leur utilisation et leur impact sur la structure d'un programme C++.
Les bases
Le C++ est un langage de programmation compilé, ce qui signifie que son code source doit être transformé en un programme exécutable avant de pouvoir être exécuté. Cette transformation passe par plusieurs étapes essentielles, à commencer par la compilation proprement dite, où chaque fichier source est converti en un fichier objet indépendant. Ces fichiers objets contiennent le code machine partiel, devant ensuite être combiné avec d'autres fichiers objets et des bibliothèques par un éditeur de liens pour produire le programme final. L'ensemble de ce processus garantit que le programme est optimisé pour une architecture matérielle spécifique et peut tirer parti des optimisations offertes par le compilateur. Un programme C++ se compose généralement de plusieurs fichiers sources organisés selon une structure modulaire, facilitant la maintenance et la réutilisation du code. Ces fichiers sont souvent accompagnés de fichiers d'entête, déclarant les fonctions et classes utilisées dans différents modules du programme.
Une fois compilé et lié, un programme exécutable est conçu pour fonctionner sur une combinaison spécifique de matériel et de système d'exploitation. Il ne peut pas être exécuté directement sur une autre plateforme sans une recompilation appropriée, car les instructions générées sont spécifiques à l'architecture cible. Par exemple, un programme compilé pour Windows sur une machine x86 ne pourra pas fonctionner sur un Mac équipé d'un processeur ARM sans adaptation. Cette dépendance au matériel et au système d'exploitation est une contrainte importante du développement en C++, mais elle est aussi une source de performance, car elle permet d'optimiser le programme pour l'environnement dans lequel il sera utilisé. Certains compilateurs offrent des options pour générer du code multiplateforme, mais cela nécessite souvent des ajustements et des bibliothèques adaptées à chaque environnement cible.
Lorsqu'on parle de portabilité en C++, il est généralement question de la capacité du code source à être compilé et exécuté sur plusieurs systèmes avec un minimum de modifications. Un code bien écrit, utilisant des bibliothèques standard et évitant les dépendances spécifiques à un système d'exploitation particulier, a plus de chances d'être portable. Cependant, certains aspects, comme la gestion des fichiers, l'allocation mémoire ou l'interaction avec le matériel, peuvent nécessiter des ajustements pour s'adapter aux différentes plateformes. Pour améliorer la portabilité, les développeurs utilisent souvent des directives de préprocesseur (#ifdef, #ifndef) pour inclure des sections de code spécifiques à une plateforme. L'utilisation de bibliothèques multiplateformes comme Boost ou Qt peut également simplifier le processus en offrant des abstractions standardisées pour différentes fonctionnalités du système.
La norme ISO C++ définit deux types d'entités :
- Les fonctionnalités de base du langage, telles que les types intégrés (par exemple, char et int) et les boucles (par exemple, les instructions for et while).
- Les composantes de la bibliothèque standard, tels que les conteneurs (par exemple, vector et map) et les opérations d'entrée/sortie (par exemple, << et getline()).
Les composantes de la bibliothèque standard sont du code C++ tout à fait ordinaire fourni par chaque implémentation C++. En d'autres termes, la bibliothèque standard C++ peut être implémentée en C++ lui-même (et ce, avec des utilisations très mineures de code machine pour des choses telles que le changement de contexte de processus léger). Cela implique que C++ est suffisamment expressif et efficace pour les tâches de programmation système les plus exigeantes.
Le C++ est un langage de programmation typé statiquement. En d'autres termes, le type de chaque entité (par exemple, objet, valeur, nom et expression) doit être connu du compilateur au moment de son utilisation. Le type d'un objet détermine l'ensemble des opérations lui étant applicables.
Un premier programme
Le programme C++ minimal est :
- int main() { } // Le programme C++ minimal
Cela définit une fonction appelée main, ne prenant aucun paramètre et ne faisant rien.
Les accolades, { }, expriment le regroupement en C++. Ici, elles indiquent le début et la fin du corps de la fonction. La double barre oblique, //, commence un commentaire qui s'étend jusqu'à la fin de la ligne. Un commentaire est destiné au lecteur humain ; ainsi le compilateur ignore les commentaires.
Chaque programme C++ doit avoir exactement une fonction globale nommée main(). Le programme commence par exécuter cette fonction. La valeur int renvoyée par main(), le cas échéant, est la valeur de retour du programme au «système». Si aucune valeur n'est renvoyée, le système recevra une valeur indiquant la réussite de l'exécution. Une valeur différente de zéro de main() indique un échec. Tous les systèmes d'exploitation et environnements d'exécution n'utilisent pas cette valeur de retour : les environnements basés sur Linux/Unix le font souvent, mais les environnements basés sur Windows le font rarement.
En général, un programme produit une sortie. Voici un programme écrivant Bonjour le monde ! :
- #include <iostream>
-
- int main() {
- std::cout << "Bonjour le monde !\n";
- }
La ligne #include <iostream> indique au compilateur d'inclure les déclarations des fonctions d'entrée/sortie de flux standard telles qu'elles se trouvent dans iostream. Sans ces déclarations, l'expression :
- std::cout << "Bonjour le monde !\n";
n'aurait aucun sens. L'opérateur << («put to») écrit son deuxième paramètre sur son premier. Dans ce cas, la chaîne de caractères littérale "Bonjour le monde !\n" est écrite sur le flux de sortie standard std::cout. Une chaîne de caractères littérale est une séquence de caractères entourée de guillemets. Dans une chaîne littérale, le caractère barre oblique inverse \ suivi d'un autre caractère désigne un seul «caractère spécial». Dans ce cas, \n est le caractère de nouvelle ligne, de sorte que les caractères écrits sont Bonjour le monde ! suivi d'un retour à la ligne.
Le std:: spécifie que le nom cout doit être trouvé dans l'espace de noms de la bibliothèque standard. Nous omettons généralement le std:: lorsque nous discutons des fonctionnalités standard.
Essentiellement, tout le code exécutable est placé dans des fonctions et appelé directement ou indirectement depuis main(). Par exemple :
Avec le code suivant :
Types, variables et arithmétique
En C++, les types de données définissent la nature des valeurs que peuvent contenir les variables. On distingue les types fondamentaux comme les entiers (int), les nombres à virgule flottante (float, double), les caractères (char) et les booléens (bool). À ces types s'ajoutent des variantes qualifiées par des mots-clefs comme unsigned (sans signe) ou long (taille étendue). Le langage de programmation permet aussi de créer des types personnalisés avec typedef ou using, et d'utiliser des structures (struct), des classes (class) ou des énumérations (enum) pour organiser les données de manière plus complexe.
Les variables en C++ doivent être déclarées avant utilisation et leur type ne peut généralement pas changer après l'initialisation. Une variable peut être définie avec une valeur initiale explicite, mais peut aussi être laissée non initialisée, ce qui peut entraîner un comportement indéfini. Le mot-clef auto permet au compilateur de déduire le type d'une variable en fonction de son initialisation, réduisant ainsi la nécessité d'écrire des types explicites. De plus, les variables peuvent être modifiées avec des qualificateurs comme const (constante) ou volatile (susceptible d'être modifiée en dehors du programme).
L'arithmétique en C++ repose sur les opérateurs de base (+, -, *, /, %) qui permettent d'effectuer des calculs sur les variables numériques. Les opérations entre types différents peuvent impliquer des conversions implicites, appelées promotions ou troncatures, selon la compatibilité des types. Des opérateurs d'incrémentation (++, --) et de composition (+=, -=, *=,...) facilitent les modifications de valeurs. Enfin, la bibliothèque <cmath> fournit des fonctions avancées comme sqrt() pour les racines carrées, pow() pour les puissances ou abs() pour la valeur absolue, enrichissant ainsi les possibilités de calcul en C++.
Chaque nom et chaque expression ont un type déterminant les opérations pouvant être effectuées sur eux. Par exemple, la déclaration :
- int pouce;
spécifie que pouce est de type de données int; c'est-à-dire que pouce est une variable entière.
Une déclaration est une instruction introduisant un nom dans le programme. Elle spécifie un type pour l'entité nommée :
- Un type définit un ensemble de valeurs possibles et un ensemble d'opérations (pour un objet).
- Un objet est une mémoire qui contient une valeur d'un certain type.
- Une valeur est un ensemble de bits interprétés selon un type.
- Une variable est un objet nommé.
Le C++ propose une variété de types fondamentaux. Par exemple :
Type de données | Description |
---|---|
bool | Booléen, les valeurs possibles sont true et false |
char | Caractère, par exemple, « a », « z » et « 9 » |
int | Entier, par exemple, -213, 42 et 1066 |
double | Nombre à virgule flottante double précision, par exemple 3,14 et 299793,0. |
Une variable char a la taille naturelle pour contenir un caractère sur une machine donnée (généralement un octet de 8 bits), et les tailles des autres types sont indiquées en multiples de la taille d'un char. La taille d'un type est définie par l'implémentation (c'est-à-dire qu'elle peut varier selon les machines) et peut être obtenue par l'opérateur sizeof ; par exemple, sizeof(char) est égal à 1 et sizeof(int) est souvent égal à 4.
Les opérateurs arithmétiques peuvent être utilisés pour des combinaisons appropriées de ces types :
Opérateur | Description |
---|---|
x+y | Plus |
+x | Unaire plus |
x-y | Moins |
-x | Moins unaire |
x*y | Multiplier |
x/y | Diviser |
x%y | Reste (module) pour les entiers |
Les opérateurs de comparaison peuvent également :
Opérateur | Description |
---|---|
x==y | Égal |
x!=y | Pas égal |
x<y | Moins que |
x>y | Plus grand que |
x<=y | Inférieur ou égal |
x>=y | Supérieur ou égal |
Dans les affectations et dans les opérations arithmétiques, C++ effectue toutes les conversions significatives entre les types de base afin qu'ils puissent être mélangés librement :
Notez que = est l'opérateur d'affectation et que == teste l'égalité.
C++ propose une variété de notations pour exprimer l'initialisation, telles que = utilisé ci-dessus, et une forme universelle basée sur des listes d'initialisation délimitées par des accolades :
- double d1 = 12.34; // initialiser d1 avec 12.34
- double d2 {12.34}; // initialiser d2 avec 12.34
-
- complex<double> z = 1; // un nombre complexe avec des scalaires à virgule flottante double précision
- complex<double> z2 {d1,d2};
- complex<double> z3 = {1,2}; // le = est facultatif avec { ... }
-
- vector<int> v {1,2,3,4,5,6,7}; // un vecteur d'entiers
La forme = est traditionnelle et remonte au C, mais en cas de doute, utilisez la forme générale de liste {}. Au moins, elle vous évite les conversions perdant des informations (conversions restreintes) :
Une constante ne peut pas rester non initialisée et une variable ne doit rester non initialisée que dans des circonstances extrêmement rares. N'introduisez pas de nom tant que vous n'avez pas de valeur appropriée pour celui-ci. Les types définis par l'utilisateur (tels que string, vecteur, Matrice, Controleur_Moteur et Guerrier_Orc) peuvent être définis pour être implicitement initialisés.
Lors de la définition d'une variable, vous n'avez pas réellement besoin d'indiquer explicitement son type lorsqu'il peut être déduit de l'initialiseur :
Avec auto, nous utilisons la syntaxe = car il n'y a pas de conversion de type impliquée qui pourrait causer des problèmes.
Nous utilisons auto lorsque nous n'avons pas de raison spécifique de mentionner explicitement le type. Les «raisons spécifiques» incluent :
- La définition est dans une portée large où nous voulons rendre le type clairement visible pour les lecteurs de notre code.
- Nous voulons être explicites sur la plage ou la précision d'une variable (par exemple, double plutôt que float).
En utilisant auto, nous évitons la redondance et l'écriture de noms de types longs. Ceci est particulièrement important dans la programmation générique où le type exact d'un objet peut être difficile à connaître pour le programmeur et les noms de types peuvent être assez longs.
En plus des opérateurs arithmétiques et logiques classiques, C++ propose des opérations plus spécifiques pour modifier une variable :
Opérateur | Description |
---|---|
x+=y | x = x+y |
++x | incrémentation : x = x+1 |
x-=y | x = x-y |
--x | Décrementation : x = x-1 |
x*=y | Mise à l'échelle : x = x*y |
x/=y | Mise à l'échelle : x = x/y |
x%=y | x = x%y |
Ces opérateurs sont concis, pratiques et très fréquemment utilisés.
Les constantes
Le C++ prend en charge deux notions d'immuabilité :
Notion | Description |
---|---|
const | Ce qui signifie approximativement «Nous promettons de ne pas modifier cette valeur». Cela sert principalement à spécifier des interfaces, afin que les données puissent être transmises aux fonctions sans crainte d'être modifiées. Le compilateur applique la promesse faite par const. |
constexpr | Ce qui signifie approximativement «à évaluer au moment de la compilation». Cela sert principalement à spécifier des constantes, à permettre le placement de données en mémoire en lecture seulement (où il est peu probable qu'elles soient corrompues) et à des fins de performances. |
Voici quelques exemples :
- const int dmv = 37; // dmv est une constante nommée
- int var = 37; // var n'est pas une constante
- constexpr double max1 = 1.5*square(dmv); // Correcte si square(37) est une expression constante
- constexpr double max2 = 1.5*square(var); // Erreur : var n'est pas une expression constante
- const double max3 = 1.5*square(var); // Correcte, peut être évalué au moment de l'exécution
- double sum(const vector<double>&); // La somme ne modifiera pas son paramètre
- vector<double> v {9.8, 7.6, 5.4}; // v n'est pas une constante
- const double s1 = sum(v); // Correcte : évalué au moment de l'exécution
- constexpr double s2 = sum(v); // Erreur : sum(v) n'est pas une expression constante
Pour qu'une fonction soit utilisable dans une expression constante, c'est-à-dire dans une expression étant évaluée par le compilateur, elle doit être définie constexpr. Par exemple :
Pour être constexpr, une fonction doit être assez simple : juste une instruction de retour calculant une valeur. Une fonction constexpr peut être utilisée pour des arguments non constants, mais lorsque cela est fait, le résultat n'est pas une expression constante. Nous autorisons une fonction constexpr à être appelée avec des arguments non constants dans des contextes ne nécessitant pas d'expressions constantes, de sorte que nous n'avons pas à définir essentiellement la même fonction deux fois : une fois pour les expressions constantes et une fois pour les variables.
Dans quelques endroits, les expressions constantes sont requises par les règles du langage (par exemple, les limites de tableau, les étiquettes de cas, certains arguments de modèle et les constantes déclarées à l'aide de constexpr). Dans d'autres cas, l'évaluation au moment de la compilation est importante pour les performances. Indépendamment des problèmes de performances, la notion d'immuabilité (d'un objet avec un état immuable) est une préoccupation de conception importante.
Les tests et les boucles
Le C++ fournit un ensemble d'instructions conventionnelles pour exprimer la sélection et la boucle. Par exemple, voici une fonction simple invitant l'utilisateur et renvoie un booléen indiquant la réponse :
Pour correspondre à l'opérateur de sortie << («put to»), l'opérateur >> («get from») est utilisé pour l'entrée; cin est le flux d'entrée standard. L'opérande de droite de >>> est la cible de l'opération d'entrée et le type de cet opérande détermine quelle entrée >> accepte. Le caractère \n à la fin de la chaîne de sortie représente une nouvelle ligne.
L'exemple pourrait être amélioré en prenant en compte une réponse n (pour «non») :
Une instruction switch teste une valeur par rapport à un ensemble de constantes. Les constantes de case doivent être distinctes, et si la valeur testée ne correspond à aucune d'entre elles, la valeur par défaut est choisie. Si aucune valeur par défaut n'est fournie, aucune action n'est entreprise si la valeur ne correspond à aucune constante de case.
Peu de programmes sont écrits sans boucles. Par exemple, nous aimerions donner à l'utilisateur quelques essais pour produire une entrée acceptable :
- bool accepte3() {
- int essaies = 1;
- while (essaies<4) {
- cout << "Voulez-vous continuer (o ou n)?\n\n"; // Écrire une question
- char reponse = 0;
- cin >> reponse; // Lire la réponse
- switch (reponse) {
- case 'o':
- return true;
- case 'n':
- return false;
- default:
- cout << "Désolé, je ne comprends pas cette réponse.\n";
- ++essaies; // incrémentation
- }
- }
- cout << "Nous prenons ça pour un non.\n";
- return false;
- }
L'instruction while s'exécute jusqu'à ce que sa condition devienne fausse.
Pointeurs, tableaux et boucles
Un tableau d'éléments de type char peut être déclaré comme ceci :
- char v[7]; // tableau de 7 caractères
De même, un pointeur peut être déclaré comme ceci :
- char* p; // pointeur vers un caractère
Dans les déclarations, [] signifie «tableau de» et * signifie «pointeur vers». Tous les tableaux ont 0 comme limite inférieure, donc v a sept éléments, v[0] à v[6]. La taille d'un tableau doit être une expression constante. Une variable pointeur peut contenir l'adresse d'un objet du type approprié :
Dans une expression, le préfixe unaire * signifie «contenu de» et le préfixe unaire & signifie «adresse de».
Considérez la copie de douze éléments d'un tableau à un autre :
Cette instruction for peut être lue comme «définir i à zéro; tant que i n'est pas 12, copier le ième élément et incrémenter i». Lorsqu'il est appliqué à une variable entière, l'opérateur d'incrémentation, ++, ajoute simplement 1. C++ propose également une instruction for plus simple, appelée instruction range-for, pour les boucles parcourant une séquence de la manière la plus simple :
La première instruction range-for peut être lue comme « pour chaque élément de vecteur, du premier au dernier, placez une copie dans x et affichez-la». Notez que nous n'avons pas besoin de spécifier une limite de tableau lorsque nous l'initialisons avec une liste. L'instruction range-for peut être utilisée pour n'importe quelle séquence d'éléments.
Si nous ne voulions pas copier les valeurs de vecteur dans la variable x, mais plutôt que x fasse simplement référence à un élément, nous pourrions écrire :
Dans une déclaration, le suffixe unaire & signifie «référence à». Une référence est similaire à un pointeur, sauf que vous n'avez pas besoin d'utiliser un préfixe * pour accéder à la valeur à laquelle la référence fait référence. De plus, une référence ne peut pas être utilisée pour faire référence à un objet différent après son initialisation. Lorsqu'ils sont utilisés dans les déclarations, les opérateurs (tels que &, * et []) sont appelés opérateurs de déclaration :
- T arr[n]; // T[n] : tableau de n T
- T* ptr; // T* : pointeur vers T
- T& ref; // T& : référence à T
- T f(A); // T(A) : fonction prenant un argument de type A renvoyant un résultat de type T
Nous essayons de garantir qu'un pointeur pointe toujours vers un objet, de sorte que le déréférencement soit valide. Lorsque nous n'avons pas d'objet vers lequel pointer ou si nous devons représenter la notion d'«aucun objet disponible» (par exemple, pour une fin de liste), nous donnons au pointeur la valeur nullptr («le pointeur nul»). Il n'y a qu'un seul nullptr partagé par tous les types de pointeurs :
Il est souvent judicieux de vérifier qu'un paramètre pointeur étant censé pointer vers quelque chose, pointe réellement vers quelque chose :
Notez comment nous pouvons déplacer un pointeur pour pointer vers l'élément suivant d'un tableau en utilisant ++ et que nous pouvons laisser de côté l'initialiseur dans une instruction for si nous n'en avons pas besoin.
La définition de compteur_x() suppose que le char* est une chaîne de caractères de style C, c'est-à-dire que le pointeur pointe vers un tableau de char terminé par zéro.
Dans le code plus ancien, 0 ou NULL est généralement utilisé à la place de nullptr. Cependant, l'utilisation de nullptr élimine la confusion potentielle entre les entiers (tels que 0 ou NULL) et les pointeurs (tels que nullptr).
Types définis par l'utilisateur
Nous appelons types intégrés les types pouvant être construits à partir des types fondamentaux, du modificateur const et des opérateurs déclarateurs. L'ensemble des types et opérations intégrés de C++ est riche, mais volontairement de bas niveau. Ils reflètent directement et efficacement les capacités du matériel informatique conventionnel. Cependant, ils ne fournissent pas au programmeur des fonctionnalités de haut niveau pour écrire facilement des applications avancées. Au lieu de cela, C++ augmente les types et opérations intégrés avec un ensemble sophistiqué de mécanismes d'abstraction à partir desquels les programmeurs peuvent créer de telles fonctionnalités de haut niveau. Les mécanismes d'abstraction de C++ sont principalement conçus pour permettre aux programmeurs de concevoir et d'implémenter leurs propres types, avec des représentations et des opérations adaptées, et pour que les programmeurs puissent utiliser ces types de manière simple et élégante. Les types construits à partir des types intégrés à l'aide des mécanismes d'abstraction de C++ sont appelés types définis par l'utilisateur. Ils sont appelés classes et énumérations.
Les structures
La première étape de la création d'un nouveau type consiste souvent à organiser les éléments dont il a besoin dans une structure de données, une struct :
Cette première version de Vecteur se compose d'un int et d'un double*.
Une variable de type Vecteur peut être définie ainsi :
- Vector vec;
Cependant, cela n'est pas très utile en soi, car le pointeur element de vec ne pointe vers rien. Pour que cela soit utile, nous devons donner à vec des éléments vers lesquels pointer. Par exemple, nous pouvons construire un Vecteur comme ceci :