1. Les vecteurs (tableaux)
2. Matrices
3. Pointeurs
4. Passage par pointeurs
5. Utilisation des pointeurs pour parcourir un vecteur
6. Structures
7. Classe
8. Initialisation des classes: Constructeur
9. Désinitialisation: Destructeur
10. Vérifications/ invariants d'une classe
0. Retour sur le devoir et les exercices
- Déclaration:
int lVect[10];
- Utilisation:
lVect[0]... lVect[9];
Exemple :
main()
{
int lVect[10];
for (int i = 0; i < 10; ++i) {
lVect[i] = 0;
cout << "lVect[" << i << "]
= " << lVect[i] << endl;
}
- Initialisation:
int lVect[10] = {1,2,3,4,5,6,7,8,9,10};
- S'il y a moins d'éléments dans les accolades que dans
la dimension du vecteur, les entrées manquantes sont mises à
0.
- S'il y en a plus, avertissement ou erreur selon les options de compilation.
- On ne peut passer une variable comme dimension à un vecteur,
à moins qu'elle ne soit constante (const)
- Passage d'un vecteur à une fonction:
void doubleEntrees(int pVect[], int pDimension);
- Le passage par référence est automatique, donc pas de
copie du vecteur.
- Si on met une dimension entre les accolades, elle est ignorée.
- Un tel vecteur est créé "statiquement" (contrairement
à "dynamiquement").
void imprimeVecteur(const int pVect[], int pDimension);
- À la déclaration de la variable, on spécifie
le nombre de lignes et de colonnes
- Le stockage d'une matrice (comme celui du vecteur) est consécutif
en mémoire: chaque ligne est stockée bout à bout
- Passage d'une matrice à une fonction:
void doubleEntrees(int pMatrice[][4], int pDimension);
- On doit donner la dimension 2 (nombre de colonnes de la matrice) et
les dimensions suivantes (s'il y a lieu) en paramètre, afin que
le compilateur sache combien de cases mémoires il doit sauter lors
d'un changement de ligne. Par exemple, les matrices lMatA[4][3] et
lMatB[2][6] ont 12 entrées. Pour lMatA, un changement de ligne
(passer de lMatA[0][0] à lMatA[1][0]) signifie qu'elle doit sauter
3 cases mémoires plus loin, alors que pour lMatB le saut est de
6.
- Le passage de ces matrices à une fonction n'est pas très
pratique, car on doit connaître la dimension à l'avance...
- Une telle matrice est créée "statiquement" (contrairement
à "dynamiquement").
- Quiz avancé: Comment écrire une fonction qui prendrait
n'importe quel type de matrice?
Solution:
template <int PTDim>
doubleEntrees(int pMatrice[][PTDim], int pDimension);
- La notion de pointeur est plus difficile à maîtriser.
En C++ on utilisera d'abord des références si possible.
- Les pointeurs sont des adresses (ex. : 0x00ff0a) typées (ex.
: int, double, etc.) vers des cases mémoires.
- Toute variable a une case mémoire (même un pointeur!).
- On peut obtenir l'adresse de la case mémoire d'une variable
en utilisant l'opérateur "&".
- Avec un pointeur, on peut consulter le contenu de la case mémoire
qu'il pointe en utilisant l'opérateur de déréférence
"*"
- Il faut faire attention à l'opérateur "&",
car il est utilisé aussi pour les références, le ET
binaire ainsi que le ET logique.
Exemple :
double lY = 6.67;
double lZ = 7.8;
double *lYPtr = 0;
lYPtr = &lY;
cout << lY << " " << lZ << " " <<
*lYPtr << endl;
cout << &lY << " " << &lZ << "
" << lYPtr << endl;
*lYPtr = 5.45;
cout << lY << " " << lZ << " " <<
*lYPtr << endl;
lYPtr = &lZ;
cout << &lY << " " << &lZ << "
" << lYPtr << endl;
*lYPtr = 3.1415;
cout << lY << " " << lZ << " " <<
*lYPtr << endl;
- Déréférencer l'adresse 0 donne automatiquement
une erreur.
- Une bonne habitude de programmation consiste à initialiser
tous les pointeurs à 0.
- On peut passer une variable par pointeur, qui revient à n'avoir
qu'une seule copie de la variable en mémoire, donc une seule case
mémoire.
- Comme le passage par référence, la modification du
pointeur affectera la valeur de la variable à l'extérieur
de la fonction.
- Côté écriture de code, le passage par pointeur
est moins "pratique", car on doit, du côté de l'appel de la
fonction, prendre l'adresse de la variable (avec le "&").
Chose que l'on ne fait pas pour un passage par référence.
De plus, à l'intérieur de la fonction, on doit utiliser partout
l'opérateur "*" pour accéder à la valeur
contenue par la case mémoire pointée.
calculeCube(int * pNb);
int main() {
int lNb = 6;
calculeCube( &lNb);
return 0;
}
5. Utilisation des pointeurs pour parcourir un vecteur
- On peut utiliser les pointeurs pour parcourir un vecteur.
- Les opérations +=, ++, -- sont permises sur les pointeurs
et n'ont pas le même effet selon le type de pointeur (le saut en
mémoire ne sera pas le même selon la dimension du pointeur).
Exemple :
int lVect[10];
int *lPtr = lVect; // int *lPtr = &lVect[0];
for (int i = 0; i < 10; ++i) {
cout << *(lPtr + i) << endl;
}
lPtr = lVect;
int* lPtrFin = &lVect[10]; // Ceci est tout à fait valide!
while (lPtr != lPtrFin) {
cout << *lPtr<< endl;
++lPtr;
}
- Les structures définissent de nouveaux types (comme les énumérations).
- Pour avoir accès à une composante (on dit "membre")
de la structure, on utilise l'opérateur d'accès ".".
struct Vecteur3D {
double aX, aY, aZ;
};
main() {
Vecteur3D lVect;
lVect.aX = 1;
lVect.aY = 2;
lVect.aZ = 3;
}
double calculeNorme(Vecteur3D pVect);
void asgnXYZ (Vecteur3D& pVect, double pX, double pY, double
pZ);
- Lorsqu'on a un pointeur à une structure, on peut utiliser l'opérateur
d'accès "->" qui déréférence le pointeur
et accède à la structure.
- On peut rassembler (on dit "encapsuler") les propriétés
et les fonctions.
- On commence à avoir ce qu'on appellera une classe.
- Les classes sont pratiquement identiques à des structures en
C++.
- Les variables membres (nommées attributs)et fonctions
membres (nommées
méthodes) font partie de la portée
( <<scope>>) de la classe.
- À l'intérieur d'une fonction membre, tous les attributs
et autres fonctions membre sont accessibles sans spécifier la portée.
- À l'extérieur de la classe, les membres sont accessibles
à travers un objet de la classe (un objet est une "instance" d'une
classe) et l'opérateur "." (comme pour la structure).
- Il y a plusieurs "buts" lors de la création d'une classe:
- encapsulation des données (protection)
et des fonctions;
- cacher l'implémentation de la classe et
offrir une interface à l'usager;
- en cachant l'implémentation, on veut minimiser
l'impact des modifications internes;
- garantir la validité des données
manipulées en tout temps
- Sections privées et publiques
- Une classe définit des sections, dans lesquelles
on peut mettre des attributs et des méthodes.
- La section publique donne accès à
l'utilisateur aux attributs et méthodes qui y sont définis
- La section privée interdit l'accès
à l'utilisateur aux attributs et méthodes qui y sont définis.
- Par contre, dans la définition d'une méthode,
on a accès aux attributs privés (on dira "à l'intérieur
de la classe, on a accès aux attributs").
Exemple :
class Vecteur3D
{
public:
void asgnXYZ (double pX, double pY, double pZ)
{
aX = pX;
aY = pY;
aZ = pZ;
};
double calculeNorme()
{
return sqrt(aX * aX
+ aY*aY + aZ*aZ);
}
private:
double aX, aY, aZ;
};
main()
{
Vecteur3D lVect;
Vecteur3D lVect2;
lVect.asgnXYZ(1,2,3);
lVect2.asgnXYZ(4,5,6);
cout << "Norme lVect: " << lVect.calculeNorme()
<< endl;
cout << "Norme lVect2: " << lVect2.calculeNorme()
<< endl;
}
8. Initialisation des classes: Constructeur
- À chaque fois que l'on crée une structure ou une classe,
elle n'est pas initialisée et elle peut contenir n'importe quelle
valeur.
- Il serait intéressant d'écrire une méthode qui
initialiserait nos objets.
Exemple :
class Vecteur3D
{
public:
void initialise() { aX = aY = aZ = 0;};
...
};
main() {
Vecteur3D lVect;
lVect.initialise();
...
}
- Maintenant, rien ne nous assure que tous les programmeurs vont faire
cela.
- Avec les classes, on a la notion de constructeur qui existe et nous
permet de faire cela automatiquement.
- Le standard définit le nom de la méthode de construction
comme étant le même que celui de la classe et qui ne retourne
rien (pas même void!).
Exemple :
class Vecteur3D
{
public:
Vecteur3D() { aX = aY = aZ = 0;};
...
};
main() {
Vecteur3D lVect; // A le même
effet que l'autre programme!
...
}
9. Désinitialisation : Destructeur
- Un objet est détruit lorsque la portée dans laquelle
il a été défini se termine... (ex. : Objet temporaire
dans fonction, sera détruit à la fin de la fonction).
- On peut vouloir faire exécuter certaines tâches (nettoyage)
à la destruction des objets.
- La fonction qui sert de destructeur porte le nom de la classe précédé
d'un tilde "~" (complément de la construction...).
- Ne détruit pas la classe en tant que telle, mais permet de
faire du nettoyage avant la fin de la vie de l'objet.
Exemple :
class Vecteur3D
{
public:
~Vecteur3D() {
// Rien à faire...
cout << "Destruction
d'un Vecteur3D" << endl;
};
...
};
main() {
Vecteur3D lVect;
...
} // le destructeur de
lVect est appelé ici..
10. Vérifications/invariantsd'une classe
- Puisque l'accès aux attributs privés est impossible,
il faut absolument passer par une méthode de la classe pour en modifier
les valeurs.
- Pourquoi créer des méthodes asgn/req plutôt que
de mettre des attributs publics?
- Pour la PROTECTION des données.
- Pour garantir la VALIDITÉ des données.
- Permet d'ajouter des VÉRIFICATIONS sur
les valeurs que l'on tente d'assigner.
- Pour avoir une VÉRIFICATION des valeurs
retournées (ex. : on peut empêcher de retourner des NaN dans
Vect3D).
- Pour pouvoir OPTIMISER le code, sans que l'usage
de la classe ne soit affecté.
Exemple :
class Heure
{
public:
Heure() {aSec = 0; aMin = 0; aHeure= 0;};
asgnSec(int pSec)
{
if (pSec >= 0 &&
pSec < 60) {
aSec = pSec;
}
else {
aSec = 0 ;
}
}
private:
int aSec, aMin, aHeure;
};