vendredi 16 décembre 2011

Rappel sur le C++ pour les habitués de Java et C#

Ce guide est composé de 3 sections. Nous parlerons d’abord de comment marche le C++, puis nous passerons à la gestion de la mémoire et enfin nous verrons un certain nombre de principes de développement qui sont adaptés à l’entreprise mais qui posent des problèmes sur l’environnement mobile.
Ceci n’est évidemment pas un guide complet sur le C++, juste quelques rappels et notions complémentaires pour pouvoir aborder le C++ par un débutant ou un développeur Java.

Comment marche le C++

En général, lorsqu’on veut créer une classe en Java, il suffit d’ajouter un fichier Java et définir la classe. En C++, vous devez définir la classe avant de pouvoir l’utiliser. Vous faites cela en incluant un fichier header dans votre code en utilisant #include ‘NomDuHeader.h’. Le header est littéralement inclut dans le code source avant la compilation. Ceci induit une problématique qui n’existe pas dans les environnements interprétés (C# ou Java). Si vous avez 2 classes Alpha et Beta, vous pouvez les inclure dans votre nouvelle classe Gamma comme suit :
#include "Alpha.h"
#include "Beta.h"
Par contre si Beta.h inclue Alpha.h, vous aurez une erreur de compilation dans Gamma. Ceci parce que le préprocesseur insert le code depuis Alpha.h, puis insert le code depuis Beta.h, qui à son tour insert le code depuis Alpha.h. Le compilateur va indiquer qu’Alpha a déjà été défini. Afin d’éviter cela, au début du header, vous devez ajouter une protection d’inclusion. Ceci est une courte directive du préprocesseur qui indique au compilateur de ne pas réinclure le header s’il a déjà été inclut. Dans Alpha.h, il suffit d’inclure un code semblable à ce qui suit :
#ifndef __ALPHA_H
#define __ALPHA_H
Quand vous créez une nouvelle classe dans MoSync, l’IDE va insérer ce code pour vous. Vous pouvez lire ceci comme ‘S’il n’y a pas de variable nommée __ALPHA_H, alors créez une variable nommée __ALPHA_H’. A la fin de du code, il y’aura un #endif afin terminer le bloc. Vous pouvez changer la convention de nommage si vous voulez, mais si vous créez un header vous-même, n’oubliez pas d’ajouter la protection d’inclusion.
Si vous voulez utiliser des classes sans spécifier leur espace de nommage (namespace) à chaque fois. Utilisez la commande suivante :
using namespace <namespace>;
Notez le point-virgule à la fin. Ceci est une commande C++ et non une commande du préprocesseur (qui commence par #)
Vous pouvez créer vos classes dans un espace de nommage dans le fichier header :
namespace MyNamespace
{
      …
};
A l’intérieur des déclarations d’espaces de nommage, vous pouvez créer une ou plusieurs classes (ou structures).
class MyClass
{
      …
};
A l’intérieur d’une classe, vous pouvez créer des fonctions. La visibilité des fonctions et variables membres est définie par groupes. Une déclaration de classe dans un fichier header est comme suit :
Class Gamma
{
public :
Gamma() ;
virtual ~Gamma() ;
void incrementCounter() ;
int getCounter();
protected:
      int mCounter;
private:
      bool isActive;
};
Le destructeur permet de libérer la mémoire et effectuer tout traitement de nettoyage.
La définition de classe se fait dans un fichier .cpp. Voici l’exemple d’une définition de classe Gamma.cpp :
#include "Gamma.h"
Gamma::Gamma()
{
mCounter = 0;
}
Gamma::~Gamma()
{
}
void Gamma::incrementCounter()
{
mCounter++;
}
int Gamma::getCounter()
{
 return mCounter;
}
Remarquez que chaque fonction commence avec Gamma::. Ceci implique que vous pouvez implémenter plusieurs classes dans le même fichier .cpp, mais ceci n’est pas recommandé. Aussi, comme vous l’avez remarqué, lors de la construction, mCounter est initialisé à 0. Il y’a un autre moyen d’accomplir cela qui est préféré en C++ pour 2 raisons. Le constructeur sera comme suit :
Gamma::Gamma() : mCounter(0)
{}
mCounter sera également initialisé à la valeur de 0. Ceci peut être important dans les environnements où il peut être possible d’accéder à une classe avant que le que constructeur ait fini son exécution. Aussi, si votre classe définit des références, elles ne peuvent pas être nulles.
Si la classe Gamma a besoin d’objets ou fonctions, incluez le fichier dans le cpp et non le header. C’est une bonne règle de n’ajouter que les includes dont vous avez besoin, et si la classe est utilisée en interne exclusivement, ajoutez l’inclusion dans le cpp.

Héritage

C++ supporte l’héritage multiple. C’est la dérivation de plusieurs classes de base (non interfaces). Quand vous spécifiez une classe, vous spécifiez les classes desquelles vous avez hérité ainsi que la visibilité de l’héritage :
class Delta : public Alpha, public Beta, private Gamma
Les interfaces en C++ sont juste des classes, il n’y a pas de déclaration d’interface. Vous créez une classe pour héritage avec des fonctions virtuelles.
class Alpha
{
public:
virtual void doSomething() =  0;
};
Les fonctions virtuelles peuvent être surchargées, mais c’est pas la peine d’utiliser la commande ‘override’.
class Beta : public Alpha
{
public:
void doSomething();
};
L’implémentation de doSomething dans Beta sera utilisée, vu qu’il n’y a pas d’implémentation dans Alpha. Ceci est appelé une fonction pure et peut être identifié par =0.

Objets, pointeurs et références

L’un des avantages des environnements où la mémoire est gérée par runtime comme Java et .net est que vous n’avez pas à vous soucier si les objets sont créés dans le heap ou dans le stack.
En guise de rappel, la mémoire stack est locale dans la portée du code où elle a été créée. La mémoire heap est partagée dans toute l’application. Vous devez savoir quand vous créez un objet s’il est dans le stack ou dans le heap. Les objets dans le heap doivent être détruits explicitement avec la commande delete. Les objets dans le stack seront détruits dès qu’ils sortent de leurs portées. La règle en C++ en général est de ne créer des objets dans le heap que si c’est vraiment nécessaire. La création d’objets dans le heap peut être bien plus lente que dans le stack. Vous créez un objet dans le heap si vous voulez par exemple contrôler sa durée de vie.
Pour créer un objet dans le stack, il suffit de le déclarer.
String myString;
Pour créer un objet dans le heap, vous devez utiliser new
String* myString = new String();
L’utilisation de * signifie que vous avez créé un pointeur vers un objet String. C’est l’adresse dans la mémoire du début de l’objet pointé.
Une référence est une adresse comme le pointeur, mais elle ne peut pas avoir la valeur null. Les références sont déclarés avec un &.
String* myString = NULL; //C’est OK
String& myString; //Va causer une erreur de compilation
Les références sont particulièrement utiles pour passer des objets dans le stack.
String& Gamma::getHeadline()
{
      return myString; 
}
Dans cet exemple, myString a précédemment été créé comme un objet dans le stack. getHeadline() va retourner une référence à myString et non l’objet en entier.
String& headline = gamma.getHeadline();
Quand vous accédez à des fonctions d’un objet, vous devez savoir si c’est un objet, référence ou pointeur. Les fonctions et membres d’un objet ou référence sont accessibles en utilisant :
myString.clear();
Les fonctions et membres d’un pointeur sont accessibles en utilisant ->
myString->clear();
Vous pouvez convertir entre pointeur et références en utilisant *
String* myString = new String();
myString->append("hello world", 11);
String& strref = *myString;
strref.clear();
Vous pouvez créer des pointeurs vers des objets même si ceux-ci sont dans le stack.
int Gamma::getStringLength(String* testString)
{
      return testString->length();
}
String hello = "Hello World";
int strLen = getStringLength(&testString);

Gestion de la mémoire

La possibilité de créer des objets dans le stack et dans le heap, ainsi que l’absence d’une libération de mémoire automatisée, implique qu’il faut gérer la durée de vie de chaque objet que vous créez. Quand vous créez un objet dans le stack, sa durée de vie est égale à sa portée.
Par exemple, si vous créez un objet dans une fonction
void Gamma::doSomething()
{
// String sera créé ici
String myString;
// String sera automatiquement détruit et la mémoire libérée parce qu’on a atteint la fin de la fonction qui est la fin de la portée de l’objet
}
Les objets encapsulés expirent également avec l’expiration de l’objet contenant
class Gamma
{
public:
Gamma();
virtual ~Gamma();
private:
String mString;
};
Ici, il y’aura un objet appelé mString dans le stack. Il sera créé quand une instance de Gamma sera créée et détruit dès que cette instance sera détruite. Vous n’avez pas à intervenir manuellement.
Vous devez être plus prudent quand vous créez des objets dans le heap. Ils ne seront pas automatiquement détruits quand vous aurez atteint la fin de la portée de la fonction. Les objets dans le heap sont justement intéressants à cause de cela.
Il y’a un design pattern qui s’appelle RAII (Resource Allocation Is Initialisation). Ce pattern recommande de créer les objets dont vous avez besoin dans le constructeur et les détruire dans le destructeur.
//Constructeur
Gamma::Gamma()
{
// Instanciez les objets don’t vous avez besoin dans le heap
myString = new String();
}
// Destructeur
virtual Gamma::~Gamma()
{
delete myString;
}
Ceci signifie que même si myString sera créé dans le heap, lorsque Gamma sera hors portée myString sera détruit.
Il y’a plusieurs cas où cette approche n’est pas appropriée. Vous pouvez avoir une application qui a besoin d’envoyer des messages complexes entre des classes. Chaque message est de classe Message
class Message
{
public:
void setMessage(const char* message);
const char* getMessage();
void setStatus(int status);
int getStatus();
private:
String mMessage;
int mStatus;
};
Nous pouvons voir que lorsque Message est instancié, un int et un String sont créés dans le stack.
La classe Gamma doit envoyer des messages à Delta. Delta a la fonction suivante :
void Delta::receiveMessage(Message* message)
{
// J’ai reçu message. Mais il peut être déjà détruit dans le stack
}
Les objets sont très puissants, mais ils peuvent être dangereux. Voici une liste à apprendre par cœur :
·         Essayez toujours de créer des objets dans le stack, pas le heap. C’est plus rapide et moins dangereux.
·         Normalement, la classe qui a créé un objet doit le détruire
·         A chaque fois que vous écrivez le mot new, pensez où le mot delete doit être. Il doit être quelque part ou vous aurez une fuite de mémoire.
·         Quand vous déléguez la responsabilité de détruire un objet (à une autre classe par exemple), insérez un commentaire pour indiquer qui s’en occupe.
·         Vous devez avoir une seul destruction. Si vous détruisez un objet 2 fois, vous aurez une erreur lors de l’exécution.

Aucun commentaire: