Les classes dans C++ (2)
Les classes, essentiellement, définissent les nouveaux types à utiliser dans le code C ++. Et les types en C ++ n’interagissent pas seulement avec le code au moyen de constructions et d’assignations. Ils interagissent également par le biais des opérateurs. Par exemple, effectuons l’opération suivante sur les types fondamentaux:
int a, b, c; a = b + c;
Ici, différentes variables de type fondamental (int) sont appliquées à l’opérateur d’addition, puis à l’opérateur d’affectation. Pour un type arithmétique fondamental, la signification de telles opérations est généralement évidente et non ambiguë, mais peut ne pas l’être pour certains types de classe. Par exemple:
struct maclasse { string produit; float prix; } a, b, c; a = b + c;
Ici, le résultat de l’opération d’addition sur b et c n’est pas évident. En fait, ce code à lui seul provoquerait une erreur de compilation, car le type maclasse n’a pas de comportement défini pour les ajouts. Cependant, C ++ permet de surcharger la plupart des opérateurs afin que leur comportement puisse être défini pour à peu près n’importe quel type, y compris les classes. Voici une liste de tous les opérateurs pouvant être surchargés:
Opérateurs surchargeables
+ – * / = < > + = – = * = / = << >> << = >> = ==! = < => = ++ – % & ^ ! | ~ & = ^ = | = && || % = [ ] ( ) , ->* -> new delete new[] delete[]
Les opérateurs sont surchargés au moyen de fonctions d’opérateur, qui sont des fonctions standard portant des noms spéciaux: leur nom commence par le mot-clé operator suivi du signe d’opérateur surchargé. La syntaxe est la suivante:
type operator signe (paramètres) {/ * ... body ... * /}
Par exemple, les vecteurs cartésiens sont des ensembles de deux coordonnées: x et y. L’opération d’addition de deux vecteurs cartésiens est définie comme l’addition des deux coordonnées x ensemble et des deux coordonnées y ensemble. Par exemple, l’ajout des vecteurs cartésiens (3,1) et (1,2) ensemble donnerait (3 + 1,1 + 2) = (4,3). Cela pourrait être implémenté en C ++ avec le code suivant:
// exemple de surcharge des opérateurs #include <iostream> using namespace std; class CVector { public: int x,y; CVector () {}; CVector (int a,int b) : x(a), y(b) {} CVector operator + (const CVector&); }; CVector CVector::operator+ (const CVector& param) { CVector temp; temp.x = x + param.x; temp.y = y + param.y; return temp; } int main () { CVector foo (3,1); CVector bar (1,2); CVector result; result = foo + bar; cout << result.x << ',' << result.y << '\n'; return 0; }
Résultat de l’exécution : 4, 3
En cas de confusion à propos de tant d’apparences de CVector, considérez que certaines d’entre elles se réfèrent au nom de classe (c’est-à-dire le type) CVector et que d’autres sont des fonctions portant ce nom (c’est-à-dire des constructeurs, qui doivent avoir le même nom que la classe). Par exemple:
CVector (int, int) : x(a), y(b) {} // fonction CVector (constructeur) CVector operator+ (const CVector&); // la fonction retourne CVector
L’opérateur de fonction + de classe CVector surcharge l’opérateur d’addition (+) pour ce type. Une fois déclarée, cette fonction peut être appelée implicitement en utilisant l’opérateur ou explicitement en utilisant son nom fonctionnel:
c = a + b; c = a.operator+ (b);
Les deux expressions sont équivalentes.
Les surcharges d’opérateurs ne sont que des fonctions régulières pouvant avoir un comportement quelconque; il n’est en fait nullement nécessaire que l’opération effectuée par cette surcharge ait un rapport avec la signification mathématique ou habituelle de l’opérateur, bien que cela soit vivement recommandé. Par exemple, une classe qui surcharge l’opérateur + à soustraire ou l’opérateur == à remplir l’objet avec des zéros est parfaitement valide, bien que l’utilisation d’une telle classe puisse être difficile.
Le paramètre attendu pour une surcharge de la fonction membre pour des opérations telles que l’opérateur + est naturellement l’opérande situé à droite de l’opérateur. Ceci est commun à tous les opérateurs binaires (ceux avec un opérande à sa gauche et un opérande à sa droite). Mais les opérateurs peuvent prendre diverses formes. Ici vous avez un tableau avec un résumé des paramètres nécessaires pour chacun des différents opérateurs pouvant être surchargés (veuillez remplacer @ par l’opérateur dans chaque cas):
Expression | Opérateur | Fonction membre | Fonction non membre |
@a | + – * & ! ~ ++ — | A::operator@() | operator@(A) |
a@ | ++ — | A::operator@(int) | operator@(A,int) |
a@b | + – * / % ^ & | < > == != <= >= << >> && || , | A::operator@(B) | operator@(A,B) |
a@b | = += -= *= /= %= ^= &= |= <<= >>= [] | A::operator@(B) | – |
a(b,c…) | () | A::operator()(B,C…) | – |
a->b | -> | A::operator->() | – |
(TYPE) a | TYPE | A::operator TYPE() | – |
Où a est un objet de classe A, b est un objet de classe B et c est un objet de classe C. TYPE est un type quelconque (les opérateurs surchargent la conversion en type TYPE).
Notez que certains opérateurs peuvent être surchargés sous deux formes: en tant que fonction membre ou en tant que fonction non membre: le premier cas a été utilisé dans l’exemple ci-dessus pour l’opérateur +. Mais certains opérateurs peuvent également être surchargés en tant que fonctions non membres; Dans ce cas, la fonction opérateur prend comme premier argument un objet de la classe appropriée.
#include <iostream> using namespace std; class CVector { public: int x,y; CVector () {} CVector (int a, int b) : x(a), y(b) {} }; CVector operator+ (const CVector& lhs, const CVector& rhs) { CVector temp; temp.x = lhs.x + rhs.x; temp.y = lhs.y + rhs.y; return temp; } int main () { CVector foo (3,1); CVector bar (1,2); CVector result; result = foo + bar; cout << result.x << ',' << result.y << '\n'; return 0; }
Résultat de l’exécution : 4, 3
Le mot clé this
Le mot clé this représente un pointeur sur l’objet dont la fonction membre est en cours d’exécution. Il est utilisé dans la fonction membre d’une classe pour faire référence à l’objet lui-même.
L’une de ses utilisations peut être de vérifier si un paramètre transmis à une fonction membre est l’objet même. Par exemple:
// exemple de this #include <iostream> using namespace std; class Dummy { public: bool isitme (Dummy& param); }; bool Dummy::isitme (Dummy& param) { if (¶m == this) return true; else return false; } int main () { Dummy a; Dummy* b = &a; if ( b->isitme(a) ) cout << "yes, &a is b\n"; return 0; }
Résultat de l’exécution : yes, &a is b
Il est également fréquemment utilisé dans les fonctions operator = membre qui renvoient des objets par référence. Après les exemples sur le vecteur cartésien vu précédemment, son operator = fonction aurait pu être défini comme:
CVector& CVector::operator= (const CVector& param) { x=param.x; y=param.y; return *this; }
En fait, cette fonction est très similaire au code que le compilateur génère implicitement pour cette classe pour operator =.
Membres statiques
Une classe peut contenir des membres statiques, des données ou des fonctions.
Un membre de données statique d’une classe est également appelé « variable de classe », car il n’y a qu’une seule variable commune pour tous les objets de cette même classe, partageant la même valeur: c’est-à-dire que sa valeur n’est pas différente d’un objet de celle-ci. classe à une autre.
Par exemple, il peut être utilisé pour une variable dans une classe pouvant contenir un compteur avec le nombre d’objets de cette classe actuellement alloués, comme dans l’exemple suivant:
// membres statiques dans des classes #include <iostream> using namespace std; class Dummy { public: static int n; Dummy () { n++; }; }; int Dummy::n=0; int main () { Dummy a; Dummy b[5]; cout << a.n << '\n'; Dummy * c = new Dummy; cout << Dummy::n << '\n'; delete c; return 0; }
En fait, les membres statiques ont les mêmes propriétés que les variables non membres, mais ils bénéficient d’une étendue de classe. Pour cette raison, et pour éviter qu’ils ne soient déclarés plusieurs fois, ils ne peuvent pas être initialisés directement dans la classe, mais doivent être initialisés quelque part en dehors de celle-ci. Comme dans l’exemple précédent:
int Dummy :: n = 0;
Comme il s’agit d’une valeur de variable commune à tous les objets de la même classe, elle peut être appelée membre de n’importe quel objet de cette classe ou même directement par le nom de la classe (cela n’est bien sûr valable que pour les membres statiques):
cout << a.n; cout << Dummy :: n;
Ces deux appels ci-dessus font référence à la même variable: la variable statique n dans la classe Dummy partagée par tous les objets de cette classe.
Encore une fois, cela ressemble à une variable non-membre, mais avec un nom qui nécessite un accès comme un membre d’une classe (ou d’un objet).
Les classes peuvent également avoir des fonctions membres statiques. Ceux-ci représentent les mêmes: membres d’une classe qui sont communs à tous les objets de cette classe, agissant exactement comme des fonctions non membres mais accessibles en tant que membres de la classe. Comme elles ressemblent à des fonctions non membres, elles ne peuvent pas accéder aux membres non statiques de la classe (ni variables membres ni fonctions membres). Ils ne peuvent pas non plus utiliser le mot-clé this.
Fonctions membres const
Lorsqu’un objet d’une classe est qualifié d’objet const:
const MaClasse myobject;
L’accès à ses membres de données en dehors de la classe est limité à la lecture seule, comme si tous ses membres de données étaient const pour ceux y accédant de l’extérieur de la classe. Notez cependant que le constructeur est toujours appelé et qu’il est autorisé à initialiser et à modifier ces membres de données:
#include <iostream> using namespace std; class MaClasse { public: int x; MaClasse(int val) : x(val) {} int get() {return x;} }; int main() { const MaClasse foo(10); // foo.x = 20; // not valid: x cannot be modified cout << foo.x << '\n'; // ok: data member x can be read return 0; }
Résultat de l’exécution : 10
Les fonctions membres d’un objet const ne peuvent être appelées que si elles sont elles-mêmes spécifiées en tant que membres const; Dans l’exemple ci-dessus, membre get (qui n’est pas spécifié en tant que const) ne peut pas être appelé depuis foo. Pour spécifier qu’un membre est un membre const, le mot-clé const doit suivre la fonction prototype, après la parenthèse fermante pour ses paramètres:
int get () const {return x;}
Notez que const peut être utilisé pour qualifier le type renvoyé par une fonction membre. Ce const n’est pas le même que celui qui spécifie un membre en tant que const. Les deux sont indépendants et sont situés à des endroits différents dans le prototype de fonction:
int get () const {return x;} // fonction membre const const int & get () {return x;} // fonction membre renvoyant une constante & const int & get () const {return x;} // fonction membre const retournant un const &
Les fonctions membres spécifiées comme const ne peuvent pas modifier les membres de données non statiques ni appeler d’autres fonctions membres non const. En substance, les membres const ne doivent pas modifier l’état d’un objet.
Les objets const sont limités pour accéder uniquement aux fonctions membres marquées comme const, mais les objets non-const ne sont pas restreints et peuvent donc accéder aux fonctions membres const et non-const.
Vous pouvez penser que de toute façon vous allez rarement déclarer des objets const, et marquer ainsi tous les membres qui ne modifient pas l’objet en tant que const ne vaut pas la peine, mais les objets const sont en réalité très courants. La plupart des fonctions prenant des classes en tant que paramètres les prennent en réalité par référence const, et donc, ces fonctions ne peuvent accéder qu’à leurs membres const:
// const objects #include <iostream> using namespace std; class MeClasse { int x; public: MaClasse(int val) : x(val) {} const int& get() const {return x;} }; void print (const MaClasse& arg) { cout << arg.get() << '\n'; } int main() { MaClasse foo (10); print(foo); return 0; }
Résultat de l’exécution : 10
Si dans cet exemple, get n’était pas spécifié en tant que membre const, l’appel à arg.get () dans la fonction print ne serait pas possible, car les objets const ont uniquement accès aux fonctions de membre const.
Les fonctions membres peuvent être surchargées sur leur constance: une classe peut avoir deux fonctions membres avec des signatures identiques, sauf que l’une est const et l’autre pas: dans ce cas, la version const n’est appelée que lorsque l’objet est lui-même const, et la version non-const est appelée lorsque l’objet est lui-même non-const.
#include <iostream> using namespace std; class MaClasse { int x; public: MaClasse(int val) : x(val) {} const int& get() const {return x;} int& get() {return x;} }; int main() { MaClasse foo (10); const MaClasse bar (20); foo.get() = 15; // ok: get() returns int& // bar.get() = 25; // not valid: get() returns const int& cout << foo.get() << '\n'; cout << bar.get() << '\n'; return 0; }