Systèmes informatiques
Fermer ×

Le langage objet C++

Historique

Le langage C++, créé par Bjarne Stroustrup dans les années 80, est une extension du langage C qui ajoute le formalisme objet au langage C.

Comme le langage C, il a fait l'objet de plusieurs normalisations :

Outils et développement

Tous les exemples de programmes C++ ont été compilés avec gcc et make et testés sous linux.

On retrouve les mêmes outils que ceux utilisés pour le langage C en remplaçant gcc par g++.

Il faut également remplacer les extensions des fichiers .c essentiellement par .cpp ou .cxx. Il convient de modifier les fichiers Makefile en conséquence :

Exemple pour un seul fichier source

CC=g++
PROG=hello
OBJ=$(PROG).o
SRC=$(PROG).cpp
CFLAGS=-Wall
LDFLAGS=

all: $(EXEC)

$(EXEC): $(OBJS)
	$(CC) $(LDFLAGS) -o $(PROG) $^

$(OBJ):
	$(CC) $(CFLAGS) -c -o $@ $(SRC)

clean:
	rm -f $(OBJ) $(PROG)
				

Exemple pour plusieurs fichiers sources

CC=g++
CFLAGS= -Wall 
LDFLAGS=
EXEC=hello
SRC= $(wildcard *.cpp)
OBJS= $(SRC:.cpp=.o)

all: $(EXEC)

$(EXEC): $(OBJS)
	$(CC) -o $(EXEC) $^  $(LDFLAGS)

%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

clean:
	rm *.o $(EXEC)
				

Principe de la programmation objet

Afin de définir un objet, on va présenter quelques différences et nouveautés par rapport au langage C

Types

En C++, il existe un type booléen nommé bool et qui prend les valeurs true et false.

La syntaxe du transtypage du C est toujours acceptée, mais in existe une autre syntaxe de la forme nouveautype(variable).

Entrée et la sortie standard

Les accès à l'entrée standard scanf et à la sortie standard printf, sont remplacés respectivement par les flux standards cin et cout de l'espace de noms std, et définis dans iostream (on ne précise plus l'extension .h).

Un exemple classique

#include <iostream>

int main(int argc, char **argv) {
	std::cout << "Bonjour le monde"  << std::endl; ;
	return EXIT_SUCCESS;
}

Pour préciser l'espace de noms, on peut soit précéder chaque nom de cet espace en utilisant l'opérateur de portée ::, soit inclure l'espace ce noms avec la directive using namespace.

endl permet d'ajouter un retour à la ligne suivante et de vider le buffer de sortie.

#include <iostream>

using namespace std;

int main(int argc, char **argv) {
	cout << "Bonjour le monde"  << endl; ;
	return EXIT_SUCCESS;
}
				

Les paramètres par adresse des fonctions

En C++, il n'est plus nécessaire d'utiliser les pointeurs, mais une référence (adresse) sur une variable en précédant le paramètre de la fonction dans la déclaration avec le symbole &, symbole qui n'est plus utilisé dans l'appel de la fonction.

Voir un exemple de transmission par référence, l'échange de variables
#include <iostream>

using namespace std;

void echange(int &a,int &b) {
	int tmp ;
	tmp = a;
	a = b;
	b = tmp;
}

int main(int argc, char **argv) {
	int x=2,y=3;
	cout << x << " " << y << endl;
	echange(x,y);
	cout << x << " " << y << endl;
	return EXIT_SUCCESS;
}
					

Le symbole & est utilisé dans la déclaration de l'entête de la fonction.

On n'ajoute aucun symbole lors de l'appel.

Pour l'affichage, on remarque que l'on envoie les données au fur et à mesure sur la sortie standard, on parle également de flot ou encore de sérialisation.

Allocation dynamique

On va également modifier la structure de pile précédente afin d'utiliser un tableau dynamique.

L'allocation dynamique se fait avec les opérateurs new pour l'allocation mémoire, et delete pour la libération mémoire.

Exemple, allocation libération mémoire pour un réel

double *ptr_valeur = new double; // allocation
// ... utilisation
delete ptr_valeur; // libération
				

Exemple, allocation libération mémoire pour un tableau de 10 réels

double *ptr_valeur = new double[10]; // allocation
// ... utilisation
delete []ptr_valeur; // libération
				

Extension des structures

En C++ les structures peuvent contenir des fonctions nommées fonctions membres, c'est à ce niveau que se situe le passage au langage objet.

Exemple de pile en modifiant celle qui est définie dans le chapitre sur les structures de données en C

Fichier de définitions Pile.h

#ifndef __PILE_H
#define __PILE_H

#define TAILLEMAX 10

struct Pile {
	int sommet;
	double tableau[TAILLEMAX];
	Pile();
	void empiler(double valeur);
	double depiler();
	bool pleine();
	bool vide();
} ;

#endif
				

On ajoute les fonctions empiler, depiler, pleine, et vide dans la structure. La fonction Pile_init de la structure C est remplacée par la fonction Pile() qui porte le même nom que celui de la structure et est appelé constructeur, elle sera appelée lors de la déclaration de la variable pile qui, ici, ne porte plus le nom de variable mais d'objet.

On ne parle pas, non plus, de déclarer une variable mais d'instancier un objet. La structure qui définit cet objet permet d'instancier plusieurs objets, est maintenant appelée classe d'objets

Tous les éléments de cette structure (donnees, fonctions membres) sont accessibles de la même façon que les éléments d'une structure C classique, on parle d'accès public.

Fichier de codage des fonctions membres Pile.cpp

#include "Pile.h"

Pile::Pile() {
	sommet = -1 ;
}

void Pile::empiler(double valeur) {
	if (sommet < (TAILLEMAX-1)) {
		tableau[++sommet] = valeur ;
	}
}

double Pile::depiler() {
	double valeur = 0 ;
	if (sommet > -1) {
		valeur = tableau[sommet--] ;
	}
	return valeur ;
}

bool Pile::pleine() {
	return (sommet == (TAILLEMAX-1)) ;
}

bool Pile::vide() {
	return (sommet == -1);
}
				
Voir un exemple de test de cette structure
#include <iostream>
#include "Pile.h"

using namespace std;

int main(int argc, char **argv) {
	double valeur ;
	Pile pile;
	for(int i=0;i<9;i+=1) {
		valeur = i ;
		pile.empiler(valeur);
	}
	if (pile.pleine()) {
		cout << "La pile est pleine" << endl;
	}
	else {
		cout << "On a encore de la place" << endl ;
	}
	while (!pile.vide()) {
		valeur = pile.depiler();
		cout << valeur << endl;
	}		
	return EXIT_SUCCESS;
}
					

On a une pile de 10 réels, on en empile 9, il reste une place.

Le résultat de l'affichage est

On a encore de la place
8
7
6
5
4
3
2
1
0
					

Ce résultat confirme bien que l'on a une structure de pile.

Les classes

Déclaration et définitions

La classe est une structure où toutes les propriétés et méthodes sont « privées » par défaut. Pour chaque classe :

Syntaxe générale de la déclaration dans le fichier .h

class nom_classe {
	prive:
		type propriete_prive;
		type_retour methode_prive(liste des types des parametres);
	public:
		nom_classe();//constructeur
		~nom_classe();//destructeur
		type propriete_public;
		type_retour methode_public(liste des types des parametres);
};
				

Syntaxe générale du codage des fonctions membres (ou méthodes) dans le fichier .cpp

nom_classe::nom_classe() { // constructeur
}

nom_classe::~nom_classe() { // destructeur
}

type_retour nom_classe::methode_prive(liste des parametres) {
}

type_retour nom_classe::methode_public(liste des parametres) {
}
				

Exemple avec la classe complexe qui permet de calculer les modules et arguments des nombres complexes

Fichier de définition Complexe.h

#ifndef __COMPLEXE_H
#define __COMPLEXE_H

class Complexe {
	private:
		double reel, imaginaire;
	
	public:
		Complexe(double,double);
		~Complexe();
		
		double Module(void);
		double Argument(void);
		double Re(void);
		double Im(void);
};

#endif
				

Les parties réelle et imaginaire sont déclarées privée, avec deux méthodes qui permettent d'obtenir ces valeurs (Re() et Im()), on a, ainsi, réalisé des propriétés accessibles uniquement en lecture.

Le constructeur permet de construire un nombre complexe à partir de la partie réelle et imaginaire

Les méthodes Module et Argument calculent et retournent, respectivement, les valeurs du module et de l'argument (en radian) du nombre complexe.

Fichier de codage des fonctions membres Complexe.cpp

#include <cmath>
#include "Complexe.h"

Complexe::Complexe(double r,double i) {
	reel = r;
	imaginaire = i;
}

Complexe::~Complexe() {}

double Complexe::Module() {
	return sqrt(reel*reel+imaginaire*imaginaire);
}

double Complexe::Argument() {
	return atan2(imaginaire,reel);
}

double Complexe::Re() {
	return reel;
}

double Complexe::Im() {
	return imaginaire;
}
				

Surcharge du constructeur et des méthodes

Cela revient à définir la même méthode avec des listes de paramètres différentes, on parle de signature différente.

Voir un exemple de surcharge du constructeur de la classe Complexe

Fichier de définition Complexe.h

#ifndef __COMPLEXE_H
#define __COMPLEXE_H

class Complexe {
	private:
		double reel, imaginaire;
	
	public:
		Complexe() ;
		Complexe(double,double) ;
		Complexe(const Complexe &) ;
		~Complexe();
		
		double Module(void);
		double Argument(void);
		double Re(void);
		double Im(void);
};

#endif
					

Le constructeur avec un paramètre de type Complexe est un constructeur de copie, il crée un nouvel objet complexe avec les valeurs du complexe transmis en paramètre. Ce paramètre est constant car il n'est pas modifié par le constructeur.

Une méthode peut être suivi du mot clé const et être ainsi déclarée constante, c'est à dire qu'elle ne modifie aucun élément de l'objet courant.

Un paramètre de méthode peut être également précédé du mot clé const qui signifie qu'il ne peut pas être modifié par la méthode.

Fichier de codage des fonctions membres Complexe.cpp

#include <cmath>
#include "Complexe.h"

Complexe::Complexe() {
	this->reel = 0;
	this->imaginaire = 0 ;
}

Complexe::Complexe(double reel,double img) {
	this->reel = reel;
	this->imaginaire = img ;
}

Complexe::Complexe(const Complexe &c) {
	reel = c.reel;
	imaginaire = c.imaginaire;
}

Complexe::~Complexe() {}

double Complexe::Module()  {
	return sqrt(reel*reel+imaginaire*imaginaire);
}

double Complexe::Argument()  {
	return atan2(imaginaire,reel);
}

double Complexe::Re()  {
	return reel;
}

double Complexe::Im()  {
	return imaginaire;
}
					

Surcharge des opérateurs

Réaliser une classe complexe qui ne calcule que les modules et arguments n'est pas suffisant, il faut pouvoir également réaliser les opérations arithmétiques sur ces nombres complexes. On peut bien sûr écrire les méthodes de calcul sur les complexes, mais le C++ offre plus en permettant d'utiliser les opérateurs sur les objets. Cela est rendu possible avec la surcharge des opérateurs.

Il y a deux catégories : les opérateurs qui ne nécessitent qu'une seule opérande comme +=, et les opérateurs qui nécessitent deux opérandes comme l'opérateur +. Ce dernier cas nécessite d'utiliser une méthode externe à la classe mais liée à la classe nommée méthode friend.

Voir un exemple de surcharge des opérateurs de la classe Complexe

Fichier de définition Complexe.h

#ifndef __COMPLEXE_H
#define __COMPLEXE_H

class Complexe {
	private:
		double reel, imaginaire;
	
	public:
		Complexe() ;
		Complexe(double,double) ;
		Complexe(const Complexe &) ; 
		~Complexe();
		
		double Module(void) ;
		double Argument(void) ;
		double Re(void) ;
		double Im(void) ;
		Complexe& operator+=( Complexe &);
		friend Complexe operator+( Complexe &,  Complexe &);
		friend std::ostream &operator<<(std::ostream &,const Complexe &);
};

#endif
					

La surcharge de l'opérateur += réalise l'addition du complexe courant avec un autre complexe, il ne nécessite qu'un seul paramètre. Il retourne une référence vers l'objet courant après avoir réalisé l'addition des valeurs de l'objet courant avec les valeurs de l'objet transmis en paramètre.

La surcharge de l'opérateur + nécessite deux paramètres de type complexe et retourne un nouvel objet complexe. Il s'agit d'une fonction friend qui n'appartient pas à la classe. La fonction réalise l'addition des valeurs des deux complexes transmis en paramètres et retourne un nouvel objet complexe en faisant appel au constructeur.

La surcharge de l'opérateur << pour l'affichage sur le flot de sortie, fait également appel à une méthode friend, car il nécessite deux paramètres qui sont l'objet complexe et l'objet de flot de sortie ostream. Il retourne la référence vers le flot de sortie après écriture des données sur ce flot.

Fichier de codage des fonctions membres Complexe.cpp

#include <cmath>
#include "Complexe.h"

Complexe::Complexe() {
	this->reel = 0;
	this->imaginaire = 0 ;
}

Complexe::Complexe(double reel,double img) {
	this->reel = reel;
	this->imaginaire = img ;
}

Complexe::Complexe(const Complexe &c) {
	reel = c.reel;
	imaginaire = c.imaginaire;
}

Complexe::~Complexe() {}

double Complexe::Module()  {
	return sqrt(reel*reel+imaginaire*imaginaire);
}

double Complexe::Argument()  {
	return atan2(imaginaire,reel);
}

double Complexe::Re()  {
	return reel;
}

double Complexe::Im()  {
	return imaginaire;
}
s
Complexe& Complexe::operator +=( Complexe &c) {
	reel += c.reel;
	imaginaire += c.imaginaire;
	return *this;
}


Complexe operator+(  Complexe &c1,  Complexe &c2) {
	double r1 = c1.Re();
	double r2 = c2.Re();
	double i1 = c1.Im();
	double i2 = c2.Im();
	return Complexe(r1 + r2 , i1 + i2);
}

std::ostream &operator<<(std::ostream &flot,const Complexe &c) {
	if (c.imaginaire >= 0) {
		flot << c.reel <<  "+" << c.imaginaire << "i" ;
	}
	else {
		flot << c.reel << c.imaginaire << "i" ;
	}
	return flot;
}
					
Voir un exemple de programme de test de la classe Complexe
#include <iostream>
#include "Complexe.h"

using namespace std;

int main(int argc, char **argv) {
	
	Complexe c1(3,4);
	Complexe c2(1,1);
	cout << "M1="<< c1.Module() << " et Arg1=" << c1.Argument() << endl ;
	cout << "M2=" << c2.Module() << " et Arg2=" << c2.Argument() << endl ;
	cout << c1 << " + " << c2 << endl;
	c1 += c2;
	cout << c1 << endl;
	Complexe c3 = c1 + c2; 
	cout << c3 << endl;
	return EXIT_SUCCESS;
}
					

La surcharge des opérateurs permet de simplifier l'utilisation de la classe complexe.

La classe string

En C++, on peut toujours utiliser le format des chaînes de caractères du C, mais il est préférable d'utiliser la classe string.

La généricité

Présentation

En C on a une pile générique en utilisant un pointeur non typé pour stocker les données. En C++ on utilise une classe générique, c'est à dire une classe dont le type de donnée est défini lors de la déclaration et de la compilation.

C'est le cas particulier où la définition de la classe et le code des méthodes sont définis dans un seul fichier .h

template<class T> class nom_classe {
	prive:
		type propriete_prive;
		type_retour methode_prive(liste des types des parametres);
	public:
		nom_classe();//constructeur
		~nom_classe();//destructeur
		type propriete_public;
		
		type_retour methode_public(liste des types des parametres);
};

template<class T> nom_classe<T>::nom_classe() {
}

template<class T> nom_classe<T>::~nom_classe() {
}

template<class T> type_retour nom_classe<T>::methode_prive(liste des parametres) {
}

template<class T> type_retour nom_classe<T>::methode_public(liste des parametres) {
}
				

La déclaration se fait en précédant le mot classe de template<class T>.

Ensuite toutes les méthodes sont précédées de template<class T> et en intercalant, en plus, <T> entre le nom de la classe et l'opérateur de portée.

Voir l'exemple de pile générique
#ifndef __PILE_H
#define __PILE_H

#define TAILLE_DEFAUT 100

template<class T> class Pile {
	private:
		T *base;
		T *sommet;
		int taille_max;
		int taille;
	public:
		Pile();
		Pile(int);
		~Pile();
		void empiler(T);
		T depiler();
		bool estvide();
		bool estplein();
		int nbElements();
};

template Pile::Pile() {
	Pile(TAILLE_DEFAUT);
}

template<class T> Pile<T>::Pile(int taille_max) {
	this->taille_max = taille_max;
	base = new T[taille_max];
	sommet = base;
	taille = 0;
}

template<class T> Pile<T>::~Pile(){
	delete[] base;
}
template<class T> void Pile<T>::empiler(T element) {
	*(sommet++) = element;
	taille += 1;
}

template<class T> T Pile<T>::depiler() {
	taille -=1 ;
	return *(--sommet);
}

template<class T> bool Pile<T>::estvide() {
	return (base == sommet);
}
template<class T> bool Pile<T>::estplein() {
	return (sommet == (base+taille_max));
}
template<class T> int Pile<T>::nbElements() {
	return taille;
}

#endif
					

Le symbole T qui représente le type générique, qui sera remplacé par le type utilisé, est utilisé comme type dans toute la classe et dans le code des méthodes

Ici on a une pile qui utilise un tableau dynamique. La mémoire est allouée dans le constructeur et libérée dans le destructeur. Il ne faut jamais oublier de libérer la mémoire.

Programme de test

#include <iostream>
#include "Pile.h"

using namespace std;

int main(int argc, char **argv) {
	double valeur ;
	Pile<double> pile(10);
	for(int i=0;i<9;i+=1) {
		valeur = i ;
		pile.empiler(valeur);
	}
	if (pile.estplein()) {
		cout << "La pile est pleine" << endl;
	}
	else {
		cout << "On a encore de la place" << endl ;
	}
	while (!pile.estvide()) {
		valeur = pile.depiler();
		cout << valeur << endl;
	}		
	return EXIT_SUCCESS;
}
					

Dans ce test, on utilise une pile de 10 réels.

L'affichage correspond bien à celui d'une pile

On a encore de la place
8
7
6
5
4
3
2
1
0
					

Les structures de données de la librairie STL

La librairie STL (Standard Template Library) contient un ensemble de classes génériques dont

Toutes ces classes sont définies dans l'espace de nom std.

Elles peuvent directement utilisée pour enregistrer les structures de données.

Voir l'exemple de calcul matriciel

Définition de la classe Matrice dans le fichier Matrice.h

#ifndef __MATRICE_H
#define __MATRICE_H

#include <vector>

using namespace std;

class Matrice {
  private :
	vector< vector<double> > matrice;
	int nlignes;
	int ncolonnes;
  public:
	Matrice();
	Matrice(double *,int ,int );
	Matrice(const Matrice &);
	~Matrice();	
	int getL() const;
	int getC() const;
	double get(int, int) const;
	void mrand(int, int);	
	void add(const Matrice &) ;
	Matrice & operator+=(const Matrice &);
	friend Matrice operator+(const Matrice &m1, const Matrice &m2);
	void print();
};

#endif
					

La matrice est représentée par une classe vector qui contient les lignes de la matrice représentées par, également une classe vector. La taille de la matrice est représentée par les propriétés nlignes et ncolonnes accessibles en lecture seule avec les méthodes getL et getC.

On a trois constructeurs, un qui permet de créer une matrice de taille nulle, une autre crée une matrice à partir d'un tableau à deux dimensions. Dans ce cas on transmets l'adresse du tableau ainsi que la taille de la matrice afin de pouvoir gérer les 2 dimensions. Le dernier constructeur est le constructeur de copie, qui crée un objet matrice à parti d'un objet matrice.

La méthode get permet d'obtenir la valeur d'un élément de la matrice à partir des indices de ligne et de colonne.

La méthode mrand génère une matrice aléatoire en précisant la taille en paramètres.

La méthode add permet d'additionner l'objet matrice courant avec un autre objet matrice, le résultat est dans l'objet courant. Cette méthode est également utilisée pour la surcharge de l'opérateur +=.

La méthode friend réalise l'addition de deux matrices, le résultat est dans une troisième matrice qui est crée. Cette méthode utilise la méthode de création de nouvelle matrice par copie, puis la méthode add pour l'addition de la première matrice copiée avec la deuxième matrice. Le résultat est dans la nouvelle matrice crée.

Enfin la méthode print affiche la matrice sous la forme de lignes avec les colonnes séparées par des virgules sur la sortie standard.

Codage des méthodes dans le fichier Matrice.cpp

#include <iostream>
#include <cmath>
#include "Matrice.h"

using namespace std;
 
Matrice::Matrice() {
	nlignes = 0;
	ncolonnes = 0;
}

Matrice::Matrice(double *ptab,int l,int c) {
	nlignes = l;
	ncolonnes = c;	
	for(int i=0;i<nlignes;i+=1) {
		vector<double> ligne ;
		for(int j=0;j<ncolonnes;j+=1) {
			ligne.push_back(*ptab++);
		}
		matrice.push_back(ligne);
	}
}

Matrice::Matrice(const Matrice &m) {
	nlignes = m.getL();
	ncolonnes = m.getC();
	for(int i=0;i<nlignes;i+=1) {
		vector<double> ligne ;
		for(int j=0;j<ncolonnes;j+=1) {
			ligne.push_back(m.get(i,j)); 
		}
		matrice.push_back(ligne);
	}
}

Matrice::~Matrice() {
	for(int i=0;i<nlignes;i+=1) {
			matrice[i].clear();
	}
	matrice.clear();
}

int Matrice::getC() const {
	return ncolonnes;
}

int Matrice::getL() const {
	return nlignes;
}

double Matrice::get(int l,int c) const {
	if ((l<= nlignes) &&(c <= ncolonnes)) {
		return matrice[l][c];
	}
	else {
		return 0;
	}
}

void Matrice::mrand(int l, int c) {	
	nlignes = l;
	ncolonnes = c;
	for(int i=0;i<nlignes;i+=1) {
		vector<double> ligne ;
		for(int j=0;j<ncolonnes;j+=1) {
			int aleaint = rand();
			ligne.push_back((double)aleaint / RAND_MAX );
		}	
		matrice.push_back(ligne);	
	}
}

void Matrice::add(const Matrice &m) {
	for(int i=0;i<nlignes;i+=1) {
		for(int j=0;j<ncolonnes;j+=1) {
			matrice[i][j]+= m.get(i,j);
		}
	}	
}

Matrice & Matrice::operator +=(const Matrice  &m) {
	for(int i=0;i<nlignes;i+=1) {
		for(int j=0;j<ncolonnes;j+=1) {
			matrice[i][j] += m.get(i,j);
		}
	}
	return *this;
}

Matrice operator+(const Matrice &m1, const Matrice &m2) {
	Matrice m(m1);
	m.add(m2);
	return m;
}

void Matrice::print() {
	for(int i=0;i<nlignes;i+=1) {
		for(int j=0;j<(ncolonnes-1);j+=1) {
			cout <<  matrice[i][j] << "," ;
		}
		cout << matrice[i][ncolonnes-1] << endl;
	}
}

					
Voir le programme de test de la classe Matrice

Programme de test

#include <iostream>
#include "Matrice.h"

using namespace std;

int main(int argc, char **argv) {
	double tab[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9,10,11,12}};
	int nlignes = 3;
	int ncolonnes = 4;
	Matrice m((double *)tab,nlignes,ncolonnes);
	cout << "M:" << endl;
	cout << "l= " << m.getL() << ",c=" << m.getC() << endl;
	m.print();
	Matrice m2;
	m2.mrand(nlignes,ncolonnes);
	cout << "M2:" << endl;
	m2.print();
	Matrice m3 = m + m2;
	cout << "M3:" << endl;
	m3.print();
	return EXIT_SUCCESS;
}
					

Résultat de l'exécution du programme

M:
l= 3,c=4
1,2,3,4
5,6,7,8
9,10,11,12
M2:
4.36075,2.57753,4.1324,4.19376
4.64659,1.79021,2.34089,4.07292
2.1111,3.21588,2.90959,3.51548
M3:
5.36075,4.57753,7.1324,8.19376
9.64659,7.79021,9.34089,12.0729
11.1111,13.2159,13.9096,15.5155
					

L'héritage

Présentation

L'héritage permet d'ajouter ou de modifier des méthodes et propriétés à une classe en créant une classe qui bénéficie des propriétés et méthodes de la classe parent. La nouvelle classe s'appelle la classe dérivée ou enfant.

Déclaration et définitions

On fait suivre la déclaration de la classe par : suivi du type d'héritage qui est public ou protected ou private, suivi du nom de la classe parent

Fichier de définition .h

class nom : public parent {
	public:
		nom(parametres constructeur);
	// suite de la définition	
};
		

Dans le fichier cpp de codage du consructeur on précise le lien avec le constructeur parent en insérant avant l'accolade de début deux-points suivi du nom du constructeur parent avec ses paramètres.

Fichier de codage .cpp

// définition du constructeur
nom::nom(parametres constructeur) : parent(parametres constructeur parent) {
}
// codage des autres constructeurs et méthodes
		

Accessibilité

L'accès aux éléments de la classe parent dépend des types d'accès à ces éléments ainsi que du type d'héritage. Le tableau ci-dessous donne ces types d'accès

Type d'héritageclasse parent membre publicclasse parent membre protégéclasse parent membre privé
publicpublicprotectedprivate
protectedprotectedprotectedprivate
privateprivateprivateprivate

Exemple d'héritage

Voir l'exemple de calcul matriciel adapté aux matrices carrées

Définition de la classe Matrice dans le fichier MatriceCarre.h

#ifndef __MATRICECARRE_H
#define __MATRICECARRE_H

#include "Matrice.h"

class MatriceCarre : public Matrice {
	
  private :
	int n;

  public:
	MatriceCarre();
	MatriceCarre(double *,int );
	MatriceCarre(const MatriceCarre &);
	~MatriceCarre();	
	int getSize() const;
	void mrand(int);
	friend MatriceCarre operator+(const MatriceCarre &m1, const MatriceCarre &m2);
};

#endif
					

On redéfinit les constructeurs, qui font appel aux constructeurs de la classe parent et initialise la propriété de la classe dérivée.

On ajoute une nouvelle méthode pour la propriété de taille n.

On redéfinit les méthodes en modifiant la signature (mrand), cette méthode fait appel à la méthode mrand de la classe parent.

On ne redéfinit pas la méthode add et l'opérateur +=, car elles utilisent la matrice parent, pour laquelle les propriétés nlignes et ncolonnes ont été initialisées avec la valeur de la propriété n de la classe dérivée.

On redéfinit la méthode friend qui n'appartient pas à la classe.

Codage des méthodes dans le fichier MatriceCarre.cpp

#include <cmath>
#include "MatriceCarre.h"
 
MatriceCarre::MatriceCarre() : Matrice() {
	n=0;
}

MatriceCarre::MatriceCarre(double *ptab,int n) : Matrice(ptab, n ,n) {
	this->n = n;
}

MatriceCarre::MatriceCarre(const MatriceCarre &m) : Matrice(m)  {
	n = m.getSize();
}

MatriceCarre::~MatriceCarre() { }


int MatriceCarre::getSize() const {
	return n;
}

void MatriceCarre::mrand(int n) {
	this->n = n;
	Matrice::mrand(n,n);
}

MatriceCarre operator+(const MatriceCarre &m1, const MatriceCarre &m2) {
	MatriceCarre m(m1);
	m.add(m2);
	return m;
}
					
Voir le programme de test

Programme de test

#include <iostream>
#include "MatriceCarre.h"

using namespace std;

int main(int argc, char **argv) {
	double tab[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };
	int n = 3;
	MatriceCarre m((double *)tab,n);
	cout << "M:" << endl;
	cout << "l= " << m.getSize() << ",c=" << m.getSize() << endl ;
	m.print();
	MatriceCarre m2;
	m2.mrand(n);
	cout << "M2:" << endl;
	m2.print();
	MatriceCarre m3 = m + m2 ;
	cout << "M3:" << endl;
	m3.print();
	return EXIT_SUCCESS;
}
					

Le programme de test utilise uniquement la classe MatriceCarre. L'utilisation de la classe Matrice est uniquement réalisé par la classe MatriceCarre.

Classes abstraites

Une classe abstraite permet, par exemple, d'utiliser des méthodes qui seront définies dans la classe dérivées. Une classe qui contient au moins une méthode virtuelle est une classe abstraite. Dans une classe abstraite le destructeur doit être virtuel. Une méthode est déclarée virtuelle si elle est précédée du mot clé virtual. Les méthodes virtuelles doivent être définies dans la classe dérivée.

Exemple

On propose d'écrire une classe qui calcul l'intégrale d'une fonction quelconque, puis d'utiliser une classe dérivée pour le calcul d'intégrale de fonctions polynomiales.

Voir la classe de calcul de l'intégrale d'une fonction quelconque

Définition de la classe Matrice dans le fichier Integrale.h

#ifndef __INTEGRALE_H
#define __INTEGRALE_H

class Integrale {
	
	protected:
		double a,b;
		double dx;

	public:
		Integrale();
		Integrale(double, double);
		virtual ~Integrale();
		
		double Calculer();
		virtual double fonction(double) =0;
};

#endif
					

Le calcul d'intégrale utilise la méthode des trapèzes.

La méthode fonction, qui est virtuelle, est utilisée par la méthode Calculer sans être définie. Elle sera définie par la classe dérivée.

Codage des méthodes dans le fichier Integrale.cpp

#include "Integrale.h"

Integrale::Integrale() {
	dx = 0.01;
	a=0;
	b=1;
}

Integrale::Integrale(double a,double b) {
	dx = 0.01;
	this->a = a;
	this->b = b;
}

Integrale::~Integrale() { }

double Integrale::Calculer() {
	double total = 0;
	double x0=a;
	for(x0=a;x0<b;x0+=dx) {
		total += (fonction(x0)+fonction(x0+dx))*dx/2;
	}
	return total;
}
					
Voir la classe de calcul de l'intégrale d'une fonction polynomiale

Définition de la classe Matrice dans le fichier IntegralePolynome.h

#ifndef __INTEGRALEPOLYNOME_H
#define __INTEGRALEPOLYNOME_H

#include "Integrale.h"

class IntegralePolynome : public Integrale {
	
	public:
		IntegralePolynome();
		IntegralePolynome(double , double);
		~IntegralePolynome();
		
		double fonction(double);
};

#endif
					

Codage des méthodes dans le fichier IntegralePolynome.cpp

#include "IntegralePolynome.h"

IntegralePolynome::IntegralePolynome() : Integrale() { }

IntegralePolynome::IntegralePolynome(double a,double b) : Integrale(a,b) { }

IntegralePolynome::~IntegralePolynome() { }

double IntegralePolynome::fonction(double x) {
	return x/2-1;
}
					

La fonction polynomiale se limite à f(x)=x/2-1.

Voir le programme de test

#include <iostream>
#include "IntegralePolynome.h"

using namespace std;

int main(int argc, char **argv) {

	IntegralePolynome integralePolynome(2,6);
	double i = integralePolynome.Calculer();
	cout << "integrale de 2 a 6 de x/2-1 = " << i << endl;
	return EXIT_SUCCESS;
}
					

Le programme calcule l'intégrale : A = 2 6 ( x 2 - 1 ) 𝑑 x = 4

integrale de 2 a 6 de x/2-1 = 4
					

Le programme affiche une valeur identique, mais ce n'est pas toujours le cas, cela dépend de la fonction. Ici la fonction est une droite qui se décompose parfaitement en éléments de la forme de trapèzes.

Héritage multiple

L'héritage multiple consiste à créer une classe qui hérite de plusieurs classes. Il faut faire attention aux problèmes d'ambiguïté si plusieurs classes ont des méthodes avec le même nom et la même signature.

Exemple

On reprend le calcul de l'intégrale d'un polynôme à partir de la classe Integrale précédente, d'une classe Polynome. On crée une classe IntegraleFonction qui dérive des classe Integrale et Polynome.

Voir la classe de calcul de la valeur d'un polynôme d'ordre n.

Définition de la classe Matrice dans le fichier Polynome.h

#ifndef __POLYNOME_H
#define __POLYNOME_H

#include <vector>

using namespace std;

class Polynome  {
	
	protected:
		vector<double> a;

	public:
		Polynome();
		Polynome(const vector<double> );
		~Polynome();
		
		double evaluer(double);
};

#endif
					

Le polynôme est représenté par un vecteur de coefficient, en commençant par le coefficient constant

Codage des méthodes dans le fichier Polynome.cpp

#include <cmath>
#include "Polynome.h"

Polynome::Polynome()  {
	a.push_back(double(0));
	a.push_back(double(1));
}

Polynome::Polynome(const vector<double> a) {
	this->a = a ;
}

Polynome::~Polynome() { }

double Polynome::evaluer(double x) {
	double y=0;
	int nb = a.size();
	for(int i=0;i<nb;i+=1) {
		y += a[i]*pow(x,double(i));
	}	
	return y;
}
					
Voir la classe de calcul de l'intégrale d'une fonction.

Définition de la classe Matrice dans le fichier IntegraleFonction.h

#ifndef __INTEGRALEFONCTION_H
#define __INTEGRALEFONCTION_H

#include <vector>
#include "Integrale.h"
#include "Polynome.h"

using namespace std;

class IntegraleFonction : public Integrale, public Polynome {
	
	protected:
		double a,b;
		double dx;

	public:
		IntegraleFonction();
		IntegraleFonction(double, double,const vector<double>);
		~IntegraleFonction();
		
		double fonction(double);
		
};

#endif
					

Codage des méthodes dans le fichier IntegraleFonction.cpp

#include "IntegraleFonction.h"

IntegraleFonction::IntegraleFonction()  { }

IntegraleFonction::IntegraleFonction(double a,double b,const vector<double> coeffs) : Integrale(a,b), Polynome(coeffs) { }

IntegraleFonction::~IntegraleFonction() { }

double IntegraleFonction::fonction(double x) {
	return evaluer(x);
}
					

Le constructeur de cette classe fait appels aux constructeurs des classes parent, en transmettant les bornes pour la classe Intégrale et le tableau des coefficients pour la classe Polynome.

La méthode fonction, qui est utilisée par la méthode calculer de la classe Integrale, utilise la méthode evaluer de la classe Polynome.

Voir le programme de test

#include <iostream>
#include <vector>
#include "IntegraleFonction.h"

using namespace std;

int main(int argc, char **argv) {
	
	vector<double> a;
	a.push_back(double(-1));
	a.push_back(0.5);

	IntegraleFonction integraleFonction(2,6,a);
	double i = integraleFonction.Calculer();
	cout << "integrale de 2 a 6 de x/2-1 = " << i << endl;
	return EXIT_SUCCESS;
}
					

Les flots

L'entrée et la sortie standard

Le fichier de définition est iostream qui contient la classe iostream qui hérite des classes istream et ostream.

Les objets prédéfinis

Le formatage des données en sortie

Le format d'affichage dépend du type de la donnée :

Il est possible de changer le type d'affichage en utilisant le transtypage, c'est ce nouveau type qui sera pris en compte pour l'affichage.

Il est également possible de préciser le format d'affichage en intercalant le symbole de modification entre la variable et l'objet flot. De plus tant que le format n'est pas modifié il est reste valable pour toutes les autres données.

Le symbole de formatage doit être situé en aval de la variable dans le sens du flot.

Exemple

#include <iostream>

using namespace std;

int main(int argc, char **argv) {
	int entier=10;
	char car='A';
	double reel = 3.14159;

	cout << "entier b10 = " << dec << entier
		 << " entier b16 = " << hex << entier
		 << " entier b8 = " << oct << entier << endl;
	cout << "car A " << car << " code b10 = " << dec << int(car) << endl;
	cout << scientific << "reel = " << reel << " = " << fixed << reel << endl;	
	
	return EXIT_SUCCESS;
}
				

Résultat de l'affichage

entier b10 = 10 entier b16 = a entier b8 = 12
car A A code b10 = 65
reel = 3.141590e+00 = 3.141590
				

Si on enlève le format dec en aval de int(car), le code ASCII du caractère sera affiché en octal, qui est le dernier format utilisé.

L'accès aux fichiers locaux

Le fichier de définition est fstream est utilisé pour l'ensemble des classes de gestion des fichiers.

Ouverture

L'ouverture peut se faire lors de la déclaration qui utilise le constructeur avec des paramètres ou bien avec la méthode open en utilisant la déclaration en utilisant un constructeur sans paramètres.

Utilisation du constructeur

fstream fichierle(nomfichier,mode);
ifstream fichierlecture(nomfichier,mode);
ofstream fichierecriture(nomfichier,mode);
				

Utilisation de la fonction open

fstream fichierle;
fichierle.open(nomfichier,mode);
ifstream fichierlecture;
fichierlecture.open(nomfichier,mode);
ofstream fichierecriture;
fichierecriture.open(nomfichier,mode);
				

La classe fstream permet d'ouvrir un fichier en lecture et écriture, la classe ifstream en lecture seule et ofstream en écriture seule

Accès aux données

Accès en lecture

Elle se fait avec l'opérateur de flot >> comme avec l'entrée standard

Il est également possible d'utiliser les méthodes des classes fstream, ifstream.

  • read(buffer,taille) : lecture et transfert dans la variable buffer de taille octets
  • getline(buffer,taille,delimiteur) lecture d'une ligne au foemat texte la lecture s'arrête au délimiteur ou bien lorsque la taille est atteinte

Accès en écriture

Elle se fait avec l'opérateur de flot << comme avec la sortie standard

Il est également possible d'utiliser les méthodes des classes fstream, ofstream.

  • write(buffer,taille) : écriture de taille octets depuis la variable buffer.

Fermeture

Elle utilise la méthode close() de l'objet ouvert.

Exemples

Ecriture de données dans un fichier texte

#include <iostream>
#include <fstream>

using namespace std;

int main(int argc, char **argv) {
	ofstream fichier("fichier.txt");
	for(int i=0;i<10;i+=1) {
		fichier << "ligne n° " << i << endl;
	}
	fichier.close();	
	return EXIT_SUCCESS;
}
				

contenu du fichier texte créé : fichier.txt

ligne n° 0
ligne n° 1
ligne n° 2
ligne n° 3
ligne n° 4
ligne n° 5
ligne n° 6
ligne n° 7
ligne n° 8
ligne n° 9
				

Affichage du contenu du fichier texte créé

#include <iostream>
#include <fstream>

using namespace std;

int main(int argc, char **argv) {
	char champ1[20] , champ2[20]  ;
	int n;
	ifstream fichier("fichier.txt");
	fichier >> champ1 >> champ2 >> n ;
	while (fichier) {
		cout << "lu :" << champ1 << " " << champ2 << n << endl ;
		fichier >> champ1 >> champ2 >> n ;
	}
	fichier.close();	
	return EXIT_SUCCESS;
}
				

Résultat de l'affichage de la lecture de fichier.txt

lu :ligne n°0
lu :ligne n°1
lu :ligne n°2
lu :ligne n°3
lu :ligne n°4
lu :ligne n°5
lu :ligne n°6
lu :ligne n°7
lu :ligne n°8
lu :ligne n°9
				

Le test sur l'objet fichier permet de détecter la fin de fichier, il est également possible d'utiliser la méthode eof().

Ecriture de réels dans un fichier binaire

#include <iostream>
#include <fstream>

using namespace std;

int main(int argc, char **argv) {
	
	ofstream fichier("reels.dat");
	for(int i=0;i<10;i+=1) {
		double nombre = double(i);
		fichier.write((char *)&nombre,sizeof(double));
	}
	fichier.close();	
	
	return EXIT_SUCCESS;
}
				

contenu du fichier texte créé : reels.dat, affiché avec la commande hexdump

hexdump -e '/8 "%f\n"' reels.dat
0,000000
1,000000
2,000000
3,000000
4,000000
5,000000
6,000000
7,000000
8,000000
9,000000
				

Affichage du contenu du fichier binaire créé

#include <iostream>
#include <fstream>

using namespace std;

int main(int argc, char **argv) {
	double nombre;
	int n;
	ifstream fichier("reels.dat",ios::binary);
	fichier.read((char *)&nombre,sizeof(double));
	while (!fichier.eof()) {
		cout << nombre << endl ;
		fichier.read((char *)&nombre,sizeof(double));
	}
	fichier.close();		
	return EXIT_SUCCESS;
}
				

Résultat de l'affichage de la lecture de reels.dat

0
1
2
3
4
5
6
7
8
9