Aller au contenu

Programmation C++ : CM séance 01

1. Description de l'UE

Présentation :

  • ECUE SINA12BL - Programmation C++
  • Responsable : Edouard THIEL
  • 6 séances : CM 1h30, TP 3h

Objectif :

  • apprentissage progressif du langage C++ ;
  • donner de bonnes bases pour les matières des semestres suivants ;
  • atteindre un niveau professionnel à la fin du Master.

Intervenants :

  • Édouard THIEL : CM, TP groupe 1
  • Laurent TICHIT : TP groupe 2

MCCC : 1

  • Note finale session unique = max { ET ; 0.7 ET + 0.3 CC }
  • À l'examen :
    • calculette : non
    • document autorisé : un pense-bête personnel sur 1 feuille A4 recto-verso
  • TP : utilisation d'IAs génératives interdite.
  • Présence aux CM et TP obligatoire.

2. Introduction

2.1. Historique

Bjarne Stroustrup (Bell Labs, 1979-2002)

  • 1979 : "C with Classes", fin thèse. C (1972), Simula (1962). Classes, classes dérivées...
  • 1983 : Nouvelle version, "C++". Fonctions virtuelles, références, surcharge opérateurs...
  • 1985 : Livre The C++ Programming Language, première utilisation commerciale.
  • 1989 : version 2.0. Héritage multiple, classe abstraite, templates...
  • 1998 : C++98, standardisation
  • 2003 : C++03, évolution mineure
  • 2011 : C++11, constexpr, rvalue reference, lambdas...
  • 2014 : C++14, amélioration lambdas
  • 2017 : C++17, fold expr, ...
  • 2020 : C++20, module, range, span, concept, ... partiellement implémenté
  • 2023 : C++23, import std, ... partiellement implémenté

Normalisé par l'ISO, portant sur les 2 aspects :

  • le cœur du langage (mots clé, types, instructions, etc) ;
  • la librairie standard (vector, map, IO, etc).

Version utilisée pour ce cours : C++17

2.2. Utilisation

Utilisation très variée : tous les secteurs de l'industrie, applications à performances critiques :

  • aviation, défense, robotique, embarqué
  • calcul scientifique, imagerie médicale
  • industrie minière (prospection sismique)
  • contrôle industriel en temps réel
  • finance haute fréquence
  • téléphonie (infrastructure, ...)
  • OS, compilateurs, interpréteurs, serveurs et BD, browsers
  • jeux vidéos, animations, VR, modélisation 3D, CAO
  • traitement audio et vidéo, MAO, studios musique
  • développement d'applications (Qt, GTKmm, SFML, ...), ...

Voir par exemple https://www.stroustrup.com/applications.html

Employabilité dans l'industrie : très forte.

2.3. Ressources

Ne pas se limiter au cours !

3. Bases

3.1. Programmes

Le langage C++ est un langage compilé :

graph LR
A[Fichier source 1] -->|compilation| B[Fichier objet 1]
C[Fichier source 2] -->|compilation| D[Fichier objet 2]
B --> E((Édition\nde liens))
D --> E
E --> F[Fichier exécutable]

L'édition de lien résout les symboles et les relie aux bibliothèques.

Les fichiers sources portent en général l'extension .cpp ; ils peuvent être accompagnés par un fichier d'entête .hpp.

Le langage est statiquement typé : le type de tout objet doit être connu lors de la compilation.

3.2. Hello world

Fichier hello.cpp :

1
2
3
4
5
6
#include <iostream>
int main()
{
    // Affiche dans le terminal
    std::cout << "Hello, World!\n";
}
  • ligne 1 : inclut les définitions du module iostream pour les E/S ;
  • ligne 2 : déclare la fonction principale main ;
  • ligne 4 : commentaire ;
  • ligne 5 : affiche Hello, world! suivi d'un retour à la ligne.

Chaque programme doit posséder exactement 1 fonction main. Renvoie un int au système, 0 (succès) par défaut.

Le préfixe std:: devant cout signifie que cout est dans l'espace de noms std, c'est-à-dire dans la librairie standard.

L'objet prédéfini cout (character output) permet d'afficher dans la sortie standard.

L'opérateur d'injection << (put to) recopie l'argument à droite dans l'argument de gauche.

On peut rendre un espace de noms visible (attention aux conflits) :

1
2
3
4
5
6
#include <iostream>
using namespace std;
int main()
{
    cout << "Hello, World!\n";
}

3.3. Compilation

Sous Linux (Debian, Ubuntu et dérivés, ou encore WSL / Ubuntu) :

$ sudo apt install g++
$ g++ --version
g++ (Ubuntu 20.04) 9.4.0
g++ (Ubuntu 24.04) 13.2.0

Versions supportées : voir C++ Standards Support in GCC

Compilation en un fichier objet puis production d'un exécutable :

graph LR
A[hello.cpp] -->|compilation| B[hello.o]
B --> E((Édition\nde liens))
E --> F[hello]
$ g++ --std=c++17 -Wall -c hello.cpp
$ ls
hello.cpp hello.o
$ g++ -o hello hello.o
$ ls
hello.cpp hello.o hello
$ ./hello
Hello, World!

Raccourci : compilation du fichier, production directe d'un exécutable et appel :

$ g++ --std=c++17 -Wall hello.cpp -o hello && ./hello
Hello, World!

ou encore, en définissant une fonction Bash :

$ g() { g++ --std=c++17 -Wall "$1.cpp" -o "$1" ;}
$ g hello && ./$_
Hello, World!

3.4. Définitions

Le langage définit un certain nombre de mots réservés (for, if, const, ...), voir la liste.

Un littéral est une valeur fixe, par exemple un entier (123), un réel (3.14), un caractère ('a'), une chaîne de caractères ("abc"), un booléen (true).

Un identificateur est un nom composé de minuscules (a-z), majuscules (A-Z), chiffres (0-9), ou du caractère souligné _, ne commençant pas par un chiffre ; il est sensible à la casse. Il ne peut pas contenir de caractère accentué.

Exemples : x, y0, _z, point_color, Rectangle, myData.

Les variables, types et fonctions sont des identificateurs.

Tout identificateur doit être déclaré (c'est-à-dire défini) avant d'être utilisé (sauf les noms prédéfinis par le langage ou le compilateur).

Chaque instruction est terminée par un ;. Les instructions peuvent être groupées par des accolades {}. Les indentations et espaces dans le code sont ignorés.

3.5. Types de base

Chaque nom et chaque expression a un type, qui permet de déterminer les opérations supportées.

Il existe de nombreux types prédéfinis. Les types de base (ou types fondamentaux) sont :

  • les types entiers

    • bool : booléen, valeurs true et false
    • char : caractère, par exemple 'a', '0'
    • int : entier, par exemple 123, -5
    • std::size_t : entier non signé résultat de l'opérateur sizeof
  • les types flottants

    • double : réel en double précision, par ex. 0., 3.14, -1.2e-34
    • float : réel simple précision
  • le type vide :

    • void : pas de valeur

Pour les entiers :

  • Les littéraux sont en base 10 ; on peut préfixer par 0x (hexadécimal), 0 (octal), 0b (binaire, C++14). Exemple : 14 = 0xe = 016 = 0b1110.

  • La taille (en bits) varie en fonction des machines (32/64 bits, ...), des options de compilation, et des modificateurs short, long et unsigned.

  • Les modificateurs peuvent être combinés et répétés, par exemple signed short int, ou unsigned long long int ; on peut omettre int.

  • un littéral peut être suffixé, par exemple 123ll est un long long, 45uz est un unsigned std::size_t, voir liste.

  • Le standard C++ garantit que

    1 == sizeof(char) ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long). 
    

Pour les flottants :

  • les littéraux contiennent un . ou un e ;
  • les chiffres peuvent être groupés par des ' pour faciliter la lecture (exemple 2,718'281'828).

On peut déclarer un alias d'un type :

typedef le_type_existant alias_du_type;
typedef unsigned long long My_ulong;

Le C++ donne aussi la possibilité de créer des user-defined literals avec des suffixes, par exemple 12_km.

3.6. Variables

Une variable est déclarée sous la forme d'un identificateur, précédé d'un type :

type_variable nom_variable;

Exemples de déclarations de variables :

int i;
double x, y;
bool is_sorted;

Toute variable doit être initialisée avant utilisation :

i = 5;
x = 3.1; y = 0.;
is_sorted = false;

Déclaration avec initialisation : évite des oublis

int i = 5;
double x = 3.1, y = 0.;
bool is_sorted = false;

Déclaration automatique avec auto : pratique lorsque le type peut être déduit par le compilateur :

auto i = 5;
auto x = 3.1, y = 0.;
auto is_sorted = false;
// mais pas : auto i = 5, x = 3.1, y = 0., is_sorted = false;

Syntaxe avec accolades

int i {5};    // ou encore : int i = {5};
i = {7};

Le = dans l'initialisation est l'opérateur provenant du C ; la forme {} est plus générale, et permet d'éviter des conversions où il y a une perte d'information :

int i = 3.14;       // conversion implicite : i devient 3
int i {3.14};       // erreur : conversion d'un réel en entier
double x = 3;       // conversion implicite : x devient 3.0
double x {3};       // conversion sans perte d'information

Exemple avec un type élaboré :

#include <complex>
std::complex<double> z {1., 2.};
z = {3, 4.12};

Durée de vie

On peut déclarer une variable n'importe où dans un bloc entre accolades { } ; la variable existe depuis sa déclaration jusqu'à la fin du bloc, où elle est "détruite".

{                                   // début bloc
    ....                            
    type_variable v;                // création de v
    ...                             // utilisation de v
}                                   // fin bloc --> destruction de v

3.7. Entrées-sorties

Les objets suivants sont prédéfinis dans <iostream> :

  • std::cin (character input) : lit dans l'entrée standard ;
  • std::cout (character output) : affiche dans la sortie standard ;
  • std::cerr (character error) : affiche dans la sortie d'erreur ;
  • std::endl (end line) : envoie "\n" puis force l'affichage (flush).

Les objets cin, cout et cerr opèrent sur un flux (stream) à l'aide des opérateurs suivants :

  • << (put in) : injection, pour les affichages ;
  • >> (get from) : extraction, pour les lectures.

L'instruction

std::cout << valeur;

évalue la valeur, puis l'injecte dans l'objet cout, qui l'affiche en fonction du type de la valeur, puis renvoie l'objet cout lui-même.

Quel est l'effet de cette ligne ?

std::cout << valeur1 << valeur2;

En fait, l'opérateur d'injection est associatif à gauche, donc la ligne est équivalente à

(std::cout << valeur1) << valeur2;

or (std::cout << valeur1) renvoie l'objet cout, donc la ligne est équivalente à

std::cout << valeur1;
std::cout << valeur2;

De même pour std::cerr << et std::cin >>.

L'extraction fonctionne comme ceci : une lecture est faite dans l'entrée standard, puis convertie selon type_variable et la valeur est mémorisée dans la variable v fournie :

type_variable v;
std::cin >> v;

Exemple d'une saisie dans le terminal :

1
2
3
4
5
6
7
8
9
#include <iostream>

int main()
{
    int a, b;
    std::cout << "Entrez deux entier :" << std::endl;
    std::cin >> a >> b;
    std::cout << "Vous avez entré " << a << " et " << b << std::endl;
}

Affiche :

Entrez deux entier :
5 7
Vous avez entré 5 et 7

L'affichage peut être paramétré avec iomanip :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <complex>      // définit std::complex
#include <iomanip>      // définit std::fixed et std::setprecision
#include <iostream>

int main()
{
    std::complex<double> z {1., 2.};
    std::cout << std::fixed << std::setprecision(1);
    std::cout << "z = " << z << "\n";
    z = {3, 4.12};
    std::cout << "z = " << z << "\n";
}

Affiche :

z = (1.0,2.0)
z = (3.0,4.1)

3.8. Fonctions

Une fonction

  • désigne un bloc de code qui peut être exécuté ;
  • possède un nom, un type de retour, des paramètres typés, un corps ;
  • doit obligatoirement être déclarée avant de pouvoir être utilisée.

Syntaxe de la déclaration :

type_retour nom_fonction (type_paramètre paramètre, ....)
{
    // corps de la fonction
    ....
    return valeur_retour;  // sortie immédiate de la fonction
    ....
}

Syntaxe d'un appel de la fonction (exécution de son corps) :

type_retour variable = nom_fonction (paramètre, ....);

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>

int compute_rect_area (int width, int height)
{
    return width * height;
}

void print_rect_area (int area)
{
    std::cout << "rect area is " << area << std::endl;
}

int main()
{
    int w = 3, h = 5;
    int a = compute_rect_area (w, h);
    print_rect_area (a);
}

Ligne 8 : un type de retour void indique que la fonction ne renvoie pas de valeur.
Lignes 16,17 : appel des fonctions.

Déclaration forward (en avance)

on peut déclarer partiellement une fonction avant de l'écrire entièrement. Syntaxe :

type_retour nom_fonction (type_paramètre paramètre, ....);  // declaration forward
type_retour nom_fonction (type_paramètre, ....);            // variante

type_retour nom_fonction (type_paramètre paramètre, ....)   // implémentation
{
    // corps de la fonction
}

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

// Déclarations forward
int compute_rect_area (int width, int height);
void print_rect_area (int area);

int main()
{
    int w = 3, h = 5;
    int a = compute_rect_area (w, h);
    print_rect_area (a);
}

void print_rect_area (int area)
{
    std::cout << "rect area is " << area << std::endl;
}

int compute_rect_area (int width, int height)
{
    return width * height;
}

Utilité :

  • rend l'ordre de l'implémentation indépendant de l'ordre d'appel ;
  • mécanisme utilisé dans les headers .hpp (ce sera vu plus tard).

Signature et surcharge d'une fonction

La signature est constituée du nom de la fonction et des types des paramètres ; le type de retour et les noms des paramètres sont omis :

nom_fonction (type_paramètre1, ....)

Le C++ permet de déclarer une fonction avec des signatures différentes : on appelle cela la surcharge de la fonction (overloading) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iomanip>      // définit std::fixed et std::setprecision
#include <iostream>

void afficher_produit (int a, int b)
{
    std::cout << a << " * " << b << " = " << a*b << std::endl;
}

void afficher_produit (int a, int b, int c)
{
    std::cout << a << " * " << b << " * " << c << " = " << a*b*c << std::endl;
}

void afficher_produit (double a, double b)
{
    std::cout << std::fixed << std::setprecision(1);
    std::cout << a << " * " << b << " = " << a*b << std::endl;
}

int main()
{
    afficher_produit (2, 3);
    afficher_produit (2, 3, 4);
    afficher_produit (2., 3.);
}

Affiche :

2 * 3 = 6
2 * 3 * 4 = 24
2.0 * 3.0 = 6.0

Remarque : afficher_produit (2., 3); provoquerait une erreur de compilation :

error: call of overloaded ‘afficher_produit(double, int)’ is ambiguous

Paramètres optionnels

On peut rendre des paramètres optionnels en leur donnant une valeur par défaut. Ces paramètres doivent être placés après les paramètres obligatoires. On peut alors les omettre lors de l'appel :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <iostream>

void afficher_coordonnees (int x, int y, int z = 0, int w = 1)
{
    std::cout << x << ", " << y << ", " << z << ", " << w << std::endl;
}

int main()
{
    afficher_coordonnees (3, 4);
    afficher_coordonnees (3, 4, 5);
    afficher_coordonnees (3, 4, 5, 6);
}

Affiche :

3, 4, 0, 1
3, 4, 5, 1
3, 4, 5, 6

3.9. Structures de contrôle

Il s'agit des branchements if else, switch case, des boucles while et for, et des exceptions.

  • condition est une expression booléenne ;
  • instruction désigne une instruction simple, composée ou d'un bloc d'instructions entourées d'accolades { }.

Branchements if-else

Syntaxe :

if (condition) instruction;
if (condition) instruction1; else instruction2;

Style préconisé par Stroustrup :

if (condition)
    instruction;

if (condition) {
    instructions;
}

if (condition) {
    instructions1;
} 
else {
    instructions2;
}

if (condition1) {
    instructions1;
} 
else if (condition2) {
    instructions2;
}
else {
    instructions3;
}

Branchement switch-case

valeur est une constante connue à la compilation.

switch (expression) {
case valeur1:
    instructionsA;
    break;
case valeur2:
    instructionsB;
    break;
case valeur3:
case valeur4:
    instructionsC;
    break;
....
default :
    instructionsN;
}

Boucle while

La boucle la plus générale :

while (condition) {
    instructions;
}

Variante, qui effectue au moins une itération :

do {
    instructions;
} while (condition);

Boucle for

for (instruction_init; condition; instruction_next) {
    instructions;
}

équivalente à

instruction_init;
while (condition) {
    instructions;
    instruction_next;
}

Dans instruction_init on peut aussi déclarer des variables, qui n'existeront que pendant la durée de la boucle. Exemple :

1
2
3
4
5
6
7
8
#include <iostream>

int main()
{
    for (int i = 0; i < 5; i++)
        std::cout << i << " ";
    std::cout << std::endl;
}

Affiche :

0 1 2 3 4 

Boucle infinie :

for (;;) {
    instructions;
}

Il existe aussi une forme de boucle for qui itère dans une collection (range-for). Syntaxe :

for (variable : collection) {
    instructions;
}

Exemple :

1
2
3
4
5
6
7
8
9
#include <iostream>

int main()
{
    int tableau[] = {5, 7, 11};
    for (int element : tableau)
        std::cout << element << " ";
    std::cout << std::endl;
}

Affiche :

5 7 11

Contrôles

  • break sort de la boucle la plus imbriquée ou d'un switch ;
  • continue passe immédiatement à l'itération suivante (la plus imbriquée) ;
  • return valeur ou return sort immédiatement d'une fonction, renvoie valeur ou rien (fonctions void) ;
  • std::exit(n) défini dans <cstdlib> termine le programme avec le code de sortie n (0 pour succès).

Exceptions

Le flot normal de l'exécution peut être interrompu par une exception :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

int main()
{
    std::cout << "avant try-catch" << "\n";
    try {
        std::cout << "avant throw" << "\n";
        throw 123;
        std::cout << "après throw" << "\n";
    }
    catch (double e) {
        std::cout << "catch double " << e << "\n";
    }
    catch (int e) {
        std::cout << "catch int " << e << "\n";
    }
    catch (...) {
        std::cout << "catch default" << "\n";
    }
    std::cout << "après try-catch" << "\n";
}

Affiche :

avant try-catch
avant throw
catch int 123
après try-catch

3.10. Opérateurs

Les opérateurs arithmétiques agissent sur les types fondamentaux :

  • x+y : addition, +x : plus unaire
  • x-y : soustraction, -x : moins unaire
  • x*y : multiplication, x/y : division, x%y : reste de la division

Opérateurs d'assignement :

  • x += y (x = x+y), ++x ou x++ (x = x+1) : incrément
  • x -= y (x = x-y), --x ou x-- (x = x-1) : décrément
  • x *= y (x = x*y), x /= y (x = x/y), x %= y (x = x%y)

Opérateurs de comparaison, résultat booléen :

  • x == y : est égal, x != y : est différent
  • x < y : est strictement inférieur, x <= y : est inférieur ou égal
  • x > y : est strictement supérieur, x >= y : est supérieur ou égal

Opérateurs logiques (sur booléens) :

  • x && y : et logique, x || y : ou logique
  • !x : négation

Opérateurs de bits (sur entiers non signés) : 2

  • x & y : et binaire, x | y : ou binaire, x ^ y : ou exclusif
  • ~x : complément
  • x << y : décalage à gauche, x >> y : décalage à droite

Il est possible de redéfinir des opérateurs par surcharge pour agir sur des objets ; ce sera vu plus tard.


  1. MCCC = Modalités de contrôle des connaissances et des compétences ; ET = Examen Terminal ; CC = Contrôle Continu. 

  2. Voir Bit Twiddling Hacks by Sean Eron Anderson