Page de garde Inside C++ [email protected] Maîtrise dinformatique Février 2002.

23
Page de garde Inside C++ [email protected] Maîtrise d’informatique Février 2002

Transcript of Page de garde Inside C++ [email protected] Maîtrise dinformatique Février 2002.

Page 1: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Page de garde

Inside C++

[email protected]

Maîtrise d’informatiqueFévrier 2002

Page 2: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Qu’est-ce qu’un ordinateur ?

Qu’est-ce qu’un ordinateur ?

Fondamentalement, un ordinateur c’est :

Un (ou des) processeur(s)

De la mémoire vive (non persistante)

Des périphériques de stockage persistants (disque dur, graveurs, bandes, …)

Des périphériques d’entrées-sorties (clavier, souris, carte vidéo, carte son, …)

La mémoire vive a trois utilisations majeures :

Stockage des instructions (le code) des processus

Pile d’exécution des processus

Espace de tas des processus (là où (m|c)alloc et new allouent les blocs demandés)

Le processeur contient des registres, dans lesquels sont mémorisés, pour le processus courant :

Le pointeur sur l’instruction en cours

Le pointeur courant de la pile

Les adresses de base des segments de code, de pile, de tas, …

Les premiers arguments des fonctions appelées, la valeur de retour, …

Les registres servent aussi de "variables intermédiaires"  dans les calculs.

Page 3: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Qu’est-ce qu’un ordinateur ?

Qu’est-ce qu’un ordinateur ?

La mémoire de pile sert à :

Préserver (empiler) les valeurs des registres pour pouvoir les restaurer (dépiler) ultérieurement, par exemple avant et après un appel de fonction

Passer les arguments des fonctions qui ne sont pas passés par des registres

Allouer de l’espace pour les variables locales et les objets locaux (+ alloca)

Par la suite, nous considèrerons que nous travaillons sur une architecture 32 bits Intel, donc avec un support de pile d’exécution fourni par le processeur (ce qui n’est pas le cas de tous les processeurs). Petit rappel : C/C++ garantissent des relations d’ordre entre les tailles des types primitifs…

Page 4: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Qu’est-ce qu’un ordinateur ?

Qu’est-ce qu’un ordinateur ?

processeur

PC : Program Counter (pointeur d’instruction)

SP : Stack Pointer (pointeur de pile)

pc

...mov reg1, [a]add reg1, [b]cmp reg1, [c]jge +5Fhmov [d], 69hjmp main+43h ...

code segment

reg1 reg2

reg3 reg4

… …

stack segmentsp

Page 5: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Appel de fonction simple

Appel de fonction simple

XXX f( … ) { … }

L’adresse de f est connue à la compilation.

stack

Schéma d’un appel à f :sp

Préservation des registres caller-saveregistres

caller-save

Empilement des argumentsarguments

Saut à la fonction après empilement de l’adresse de retour

return addressRéservation d’espace de pile pour les variables/objets locaux

variableslocales

Préservation des registres callee-save

registrescallee-save

f fait son boulot…

Restauration des registres callee-save utilisés par la fonction

Dépilement des variables locales

Retour à l’instruction suivant l’appel de f

Dépilement des arguments

Restauration des registres caller-save

Page 6: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Appel de fonction simple

Appel de fonction simple

Les registres caller-save sont les registres utilisés par la fonction appelante et que les fonctions appelées ont le droit de modifier.

Les registres caller-save sont donc préservés par la fonction appelante même si la fonction appelée ne les modifie pas…

Les registres callee-save sont les registres utilisés par la fonction appelée, qui sont donc préservés par la fonction appelée même si les fonctions appelantes ne les utilisent pas…

Le stockage des variables locales dans la pile est le mécanisme permettant, par exemple, aux différentes "instances" d’une fonction récursive d’avoir chacune leurs propres variables locales. Les variables locales static ne sont que des variables globales à visibilité réduite.

Les conventions d’appels (passage des arguments par registre ou dans la pile, ordre de passage des arguments, responsabilité des dépilements d’arguments, registres callee-save ou caller-save, …) dépendent à la fois de l’architecture matérielle (cf. doc constructeur) et du language (extern "C", extern "Pascal", …).

Page 7: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Appel de méthode non virtuelle

Appel de méthode non virtuelle

Le mécanisme d’appel de méthodes static est strictement identique à celui des appels de fonctions.

Les méthodes non-static ont un paramètre semi-caché supplémentaire : le pointeur constant this.

Page 8: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Fonctions et méthodes inline

Fonctions et méthodes inline

Le mécanisme d’inlining vise à réduire le coût des appels de fonctions/méthodes.

Si, au moment de la génération de code pour un appel de fonction ou de méthode :

Sa "version" / son adresse est connue (c’est une fonction ou, dans le cas d’une méthode, on connaît le type dynamique de l’objet).

Son implémentation est accessible.

Alors cet appel de fonction est candidat à l’inlining, c’est-à-dire au remplacement de tout le mécanisme d’appel par l’implémentation de la fonction elle-même.

Le compilateur vérifie, pour chaque inlining potentiel, que la taille du code expansé ne soit pas trop supérieure à celle du code avec un appel de fonction, ce qui est généralement le cas avec les accesseurs et les modifieurs.

L’inlining augmente généralement la portée des optimisation du compilateur (propagation de constantes dans le corps de la fonction, …).

Page 9: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Memory layout des objets simples

Memory layout des objets simples

class|struct{ bool b; char c; double d; float f; int i; long l; void *ptr; char c2; bool bb[2]; int ii[3];} a;

&a+0

&a+4

&a+8

&a+12

&a+16

&a+20

&a+24

&a+28

&a+32

&a+36

&a+40

&a+44

b c

d

f

i

l

ptr

bb

ii

c2

Dans les unions, tous les membres commencent à la même adresse et la taille d’une union est celle du plus grand de ses membres.

Page 10: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Memory layout des objets simples

Memory layout des objets simples

Les données sont disposées dans l’ordre de déclaration.

Par défaut, les données sont alignées en fonction de leur taille en octets : multiple de 1 pour les bool et les char, multiple de 4 pour les int, les long, les float, les pointeurs, …, multiple de 8 pour les double.

Il peut donc y avoir de l’espace inutilisé dans les structures…

On peut forcer le compilateur à aligner les données différemment (packing) mais, généralement, un processeur travaillant sur un certain nombre de bits, 32 par exemple, n’aime guère les adresses qui ne sont pas des multiples de 4 octets, ou les données dont la taille ne correspond pas à 4 octets…

Page 11: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Héritage simple

Héritage simple

En cas d’héritage simple, les données sont simplement accolées (dans un ordre dépendant de l’implémentation) :

class A { AAA a;};

class B: public A { BBB b;};

class C: public B { CCC c;};

C x;

C:: B:: A::a

b

c

B b= c; B:: A::a

b

Ici, l’initialisation de b par copie de c entraîne le slicing des données de c provenant de la dérivation de la classe B.

A& a= c;

a étant une référence (un "pointeur constant") vers c, aucune donnée n’est dupliquée.

Page 12: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Héritage simple et fonctions non virtuelles

Héritage simple et fonctions non virtuelles

Lorsque les fonctions ne sont pas virtuelles, leur adresse est résolue lors de la compilation en fonction du type statique.

class A { public: void f();};

B b;

b.f(); // invoque B::f()

A a= b; // slicing de b

a.f(); // invoque A::f()

A& c= b;

c.f(); // invoque A::f() bien que c soit en fait “un B”

class B: public A { public: void f();};

Page 13: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Fonctions virtuelles

Fonctions virtuelles

Dès lors qu’une classe a au moins une méthode virtuelle, ses instances ont un pointeur caché ("vptr") vers la table des méthodes virtuelles ("vtbl"). Les tables de méthodes virtuelles sont partagées par toutes les instances d’une même classe.

class A { XXX a; public: virtual ~A(); virtual void f(); void g();};

A::vtbl

~()

f()

class B: public A { YYY b; public: virtual ~B(); virtual void f(); void g(); virtual void h();};

B::vtbl

~()

f()

h()

B b;

vptr

A::a

b

B::

b.f();

b.g();

A& a= b;

a.f();

a.g();

Page 14: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Casts de pointeur polymorphe en pointeur non-polymorphe

Casts de pointeur polymorphe en pointeur non-polymorphe

Dans les implémentations de compilateurs C++ où le vptr est placé au début des objets, le cast d’un pointeur vers une classe ayant au moins une méthode virtuelle en un pointeur vers une classe n’ayant pas la moindre fonction virtuelle nécessite un décalage.

class A { AAA a;};

class B: public A{ BBB b; public: virtual void f();};

B * b= new B();

vptr

A::a

b

B::

A * a= b;

Le compilateur doit tout d’abord vérifier que b ne soit pas 0/NULL, puis lui ajouter sizeof( vptr ).

De même, pour pouvoir comparer a et b (==, !=), le compilateur sait qu’il faut normaliser les pointeurs.

Par contre, delete a génère du code “faux” : a ne pointe plus sur un début de bloc alloué par new !

Ainsi, les casts (implicites ou non) peuvent nécessiter de générer du code…

Page 15: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Héritage multiple sans fonctions virtuelles

Héritage multiple sans fonctions virtuelles

class A { AAA a;};

class B: A { BBB b;};

class C: A { CCC c;};

class D: B, C { DDD d;};

D:: B:: A::a

b

C:: A::a

c

d

D * d= new D();

C * c= d;

(c == d) true !

delete c;

Page 16: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Héritage multiple avec fonctions virtuelles

Héritage multiple avec fonctions virtuelles

class A { AAA a; public: virtual ~A();};

class B: A { BBB b;};

class C: A { CCC c;};

class D: B, C { DDD d;};

D * d= new D();

D:: B:: A:: vptr

a

b

C:: A:: vptr

a

c

d

B * d_b= d;

A * d_b_a= d_b;

C * d_c= d;

A * d_c_a= d_c;

Avec, toujours :

d == d_c true !

Mais :

delete d_c;

Page 17: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Héritage virtuel

Héritage virtuel

Les données des classes héritées virtuellement ne sont pas "dupliquées".

Chaque classe héritant vituellement d’une classe de base contient un pointeur vers les données de cette classe.

class A { AAA a;};

class B: virtual A { BBB b;};

class C: virtual A { CCC c;};

class D: B, C { DDD d;};

D:: B:: A*

b

C:: A*

c

d

A:: a

Page 18: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Le RTTI

Le RTTI

.....Lorsque le RTTI est activé, le compilateur initialise une instance de la classe type_info par classe, pour les références, plus ce qu’il faut pour tous les pointeurs rencontrés ainsi que les types primitifs. Ce sont des références vers ces instances (constantes) qui seront renvoyées par typeid.

Le résultat de typeid peut-être connu lors de la compilation :

Pour les types primitifs :

int i;const type_info& i_ti= typeid( i );

Pour les pointeurs (seul le type statique de l’objet pointé intervient) :

A * ptr= new C(); // C dérive de Aconst type_info& ptr_ti= typeid( ptr );

Pour les références (non triviales), le résultat de typeid est déduit du vptr.

Page 19: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

typeid et références non triviales

typeid et références non triviales

Pour les références non triviales, le résultat de typeid est déduit du vptr.

La référence vers l’instance de type_info correspondant à la vtbl peut, par exemple, être stockée dans la cellule -1:

Avant d’accéder au vptr, le compilateur insère du code visant à vérifier que la référence ne soit pas 0/NULL…

X& x= *(new Y());

Y:: vptr

-1

0

1

2

Y::vtbl

type_info

Tout cela est fait par une fonction dont le coût n’est pas négligeable !

Page 20: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Construction d’objets

Construction d’objets

.....La construction d’un objet se fait en deux étapes distinctes :

1 Allocation de l’espace mémoire :

Si l’objet est alloué en pile, par simple décalage du pointeur de pile.

Si l’objet est alloué dans le tas (par appel à new), par appel de l’operator new de la classe, s’il a été défini ou hérité, ou de l’operator new global.

2 Le constructeur adéquat est invoqué, ce qui peut générer une cascade d’appels aux constructeurs des classes de base.Les données seront construites dans l’ordre des déclarations (au besoin, le compilateur les réordonne avec avertissement selon le niveau de warning).

Chaque entrée dans un constructeur implique la mise à jour du vptr (si vptr il y a) : attention aux méthodes virtuelles pures !

Pour les tableaux d’objets :

1 Allocation de l’espace mémoire : similaire si l’objet est alloué en pile ; dans le tas, l’allocation est faite par l’operator new[] adéquat (de classe ou global).

2 Le constructeur par défaut est invoqué de manière itérative pour initialiser chaque cellule (ce qui peut encore générer une cascade d’appels aux constructeurs des classes de base, etc.)

Page 21: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Destruction d’objets

Destruction d’objets

.....Inversement, la destruction d’un objet se fait en deux étapes inverses :

1 Invocation ascendante des destructeurs (le vtpr, si vptr il y a, est mis à jour à chaque étage).

2 Libération de la mémoire :

Si l’objet est alloué en pile, par simple décalage du pointeur de pile.

Si l’objet est alloué dans le tas (par appel à new), par appel de l’operator delete de la classe, s’il a été défini ou hérité, ou de l’operator delete global.

Pour les tableaux d’objets, c’est ric-rac.

Rappel : attention aux classes polymorphes dont le destructeur n’est pas virtuel !!!!!

Page 22: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

operator new et operator delete

operator new et operator delete

.....Les operator new et delete globaux ou d’une classe peuvent être redéfinis, dans le but de fournir une gestion spécifique de la mémoire :

#include <cstddef> // à voir …

void * operator new( size_t size );void operator delete( void * ptr );void operator delete( void * ptr, size_t size );

void * operator new[]( size_t size );void operator delete[]( void * ptr );void operator delete[]( void * ptr, size_t size );

La définition des versions avec taille des operator delete sont selon votre bon vouloir…

En règle générale, si vous redéfinissez operator new, redéfinissez operator delete !

Exemple d’application : allocateur mémoire rétenseur (qui ne libère jamais vraiment la mémoire), qui chaîne les blocs libérés pour les retourner à la prochaine demande d’allocation.

Page 23: Page de garde Inside C++ Yannis.BRES@cma.inria.fr Maîtrise dinformatique Février 2002.

Placement operator new

Placement operator new

.....Les operator new (scalaires ou de tableaux) peuvent être surchargés, le premier argument devant être de type size_t.

class A { public: A( XXX, YYY ); void * operator new( size_t, WhatEver1, WhatEver2 ); void operator delete( void *, WhatEver1, WhatEver2 ); …}

(idem pour les constructeurs de tableaux)

De tels constructeurs sont invoqués par :

new ( whatever1, whatever2 ) A( xxx, yyy );

L’operator delete correspondant, s’il a été défini, n’est appelé automatiquement que si le constructeur lance une exception.