5ième Classe (Mercredi, 19 octobre) Prog CSI2572
La programmation orientée objet cherche à modéliser informatiquement des éléments du monde réel en entités informatiques appelées objets. Les objets sont des données informatiques regroupant les principales caractéristiques des éléments du monde réel (taille, couleur,...). La difficulté du processus de modélisation est dans la création d'une représentation abstraite d'entités ayant une existence matérielle (chien, voiture, ampoule,...) ou bien virtuelle (sécurité sociale, temps,...). La semaine dernière:
Dans le monde réél, deux T-shirts peuvent être identique et distincts. La classe, c'est la structure d'un objet, c'est-à-dire la déclaration de l'ensemble des entités qui composeront un objet. Une classe peut être considérée comme un moule à partir duquel on peut créer des objets. Un objet est donc "issu" d'une classe. C'est une instanciation d'une classe, c'est la raison pour laquelle on pourra parler indifféremment d'objet ou d'instance. La semaine dernière:
Une classe est composée de deux parties: Les attributs (parfois appelés données membres): il s'agit des données représentant l'état de l'objet Les méthodes (parfois appelées fonctions membres): il s'agit des opérations applicables aux objets Si on définit la classe voiture, les objets Toyota_Civic, Mustang2003 seront des instanciations de cette classe. Il pourra éventuellement exister plusieurs objets Toyota_Civic, différenciés par leur numéro de série. Deux instanciations de classes pourront même avoir tous leurs attributs égaux sans pour autant être le même objet. La semaine dernière:
Dans ce modèle, un véhicule est représenté par une chaîne de caractères (sa marque) et trois entiers : la puissance fiscale, la vitesse maximale et la vitesse courante. chaque objet véhicule aura sa propre copie de ses données : on parle alors d'attribut d'instance. L'opération d'instanciation qui permet de créer un objet à partir d'une classe consiste précisément à fournir des valeurs particulières pour chacun des attributs d'instance. La semaine dernière:
La déclaration de la classe commence par le mot clef class et est encadrée par une paire d'accolades. L'accolade finale est suivie d'un point virgule. Les membres déclarés après le mot clef public forment l'interface de la classe. Ceux qui suivent le mot private sont invisibles de l'utilisateur ; > L'ordre de déclaration des méthodes et des attributs est laissé au libre arbitre du programmeur. > La déclaration des attributs est semblable à la déclaration d'une variable. Celle d'une méthode au prototype d’une fonction. La semaine dernière:
Lors de l'implémentation des méthodes, il est nécessaire de préfixer le nom de la méthode implémentée du nom de la classe suivi de ' :: '. Par exemple, l'implémentation de la méthode deplacerVers de la classe Point se fait en spécifiant: Point::deplacerVers La semaine dernière:
Encapsulation, polymorphism et héritage Encapsulation: Le rassemblement des données et du code les utilisant dans une entité unique (objet). La séparation nette entre la partie publique d'un objet (ou interface) seule connue de l'utilisateur de la partie privée ou implémentation qui reste masquée. Polymorphism: Une méthode peut adopter plusieurs formes différentes. Héritage Possibilité de définir des familles de classes traduisant le principe de généralisation / spécialisation. « La classe dérivée est une version spécialisée de sa classe de base »
L'encapsulation consiste à masquer l'accès à certains attributs et méthodes d'une classe. Pourquoi masquer? C Cacher les détails d'implémentation des objets à l'utilisateur permet de modifier, par exemple la structure de données interne d'une classe (remplacer un tableau par une liste chaînée) sans pour autant entraîner de modifications dans le code de l’utilisateur, l’interface n’étant pas atteinte. C Abstraction de données : la structure d'un objet n'est pas visible de l'extérieur, son interface est constituée de messages invocables par un utilisateur. La réception d'un message déclenche l'exécution de la méthode correspondant à ce message. C Abstraction procédurale : Du point de vue de l'extérieur (c’est-à-dire en fait du client de l’objet), l'invocation d'un message est une opération atomique. L'utilisateur n'a aucun élément d'information sur la mécanique interne mise en œuvre. Par exemple, il ne sait pas si le traitement requis a demandé l’intervention de plusieurs méthodes ou même la création d’objets temporaires etc. Encapsulation I
Encapsulation II En C++, on choisit le paramètres d'encapsulation à l'aide des mots clés : private : les membres privés ne sont accessibles que par les fonctions membres de la classe. protected : les membres protégés sont comme les membres privés. Mais ils sont aussi accessibles par les fonctions membres des classes dérivées. public : les membres publics sont accessibles par tous. La partie publique est appelée interface. Les mots réservés private, protected et public peuvent figurer plusieurs fois dans la déclaration de la classe. Le droit d'accès ne change pas tant qu'un nouveau droit n'est pas spécifié.
class Avion { public : // fonctions membres publiques void init(char [], char *, float); void affiche(); private : // membres privées char immatriculation[6], *type; float poids; // fonction membre privée void erreur(char *message); }; // n'oubliez pas ce ; après l'accolade
Les fonctions membres sont définies dans un module séparé ou plus loin dans le code source. Syntaxe de la définition hors de la classe d'une méthode : type Classe::nom_méthode( paramètres_formels ) { // corps de la fonction } La semaine dernière:
class Avion { public : // fonctions membres publiques void init(char [], char *, float); void affiche(); private : // membres privées char immatriculation[6], *type; float poids; // fonction membre privée void erreur(char *message); }; // n'oubliez pas ce ; après l'accolade void Avion::init(char m[], char *t, float p) { if ( strlen(m) != 5 ) { erreur("Immatriculation invalide"); strcpy( immatriculation, "?????"); } else strcpy(immatriculation, m); type = new char [strlen(t)+1]; strcpy(type, t); poids = p; } void Avion::affiche() { cout << immatriculation << " " << type; cout << " " << poids << endl; }
Comme pour struct, le nom de la classe représente un nouveau type de donnée. On peut donc définir des variables de ce nouveau type; (créer des objets ou des instances) Avion av1; // une instance simple (statique) Avion *av2; // un pointeur (non initialisé) Avion compagnie[10]; // un tableau d'instances av2 = new Avion; // création (dynamique) d'une instance
Après avoir créé une instance (de façon statique ou dynamique) on peut accéder aux attributs et méthodes de la classe. Cet accès se fait comme pour les structures à l'aide de l'opérateur. ou ->. av1.init("FGBCD", "TB20", 1.47); av2->init("FGDEF", "ATR 42", 80.0); compagnie[0].init("FEFGH","A320", 150.0); av1.affiche(); av2->affiche(); compagnie[0].affiche(); av1.poids = 0; // erreur, poids est un membre privé
H Le constructeur est une méthode qui porte le même nom que la classe. C’est dans le constructeur que les attributs d’un objet sont initialisées lors de sa création. H Un constructeur ne peut pas spécifier de valeur ou type de retour. H Le constructeur d’une classe est éxécuté automatiquement à chaque fois qu’une instance de la classe est crée. Que se passe t ’il si l’instance est passée par valeur comme paramètre à une fonction? H Une classe peut avoir ou non un constructeur par défaut. Il s’agit d’un constructeur sans aucun paramètre. Le constructeur I
H Si aucun constructeur n’a été défini dans la classe, C++ en crée un automatiquement. C’est un constructeur par défaut, et il ne fait rien. Une classe a donc toujours un constructeur, mais pas forcément un constructeur par défaut ; en effet, si on ne définit que des constructeurs qui prennent des paramètres, C++ ne fournit pas le constructeur par défaut automatiquement. H Lorsqu’un constructeur par défaut n’existe pas, on ne peut déclarer une instance de classe sans préciser de paramètres. Par exemple, pour l’exemple suivant, l’appel: autre a; est illégal. class autre { double d; public: autre(double dd) { d = dd; } } Le constructeur II
H Si on veut créer un tableau d’instances de la classe sans donner de valeurs initiales, il faut définir un constructeur par défaut. Il est alors appelé pour tous les éléments du tableau : Avion A[10]; //10 appels du constructeur par défaut de Avion important: bien que les constructeurs aussi peuvent avoir des arguments par défaut comme toute autre fonction: autre a; // ok autre tab[3]; //NON, car pas de constructeur par défaut class autre { double d; public: autre(double dd = 0) { d = dd; } }; Le constructeur III
H Un appel à l ’opérateur new (création d ’une instance de manière dynamique) provoquera un appel de constructeur. Plane* C = new Plane(...); Le constructeur IV
H Le compilateur gère lui-même les initialisations de variables automatiques. Les arguments de fonction sont des variables automatiques. Elles sont donc elles aussi gérées par le compilateur. H C'est la raison pour laquelle l'exemple suivant est valide: Le constructeur VI class show{ private: int a; public: show(int); int get_a(){return a;} }; show::show(int i){ a = i; cout<< "Appel du constructeur: show(int i)"; } int foo(show a){ cout<<"Dans foo"<<endl; return a.get_a(); } int main(){ foo(1); return 1;}
H De la même manière, l'appel suivant serait parfaitement légitime: foo( show(1) ); H Dans ces cas les constructeurs adéquats sont appelés à l’entrée de la fonction (et les destructeurs à la sortie). H Un constructeur ne peut pas être appelé autrement que lors d’une initialisation. Cependant, il peut l’être de différentes façons. Par exemple, s’il existe un constructeur qui n’admet qu’un seul paramètre, ou plusieurs mais tel que tous les arguments sauf le premier ont une valeur par défaut, on peut l’appeler en écrivant le signe égal suivi du paramètre: autre au = 1.2; // appel de autre::autre(1.2) Le constructeur V
H Cette écriture est équivalente à la forme classique : autre au(1.2); H En outre, il est possible d’initialiser des tableaux de cette façon : autre atab[4] = { 1.2, 2, 0.7, -9.9 }; Par contre, dans ce cas, il faut absolument préciser toutes les valeurs initiales parce que il n'y a pas de constructeur par défaut dans notre implémentation de la classe autre. Le constructeur VI
H Il est parfaitement possible de préciser une valeur par défaut à un argument de type classe, pourvu qu’on utilise un constructeur : void f(exemple ex = exemple() ); void f2(exemple ex = exemple(1, 1) ); void g(autre au = 0); Dans le dernier cas, on a encore utilisé le changement de type automatique. Le constructeur VI
H Il y a un type spécial de constructeur: le constructeur par copie: H Si un constructeur par copie est spécifié, il sera appelé à chaque fois qu’un objet est rendu par une fonction, et à chaque fois qu’un objet est passé par valeur à une fonction. H C’est grâce au constructeur par copie que le programmeur évite un certain nombre de problèmes d’allocation de mémoire lorsque celle ci est référée par des variables membres de l’objet. Le constructeur VII
H Les constructeurs d’une classe donnée classe peuvent avoir n’importe quoi comme arguments, sauf des données de type classe. Ils peuvent avoir des pointeurs *classe comme arguments, ainsi que des références &classe. Cependant, dans ce dernier cas, le constructeur ne doit avoir qu’un seul argument &classe, et les autres arguments, s’il y en a, doivent avoir une valeur par défaut. H Ce constructeur est alors appelé constructeur de copie. Il sert lors d’affectations du genre : Le constructeur VIII class classexmpl { // champs... public : classexmpl(); // constructeur par défaut classexmpl(int i); // un autre constructeur classexmpl(classexmpl& c); // constructeur de copie // méthodes... }; classexmpl c1; // constructeur par défaut classexmpl c2 = c1; // appel du constructeur de copie // équivaut à classexmpl c2(c1);
Toute classe a nécessairement un constructeur de copie. Lorsqu’aucun n’est défini explicitement, le compilateur en crée un automatiquement, qui se contente de recopier champ par champ l’argument dans this. H Le constructeur de copie n’est appelé (comme tout constructeur) que lors d’une initialisation. Donc, si on écrit : c2 = c1; Ce n’est pas le constructeur qui est appelé, mais l’opérateur d’affectation =, qui par défaut recopie les champs un à un ; il faut donc également le redéfinir. Le constructeur par copie ++
H Cette remarque met en relief un fait essentiel qui est que lors des deux écritures : exemple c2 = c1; // appel du constructeur de copie c2 = c1 // appel de l'opérateur d'affectation; H L’opérateur d’affectation n’est appelé qu’une fois (la seconde), tandis que c’est le constructeur de copie qui est appelé la première fois. Par défaut les deux appels provoquent le même effet ; Ce qui n'est pas le cas dans des classes définies par un programmeur. Le constructeur par copie ++
Que ce passe t'il si on ne définit pas de constructeur par copie ? Le constructeur par copie sert à: class Obj{ private: int* a; public: Obj(); ~Obj(); }; Obj::Obj(){ printf("\n --> In Obj::Obj()\n"); a = new int[10]; for(int i=0; i<10;i++){a[i]=i;} } Obj::~Obj(){ printf("\n --> In Obj::~Obj()\n"); delete[] a; a = (int*)0; } void foo(Obj Inst){ printf("\n --> In foo(Obj)\n"); }
Maintenant: int main(){ Obj test; foo(test); } Le constructeur par copie sert à: a test Dans foo(obj) a Copy de test Allocation mémoire du tableau
int main(){ Obj test; foo(test); } a test Dans foo(obj) a Copy de test Allocation mémoire du tableau de l'objet d'origine Allocation mémoire de la copie de l'objet d'origine Où les deux tableaux sont égals, valeurs par valeurs.
class Obj{ private: int* a; public: Obj(); Obj(const Obj&); ~Obj(); }; Obj::Obj(const Obj& cpy){ a = new int[10]; for(int i=0; i<10;i++) { a[i]=cpy[i]; } La déclaration ressemble à:
H Le destructeur est une méthode particulière qui porte le même nom que la classe précédé du symbol ‘~’. Le destructeur est la dernière methode appelée par une instance lorque le bloc de code dans laquelle celle ci évoluait a terminé son exécution. H Le destructeur est aussi appelé lors d ’un appel d ’opérateur delete sur l ’instance. H C’est dans le destructeur que la mémoire qui avait été allouée à la construction et pendant la vie de l ’instance est rendue au système. Que se passe t ’il si deux blocs mémoires sont partagé par deux instances de la même classe? Le destructeur
H Un destructeur n’a aucun résultat, comme les constructeurs, et n’admet aucun argument ; de ce fait, il ne peut y avoir qu’un destructeur par classe. H D’une façon générale, le destructeur doit tout « remettre en ordre dans ce que l’instance de classe peut avoir modifié. Outre la libération de la mémoire prise, il peut aussi avoir à fermer des fichiers ouverts, à détruire des éléments provisoires, etc. H Le destructeur standard (fournit par défaut) ne fait rien. Le destructeur II
H L’opérateur new réserve la place mémoire nécessaire à l’objet dans le heap ; il appelle aussi un constructeur. Inversement, delete appelle d’abord le destructeur, puis libère la place mémoire. H Comme une classe peut avoir plusieurs constructeurs, on peut préciser quel constructeur est appelé au moment de l’appel de new. Il suffit pour cela d’écrire la liste des arguments derrière le nom de classe qui suit new. exemple *pe1 = new exemple(1, 2); // appel du constructeur 2 exemple *pe2 = new exemple; // appel du constructeur 1 classexmpl *c2 = new classexmpl(*c1); // constructeur de copie new et delete avec constructeurs et destructeurs
H Lorsqu’aucun paramètre n’est précisé le constructeur par défaut est appelé ; s’il n’existe pas, une erreur de compilation se produit. H Il est possible de créer un tableau avec new, mais dans ce cas c’est le constructeur par défaut qui est obligatoirement appelé ; il n’y a pas de moyen d’en préciser un autre (contrairement aux tableaux statiques qui peuvent être initialisés par un constructeur à argument unique). H Pour ce qui est de l’instruction delete, il n’y a pas le choix : chaque classe ayant un seul destructeur (possiblement implicite), c’est celui-là qui est appelé avant de supprimer la place mémoire. new et delete avec constructeurs et destructeurs
H Problème particulier aux tableaux: exemple *pex = new exemple[10];... delete pex // incorrect; H Le compilateur, qui n’a aucun moyen de connaître la taille du tableau pointé par pex, n’appellera le destructeur que pour le premier élément, ce qui peut poser problème. Pour lui demander de tout détruire, il faut préciser explicitement avec delete, le nombre d’éléments à supprimer : exemple *pex = new exemple[10];... delete[10] pex; H Il faut se rappeler de ceci dans la déclaration du destructeur. new et delete avec constructeurs et destructeurs
H L’opérateur d’assignement sert à assigner la valeur d’un objet à un autre objet. Quelle est la différence entre l’opérateur d’assignement et le constructeur par copie? H L’opérateur d’assignement est souvent surchargé. Comme pour le constructeur par copie, il faut faire attention à la manière dont les variables membres sont copiées lorsqu’elles référent une adresse mémoire. class student { private: char * name; int stno;... public: student& operator=(const student&);... }; L'opérateur d'assignement (=) student& student::operator=(const student& s) { if (&s == this) return *this; delete [] name; name = new char[strlen(s.name) + 1]; strcpy(name,s.name); return *this; }