| auteur : Aurélien Regat-Barrel |
Quand une fonction membre (non statique) d'une classe ne modifie pas cette dernière, il est judicieux en C++ de la rendre constante en ajoutant le mot-clé const à la fin de son prototype. Cela rappelle que cette fonction ne modifie et ne doit pas modifier l'objet ce qui permet de l'utiliser sur des objets constants en plus d'aider le compilateur à effectuer des optimisations.
class Test
{
public :
std:: string F () const
{
return " F() const " ;
}
std:: string F ()
{
return " F() " ;
}
} ;
int main ()
{
Test t1;
cout < < t1.F () < < ' \n ' ;
const Test & t2 = t1;
cout < < t2.F () < < ' \n ' ;
}
|
Dans l'exemple précédent, si la fonction membre F() const n'existait pas, on n'aurait pas pu appeler F() sur l'objet t2.
Notez que le fait d'avoir rajouté le mot clé const a provoqué une surcharge de la fonction F() au même titre qu'une surcharge effectuée avec un nombre de paramètres différents.
|
| auteurs : LFE, Aurélien Regat-Barrel |
Une fonction membre déclarée static a la particularité de pouvoir être appelée sans devoir instancier la classe.
Elle ne peut utiliser que des variables et des fonctions membres static elles aussi, c'est-à-dire qui ont une existence en dehors de toute
instance.
class A
{
public :
int var1;
void f1 () { } ;
static int var2;
static void f2 () { } ;
} ;
int A:: var2 = 0 ;
int main ()
{
A a;
a.var1 = 1 ;
a.f1 ();
A:: var2 = 1 ;
A:: f2 ();
}
|
|
| auteur : Laurent Gomila |
On parle de masquage de fonction lorsqu'on définit dans une classe dérivée une fonction de même nom qu'une fonction d'une classe
de base, mais avec un prototype différent. Voici un exemple qui illustre ce problème :
# include <iostream>
# include <string>
struct Base
{
void F (int );
} ;
struct Derivee : Base
{
void F (std:: string);
} ;
Derivee d;
d.F (" salut " );
d.F (5 );
|
Dans cet exemple, la fonction F de la classe de base n'est non pas surchargée mais masquée, ce qui signifie qu'elle n'est plus
accessible dans la classe dérivée. Pour palier ce problème il suffit d'utiliser la directive using pour réimporter la
fonction masquée dans la portée de la classe dérivée :
struct Derivee : Base
{
using Base:: F;
void F (std:: string s);
} ;
Derivee d;
d.F (" salut " );
d.F (5 );
|
On peut également régler le problème en spécifiant explicitement lors de l'appel d'où vient la fonction que l'on souhaite utiliser :
|
| auteur : Laurent Gomila |
En C++, il est possible de passer des pointeurs de fonctions en paramètre d'autres fonctions. Mais peut-être aurez-vous remarqué que le compilateur râle parfois lorsque vous essayez de passer un pointeur sur fonction membre. Voici un exemple courant, la création de threads (sous Windows) :
DWORD WINAPI Fonction1 ( void * Param )
{
return 0 ;
}
DWORD WINAPI MaClasse:: Fonction2 ( void * Param )
{
return 0 ;
}
MaClasse Param;
CreateThread ( NULL , 0 , Fonction1, & Param, 0 , NULL );
CreateThread ( NULL , 0 , & MaClasse:: Fonction2, & Param, 0 , NULL );
|
Pourquoi ce code ne compile pas avec une fonction membre ? Parce que le type de Fonction1 et MaClasse::Fonction2 n'est pas le même.
La fonction globale Fonction1 a pour type DWORD (*)(void*).
La fonction membre Fonction2 a pour type DWORD (MaClasse::*)(void*).
On comprend facilement cette différence, étant donné que Fonction2 aura besoin d'une instance de MaClasse pour être appelée, au contraire de Fonction1 qui pourra être appelée "librement". A noter que le type des fonctions membres statiques peut être assimilé à celui des fonctions globales, puisque celles-ci peuvent être également appelées sans instance de la classe.
Ainsi pour contourner le problème, il faudrait (par exemple) procéder ainsi :
class MaClasse
{
public :
static DWORD WINAPI StaticThreadFunc ( void * Param )
{
MaClasse* Obj = reinterpret_cast < MaClasse* > ( Param );
return Obj- > ThreadFunc ();
}
private :
DWORD ThreadFunc ()
{
return 0 ;
}
} ;
MaClasse Param;
CreateThread ( NULL , 0 , & MaClasse:: StaticThreadFunc, & Param, 0 , NULL );
|
A noter qu'on peut tout à fait demander à une fonction de recevoir comme paramètre un pointeur sur fonction membre, il suffit d'indiquer le bon type, comme expliqué ci-dessus.
|
| auteur : Laurent Gomila |
En C, il est possible de déclarer une fonction acceptant un nombre de paramètres variables via "..." (c'est ce qu'on appelle l'ellipse).
L'exemple le plus connu est celui de la fonction d'affichage printf.
En C++ il est bien entendu toujours possible d'utiliser cette méthode mais il y a mieux : le chaînage d'appels. Les avantages sont
multiples :
- Typage beaucoup plus fort.
- Pas besoin de manipuler des macros bizarroïdes pour récupérer les paramètres.
- Pas besoin d'indication supplémentaire pour marquer le nombre et le type des paramètres.
- Beaucoup plus simple à écrire, et plus flexible.
Cette méthode est intensivement utilisée par exemple pour manipuler les flux standards :
# include <iostream>
std:: cout < < x < < y < < z;
|
On peut également imaginer d'autres formes de chaînages pour d'autres applications :
# include <vector>
class Polygon
{
pulic :
Polygon& Add (int x, int y)
{
Points_.push_back (Point (x, y));
return * this ;
}
private :
std:: vector< Point> Points_;
} ;
Polygon Poly;
Poly.Add (1 , 2 )
.Add (5 , 8 )
.Add (15 , 19 )
.Add (0 , 54 );
|
# include <sstream>
# include <string>
class StringBuilder
{
pulic :
template < typename T>
StringBuilder& operator ()(const T& t)
{
std:: ostringstream oss;
oss < < t;
String_ + = oss.str ();
return * this ;
}
private :
std:: string String_;
} ;
StringBuilder s;
s (" salut j'ai " )(24 )(" ans " );
|
Comme vous le voyez, ce qui rend possible le chaînage est le renvoi de l'instance courante par la fonction. Ainsi, Poly.Add(x, y) renvoie
Poly, sur lequel on peut de nouveau appeler Add etc...
|
| auteur : Laurent Gomila |
Par défaut les paramètres de fonctions sont passés par valeur, c'est-à-dire que c'est une copie du paramètre passé qui est manipulée
par la fonction et non l'original. Cela peut paraître anodin lorsqu'il s'agit de passer un type de base, mais cela devient vite pénalisant
lorsqu'il s'agit d'une instance de classe dont la copie peut s'avérer coûteuse (par exemple un vector de string). Cela peut également être
un problème si l'on souhaite passer en paramètre une classe qui n'est tout simplement pas copiable (par exemple un flux standard). Pour
régler le problème, on utilise ainsi ce qu'on appelle le passage par référence ou par référence constante. En passant une
référence, on s'assure que c'est l'objet initial qui est manipulé dans la fonction et donc qu'aucune recopie indésirable n'est effectuée.
En passant une référence constante, on s'assure également que notre paramètre ne pourra pas être modifié par la fonction. Une bonne
habitude est donc de prendre tout paramètre non modifiable par référence constante, excepté les types primitifs. D'autant plus que cela n'a
strictement aucune autre conséquence, ni au niveau de la fonction ni au niveau de l'appelant.
# include <string>
# include <vector>
void FonctionPasOptimisee (std:: vector< std:: string> Tab)
{
}
void FonctionOptimisee (const std:: vector< std:: string> & Tab)
{
}
std:: vector< std:: string> v (5000 , std:: string (1000 , ' - ' ));
FonctionPasOptimisee (v);
FonctionOptimisee (v);
|
Attention à ne pas oublier le const si le paramètre n'est pas modifié dans la fonction : cela permet en effet de passer ce
que l'on appelle des temporaires non nommés.
# include <string>
# include <vector>
void FonctionIncorrecte (std:: string& s)
{
}
void FonctionCorrecte (const std:: string& s)
{
}
std:: string s1 = " salut " ;
std:: string s2 = " hello " ;
FonctionIncorrecte (s1);
FonctionCorrecte (s1);
FonctionIncorrecte (s1 + s2);
FonctionCorrecte (s1 + s2);
FonctionIncorrecte (" bonjour " );
FonctionCorrecte (" bonjour " );
|
Cette remarque vaut également pour les pointeurs :
void FonctionIncorrecte (char * s)
{
}
void FonctionCorrecte (const char * s)
{
}
FonctionIncorrecte (" bonjour " );
FonctionCorrecte (" bonjour " );
|
|
| auteur : LFE |
Définir dans une classe une fonction membre qui a le même nom qu'une fonction standard est possible, mais risque de poser problème lors de
l'utilisation de cette fonction membre à l'intérieur de la classe. La fonction membre masque la fonction standard.
Il reste toutefois possible d'utiliser la fonction standard en faisant précéder son nom de l'opérateur de résolution de portée ::.
class MaClasse
{
int abs (int x);
}
int MaClasse:: abs (int x)
{
return :: abs (x);
}
|
|
| auteur : Aurélien Regat-Barrel |
En C++, en général, on évite d'utiliser les fonctions callback au profit d'alternatives un peu plus "objet"
(tel les foncteurs par exemple). Cependant, on est parfois obligé d'y recourir,
typiquement pour s'interfacer
avec une autre bibliothèque écrite en C. Si tel est votre cas, il vous faut alors être prudent et veiller à ce
que votre fonction callback C++ passée à la bibliothèque C ne lève pas d'exception, surtout si vous l'utilisez
déja compilée (dll). La raison est que les exceptions C++ ne sont pas supportée en C, et les conséquences d'une
exception levée dans votre callback C++ et remontant jusqu'au code C appelant peuvent étre fâcheuses. Et d'une
manière plus générale, les exceptions posent problème dès qu'il s'agit de franchir les limites d'un module
compilé (telle une dll), même entre differents modules développés en C++ (ABI incompatibles).
On peut toutefois préciser que sous Windows, un système d'exceptions
( Structured Exception Handling, ou SEH) est
integré au sein meme du système. Certains compilateurs l'exploitent, y compris en langage C
(au moyen de mots clés
spécifiques), ce qui permet à du code C d'être traversé sans problème par des exceptions lancées depuis un code C++,
si celles-ci ont été émises sous forme de SEH. Certains compilateurs s'appuient sur SEH pour implementer leurs
exceptions C++ (c'est le cas de Visual C++ par exemple), les rendant ainsi
compatibles avec n'importe quel autre code compilé. Consultez la
documentation de votre compilateur pour plus de détails.
|
| auteur : white_tentacle |
Parce qu'on vous a menti !
Cette pratique provient d'anciens langages de programmation, qui ne
disposaient pas de mécanismes élégants pour la gestion de la mémoire.
Ainsi, on n'avait coutume d'écrire des choses qui ressemblent à ça :
int f ()
{
char * my_string = (char * ) malloc (20 * sizeof (char ) );
int result;
if (error)
{
result = - 1 ;
goto end_f;
}
:end_f
free (my_string);
return result;
}
|
Ceci permettait de gérer toute la libération mémoire dans un seul
bloc de code. Mais pour passer dans ce bloc de code à coup sûr, il
était nécessaire d'utiliser, soit des imbrications de if à n'en
plus finir, soit des gotos. Les return multiples étaient sources de
nombreuses fuites mémoires, difficiles à diagnostiquer.
Heureusement, C++ possède un mécanisme très puissant, le destructeur.
Ainsi, le code précédent s'écrira :
int f ()
{
std:: string my_string;
if (error)
return - 1 ;
}
|
Plus besoin de gérer l'allocation mémoire, elle sera libérée par le
destructeur de std::string. Et si l'on est obligé d'allouer
avec new, C++ nous offre là encore un moyen simple de gérer
celà, std::auto_ptr :
int f ()
{
std:: auto_ptr< Obj> local_object = new CMyClass ();
}
|
local_object sera libéré (appel de delete) dès que
l'on sort de la portée de la variable.
Et pour les objets alloués avec new[] ? C++ ne fournit pas en
standard de moyen... sauf d'utiliser plutôt std::vector !
Et comme souvent, si ce n'est pas possible, Boost vient à la
rescousse avec boost::scoped_array.
Et pour les objets dont l'allocation est faite par une fonction
d'une bibliothèque, à libérer en appelant une autre fonction de
cette bibliothèque ? C++ nous offre la possibilité d'utiliser des
scope_guard, qui appelleront automatiquement la fonction de
libération lors de leur destruction.
Il n'y a donc de nos jours plus aucune raison de se priver de
la possibilité d'utiliser plusieurs instructions return, lorsque
cela apporte plus de clarté et de lisibilité au code.
|
lien : Comment créer et utiliser un tableau avec std::vector ?
lien : Qu'est-ce qu'un pointeur intelligent ?
lien : Comment gérer proprement des allocations / désallocations de ressources ? Le RAII !
lien :
Boost.SmartPtr : les pointeurs intelligents de Boost, par Matthieu Brucher
|
Consultez les autres F.A.Q.
|
|