
Envoyé par
Reminouche
Lequel est le bon ?
Le premier (avec le &) est un passage par référence. C'est un peu comme java il me semble : l'objet n'est pas copié, c'est une référence vers cet objet qui est transmise.
Le second (sans le &) est un passage par valeur. Lors de l'appel du constructeur (ou de la fonction) un nouvel objet temporaire est créé, le paramètre est copié dans cet objet temporaire et finalement, c'est cet objet temporaire qui est donné à la fonction/constructeur.
Lequel préférer ? Tout dépend. Et on entre dans les subtilités du C++... Il est évident que le passage par référence permet un passage de paramètre plus rapide car l'objet n'est pas recopié. Cependant le passage par valeur permet de bénéficier d'une optimisation des compilateurs C++ appelée (N)RVO qui consiste à 'factoriser' des objets transmis en cascade. De même, le passage par valeur est souvent plus efficace pour les types de base que le passage par référence. Histoire de ne pas trop se compliquer la vie, je te conseillerais de transmettre par référence constante les objets et par valeur les types de base. Puis d'entrer dans les subtilités plus tard.
Encore quelques précisions :
- Le passage par valeur : la modification du paramètre dans la fonction n'a pas d'impact pour le paramètre donné par l'appelant.
- Le passage par référence constante : la modification du paramètre dans la fonction va provoquer une erreur de compilation.
- Le passage par référence non constante : la modification du paramètre dans la fonction modifie le paramètre donné par l'appelant.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void valeur(int i_)
{
++i_;
std::cout<<"i dans valeur : "<<i_<<std::endl;
}
void reference_constante(int const &i_)
{
++i_; // Erreur : vous ne pouvez pas assigner une variable const
std::cout<<"i dans reference_constante : "<<i_<<std::endl;
}
void reference_non_constante(int &i_)
{
++i_;
std::cout<<"i dans reference_non_constante : "<<i_<<std::endl;
} |
Si on supprimer la fonction reference_constante, et qu'on écrit :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include <iostream>
void valeur(int i_)
{
++i_;
std::cout<<"i dans valeur : "<<i_<<std::endl;
}
void reference_non_constante(int &i_)
{
++i_;
std::cout<<"i dans reference_non_constante : "<<i_<<std::endl;
}
int main()
{
int ici(42);
std::cout<<"avant : "<<ici<<std::endl;
valeur(ici);
std::cout<<"après : "<<ici<<std::endl;
std::cout<<"avant : "<<ici<<std::endl;
reference_non_constante(ici);
std::cout<<"après : "<<ici<<std::endl;
return 0;
} |
avant : 42
i dans valeur : 43
apres : 42
avant : 42
i dans reference_non_constante : 43
apres : 43
On obtient :

Envoyé par
Reminouche
EDIT : Que signigie : " warning: `class Prisonnier' has virtual functions but non-virtual destructor| "
La réponse est dans la F.A.Q. : en gros : ou ton destructeur est public et virtuel car tu peux détruire l'objet de façon polymorphe (c'est à dire avec sa classe de base) ou ton destructeur est protégé et non virtuel et tu interdis ainsi de détruire l'objet à partir d'une référence de sa classe de base.

Envoyé par
Reminouche
J'ai une erreur de type : " cannot declare parameter `perso1' to be of type `Prisonnier'|" , pk cela ?
Normalement tu as du déclarer Prisonnier dans un fichier d'en-tête .h (mettons Prisionner.h) et définir ses méthodes dans un fichier source .cpp.
Lorsque tu as besoin d'utiliser la classe Prisionner, il faut rajouter #include "Prisonnier.h" pour indiquer au compilateur où trouver la déclaration du type Prisonnier.
Je reviens sur l'abstract et quelques notions d'héritage et de virtualité.
Commençons par deux définitions :
-> Le type statique d'une variable est le type tel qu'il apparaît dans le code
-> Le type dynamique d'une variable est le type possédé effectivement à l'exécution :
1 2 3 4 5 6 7 8 9 10 11
|
class A{};
class B : public A{};
int main()
{
A a;
B b;
A &ra = a;
A &rb = b;
return 0;
} |
Le type statique de a est A. Le type dynamique de a est A.
Le type statique de b est B. Le type dynamique de b est B.
Le type statique de ra est A. Le type dynamique de ra est A.
Le type statique de rb est A. Le type dynamique de rb est B.
On accède au type dynamique avec les références ou les pointeurs :
1 2 3 4 5
| A a;
B b;
A a2 = b;
A &ra = b;
A *pa = &b; |
Le type statique de a est A. Le type dynamique de a est A.
Le type statique de b est B. Le type dynamique de b est B.
Le type statique de a2 est A. Le type dynamique de a2 est A. En fait, a2 est une copie de b et n'a plus rien à voir avec b.
Le type statique de ra est A. Le type dynamique de ra est A.
Le type statique de pa est A. Le type dynamique de pa est B.
Par défaut, la fonction appelée pour un objet est celle définie dans son type statique :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
#include <iostream>
class A{
public:
void qui_suis_je() const
{
std::cout<<"je suis un A"<<std::endl;
}
};
class B : public A{
public:
void qui_suis_je() const
{
std::cout<<"je suis un B"<<std::endl;
}
};
int main()
{
A a;
a.qui_suis_je();
B b;
b.qui_suis_je();
A &ra = a;
ra.qui_suis_je();
A &rb = b;
rb.qui_suis_je();
return 0;
} |
Ce code produit comme résultat :
je suis un A
je suis un B
je suis un A
je suis un A
Le mot clé virtual permet de dire que la fonction à prendre en compte est celle la plus spécialisée compte tenu du type dynamique de l'objet. Si on reprend notre exemple :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <iostream>
class A{
public:
virtual void qui_suis_je() const
{
std::cout<<"je suis un A"<<std::endl;
}
};
class B : public A{
public:
virtual void qui_suis_je() const
{
std::cout<<"je suis un B"<<std::endl;
}
};
int main()
{
A a;
a.qui_suis_je();
B b;
b.qui_suis_je();
A &ra = a;
ra.qui_suis_je();
A &rb = b;
rb.qui_suis_je();
return 0;
} |
On a alors :
je suis un A
je suis un B
je suis un A
je suis un B
Toute les fonctions d'une classe peuvent être virtuelles à l'exception du constructeur (ça n'aurait pas vraiment de sens en fait).
Donc le destructeur peut être virtuel. Ce qui permet que ce soit bien le destructeur le plus spécialisé qui est appelé lorsqu'on détruit une variable d'un type statique plus abstrait :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <iostream>
class A{
public:
virtual ~A(){
std::cout<<"destructeur de A"<<std::endl;
}
};
class B : public A{
public:
virtual ~B(){
std::cout<<"destructeur de B"<<std::endl;
}
};
int main()
{
std::cout<<"Pour A : "<<std::endl;
A *p_a = new A;
delete p_a;
std::cout<<"Pour B : "<<std::endl;
A *p_b = new B;
delete p_b;
return 0;
} |
Pour A :
destructeur de A
Pour B :
destructeur de B
destructeur de A
Si le destructeur n'avait pas été virtuel, alors le comportement aurait été indéterminé : cela aurait pu appeler que le destructeur de A ou planter ou faire le café.
Une classe dérivant une classe de base n'est pas obligée de spécialiser les méthodes virtuelles de la classe de base. Le comportement est alors celui de la dernière spécialisation :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| #include <iostream>
class A{
public:
virtual void qui_suis_je()const
{
std::cout<<"je crois que je suis un A"<<std::endl;
}
virtual ~A(){}
};
class B : public A{
public:
virtual void qui_suis_je()const
{
std::cout<<"je crois que je suis un B"<<std::endl;
}
virtual ~B(){}
};
class C : public A
{
};
class D : public B
{
};
void qui_es_tu(A const&ra_)
{
ra_.qui_suis_je();
}
int main()
{
A a;
qui_es_tu(a);
B b;
qui_es_tu(b);
C c;
qui_es_tu(c);
D d;
qui_es_tu(d);
return 0;
} |
Produit :
je crois que je suis un A
je crois que je suis un B
je crois que je suis un A
je crois que je suis un B
Lorsqu'on veut forcer la spécialisation par une classe dérivée, on adjoint '=0' à la méthode :
1 2 3 4 5
| class A{
public:
virtual void qui_suis_je()const = 0;
virtual ~A(){}
}; |
On dit que qui_suis_je est une méthode abstraite. Une classe qui possède au moins une méthode abstraite est une classe abstraite. Une classe abstraite ne peut être instanciée :
A a;// --> 'A'*: impossible d'instancier une classe abstraite
Une classe dérivant de A devra spécialiser les méthodes abstraites pour pouvoir être instanciée Sans quoi elle sera elle aussi abstraite :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <iostream>
class A{
public:
virtual void qui_suis_je()const = 0;
virtual ~A(){}
};
class B : public A{
public:
virtual void qui_suis_je()const
{
std::cout<<"je crois que je suis un B"<<std::endl;
}
virtual ~B(){}
};
class C : public A
{
};
class D : public B
{
};
int main()
{
A a; // Erreur A est une classe abstraite
B b; // OK B spécialise bien qui_suis_je()
C c;// Erreur C ne spécialise pas qui_suis_je() donc conserve le caractère abstrait de A
D d; // OK D ne spécialise pas qui_suis_je() mais bénéficie de la spécialisation de cette méthode par B.
return 0;
} |
Comme le destructeur peut être virtuel, il peut aussi être abstrait :
1 2 3 4
| class A{
public:
virtual ~A()=0;
}; |
C'est souvent le meilleur moyen d'indiquer qu'une classe est abstraite :
-> car ta classe peut ne pas avoir besoin de spécifier d'autres méthodes abstraites
-> car si les autres méthodes de la classe peuvent varier en fonction de l'évolution du projet (par exemple, on décide de ne plus la rendre virtuelle), le destructeur reste stable. Et on ne risque pas de perdre cette info suite à une inattention dans une évolution.
Deux derniers points :
-> Même si les méthodes abstraites peuvent avoir une définition (un corps), en général ce n'est pas le cas. On se rend compte rapidement qu'il s'agit souvent d'une conception bancale.
-> Exception au dernier point : le destructeur même abstrait doit être défini :
1 2 3 4 5 6
|
class A{
public:
virtual void qui_suis_je()const = 0; // pas obligé de le définir
virtual ~A()=0{} // obligé de le définir
}; |
Partager