Designing Classes
Table de Matière
Les Classe C++
Pouqu’oi OOP
La programmation Orienté objecté offrent plusieurs avantages comparés à celle de l’approche fonctionnelle.
- Décapsulation des données.
- Héritage.
- Polymorphisme.
- Modularité et sécurité des données.
Afin d’illustrer ces deux approches, on va considérer un code haut niveau
pour
le traitement des images.
Une image ( en grille) est caractérisée par son nombre de lignes (height
) and
colonnes (width
). Chaque pixel de cette image est une valeur qui représente le
niveau de gris.
Approche procédurale.
void load_image(string &filename, vector<int> & data, int & rows, int & cols)
{
....
}
void save_image(string &filename, vector<int> & data, int & rows, int & cols)
{
...
}
bool isEmpty(vector<int> & data, int rows, int cols)
{
if( data.size() == 0 )
return true;
}
Si on veut utiliser ces fonctions, voici un aperçu d’une simple fonction main
int main()
{
//Data of the image
vector<int> data;
int rows;
intn cols;
//loading the image
load_image("Lena.png", data, rows, cols);
//checking if the data
if (! isEmpty(data, rows, cols) )
{
//do something
}
//Saving the new image
save_image("new_image.png", data, rows, cols);
}
Approche OOP:
Le code listé représente plusieurs erreurs de représentation surtout pour un
langage comme c++
. On peut mentionner les problèmes suivants:
- A chaque fois qu’on veut se référer à une image, on doit mentionner les trois
données:
data
,rows
etcols
.
-
On peut facilement se tromper dans le passage d’arguments.
- Aucune sécurité dans les données, on peut facilement remplacer le vecteur
data
et passer pour une autre image.
Comparons maintenant à une nouvelle approche qui utilise l’approche OOP
.
class Image{
//private data
private:
vector<int> data_;
int rows;
int cols;
//Public part
public:
Image(); //Constructor
Image(string &filename);
~Image(); //Destructor
//methods
void save(string &filename);
bool empty();
};
La fonction main
devient plus simple:
int main()
{
//Creating Image
Image lena("lena.png");
//do something
if ( lena.empty() )
{
....
}
//Saving the image
lena.save("new_lena.png");
}
Elements of a class
- Une classe est utilisée pour encapsuler des données avec un ensemble de méthodes pour leur traitements.
- Une classe définit un nouveau type de donnée. Voici la structure générale d’une classe en C++.
class NewType {
//Constructor Desctuteurs
public:
NewType();
~NewType();
// Member functions
public:
void MemberFunction1();
void MemberFunctions2() const;
static void StaticFunction();
//Operators
public:
NewType &operator+=(const NewType & other);
friend ostream &operator<<(ostream &os, const NewType &obj);
//Private data
private:
int a_;
vector<float> data_;
OtherType member_;
};
Ainsi on peut lister les éléments d’une classe comme suit:
Attributs
: Définit l’état d’un objet.- Appelés aussi member variables.
- Doivent être privés à l’objet.
- Chaque objet possède sa propre copie.
Méthodes
: Ensemble de fonctions ou comportement d’un objet.- Appelés aussi
member functions
. - Chaque objet crée possède sa propre copie.
- Ces méthodes peuvent accéder directement aux attributs.
- Appelés aussi
Constructor/Destructor
: Fonction spéciales pour initialiser et détruire les objets.- Création et initialisation des attributs.
- Destruction de ressources prises par un objet (Mémoire, fichier, …)
-
Operateurs
: Partie pour surchager les opérateurs de base.- Souvent utilisés définir les opérateur de bases comme +.
- Amplement encouragés puisque ils facilitent la conception du code.
Constructeurs
Les constructeurs méritent une discussion détaillée en c++.
- Une classe doit posséder au moins un constructeur.
- Une classe possède un unique destructeur.
- Constructeurs:
- Des fonctions qui ne possèdent pas de valeurs de retour.
- Possède le même nom que la classe.
- On peut définir plusieurs constructeurs.
- Si on définit aucun constructeur, C++ génère un par défaut.
- Destructeur:
- Fonction qui porte le nom
~ClassName
. - Appelé automatiquement lorsque un objet est détruit.
- Aussi généré explicitement par c++ si aucun destructeur n’est définit.
- Fonction qui porte le nom
class SomeClass
{
public:
SomeClass (); //Default constuctor
SomeClass(int a); //Custom constructor
SomeClass(int a, float b); //With two values
~SomeClass (); //Destructor
};
int main(int argc, char *argv[])
{
SomeClass var_1; //Call default
SomeClass var_2(2); //Second constructor
SomeClass var_3(1, 4.2); //third constructror
//Also recommended unifrom initializer
SomeClass var_4{};
SomeClass var_5 = {10, 10.0};
return 0;
}
Structure d’une classe
Quand on veut construire une classe en c++, on fournit une interface
à
l’utilisateur qui décrit les détails de celle ci. L’utilisateur n’as pas besoin
d’accéder à l’implémentation de ces méthodes.
- L’interface d’une classe est mise dans un header. e.g
class_name.h
. -
Le client accède à cet entête pour obtenir les déclarations des méthodes.
- Le fichier source est réservé dans un fichier
class_name.cpp
. - Ces fichiers ne sont pas partagés avec l’utilisateur.
- Ils sont délivrés à l’utilisateur dans une forme compilée.
Structure d’un fichier entête.
#pragma once
class className {
public:
ClassName(); // constructor
returnType func1(paremeters); //member functions
returnType func2(parameters);
returnType func3(paremters);
private:
type var1; //member variables
type var2;
};
-
pragma once
: assure que ce fichier est inclut qu’une seule fois. -
Le client peut utiliser tous les méthodes par:
MyClass a;
a.func1(arguments);
- Le client par contre ne peut pas accéder aux données privées.
MyClass a;
a.var1 = 2; //Génère une erreur
Pour les différents modes d’accès à une variable, on possède:
public
: Accessible par tout dans un programme.protected
: Accessible seulement à l’intérieur de la classes et ceux qui héritent de celle ci.private
: Accessible uniquement à l’intérieur de la classe.
Classes and pointers
Objet pointeur
Dans cette section, nous discutons la relation qui existe entre les classes ( structures) et les pointeurs. Les facteurs qui changes sont:
- L’opérateur d’accès aux membre et aux
fonctions
. - Gestion de mémoire avec l’opérateur
new
.
- On suppose qu’on dispose d’un classe
Point
avec deux attributs qui sont l’abscisse et l’ordonnée. - Cette classe Possède deux constructeurs:
- Un constructeur par défaut.
- Un constructeur par deux arguments.
- Une méthode
move
pour déplacer cet objet. - Une fonction
Print
pour afficher le point.
class Point
{
public:
Point();
Point(float x, float y);
~Point();
void move(float dx, float dy);
private:
float x;
float y;
}
Supposons maintenant qu’on cherche a manipuler un Point par un pointeur.
- Dans un premier exemple, on suppose que le Point est crée normalement.
Remarquer l’utilisation de l’opérateur
->
au lieur de.
pour accéder aux attributs et méthodes.
Point A; //Default constructor
Point B(2., 3.4); //Classical constructor
//Creating a pointer of type Point
Point* P = nullptr;
//Pointing on A;
P = &A;
cout<<"P point on A in: "<<endl;
P->Print();
//Manipulating A with P ( -> )
cout<<"Moving P by (2, 1)"<<endl;
P->move(2, 1);
//Print A state
cout<<"Now A is on : "<<endl;
P->Print();
//changing P adress
P = &B;
cout<<"Now P point on B"<<endl;
B.Print();
//Moving
cout<<"Moving B by (-1,1)"<<endl;
P->move(-1,1);
P->Print();
- Dans le deuxième exemple, on va créer nous même le Point avec l’opérateur
new
.
cout<<"Creating a new empty point"<<endl;
P = new Point; // Calling new on on default argument
P->Print();
delete P;
//Pointer with no default
cout<<"Creating a Point on position (4, 5)"<<endl;
P = new Point(4, 5); //New with classical
P->Print();
delete P;
//Creating an array of pointers
Point * arr = new Point[5];
//Printing the content of the arry
for( P = arr; P - arr < 5; P++)
{
P->Print();
cout<<" ";
}
cout<<endl;
delete [] arr;
Pointeur Implicit (this)
Comme vous avez appris en Java
, on peut utiliser l’argument implicite dans
chaque méthode. Cette variable se réfère à l’objet courant.
En java la variable this est un pointeur.
Voici un exemple qui utilise cette variable pour initier les instances de la classe Point.
Point( float x, float y)
{
//Utilisation this pour faire la différence avec celle x (argument)
this->x = x;
this->y = y
}
Operator Overloading
En C++, on peut implémenter différents opérateurs
pour les types définies
par l’utilisateur. Ceci est connu par Operator Ovearlong
Prenons par exemple la classe Point
, pour afficher un point, on est étais
toujours ramener à appeler cette fonction pour
- afficher un point.
- Ajouter un autre point.
Point A;
Point B(3, 4);
A.Print(); //Afficher
A.add(B); //Ajouter B
Une manière plus simple et qui est encouragée dans la philosophie de C++ est de surcharger les opérateurs de base pour réaliser ces opérations.
Imaginons qu’on puisse écrire:
Point A;
Point B(3, 4);
cout<< A ; //Afficher
A += B; //Ajouter B
Ceci est réalisable en définissant des méthodes spéciales qui possède le mot
operator
.
On définit cette méthode à l’intérieur de la classe avec la syntaxe suivante:
class clasName {
... ... ...
public:
returnType operator symbol (arguments) {
...
}
};
- returnType: est le type de retour de cet opérateur.
- symbol : est le symbole qui représente cet opérateur. Comme
+
,<
++
. - arguments: La listes des arguments pour cette fonction.
Pour des opérateurs, on remarque deux classes, ceux qui sont unaires (utilise un seul élément comme ++), ou binaire (utilisant deux élément comme +).
Unary operators
Les opérateurs unaires s’appliquent à une seule instance de la classe. Exemples de ces opérateurs sont:
++
,--
: L’opérateur d’incrémentation et décrementation.+=
: Incrémentation avec une valeur.<<
: Flux de sortie.
Pour illustrer ce concept, nous allons considérer l’exemple d’une classe simple counter
qui
stocke le nombre d’itération d’un programme.
// Class definition
class Counter {
public:
Counter() : count_(0){};
// opeartors
public:
void operator++ () //Prefix increment (++var)
{
count_ ++;
}
void operator++ (int) //Postfix incrment (var ++)
{
count_ += 2;
}
void operator+=(int value) //increment with value +=
{
count_ += value;
}
private:
int count_;
//Friends
friend ostream & operator<<(ostream&, const Counter &);
};
ostream & operator<<(ostream & out, const Counter & C)
{
out<< "Counter("<< C.count_<< ")";
return out;
}
Remarquer la différence entre les deux opérateurs d’incrémentation.
- L’opérateur d’incrémentation normale
++var
est déclaré normalement. - Pour la différence avec le post incrémentation
var++
, remarquer l’ajout d’une variableint
dans les arguments. -
L’opérateur de surchage doit être déclarer à l’extérieur de la classe.
- Ainsi, si on veut que cet opérateur puisse accéder aux attributs privés, il
doit être déclaré comme
fonction amie
.
friend ostream & operator<<(ostream& , const Counter &);
Avec cette déclaration, on peut utiliser facilement comme suit:
int main() {
Counter c;
// Augmenter la valeur de c par 1
++c;
cout<< c<<endl;
// Augmenter la valeur de c par 4
c += 5;
cout<< c << endl;
return 0;
}
Binary operators
Pour illustrer les opérateurs binaires, on invoque un classique qui est
celui d’une classe de nombre complexes. Le but est d’implémenter tous les
opérateurs mathématiques +,-, *, /
pour cet classe.
class Complex
{
public:
Complex( double r =0, double im= 0):real_(r),imag_(im){};
//operators
Complex operator+(Complex & other)
{
return Complex(this->real_ + other.real_,
this->imag_ + other.imag_);
}
friend ostream& operator<<(ostream&, const Complex &);
private:
/* data */
double real_;
double imag_;
};
ostream& operator<<(ostream& out, const Complex & C)
{
out <<"("<< C.real_ <<"+ "<<C.imag_<<"j)";
return out;
}
int main(int argc, char *argv[])
{
Complex C1(2, 1);
Complex C2(4, 3);
//Ajouter C1 et C2
auto C3 = C1 + C2;
cout<< C1 <<" + " << C2 << "= "<< C3 <<endl;
return 0;
}
Remarquer que l’appel de l’instruction ` C3 = C1 + C2` implicitement invoque l’appel de l’opérateur comme illustré dans la figure:
Templates
les templates permettent d’écrire valide pour différents types. Cela permet d’éviter de réecrire la même fonction pour chaque type.
L’avantage majeur des templates est de permettre d’écrire un code qui est a valide pour différents type. Etant donné que le type respect les opérateur introduits dans ces fonctions.
Templates functions
Prenons l’exemple simple d’une fonction add
qui calcule simplement la somme
de ces deux arguments.
int add(int a, int b) {
return a + b;}
Supposons qu’on possède la fonction main suivante:
int main()
{
auto a = 2;
auto b = 4;
cout<< add(a, b) << endl;
return 0;
}
Le programme se compile et va afficher la valeur de la somme \(5\).
Supposons maintenant qu’on possède d’autre valeurs mais qui sont des doubles.
auto c = 4.2;
auto d = 3.2;
cout << add(c, d) << endl;
Selon les règle de conversion implicite ce programme va se compiler et il va afficher la valeur 7.
Mais supposons qu’on veut réaliser la somme réelle qui garde la partie flottante. On serait ramener à écrire une deuxième fonction réservée juste pour les double.
double add( double a, double b)
{
return a + b;
}
Imaginons maintenant que veut réaliser cette opération, mais entre des
string
!. On doit aussi développer une troisième fonction qui répète le
même code.
Pour éviter ces répétitions, on peut écrire une seule fonction
template qui peut
générer une version compilé de tous les types. La syntaxe des template est la
suivante:
template < class T > // T est un type abstrait
//Déclarer la fonction normalement en utilisant T comme type
Pour une meilleure visibilité, on met la fonction add
pour int
et sa version
template.
//Version int
int add(int a, int b)
{
return a + b;
}
//Version template
template <class T>
T add( T a, T b)
{
return a + b;
}
Avec cette version on peut appeler la même fonction pour tous les types qui
accepte l’opérateur +
.
int main()
{
cout << add(3, 4) << endl; //Call pour int
cout << add(3.4, 4.5) <<endl; //Call pour double
cout << add(string("EU"), string("MF")) << endl; //string
vector<int> v1(5, 1);
vector<int> v2(5, 2);
cout << add(v1, v2) << endl // A votre avis?
}
Templates classes
Similaire aux fonctions, on peut déclarer des classes templates
qui dépdendent
d’un (ou plusieurs) types.
La syntaxe reste la même, ou il faut précéder la déclaration par
template<class T>
//Définition de la classe
Une classe template qu’on utilise intensivement est celle vector<int>
.
Supposons qu’on nous demande ( D’ailleurs vous allez programmer ceci dans votre
homework) de coder cette classe:
template <class T>
class vector
{
public:
vector();
vector(int size);
vector(int size, T value);
~vector();
int size()const;
T & operator[] (int i)const;
private:
T * data_= nullptr;
int size_;
};