Les classes dans C++
Les classes sont un concept étendu de structures de données: comme les structures de données, elles peuvent contenir des membres de données, mais elles peuvent également contenir des fonctions en tant que membres.
Un objet est une instanciation d’une classe. En termes de variables, une classe serait le type et un objet serait la variable.
Les classes sont définies en utilisant soit le mot clé class, soit le mot clé struct, avec la syntaxe suivante:
class nom_classe { access_specifier_1: membre1; access_specifier_2: membre2; ... } object_names;
Où nom_classe est un identificateur valide pour la classe, nom_objet est une liste facultative de noms pour les objets de cette classe. Le corps de la déclaration peut contenir des membres, qui peuvent être des déclarations de données ou de fonctions, et éventuellement des spécificateurs d’accès.
Les classes ont le même format que les structures de données en clair, sauf qu’elles peuvent également inclure des fonctions et avoir ces nouvelles choses appelées spécificateurs d’accès. Un spécificateur d’accès est l’un des trois mots clés suivants: privé, public ou protégé. Ces spécificateurs modifient les droits d’accès des membres qui les suivent:
- Les membres privés d’une classe ne sont accessibles que depuis d’autres membres de la même classe (ou de leurs « amis »).
- les membres protégés sont accessibles depuis d’autres membres de la même classe (ou de leurs « amis »), mais aussi depuis les membres de leurs classes dérivées.
- Enfin, les membres publics sont accessibles de n’importe où où l’objet est visible.
Par défaut, tous les membres d’une classe déclarée avec le mot clé class ont un accès privé pour tous ses membres. Par conséquent, tout membre déclaré avant tout autre spécificateur d’accès dispose automatiquement d’un accès privé. Par exemple:
class Rectangle { int largeur, hauteur; Public: void set_values (int, int); int zone (void); } rect;
Déclare une classe (c’est-à-dire un type) appelée Rectangle et un objet (c’est-à-dire une variable) de cette classe, appelé rect. Cette classe contient quatre membres: deux membres de données de type int (largeur et hauteur de membre) avec accès privé (car privé est le niveau d’accès par défaut) et deux fonctions membres avec accès public: les fonctions set_values et area, ont seulement inclus leur déclaration, mais pas leur définition.
Remarquez la différence entre le nom de la classe et le nom de l’objet: dans l’exemple précédent, Rectangle était le nom de la classe (c’est-à-dire le type), alors que rect était un objet de type Rectangle. C’est la même relation int et a have dans la déclaration suivante:
int a;
où int est le nom du type (la classe) et a le nom de la variable (l’objet).
Après les déclarations de Rectangle et de rect, l’un des membres publics d’objet rect est accessible comme s’il s’agissait de fonctions normales ou de variables normales, en insérant simplement un point (.) Entre le nom de l’objet et le nom du membre. Cela suit la même syntaxe que pour accéder aux membres des structures de données simples. Par exemple:
rect.set_values (3,4); myarea = rect.area ();
Les seuls membres de rect auxquels il est impossible d’accéder de l’extérieur de la classe sont la largeur et la hauteur, car ils ont un accès privé et que seuls les autres membres de cette même classe peuvent s’y référer.
Voici l’exemple complet de la classe Rectangle:
// exemple de classe #include <iostream> using namespace std; class Rectangle { int largeur, hauteur; Public: void set_values (int, int); int area () {return largeur * hauteur;} }; void Rectangle :: set_values (int x, int y) { largeur = x; hauteur = y; } int main () { Rectangle rect; rect.set_values (3,4); cout << "area:" << rect.area (); return 0; }
Résultat de l’exécution :
surface: 12 Modifier et exécuter
Cet exemple réintroduit l’opérateur scope (::, deux points), vu dans les chapitres précédents en relation avec les espaces de noms. Ici, il est utilisé dans la définition de la fonction set_values pour définir un membre d’une classe en dehors de la classe elle-même.
Notez que la définition de la zone de fonction membre a été incluse directement dans la définition de la classe Rectangle en raison de sa simplicité extrême. Inversement, set_values est simplement déclaré avec son prototype dans la classe, mais sa définition ne l’appartient pas. Dans cette définition externe, l’opérateur de portée ( : : ) est utilisé pour spécifier que la fonction en cours de définition est membre de la classe Rectangle et non pas une fonction régulière non membre.
L’opérateur de portée ( : : ) spécifie la classe à laquelle appartient le membre en cours de définition, attribuant exactement les mêmes propriétés de portée que si cette définition de fonction était directement incluse dans la définition de classe. Par exemple, la fonction set_values de l’exemple précédent a accès aux variables largeur et hauteur , qui sont des membres privés de la classe Rectangle et donc uniquement accessibles à partir d’autres membres de la classe, tels que celui-ci.
La seule différence entre définir complètement une fonction membre dans la définition de la classe ou simplement inclure sa déclaration dans la fonction et la définir ultérieurement en dehors de la classe est que, dans le premier cas, la fonction est automatiquement considérée comme une fonction membre inline par le compilateur. dans le second, il s’agit d’une fonction de membre de classe normale (no inline). Cela ne provoque aucune différence de comportement, mais uniquement sur les optimisations possibles du compilateur.
Les membres largeur et hauteur ont un accès privé (rappelez-vous que si rien d’autre n’est spécifié, tous les membres d’une classe définie avec mot-clé class ont un accès privé). En les déclarant privés, l’accès depuis l’extérieur de la classe n’est pas autorisé. Cela a du sens, car nous avons déjà défini une fonction membre pour définir les valeurs de ces membres dans l’objet: la fonction membre set_values. Par conséquent, le reste du programme n’a pas besoin d’y avoir un accès direct. Peut-être dans un exemple aussi simple, il est difficile de voir à quel point il peut être utile de restreindre l’accès à ces variables, mais dans le cas de projets plus importants, il peut être très important que les valeurs ne puissent pas être modifiées de manière inattendue ).
La propriété la plus importante d’une classe est qu’il s’agit d’un type. Nous pouvons donc en déclarer plusieurs objets. Par exemple, avec l’exemple précédent de la classe Rectangle, nous aurions pu déclarer l’objet rectb en plus de l’objet rect:
// exemple: une classe, deux objets #include <iostream> using namespace std; class Rectangle { int largeur, hauteur; Public: void set_values (int, int); int area () {return largeur * hauteur;} }; void Rectangle :: set_values (int x, int y) { largeur = x; hauteur = y; } int main () { Rectangle rect, rectb; rect.set_values (3,4); rectb.set_values (5,6); cout << "rect area:" << rect.area () << endl; cout << "rectb area:" << rectb.area () << endl; return 0; }
Résultat de l’exécution :
superficie du terrain: 12 surface rectb: 30
Dans ce cas particulier, la classe (type des objets) est Rectangle, avec deux instances (objets): rect et rectb. Chacun d’eux a ses propres variables membres et fonctions membres.
Notez que l’appel à rect.area () ne donne pas le même résultat que l’appel à rectb.area (). Cela est dû au fait que chaque objet de la classe Rectangle a ses propres variables de largeur et de hauteur, car ils ont également, d’une certaine manière, leurs propres membres de fonction set_value et area qui agissent sur les propres variables de membre de l’objet.
Les classes permettent de programmer en utilisant des paradigmes orientés objet: les données et les fonctions sont des membres de l’objet, ce qui réduit le besoin de transmettre et de transporter des gestionnaires ou d’autres variables d’état comme arguments de fonctions, car elles font partie de l’objet dont le membre est appelé. Notez qu’aucun argument n’a été transmis sur les appels à rect.area ou rectb.area. Ces fonctions membres utilisaient directement les données membres de leurs objets respectifs rect et rectb.
Sommaire
Les constructeurs
Que se passerait-il dans l’exemple précédent si nous appelions la zone de fonction membre avant d’appeler set_values? Un résultat indéterminé, car la largeur et la hauteur des membres n’ont jamais été affectées à une valeur.
Pour éviter cela, une classe peut inclure une fonction spéciale appelée son constructeur, qui est automatiquement appelée chaque fois qu’un nouvel objet de cette classe est créé, ce qui permet à la classe d’initialiser des variables membres ou d’allouer un stockage.
Cette fonction constructeur est déclarée comme une fonction membre régulière, mais avec un nom qui correspond au nom de la classe et sans aucun type de retour; pas même nul.
La classe Rectangle ci-dessus peut facilement être améliorée en implémentant un constructeur:
// exemple: constructeur de classe #include <iostream> using namespace std; class Rectangle { int largeur, hauteur; Publique: Rectangle (int, int); int area () {return (largeur * hauteur);} }; Rectangle :: Rectangle (int a, int b) { largeur = a; hauteur = b; } int main () { Rectangle rect (3,4); Rectangle rectb (5,6); cout << "rect area:" << rect.area () << endl; cout << "rectb area:" << rectb.area () << endl; return 0; }
Résultat de l’exécution :
superficie du terrain: 12 surface rectb: 30
Les résultats de cet exemple sont identiques à ceux de l’exemple précédent. Mais maintenant, la classe Rectangle n’a pas de fonction membre set_values, mais un constructeur qui effectue une action similaire: il initialise les valeurs de largeur et de hauteur avec les arguments qui lui sont transmis.
Remarquez comment ces arguments sont passés au constructeur au moment où les objets de cette classe sont créés:
Rectangle rect (3,4); Rectangle rectb (5,6);
Les constructeurs ne peuvent pas être appelés explicitement comme s’il s’agissait de fonctions membres régulières. Ils ne sont exécutés qu’une seule fois, lorsqu’un nouvel objet de cette classe est créé.
Notez que ni la déclaration du prototype du constructeur (dans la classe) ni la dernière définition du constructeur n’ont de valeur de retour; pas même void: les constructeurs ne renvoient jamais de valeurs, ils initialisent simplement l’objet.
Surcharge des constructeurs
Comme toute autre fonction, un constructeur peut également être surchargé avec différentes versions prenant différents paramètres: avec un nombre différent de paramètres et / ou des paramètres de types différents. Le compilateur appellera automatiquement celui dont les paramètres correspondent aux arguments:
// surcharge des constructeurs de classe #include <iostream> using namespace std; class Rectangle { int largeur, hauteur; Public: Rectangle (); Rectangle (int, int); int area (void) {return (largeur * hauteur);} }; Rectangle :: Rectangle () { largeur = 5; hauteur = 5; } Rectangle :: Rectangle (int a, int b) { largeur = a; hauteur = b; } int main () { Rectangle rect (3,4); Rectangle rectb; cout << "rect area:" << rect.area () << endl; cout << "rectb area:" << rectb.area () << endl; return 0; }
Résultat de l’exécution :
superficie du terrain: 12 surface rectb: 25
Dans l’exemple ci-dessus, deux objets de la classe Rectangle sont construits: rect et rectb. rect est construit avec deux arguments, comme dans l’exemple précédent.
Mais cet exemple introduit également un constructeur de type spécial: le constructeur par défaut. Le constructeur par défaut est le constructeur qui ne prend aucun paramètre. Il est spécial car il est appelé lorsqu’un objet est déclaré mais qu’il n’est pas initialisé avec un argument. Dans l’exemple ci-dessus, le constructeur par défaut est appelé pour rectb. Notez que rectb n’est même pas construit avec un jeu vide de parenthèses – en fait, les parenthèses vides ne peuvent pas être utilisées pour appeler le constructeur par défaut:
Rectangle rectb; // ok, constructeur par défaut appelé Rectangle rectc (); // oops, constructeur par défaut NON appelé
En effet, l’ensemble de parenthèses vides ferait de rectc une déclaration de fonction au lieu d’une déclaration d’objet: il s’agirait d’une fonction qui ne prend aucun argument et retourne une valeur de type Rectangle.
Initialisation uniforme
La manière d’appeler les constructeurs en plaçant leurs arguments entre parenthèses, comme indiqué ci-dessus, est appelée forme fonctionnelle. Mais les constructeurs peuvent aussi être appelés avec d’autres syntaxes:
Premièrement, les constructeurs avec un seul paramètre peuvent être appelés en utilisant la syntaxe d’initialisation de variable (un signe égal suivi de l’argument):
nom_classe nom_objet = valeur_initialisation;
Plus récemment, C ++ a introduit la possibilité d’appeler les constructeurs en utilisant une initialisation uniforme, qui est essentiellement la même que la forme fonctionnelle, mais en utilisant des accolades ({}) au lieu de parenthèses (()):
nom_classe nom_objet {valeur, valeur, valeur, …}
Cette dernière syntaxe peut éventuellement inclure un signe égal avant les accolades.
Voici un exemple avec quatre manières de construire des objets d’une classe dont le constructeur prend un seul paramètre:
// classes et initialisation uniforme #include <iostream> using namespace std; class Cercle { double rayon; Public: Cercle (double r) {rayon = r; } double circum () {return 2*rayon*3.14159265;} }; int main () { Cercle foo (10,0); // forme fonctionnelle Cercle barre = 20,0; // affectation init. Cercle baz {30.0}; // uniforme init. Cercle qux = {40,0}; // comme POD cout << "circonférence de foo:" << foo.circum () << '\ n'; return 0; }
Résultat de l’exécution :
circonférence de foo: 62.8319
L’un des avantages de l’initialisation uniforme par rapport à la forme fonctionnelle est que, contrairement aux parenthèses, les accolades ne peuvent pas être confondues avec les déclarations de fonction et peuvent donc être utilisées pour appeler explicitement les constructeurs par défaut:
Rectangle rectb; // constructeur par défaut appelé Rectangle rectc (); // déclaration de fonction (constructeur par défaut NON appelé) Rectangle rectd {}; // constructeur par défaut appelé
Le choix de la syntaxe pour appeler des constructeurs est en grande partie une question de style. La plupart du code existant utilise actuellement une forme fonctionnelle, et certains guides de style plus récents suggèrent de choisir une initialisation uniforme par rapport aux autres, même s’il présente également des pièges potentiels liés à sa préférence, initializer_list.
Initialisation des membres dans les constructeurs
Lorsqu’un constructeur est utilisé pour initialiser d’autres membres, ces autres membres peuvent être initialisés directement, sans recourir à des déclarations contenues dans son corps. Cela se fait en insérant, avant le corps du constructeur, un signe deux-points (:) et une liste d’initialisations pour les membres de la classe. Par exemple, considérons une classe avec la déclaration suivante:
class Rectangle { int largeur, hauteur; Public: Rectangle (int, int); int area () {return largeur*hauteur ;} };
Le constructeur de cette classe pourrait être défini, comme d’habitude, comme suit:
Rectangle :: Rectangle (int x, int y) {largeur = x; hauteur = y; }
Mais il pourrait aussi être défini en utilisant l’initialisation du membre comme:
Rectangle :: Rectangle (int x, int y): largeur (x) {hauteur = y; }
Ou même:
Rectangle :: Rectangle (int x, int y): largeur (x), hauteur (y) {}
Notez que dans ce dernier cas, le constructeur ne fait qu’initialiser ses membres, d’où son corps de fonction vide.
Pour les membres de types fondamentaux, peu importe la définition du chemin au-dessus du constructeur, car ils ne sont pas initialisés par défaut, mais pour les objets membres (ceux dont le type est une classe), s’ils ne sont pas initialisés après les deux-points, ils sont construits par défaut.
La construction par défaut de tous les membres d’une classe peut être ou ne pas toujours être pratique: dans certains cas, il s’agit d’un gaspillage (lorsque le membre est ensuite réinitialisé autrement dans le constructeur), mais dans d’autres cas, la construction par défaut n’est même pas possible (quand la classe n’a pas de constructeur par défaut). Dans ce cas, les membres doivent être initialisés dans la liste d’initialisation des membres. Par exemple:
// initialisation du membre #include <iostream> using namespace std; class Cercle { double rayon; Public: Cercle (double r): rayon (r) {} zone double () {return rayon*rayon*3.14159265;} }; class Cylindre { Cercle base; double hauteur; Public: Cylindre (double r, double h): base (r), hauteur (h) {} double volume () {return base.area () * hauteur;} }; int main () { Cylindre foo (10,20); cout << "le volume de foo:" << foo.volume () << '\ n'; return 0; }
Résultat de l’exécution :
le volume de foo: 6283.19
Dans cet exemple, la classe Cylinder a un objet membre dont le type est une autre classe (le type de base est Circle). Comme les objets de la classe Circle ne peuvent être construits qu’avec un paramètre, le constructeur de Cylinder doit appeler le constructeur de la base. La seule façon de procéder consiste à faire cela dans la liste d’initialisation des membres.
Ces initialisations peuvent également utiliser une syntaxe d’initialisation uniforme, en utilisant des accolades {} au lieu de parenthèses ():
Cylindre :: Cylindre (double r, double h): base {r}, hauteur {h} {}
Pointeurs vers classe
Les objets peuvent également être pointés par des pointeurs: une fois déclarée, une classe devient un type valide. Elle peut donc être utilisée comme type pointé par un pointeur. Par exemple:
Rectangle * prect;
est un pointeur sur un objet de la classe Rectangle.
De même que pour les structures de données simples, les membres d’un objet sont accessibles directement à partir d’un pointeur à l’aide de l’opérateur de flèche (->). Voici un exemple avec quelques combinaisons possibles:
// exemple de pointeur sur les classes #include <iostream> using namespace std; class Rectangle { int largeur, hauteur; Public: Rectangle (int x, int y): largeur (x), hauteur (y) {} int zone (void) {return largeur * hauteur; } }; int main() { Rectangle obj (3, 4); Rectangle * foo, * bar, * baz; foo = & obj; bar = new rectangle (5, 6); baz = new Rectangle [2] {{2,5}, {3,6}}; cout << "La zone de obj:" << obj.area () << '\ n'; cout << "* la zone de foo:" << foo-> area () << '\ n'; cout << "* zone du bar:" << bar-> zone () << '\ n'; cout << "La zone de baz [0]:" << baz [0] .area () << '\ n'; cout << "Zone de baz [1]:" << baz [1] .area () << '\ n'; delete barre; delete [] baz; return 0; }
La plupart de ces expressions ont été introduites dans les chapitres précédents. Plus particulièrement, le chapitre sur les tableaux a présenté l’opérateur de décalage ([]) et le chapitre sur les structures de données simples a présenté l’opérateur de flèche (->).
Classes définies avec struct et union
Les classes peuvent être définies non seulement avec la classe mot-clé, mais également avec les mots-clés struct et union.
Le mot clé struct, généralement utilisé pour déclarer des structures de données simples, peut également être utilisé pour déclarer des classes ayant des fonctions membres, avec la même syntaxe que pour le mot clé class. La seule différence entre les deux réside dans le fait que les membres des classes déclarées avec le mot-clé struct ont un accès public par défaut, tandis que les membres des classes déclarées avec le mot-clé class ont un accès privé par défaut. Pour toutes les autres fins, les deux mots-clés sont équivalents dans ce contexte.
Inversement, le concept d’unions diffère de celui de classes déclarées avec struct et class, car les unions ne stockent qu’un seul membre de données à la fois, mais sont néanmoins aussi des classes et peuvent donc également remplir des fonctions membres. L’accès par défaut dans les classes d’union est public.