Le type class


- les membres données (attributs)
- les membres fonctions (méthodes)
public : valeur par défaut des struct
protected :
private : valeur par défaut des class
      class complexe {…}
ou
   
    struct complexe {…}
complexe C, *PC=& C;
float module (complexe &)

Quelques attributs

Class Ecran {
short hauteur, largeur ; // nb lignes, colonnes
char * curseur ; // position courante
char * écran ; // stockage
};

Déclaration de class :

class truc ; // déclaration préalable

class Pile de truc {

short position ;
truc * pile ;

};
class truc {…} ; // définition

class string {

static string le null ;
string * suivi ;
truc information ;

};

 


Les fonctions membres

Z

  - attention à l’accès

  - toutes les fonctions membres doivent être prototypées dans la définition

class

Ecran {… // attributs

 

public :// il faut pouvoir les appeler
        // de l’extérieur !

 

 

Void haut ( ); // déclaration

 

 

char courant( ){

return * curseur ;}
// définition dans le
// corps de la classe

 

};

 

 

 

 

Toutes les fonctions membres définies dans le corps de la classe sont automatiquement inline.

Définition extérieure :
                           Inline : Void Ecran :: haut( ){curseur = écran ;}


La portée

Class A
friend of A                  

 )
 ) 

  private    

 )
 )
 )
 )
 

  protected    

 )
 )
 )
 
)
 ) 

public

Class A' : A
friend of A'   

|A' hérite
|de A

 

 

 

 

Code extérieur

 

 

 

Les membres d’une class sont tous dans la portée des fonctions membres de la classe.

L’encapsulation doit être matérialisée dès l’écriture de l’interface de la classe.

Règle de programmation :

Tous les membres données sont private:


Exemple d’encapsulation sans frais

class Ecran{
    public :



int hauteur ( ){return h ;}

int largeur ( ){return l ;}
char courant( ){return * curseur ;}

    const char & cat xy (int x,int y){return écran [x][y];}

 

...

    private :


short h,l
char x *écran,*curseur ;

};

 

  // comparaison
  // 0 si ¹ 1 si = =
  int égaux (Ecran & e1, Ecran * e2)

{if((e1.hauteur( )!= e2 ® hauteur( ))||
     (e1.largeur( )! = e2 ® longueur( ))) return Æ ;
  int x,y ;
  for(x = 0 ; x < e1.largeur ( ); x ++)
  for(y = 0 ; y < e2.hauteur ( ); y ++)
  if (e1.catxy (x,y)! = e2 ® catxy (x,y))
                     return 0 ;
  return 1 ;
}
Ecran a,*b = new Ecran ;
  if (égaux (a, b)) { // heureusement…


L'opérateur :: de résolution de portée

int max ;

class F {
     
public : F( ){max = 0 ;}// F :: max = 0 ; 
           G( );H( );K( );
     
private: int max ;
}
;

les membres données sont visibles avant leur définition et cachent les variables globales de même nom

F :: H( ){max = 3 ;}// F :: max = 3 ;

mais il est possible d’accéder à la variable globale grâce à l’opérateur de résolution de portée

F :: G ( )

{max = :: max ; // globale
 :: max ++ ; // globale
}

F :: K ( )

{int m = :: max ; // globale
 int g = max ; // F :: max ;

//

int max = max ; // erreur
int max = :: max ; // dangereux
int h = max ; // F :: K :: max

}

 


Les classes locales

Une classe peut être définie de façon globale ou locale.

class Arbre {
    
public : class noeud {…}; // classe imbriquée
      void tri ( ){
                    class paire {…}; // classe locale
                    paire p ;
                    noeud * pn ;
                …
             }
    
private :
               paire q ; // erreur
               noeud * fg ; * fd ;
};
noeud * racine ; // erreur
Arbre :: noeud * racine ; // ok !

Résolution de la portée

I.
II.
III.
IV.

Dans le bloc
Dans le bloc englobant si c’est une fonction
Dans le corps de classe englobant
Dans le bloc englobant la class
  a) global
® classique
  b) imbrication
® III)
  c) class locale
® II)
      héritage
® l’union des corps des classes de base


Des classes sur une base d’union

ou

   un constructeur

   un destructeur

Union token { int ival ;
      public :
              long lval ;
              long get_Lval( ) {return Lval ;}
};

Les unions anonymes

class token {public :
              int type;
              union {int ival ;
                     char cval ;
                     long lval ;
                     
              
} ;
              

} ;

token::get_ival( ){assert(type == INT)
                    return ival ;
                   }
Accès direct aux membres de l’union

Les fonctions membres

Un "canvas" d’écriture :

Une présentation standard :

// maclass - h


class maclass {présentation "logique"} ;

// maclass - c


ordre alphabétique


Instances constantes

L'objectif de créer des types intégrés nous impose d'avoir des instances constantes.

Mais le compilateur ne peut présumer des fonctions membres utilisables ou non sur une instance constante (ou volatile).

 

 

  if (ch)

Class Ecran

{void set cur (char ch) {

* curseur = ch ;}

 

char & chaxy (int x, int y){…}// v1

     const char & chaxy (int x,int y) const{…}// v2
};
const Ecran non écran ;
      non écran . cha xy (1, 1) ; // Ok v2
      non écran . set cur ('A') ; // erreur


Le compilateur refusera

Ecran :: set cur (char ch) const {

if (ch)}


}

* curseur = ch ; // erreur


Seuls constructeur et destructeur fonctionnent sur une instance constante (mais pas d'appel explicite).


Les méthodes de gestion

  1. Constructeur & destructeur
    initialisation déallocation
  1. Les opérateurs
    affectation, copie, allocation, libération etc
  1. Les opérateurs de coercition

L'appel de ces fonctions est en général transparent à l'utilisation, ce qui permet une utilisation naturelle et ainsi d'ajouter le type au langage.


Initialisation des instances

Deux formes d'initialisation :

Class B{





};

int b ; char*r ;
public :
B(char*ir, int ib)
 {b = ib ; r = new char [str lev(ir)+ 1];
          assert r ;
       strcpy (r,ir);
  }

Permet deux initialisations :

et

 B a("Text",18);

 B b={20, "truc"};


Surcharge du constructeur

class Mot {int occ ;
            char*string ;

         Public :
            Mot( ) {occ = 0 ; string = "" ;}
            Mot(char *, int = 1);
};

Exemples d'appels :

main ( ) {
Mot A ; //. Mot :: Mot ( ) Û Mot :: Mot ("", 0);
Mot outil ("Marteau");   // Mot :: Mot ("Marteau", 1);
Mot véhicule ("806", 2); // Mot :: Mot ("806", 2);
Mot truc = "machin" ;    // Mot :: Mot ("machin", 1);
Mot * pmot ;
pmot = new Mot ("objective C");// Mot :: Mot ("objectiveC",1);
}

Le cas des instances constantes

   const Truc   A ;

 // ok Truc :: Truc( ) existe

   const machin B ;

 // erreur Machin :: Machin ( )
 // n'existe pas

const machine B(b);

 // ok Machin :: Machin (int)existe


Les destructeurs

Le destructeur permet de définir les opérations à réaliser au moment de la libération de l'instance (déallocation).

Déclaration / Définition

Même nom que le constructeur précédé d'un ~ (tilde)

class Bool

{…
public :

 

 

 

~ Bool( ){}

 

 

};

 

 


Tableaux d'objets

De la même façon qu'en C il n'y a pas de tableau en C++ mais la possibilité d'allouer des groupes d'objets de même type.

Const int taille = 16 ;
Point tp [taille] ;
string * tstr = new string [taille] ;

L'initialisation des différents éléments du tableau est réalisée par une application du constructeur par défaut. Si il n'existe pas de constructeur par défaut, il faut une liste d'initialisation complète :

string tab [ ] = {"arbre", "vallée"} ;
string tab2 [3] = { string (6), string (4), string ("toto")} ;

sinon si le constructeur par défaut existe

string tab3[6] = { 1024, string (12), "toto" } ;
      // construction   string :: string( )pour tab[3-5]

Destruction

 

 delete [ ]tab;

// appel du destructeur sur chaque
// élément

Z

 
delete [ ]tab ;


// destructeur uniquement sur
//le premier élément

Pas d'initialisation de tableau du tas comme en C.


Un attribut de type class

class Mot {

public:

Mot(const char *, int = 0) ;
Mot(const string &, int = 0) ;

 

private :
     int occurrences ;
     string nom ;

};

 

 

2 constructeurs sont appelés pour chaque Mot :

- Mot :: Mot (?)
- string :: string (?)
  1. Y a t'il un ordre d'appel ?

  2. Comment fournir les arguments au constructeur de l'attribut ?


L'ordre d'appel est celui des déclarations des attributs dans le corps de class puis celui de la classe (ici string, mot)

  1. La définition du constructeur peut contenir une "member initialization list"

Mot :: Mot(const char * s,int(e)
: nom (s),occurrence (e)
{ } // ¬ rien !
L'initialisation des membres est faite pour tous les membres ayant un constructeur par défaut.

Remarque : Un constructeur qui se limite à une "member initialization list" nous assure que la classe ne posera pas de prolèmes d'allocation.


X // X (const X &) ;

Initialisation par une instance
Initialisation membre à membre
Constructeur par recopie

String voyelle ("a");String mot = voyelle ;

Un constructeur membre à membre est fourni par défaut par C++ mais il n'est pas toujours adapté, exemple pour String :

string :: string (const string & s)
{longueur = s.longueur ;
  str = s.str ; // Danger !!!
}

Le constructeur membre à membre a de multiples utilisations :

  1. Initialisation (cf. voyelle)

  2. Argument d'appel de fonction :
    extern int notich (string s,char c)
                                              
    ­
                           la variable locale s'est initialisée par
                            le
     paramètre d'appel de type string

  3. La valeur de retour d'une fonction
    extern string substituer(string &,char,char);
    main( ){string umot("invisibilité");
         cont << substituer" (umot,'i','I')<< endl ;
    }

Utiliser plutôt le passage par référence qui évite l'appel au constructeur membre à membre.

La définition d'un constructeur membre à membre non standard n'est nécessaire que dans le cas où une allocation et un pointeur sont utilisés dans la classe (c.a.d. quand les constructeurs sont vides).

Ainsi il n'est pas nécessaire de redéfinir le constructeur membre à membre dans le cas de membres objets.

Par exemple :

class Index{

int occurrence ;
string mot ;

Public :

Index(char * s,int occ = 0):mot(s)
                       occurrence (occ)
                       { }
Index string & s,int occ = 0): mot(s)
                       occurrence (occ)
                       { }

   …
}
;

 

Le constructeur Index (const Index &) n'est pas défini car le constructeur membre à membre de mot sera automatiquement utilisé par le constructeur membre à membre par défaut de Index.

Dans le cas où vous voulez définir un constructeur membre à membre pour Index, attention au mode d'appel du constructeur de string.

L'implémentation suivante est incorrecte

Index :: Index (const Index & i)

{occurrence = 0 ;

// raison de l'écriture

 mot = i.mot ;


}

// erreur utilisation
   de l'opérateur
   d'affectation

Ecriture correcte

Index :: Index (const Index & i): mot(i.mot)
                                                                   Ok initialisation
{occurrence = 0 ;
 // ou occurrence = i.occurrence ;
                  
­
                             Ok car type int

Remarque : On appelle le constructeur membre à membre constructeur par recopie.


Les opérateurs

Le concepteur de la classe peut fournir un certain nombre d'opérateurs qui ne sont pas nécessairement des fonctions membres mais qui ont au moins un argument de type class.

Cette règle assure que l'on ne surcharge pas un opérateur d'un type prédéfini.

La définition d'un opérateur est identique à celle d'une fonction à part le nom qui est de la forme operator 0()où 0 est un des opérateurs redéfinissables.

Exemple :

string &
string::operator + = (const string & s)
{ len + = s.len ;
   char * p = new char [len + 1] ;
   assert (p)
   strcpy (p, str) ; delete str ;
   strcat (p, s.str) ;
   str = p ;
return * this ;
}

Opérateurs surchargeables

+ * - / %           ^ | & ~          ! && ||     ,
= = < = > = ! =    >> <<     + + - - < >
+ =   * =   - =   % =   ^ =   & =   | =   >> =   << =
[   ]  (    )   (  )    ®  ® *   new delete
         
­
       cast
* & =
La précédence des opérateurs du C est conservée
L'arité est conservée
Les opérateurs + - * & ont 2 arités
Les arguments par défaut sont interdits

Opérateur membre ou non membre

Deux possibilités

non membre

int operator!(const string& s){return s.longueur( )== 0 ;}

membre

int string :: operator!( ) const {return len == 0;}

Par exemple :
             string s1 ("les stream")
             cout << s1 << endl ;
Le premier paramètre est un stream (cout) d'où la fonction amie suivante :

include < iostream.h >
class string {…     

 

      friend ostream & operator <<

(ostream & os,
 string & s);


}
;

 

ostream & operator << (ostream & os,string & s)

  {


if (!s) return as << "<vide>" ;
else return as << s.str ;

  }

 

Par exemple :
quel ensemble d'opérateurs nous permettra d'écrire les trois expressions suivantes :

string s1, s2 ;
         s1 + s2

// e1

"no" + s1

//e2

       s1 + "us"

//e3

Si string a un constructeur string(const char*) ;
l'opérateur suivant fonctionne sur e1 et e2.
string :: operator + (const string &);
De même en utilisant encore le constructeur la fonction non membre suivante fonctionne pour e3.
string operator + (const string &, const chachaineicirc;ne &); // V X
au lieu d'écrire
string operator + (const char *, const string &);
Le défaut de VX vient du fait de l'appel implicite du constructeur qui prend du temps.
Remarque :
"cordon" + "bleu" n'appelle pas l'opérateur VX car on a 2 types prédéfinis.
(mais "a" "b" fonctionne comme en C)

Quelques opérateurs

operator [   ]
On veut que le code suivant fonctionne :
string s1 ("Vive" C++");
string s2 (s1.len( )) ;
for(int i = 0) L= L1 . L en c);i < L ;i ++)
         s2 [i] = s1[i];
Pour cela on définit l'opérateur suivant

char & chachaineicirc;ne :: operator [ ](int elem){

                        

assert(elen > = 0&& elem < len);
return str [elem]

}

 

Operator (  )
L'itérateur, cet opérateur parcourt l'instance, il permet d'écrire du code ayant le sens suivant :
"pour tous les caractères faire"
"pour tous les éléments faire"
ce qui s'écrit
                        While(string 1( )){faire ;}
Cette conception est dangereuse et peu commode
Attention au fait que l'itérateur doit retourner zéro en fin d'itération

L'itérateur externe de string

class string Itérateur {

               

public :


string Itérateur (const string & s)
               :p(& s),index(0){ }

 

private :


string * p ;
int index ;

 

public :


char operator( )( ){
if (index < p ® len) //*
    return p ® str [index ++];
return index = 0 ; // pas p ® str [p ® len]!

 

}

 

};

 

 

- soit friend class string Iterator ;
           pour que toutes les fonctions membres de string Iterator soient amies de string
- soit friend string Iterator :: operator( )( );
           pour que seule cette fonction soit amie

X :: operator = (const X &)

L'opérateur d'affectation

L'opérateur d'affectation par défaut fonctionne comme le constructeur membre à membre sur les données non statiques de la classe.

Problèmes :

  1. les pointeurs sont recopiés tels quels

  2. les zones pointées ne sont pas libérées

    s1 = s2 ;

    // l'espace alloué pour les chars de s1

     

    // n'est pas libéré

  3. que fait on pour les itérateurs internes

Þ

Dans le cas où notre classe réalise une allocation "cachée" il faut redéfinir l'opérateur d'affectation.

string & string :: operator = (const string & s)

 

{
  len = s . len ;
  delete str ; // oops Bug !
  str = new char [len + 1] ;
  strcpy (str, s . str ) ;
  return * this ; // et oui !
}

On a un petit problème dans le cas suivant :
                        
s1 = s1 ;
d'où un test supplémentaire
if (this = = & s) return * this ;
dans le cas d'une auto-affectation ne rien faire !
Dans le cas de membres de type class

class mot{

int occ ;
string s ;

};

 

Si l'on ne redéfinit pas l'opérateur = de mot l'opérateur par défaut va appeler
chachaineicirc;ne & string :: operator = (const string &) ;
Si l'on veut éviter cet appel il faut redéfinir
Mot & Mot :: operator = (const Mot &);
Attention au problème d'efficacité suivant
class X {
public:
X ( );
X (int);
X (const X&);
X & operator = (const X&); };

class y {X x ; public : Y ( );}
Si l'on écrit   Y::Y(){x = 0;} on a appel de X :: X( ) ; pour initialiser X si X & operator =(int) n'existe pas alors
a) transformation de 0 en X  grâce à X(int) ;
b) appel de l'opérateur  X & operator = const X &) ;  pour affecter 0 dans X !
écrire 
Y::Y():x(0){ }

Operator

L'opérateur unaire de sélection est une fonction membre qui doit retourner un pointeur sur un objet de type class ou bien une référence sur un objet dont la classe redéfinit l'opérateur de sélection.

class X ; class Y ;
X * p ;
  p ® m ( ); // utilisation du ® standard (* p).m ( )
Xx ; x ® m( );

début :

si il n'y a pas d'opérateur ® erreur
sinon appel de l'opérateur
®
        soit y la valeur de retour (* y). m( )
    
si y de type y* évaluation de y ® m ( ) standard
        sinon si
y de type référence reprendre l'algorithme au début
x ® m( )º(x.operateur ®( ))® m( )


Operator ++ et --

Les opérateurs ++ et -- sont par défaut préfixes.

Pour avoir la possibilité d'avoir un opérateur postfixé, on ajoute un faux paramètre de type int pour indiquer que l'on veut la version postfixée.

class Ecran {public:
char operator ++( ) { return * ++curseur ;} // préfix
char operator ++(int) { return * curseur ++ ; } // postfix
};

Quelques remarques de conception

Deux opérateurs prédéfinis par C++ qui sont l'opérateur d'affectation = et d'adresse &. Tous les autres doivent être explicitement écrits.

Le fait de fabriquer = et + ne fabrique pas + =

Ne pas fabriquer d'opérateurs ambigus !

Que fait ++ sur une string ?
  - minuscule ou majuscule
  - itérateur
  - autre …

On travaille en deux étapes :

  1. création des fonctions d'implémentation et d'accès

  2. transformation de celles qui ont un comportement d'opérateur en opérateur


Opérateurs de conversion

Les conversions permettent de minimiser le nombre d'opérateurs à définir.

Si l'on veut définir l'addition pour les int, les char et les short il nous faut dans opérateurs de conversion définir 6 opérateurs + supplémentaires ou 3 opérateurs de base.

D'autre part nous devons définir des opérateurs de conversion pour être plus conforme au comportement standard du C qui veut que les conversions standards implicites aient lieu vers un type plus grand.

Class short int {int valeur ;
      public :
              friend operator + (shortint &, int) ;
  …
};

shortint d(3) ;
d + 3.14159 // ® 6 délicat non ?

Les constructeurs sont des opérateurs de coercition

Tous les constructeurs à un argument sont implicitement des opérateurs de coercition.

Ces conversions sont dites "standard".
Elles sont utilisées dans les affectations dans le type de la classe et pour la conversion dans les appels de fonctions ou autres qui utilisent le type défini par la class.

Ambiguïté avec une conversion standard.

Si deux conversions sont possibles que l'une est standard et l'autre définie par l'utilisateur, c'est la conversion standard qui est effectuée.

Ex. :


extern void f(short in) ; // il existe un operator int( )
Extern void f(double) ;
int ival ;
f(ival); // appel de f double


Attention à l'encapsulation

string :: operator char*( ){return str ;}

Dangereux :

string gagnant ("Dick") ;
char * ps = gagnant ; // conversion
*ps = 'N'; // oops

D'où l'opérateur suivant qui nécessite une définition de type :

typedef const char * p cchar ;
inline string :: operator p cchar ( ){return str ;}

Z 


l'opérateur = = de la classe string ne fonctionne plus
Void tata (char * ch)
{ string s ("K.?…") :
  …

 

  if (ch = = s)
  …

// ambiguïté 2 opérateurs possibles
// constructeur char * ® string
   coercition string ® constchar*
// avec la même priorité

 

}

 

        Nous devons redéfinir = =  
    inline string :: operator == (const char * s)
        {return strcomp (str,s) == 0 ;}


Ambiguïtés entre coercition

Ambiguïté entre deux résultats distincts

extern void f (int) ;
extern Int f (shortint) ;

Token Tok ("vanille", 34) ;
// Token :: operator int ( )
// Token :: operator shortint ( )
// Token :: operator floot ( )
f (tok) ; // erreur : ambiguïté

solution résoudre à la main

f(int(tok));ou f((int)tok);

Si token :: operator int( ) n'était pas défini, du fait que short int :: operator int( )existe nous pouvons imaginer une ambiguïté mais elle n'existe pas car il faut une conversion de plus. Mais dans le cas suivant l'ambiguïté existe car les 2 conversions sont au même niveau.

long Lval = tok ; // floot( )ou int( )error

2 chemins de conversion

A :: A(B); et B :: operator A( ){…}
B x ; Void f(A);
    f(x); //ambiguïté
    f(A(x)); //ambiguïté

Il faut écrire  soit


 f(A(x));

 soit

 
 f(x.operator A( )) ;