Académique Documents
Professionnel Documents
Culture Documents
I Rappels de C
I Adresses, pointeurs
I Passage de paramètres
I Types de données, structures I B. Stroustrup. The C++ Programming Language. 4th edition, Addison
I C++ Wesley, 2013
I Entrées/sorties I C. Delannoy. Programmer en langage C++. 6ème édition, Eyrolles, 2004
I Références I S. Meyers. Effective C++. 3rd Edition, Addison Wesley, 2005
I Opérateurs new et delete
I Surcharge d’opérateurs I https://cplusplus.com, https://en.cppreference.com
I Programmation orientée objet en C++
I Classe, attribut, méthode
I Abstraction, encapsulation
I Héritage
3 4
nom de variable
I Remarque : contenu et valeur sont synonymes
5 6
Fonctionnement de la mémoire Représentation des variables en mémoire
I Rigoureusement, l’adresse d’une variable est en fait l’adresse du premier
1 octet occupé par cette variable
I Une adresse est écrite par convention en hexadécimal I Souvent, le compilateur va positionner les variables locales d’une fonction à
I La base hexadécimale comporte 16 chiffres, de 0 à F des adresses contigües
I En C, le préfixe 0x indique que l’entier qui suit est en hexadécimal I Dans l’exemple suivant, les positions des variables sont contigües et suivent
l’ordre de leur déclaration (ce n’est pas toujours le cas)
Décimal Héxadécimal Décimal Héxadécimal Décimal Héxadécimal
int main()
0 0x0 11 0xB 22 0x16
{
1 0x1 12 0xC 23 0x17
2 0x2 13 0xD 24 0x18
int x1;
3 0x3 14 0xE 25 0x19 int x2;
4 0x4 15 0xF 26 0x1A int x3;
5 0x5 16 0x10 27 0x1B x1 = 10;
6 0x6 17 0x11 28 0x1C x2 = 15;
7 0x7 18 0x12 29 0x1D x3 = x1+x2;
8 0x8 19 0x13 30 0x1E ...
9 0x9 20 0x14 31 0x1F
10 0xA 21 0x15 32 0x20 A A+1 ... A+4 ... A+8 ...
10 15 25
Pointeurs Pointeurs
9 10
Pointeurs Pointeurs
11 12
Pointeurs Pointeurs et tableaux
I Exercice : donnez la sortie à l’écran du programme suivant :
int main()
{ I Un tableau est en réalité un pointeur vers un nombre fini de cases mémoire
int var1 = 5, var2 = 10; allouées (= réservées pour stocker les élements du tableau).
int * p; I Les éléments du tableau sont obligatoirement contigus, et à des adresses
p = &var1; croissantes.
printf("var1 = %i, valeur pointée par p = %i\n", I Le compilateur traduit le nom du tableau en l’adresse de la première case
var1,*p); du tableau.
var1 = 20; I Soit la déclaration suivante :
printf("valeur pointée par p = %i\n",*p);
*p = 2; int tab[10];
printf("var1 = %i\n",var1); I tab est en fait le nom du pointeur contenant l’adresse de la première case
p = &var2; du tableau. On sait qu’à partir de l’adresse tab, 10 fois la taille d’un entier
printf("valeur pointée par p = %i\n",*p); a été allouée.
return 0;
}
13 14
I Il est possible d’accéder aux valeurs ou aux adresses des différents éléments
du tableau en décalant le pointeur tab. I Exercice : soit la déclaration suivante
int tab[10]; int tab[]={12, 23, 34, 45, 56, 67, 78, 89, 90};
int i=0;
*(tab + 4) = 2; I Quelles valeurs ou adresses fournissent ces expressions ? (on considère que
for (i=0;i<10;i++) tab pointe sur l’adresse 0x500)
*(tab + i) = 0; tab+1
*tab+2
I Quand on ajoute une valeur entière x à un pointeur, il est décalé d’un *(tab+2)
nombre de cases égal à la taille du type multiplié par x &tab[4]-3
I Il est également possible d’accéder aux adresses des différents éléments du tab+2
tableau en décalant le pointeur : &tab[0]
tab+(*tab-10)
tab = adresse de la première case
*(tab+*(tab+8)-tab[7])
tab + i = adresse de la ième case du tableau
I Les expressions tab+i et &tab[i] sont donc équivalentes !
15 16
I Syntaxe de l’appel à malloc : I Représentation en mémoire : dans l’exemple suivant, on considère une
pointeur = malloc(<nb_éléments>*sizeof(<type_élément>)); architecture 32 bits (les adresses sont stockées sur 4 octets)
int *tab1;
I La fonction retourne la constante NULL si l’allocation a échoué. double *tab2;
I Exemple 1 : allocation de mémoire pour 2 entiers (8 octets) tab1 = malloc(3*sizeof(int));
tab2 = malloc(2*sizeof(double));
int* tab1;
tab1 = malloc(2*sizeof(int));
23 24
Tableaux dynamiques Tableaux dynamiques
25 26
27 28
29 30
Fonctions et tableaux Plan
I ... qu’avec un tableau dynamique
int main()
{
float *tableau;
int taille = 4; Rappels de C : pointeurs et tableaux
float moy;
moy = moyenne(tableau,taille);
C++ : programmation orientée objet avec les classes
printf("Moyenne = %f\n", moy);
free(tableau);
return 0;
}
31 32
33 34
I Intérêt d’utiliser le type scalaire de l’exemple précédent : si, à un moment I Les énumérations sont utiles pour représenter des variables prenant un
donné, on souhaite travailler avec des double à la place des float, il suffit nombre fini (et relativement petit) de valeurs (ex : mois, jour de la
de modifier la définition du type → le code est plus modulaire ! semaine, booléen, ...)
I Types ”synonymes” et opérateurs : si le type de base est doté d’un certain I Intérêt : rendre le code plus lisible
opérateur, le type synonyme l’est aussi. I Déclaration avec le mot-clé enum
I Exemple : tous les opérateurs valides sur float le sont aussi sur enum {VALEUR1, VALEUR2,...} maVar;
scalaire :
typedef float scalaire; I maVar est une variable pouvant prendre les valeurs VALEUR1, VALEUR2, ...
...
typedef enum {VALEUR1, VALEUR2, ...} monType;
scalaire a, b, c;
...
I monType est un type, dont les variables pourront prendre les valeurs
if (a*b==c || a<=b)
c += a; VALEUR1, VALEUR2, ...
I Par convention, les valeurs d’une énumération sont écrites en majuscules
37 38
Enumérations Enumérations
I Exemple avec définition de type
I Exemple sans définition de type : typedef enum {LUNDI, MARDI, MERCREDI, JEUDI,
int main() VENDREDI, SAMEDI, DIMANCHE} Jour;
{
enum {CONCIS, VERBEUX} mode; int main()
... {
mode = CONCIS; Jour j1, j2;
... ...
if (mode==VERBEUX) j1 = MARDI;
printf("Blablabla\n"); j2 = VENDREDI;
... ...
} if (j1==j2 || j1==DIMANCHE)
...
I Ici, mode est une variable }
I Si le programme ne comporte qu’une seule variable pouvant prendre ces
I Ici, Jour est un type (on parle de type énuméré)
valeurs, la définition d’un nouveau type n’est pas justifiée
I Si on est amené à utiliser plusieurs variables contenant les jours de la
semaine, cette définition de type est justifiée
39 40
Enumérations Enumérations
I Correspondance entre valeurs d’une énumération et entiers I Tous les opérateurs sur les variables de types numériques s’appliquent
I Les variables de types énumérés sont traitées par le compilateur comme des également sur les variable de types énumérés
entiers + - * / %
I Une valeur entière est associée à chaque élément de l’énumération. Par += -= *= /= %=
++ --
défaut, la première vaut 0 et les suivantes sont incrémentées.
== != < > <= >=
typedef enum {LUNDI, MARDI, MERCREDI,
JEUDI, VENDREDI, SAMEDI, DIMANCHE} Jour;
I Par exemple, l’expression logique (LUNDI < MERCREDI) est vraie.
I Exemple d’utilisation : une boucle for peut être contrôlée par une variable
I Dans l’exemple précédent, LUNDI vaut 0, MARDI vaut 1, MERCREDI vaut 2,
de type énuméré.
...
I Possibilité d’imposer les valeurs (quand aucune valeur n’est précisée, Jour j;
for (j=LUNDI; j<=DIMANCHE; j++)
l’incrémentation reprend). Exemple :
{
typedef enum {LUNDI=1, MARDI, MERCREDI, ...
JEUDI, VENDREDI, SAMEDI, DIMANCHE} Jour; }
41 42
Structures Structures
I Syntaxe de définition d’un nouveau type structure (que l’on utilisera
beaucoup !)
I Une structure regroupe des variables de types quelconques mais ayant un typedef struct {
lien au sein d’une même entité. typeChamp1 nomChamp1;
I Elle permet de représenter des objets ou concepts de la vie réelle. typeChamp2 nomChamp2;
...
I La structure traduit une relation de composition.
} monType;
I Les variables qui composent la structure sont appelées les champs
I Une structure est définie à l’aide du mot-clé struct I La syntaxe suivante est également possible :
I Syntaxe de déclaration d’une variable structure (que l’on utilisera peu !) struct maStructure {
struct { typeChamp1 nomChamp1;
typeChamp1 nomChamp1; typeChamp2 nomChamp2;
typeChamp2 nomChamp2; };
... typedef struct maStructure monType;
} maVar;
I Accès aux champs avec le point. Exemple :
monType maVar;
maVar.nomChamp1 = VALEUR_CHAMP1;
43 44
Structures Structures
I Exemple : on souhaite écrire en C une application de gestion d’une flotte de I Exemple 1 : définition et initialisation de deux vélos :
vélos. Dans notre application, un vélo est défini par un nom de marque, un int main()
nom de modèle, un diamètre de roue (en pouces) et un nombre de vitesses {
I Définition d’un nouveau type Velo : Velo unVelo, unAutreVelo;
typedef struct strcpy(unVelo.marque,"Motobecane");
{ strcpy(unVelo.modele,"Confort");
char marque[250]; unVelo.diametre = 26;
char modele[250]; unVelo.nbVitesses = 12;
int diametre;
int nbVitesses; strcpy(unAutreVelo.marque,"Gitane");
} Velo; strcpy(unAutreVelo.modele,"Racer");
unAutreVelo.diametre = 28;
unAutreVelo.nbVitesses = 18;
I Toute future variable de type Velo contiendra automatiquement une valeur
...
pour chacun de ses champs (marque, modèle, diamètre de roue et nombre
return 0;
de vitesses).
}
I En mémoire, les champs sont contigus
45 46
Structures Structures
I Exemple 2a : définition et initialisation d’un tableau statique de vélos I Exemple 2b : définition et initialisation d’un tableau dynamique de vélos
47 48
Structures et passage de paramètre Structures et passage de paramètre
I Fonction prenant en paramètre un vélo et ne le modifiant pas → passage I Fonction prenant en paramètre un vélo et le modifiant → passage de
de paramètre par valeur paramètre par pointeur (= passage par adresse)
void afficheVelo(Velo v) void initVelo(Velo *pv)
{ {
printf("%s %s, diamètre de roues : %d, nombre de vitesses : strcpy(pv->marque, "INCONNUE");
%d\n", v.marque, v.modele, v.diametre, strcpy(pv->modele, "INCONNUE");
v.nbVitesses); pv->diametre = 0;
} pv->nbVitesses = 0;
}
I Recopie de la variable structurée
I Lorsqu’on dispose d’un pointeur sur une variable structurée, on peut utiliser
I Nous verrons plus loin qu’un autre passage de paramètre est possible :
directement l’opérateur − >
passage par pointeur sur constante
I pv− >champ est équivalent à (*pv).champ
49 50
I Une variable qualifiée const ne pourra pas être modifiée dans le bloc de I Syntaxe : (type cible)expression
code où elle est visible
I Les types numériques peuvent être convertis entre eux
I Lorsqu’il est utilisé dans la définition d’un paramètre, le qualificatif const
I Exemple : arrondir un réel positif à l’entier le plus proche
peut renseigner sur le rôle d’un paramètre dans la fonction : on sait déjà
que la fonction ne modifie pas ce paramètre (paramètre en lecture seule) float f = ...;
int i = (int)(f+0.5);
/* dst = destination, src = source */
void copy(char *dst, const char *src)
{ I Les pointeurs peuvent toujours être convertis entre eux
dst[0] = ’\n’; // Ok I Exemple : convertir un pointeur sur type énuméré en pointeur sur entier
src[0] = ’\n’; // Erreur de compilation typedef enum {...} MonEnum;
... ...
int *pi;
I A utiliser dès que possible ! MonEnum e = ...;
I Pour les structures passées en lecture seule : passage par pointeur sur pi = (int *)(&e);
constante : const MonType *
51 52
I Fichier livre.h
#ifndef LIVRE_H
I Module = couple de fichiers monModule.h et monModule.c #define LIVRE_H
I Les définitions de type(s) et les déclarations de fonctions travaillant sur
typedef enum {JANVIER=1, FEVRIER, MARS, AVRIL, MAI,
ce(s) type(s) doivent être dans le fichier d’en-tête (header) monModule.h
JUIN, JUILLET, AOUT, SEPTEMBRE,
I Les définitions des fonctions (déclarées dans le .h) doivent être dans le OCTOBRE, NOVEMBRE, DECEMBRE } mois;
fichier source monModule.c
I Tout autre module utilisant les types et les fonctions de monModule doit typedef char texte[200];
l’inclure :
#include "monModule.h" typedef struct
{
mois m;
int annee;
} date;
53 54
Organisation en modules, inclusion Directives #ifndef ... #endif
55 56
59 60
Quelques caractéristiques du C++ Divers
Commentaires
I Une surcouche au C
I Commentaires sur une seule ligne : débutent par //
I Proche du matériel (comme le C) : manipulation explicite des adresses,
I Il est toujours possible d’utiliser /* ... */
gestion manuelle de la mémoire
I Nouveaux opérateurs (<<, >>, new, delete, ...) Entrées/sorties
I Déclarations dans l’en-tête <iostream>
I Nouveaux mots-clés (class, public, private, ...)
I Objets cout (classe ostream) et cin (classe istream), déclarés dans
I Nouvelle bibliothèque standard (en-têtes cmath, cassert, ...)
l’espace de noms 2 std
I Nouveaux mécanismes : appels automatiques de fonctions !
I Sortie standard (console) via l’objet cout, entrée standard (saisie au
I Toutes les fonctions standards C (printf, scanf, malloc, free, ...) clavier) via l’objet cin
restent utilisables dans un programme compilé en C++
I Opérateurs << et >>
I Un langage qui évolue
I En remplacement de printf et scanf
I Première édition publiée en 1985
I Normalisé par l’ISO depuis 1998 I Plus besoin de formatage des données : adieu %i, %d, %f...
I Mise à jour majeure en 2020 (norme C++20), mise à jour en cours de I Possibilité d’afficher et de saisir des entiers, des réels, des caractères, des
finalisation pour 2023 (norme C++23) chaı̂nes de caractères
Entrées/sorties Entrées/sorties
I Exemple :
#include <iostream>
using namespace std; // Pour alléger l’écriture, évite
I Extrait, très simplifié, du fichier iostream : // d’avoir à spécifier l’espace de nommage std::
namespace std // avant chaque accès à cout, cin, ...
{ int main()
... {
ostream cout; char nom[100];
... int age;
istream cin; cout<<"Entrez votre nom :"<<endl;
... cin>>nom;
} cout<<"Entrez votre age :"<<endl;
cin>>age;
I cout et cin sont bien des variables (en fait, des objets), pas des fonctions ! cout<<"Bonjour "<<nom<<". Vous avez "<<age
<<" an(s)"<<endl;
...
}
Référence Référence
I Le C++ ajoute la notion de référence I Toute opération effectuée sur la référence se répercute sur la variable
référencée
I Une référence peut être considérée comme un alias sur une variable
I On ne peut pas ré-affecter une référence (alors que l’on peut modifier un
I Peut être utilisée pour définir une variable locale, un paramètre de fonction
pointeur)
ou la valeur de retour d’une fonction
I Retour sur les opérateurs & et * : leur rôle est différent selon que ces
I Une référence est obligatoirement affectée lors de sa définition
opérateurs apparaissent dans une déclaration de variable ou dans une
I Une référence n’est rien d’autre qu’un pointeur déguisé ! expression
I A ne pas confondre avec l’opérateur d’accès à l’adresse & ! I Il est très important de savoir identifier la signification des symboles * et &
int n = 5; en tout lieu
int &r = n; // Définition d’une référence sur n I Si * ou & est précédé d’un nom de type, nous sommes en présence d’une
// Si r est modifié, n l’est aussi déclaration de variable (variable locale, paramètre ou retour d’une
fonction). Sinon, nous sommes dans le cas d’une expression
65 66
Opérateur * (rappel) et & Pointeur ou référence ?
I Pour un paramètre en lecture/écriture, nous avons le choix entre pointeur
et référence. Exemple avec une fonction qui échange deux entiers :
Opérateur *
I Dans le cas d’une déclaration de variable : définition d’un pointeur Solution 1 : passage par pointeur : Solution 2 : passage par référence :
I Dans le cas d’une expression : accès à la valeur pointée (opérateur de void swap1(int *pa, int *pb) void swap2(int &a, int &b)
déréférencement, également appelé opérateur d’indirection) { {
int temp; int temp;
I Si p est de type MonType *, (*p) est de type MonType
temp = *pa; temp = a;
Opérateur & *pa = *pb; a = b;
I Dans le cas d’une déclaration de variable : référence (en C++ seulement) *pb = temp; b = temp;
I Dans le cas d’une expression : accès à l’adresse } }
I Si p est de type MonType, &p est de type MonType *
Appel : Appel :
(exemples au tableau)
int i, j; int i, j;
... ...
swap1(&i, &j); swap2(i, j);
67 68
73 74
void maFonction()
I Le C++ autorise la définition de plusieurs fonctions portant le même nom, {
mais avec des paramètres différents int i=4, j=-2;
I Les appels ne doivent pas être ambigus : le compilateur doit toujours float f=4.8, g=-9.14;
pouvoir déterminer quelle fonction appeler
cout<<mul(i,j)<<" "<<mul(f,g)<<endl; // Ok
int mul(int a, int b)
cout<<mul(i,f)<<endl; // Erreur de compilation
{
cout<<mul(g,j)<<endl; // Erreur de compilation
return a*b;
}
}
I Les deux derniers appels à mul sont ambigus
float mul(float a, float b)
{ I Lorsque les types des arguments sont différents de ceux des paramètres, le
return a*b; compilateur tente des transtypages (conversion de type) pour correspondre
} à un prototype disponible
I Ici, le compilateur ne sait pas s’il doit faire la conversion (int → float),
pour appeler la version avec float, ou (float → int) pour appeler la
version avec int
75 76
I Doté des opérateurs &&, ||, !, ==, != I Cas particuliers : les opérateurs = [] () et − > ne peuvent être
surchargés qu’en méthodes de classe (abordées plus loin dans le cours)
I Pour attribuer un nouveau rôle à un opérateur, définition d’une fonction à
l’aide du mot-clé operator
77 78
Surcharge d’opérateurs Surcharge d’opérateurs
typedef struct
{
float x, y, z; I L’addition et la multiplication retournent un vector3D. La comparaison
} vector3D;
retourne un booléen
vector3D u, v; I Ces opérations ne modifient pas les vector3D passés en paramètres
I Déclarations :
vector3D operator+(const vector3D &, const vector3D &);
I On aimerait pouvoir additionner ou comparer des vector3D entre eux, et
multiplier un vector3D par un réel. De base, le compilateur ne sait pas vector3D operator*(const vector3D &, float);
traiter des expressions telles que u+v, u*2.0, ou u==v, car le type
vector3D n’est, pour l’instant, pas doté des opérateurs d’addition, de bool operator==(const vector3D &, const vector3D &);
multiplication et de comparaison !
I Les opérateurs peuvent être unaires (ex : - unaire, ++, --, !) ou binaires
(=, +, - binaire, *, /, %, ...)
I Un opérateur unaire (resp. binaire) sera défini comme une fonction prenant
un (resp. deux) paramètre(s)
79 80
I Si l’on souhaite que la norme euclidienne soit celle calculée par défaut :
vector3d.hpp Rappels de C : pointeurs et tableaux
...
float norme(const vector3D &, float typeNorme=2.0);
... Rappels de C : types de données
89 90
L’approche objet ? Notion d’objet
91 92
93 94
97 98
99 100
101 102
Constructeurs : définitions et appels Constructeurs : définitions et appels
103 104
105 106
107 108
Constructeur de recopie Destructeur
I ch2 et ch1 pointent au même endroit ! (schéma au tableau)
I Le destructeur est exécuté lors de la suppression de l’objet
→ mauvaise gestion de la mémoire, source de bugs
I Appelé automatiquement ...
I Pour avoir des tableaux bien distincts en mémoire, les éléments des
I ... en fin de bloc, pour les instances locales, ou dans le destructeur d’une
tableaux doivent être recopiés un à un
classe contenante
I Utiliser strcpy pour les chaı̂nes de caractères I ... lors de la désallocation avec delete, pour les instances précédemment
I Ecrire des boucles pour les autres tableaux allouées avec new
I Dans la classe Student, si le constructeur de recopie n’était pas défini, I Que met-on dedans ?
quel serait l’affichage donné par le code suivant ? (correction au tableau) I delete sur les attributs alloués avec new
I Fermeture des flux de données (fichiers, streams, ...) pouvant avoir été
Student st1(20156743, "Doudou", "Marcel"); ouverts auparavant
Student st2(st1);
I Nom du destructeur : ∼ suivi du nom de la classe
cout<<st1.getSurname()<<endl; Student::~Student() {
delete[] surname;
st2.setSurname("Jeanjean"); delete[] firstname;
}
cout<<st1.getSurname()<<endl;
109 110
111 112
I Si un objet est const, seules les méthodes const peuvent être appelées
115 dessus 116
Complément sur les constructeurs : appel explicite Complément sur les constructeurs : appel explicite
119 120
Complément sur les constructeurs : listes d’initialisation Complément sur les constructeurs : listes d’initialisation
121 122
Complément sur les constructeurs : listes d’initialisation Complément sur les constructeurs : composition
I Si l’attribut est un objet, l’initialisation dans la liste indique quel I Le constructeur par défaut d’une classe composante est automatiquement
constructeur (de l’attribut) est appelé appelé par tout constructeur de la classe composite
I Dans l’exemple suivant, B contient des attributs de type A (A est la classe
class StudentPair
composante, B la classe composite)
{
class A
private:
{
Student st1, st2;
private:
int a;
public:
public:
StudentPair();
A() {cout<<"ctor default A"<<endl;}
...
A(int i) {a=i; cout<<"ctor int A"<<endl;}
};
};
class B
StudentPair::StudentPair():st1(0), st2(1, "Loulou", "Dudu")
{
{
private:
}
A a1, a2;
public:
→ appel au constructeur Student(int) sur st1 et Student(int,const B() {cout<<"ctor default B"<<endl;}
char *,const char *) sur st2 B(int i) {cout<<"ctor int B"<<endl;}
I Si la liste était vide, le constructeur par défaut de Student serait appelé 123 }; 124
153 154
Redéfinition de méthodes
I Accès aux membres de la classe mère (s’ils sont protégés ou publics) avec
l’opérateur de résolution de portée précédé du nom de la classe mère
workaholic.hpp
#include "worker.hpp"
class Workaholic : public Worker
{
protected:
... // Attributs supplémentaires propres
// au bourreau de travail
public:
Workaholic() {...}
void work()
{
Worker::work(); // Appel à work() de Worker
... // Travail supplémentaire !
}
...
};
155