Cours C++ #1
Théorie
Contenu:
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

1. Les vecteurs (tableaux)

- 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);
 

2. Matrices

- À 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);

3. Pointeurs

- 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.

4. Passage par pointeurs

- 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;
}

6. Structures

- 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.

7. 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;
};