[Archive — École 42] C++ Module 00

Mahaut Latinis
8 min readOct 9, 2023

--

💡 Cet article est un résumé d’une vidéo éducative, mise à disposition des étudiants de l’école 42 afin d’appréhender le language C++ et la POO (programmation orientée objet).

Les concepts qui en découlent sont souvent attendus lors de tests techniques (recrutement en entreprise) et c’est l’une des raisons pour lesquels il est crucial pour tout développeur de les maîtriser.

L’école 42 forme ses étudiants à ce paradigme après qu’ils se soient approprié un language de programmation impérative (le C). On retrouve de nombreux points communs entre le C et le C++ (standard 98 notamment), c’est pourquoi le C++ apparaît comme un choix naturel pour s’initier à l’orienté-objet.

L’école a donc mis en place une série de vidéos de formations et de projets / exercices permettant à tout un chacun de se former sur le sujet.

Dans le premier module, les notions suivantes sont abordées :

  1. Namespace et opérateur de résolution de portée
  2. Les flux d’entrée, de sortie et d’erreur (stdio streams)
  3. Classes et instances
  4. Attributs et fonctions membres
  5. Pointeur sur l’instance courante
  6. Liste d’initialisation
  7. Les attributs et fonctions const
  8. Encapsulation (public, private, puis protected)
  9. Différence entre structure et classe
  10. Accesseurs (getters, setters)
  11. Attributs et fonctions non membres (= static, de classe)
  12. Pointeurs sur attributs membres, pointeurs sur fonctions membres

Namespace & opérateur de résolution de portée

https://cdn.intra.42.fr/video/video/92/piscine_c___-_d00_-_00_intro.mp4

Un namespace permet de regrouper des “symboles” ayant un “rapport sémantique” au sein d’un même ensemble (= le namespace). Les éléments de cet ensemble ne seront pas nécessairement rassemblés dans la même unité de compilation (= fichier).

L’opérateur de résolution de portée permet quant à lui d’indiquer de quel namespace il est question.

Exemples :

// Scope global
int gl_var = 1;
int ( void ) { return 2 ;}

// Pas de conflit avec plusieurs variables de même nom, puisque le scope est différent.
namespace Foo {
int gl_var = 3;
int ( void ) { return 4 ;}
}

namespace Bar {
int gl_var = 5;
int ( void ) { return 6 ;}
}

// "Aliasing de namespace"
namespace Muf = Bar;
// Définition d'un nouveau namespace comme étant égal à un autre namespace
int main(void)
{
// Opérateur de résolution de portée ::
// "Au sein du namespace Bar, je souhaite accéder à l'attribut gl_var"
printf("Bar::gl_var: [%d]\n", Bar::gl_var);

//scope global
printf("Bar::gl_var: [%d]\n", gl_var);
printf("Bar::gl_var: [%d]\n", ::gl_var); // Ces deux notations sont parfaitement équivalentes
return (0);
}

Les flux d’entrée, de sortie et d’erreur (stdio streams)

https://cdn.intra.42.fr/video/video/99/piscine_c___-_d00_-_01_stdio_streams.mp4

C++ met à disposition des “objets” (accessible depuis le namespace std — librairie standard).

L’entrée standard est représentée par std::cin tandis la sortie standard est accessible via std::cout.

Ces objets ne sont pas des “file descriptors” (comme c’était le cas en C) mais ils permettent d’éviter la complexité rencontrée lors de l’utilisation de read et write en C.

C++ met également à disposition d’autres opérateurs, << et >>, qui vont permettre d’indiquer “la direction” des flux.

int main(void) 
{
// Redirection de la string "Hello world" vers la sortie standard
std::cout << "Hello world!" << std::endl;

char buff[512];

std::cout << "Enter a word: ";

// >> Stockage du buffer écrit depuis l'entrée
std::cin >> buff;

std::cout << "You entered" << buff << std::endl;
return (0);
}

std::endl étant le “remplaçant” de “\n” (correspond à un retour à la ligne, qui s’adapte à l’OS sur lequel s’exécute le programme).

Déclarer une classe (similaire à la déclaration d’une structure en C) :

//Nom du fichier Sample.class.hpp ou Sample.hpp (selon convention de nommage)

// Protection contre les inclusions multiples (infinies)
#pragma once

// ou

#ifndef SAMPLE_CLASS_HPP
# define SAMPLE_CLASS_HPP

class Sample {
public:
// Procédures n'ayant pas de type de retour
Sample(void); //Constructeur
~Sample(void); //Destructeur (nous verrons plus tard qu'il doit être virtual
}; //Ne pas oublier ce ; pour éviter des erreurs de compilation

#endif

Définir une classe

//Nom du fichier Sample.class.cpp ou Sample.cpp (selon convention de nommage)
#include <iostream>
#include "Sample.class.hpp"

Sample::Sample(void)
{
return ;
}

Sample::~Sample(void)
{
return ;
}

Déclaration d’une instance

#include "Sample.class.hpp"

int main(void)
{
// Instanciation de la classe: appel automatique au constructeur
Sample instance;
return (0);
}
// Appel du destructeur à la sortie du scope (variable locale)

Attributs et fonctions membres

https://cdn.intra.42.fr/video/video/102/piscine_c___-_d00_-_03_member_attributs_and_member_function.mp4

Attribut membre: propriété (variable) possédée par chaque instance de notre classe.

Fonction membre: fonction possédée par chaque instance de notre classe (même définition, même déclaration).

#pragma once 

class Sample {
public:
Sample(void);
~Sample(void);

// attribut membre
int foo;

// fonction membre
void bar(void);// Par défaut, C++ passe toujours un paramètre qui n'est autre
// qu'un pointeur sur l'instance courante (this)
};

Définition d’une fonction membre

include <iostream>
#include "Sample.class.hpp"

// Constructeur
Sample::Sample(void)
{
return ;
}

// Destructeur
Sample::~Sample(void)
{
return ;
}

// fonction membre
void Sample::bar(void)
{
// do whatever
return ;
}
#include "Sample.class.hpp"

int main(void)
{
Sample instance;

instance.foo = 42;// si foo était un pointeur on écrirait instance->foo = 42
instance.bar();
return (0);
}

Pointeur sur l’instance courante

https://cdn.intra.42.fr/video/video/103/piscine_c___-_d00_-_04_this.mp4

Vous rencontrerez souvent le mot clé “this”. Il s’agit d’un pointeur sur sur l’instance courante.

Il est passé systématiquement (et discrètement) en paramètre de chaque fonction membre.

include <iostream>
#include "Sample.class.hpp"

Sample::Sample(void)
{
// Initialisation dans le constructeur
this->foo = 42; // -> permet de déréférencer (pointeur) - comme les structures en C
this->bar(); // this est un pointeur qui pointe sur l'instance courante
return ;
}

Sample::~Sample(void)
{
return ;
}

void Sample::bar(void)
{
// do whatever
return ;
}

Liste d’initialisation

Les constructeurs peuvent prendre des paramètres, afin que nous puissions initialiser ses attributs membres. Plutôt que de le faire dans la déclaration de la fonction, nous pouvons utiliser une “liste d’initialisation”, à la syntaxe particulière.

Cela permettra notamment d’initialiser des variables constantes (const).

https://cdn.intra.42.fr/video/video/104/piscine_c___-_d00_-_05_init_list.mp4

#pragma once 

class Sample {
public:
int a1;
char a2;
float a3;

//Un constructeur avec 3 arguments
Sample(int p1, char p2, float p3);
// On pourrait également avoir un autre constructeur sans argument
Sample(void);

~Sample(void);

void bar(void);
};
// liste initialisation (permet d'initialiser des constantes (ou variables non constantes))
Sample::Sample(int p1, char p2, float p3): a1(p1), a2(p2), a3(p3)
{
return ;
}

Sample::Sample(void)
{
return ;
}

Sample::~Sample(void)
{
return ;
}

void Sample::bar(void)
{
// do whatever
return ;
}
#include "Sample.class.hpp"
#include <iostream>

int main(void)
{
Sample instance(4, 'c', 2.0f);
return (void)
}

Les attributs et fonctions const

https://cdn.intra.42.fr/video/video/105/piscine_c___-_d00_-_06_const.mp4

#pragma once 

class Sample {
public:
// attribut const
int const a1;
char a2;
float a3;

//Un constructeur avec 3 arguments
Sample(int const p1, char const p2, float p3);
// On pourrait également avoir un autre constructeur sans argument
Sample(void);

~Sample(void);

// Autre utilisation de const
void bar(void) const;
};
// liste initialisation (permet d'initialiser des constantes (ou variables non constantes))
Sample::Sample(int const p1, char p2, float p3): a1(p1), a2(p2), a3(p3)
{
return ;
}

Sample::Sample(void)
{
return ;
}

Sample::~Sample(void)
{
return ;
}

// Avec const on signifie que cette fonction membre ne modifiera jamais l'instance courante
void Sample::bar(void) const
{
// do whatever
return ;
}

Fonctions const

Avec const (avant le ; ou les {} d’une fonction) on signifie que cette fonction membre ne modifiera jamais l’instance courante = l’instance est en “read-only”

Encapsulation (public, private, puis protected)

https://cdn.intra.42.fr/video/video/107/piscine_c___-_d00_-_07_encapsulation.mp4

#pragma once 

class Sample {
public:
//tout ce qui se trouve en dessous du mot clé est public
int const a1;
char a2;
float a3;
Sample(int const p1, char const p2, float p3);
Sample(void);
~Sample(void);
void bar(void) const;

private:
//tout ce qui se trouve en dessous du mot clé est private
int _privateFoo;
int _privateBar(void) const;

};

Public et private permettent de contrôler l’encapsulation des membres de notre classe (cela permet de définir/savoir si ces attributs ou fonctions seront accessibles depuis l’extérieur de la classe ou uniquement l’intérieur).

Choisir de rendre les attributs et fonctions privées permet de masquer les détails d’implémentation de notre classe et de laisser visible la partie qui pourra être utile à l’utilisateur (dans le main, par exemple).

Il pourra être utile de mettre un constructeur dans la partie privée, même si par défaut le constructeur est public, comme le destructeur (nous verrons plus tard les classes canoniques).

Les attributs privés doivent (par convention) être notés (préfixés) avec des _ (underscore) .

Différence entre structure et classe

https://cdn.intra.42.fr/video/video/106/piscine_c___-_d00_-_08_class_vs_struct.mp4

En cpp, les structures ont des scopes (par défaut) publiques, tandis qu’une classe a un scope (par défaut) privé.

Accesseurs (getters, setters)

https://cdn.intra.42.fr/video/video/108/piscine_c___-_d00_-_09_accessors.mp4

Pour pouvoir influer sur la valeur d’un attribut privé (lecture: getter ou écriture: setter), on va définir des accesseurs (qui seront dont publiques).

On n’oubliera pas de définir des getters const puisque l’instance courante ne sera pas modifiée.

#pragma once 

class Sample {
public:
Sample(void);
~Sample(void);

// Les accesseurs doivent être publiques !
int getFoo(void) const;
void setFoo(int value);

private:
int _foo;
};
Sample::Sample(void)
{
return ;
}

Sample::~Sample(void)
{
return ;
}

int Sample::getFoo(void) const
{
return this->foo;
}

void Sample::setFoo(int value)
{
this->foo = value;
return ;
}

Attributs et fonctions non membres (= static, de classe)

https://cdn.intra.42.fr/video/video/112/piscine_c___-_d00_-_11_non_membres_attributs_and_non_membres_fonctions.mp4

Attributs et fonctions membres peuvent également être appelés “variables et fonctions d’instance”.

Il existe des attributs et des fonctions non membres, aussi appelés “variables et fonctions de classe”.

On utilisera le mot clé “static” pour identifier ces attributs et fonctions de classe.

#pragma once 

class Sample {
public:
Sample(void);
~Sample(void);

// fonction non membre
static int getNbInstances(void) const;

private:
// attribut non membre (ne s'initialisera pas dans un constructeur)
static int _nbInstances;
};
Sample::Sample(void)
{
Sample::nbInstances += 1;
return ;
}

Sample::~Sample(void)
{
Sample::nbInstances -= 1;
return ;
}

// Ici on ne re mentionne pas static
// De plus, this n'est pas passé implicitement en argument ici
// Il ne sera jamais accessible dans une fonction de classe.
int Sample::getNbInstance(void) const
{
return Sample::_nbIntances;
}

int Sample::_nbInstances = 0;

Pointeurs sur attributs membres, pointeurs sur fonctions membres

https://cdn.intra.42.fr/video/video/113/piscine_c___-_d00_-_12_pointers_to_members.mp4

Le language C permettait de définir des pointeurs sur tous types de données, dont des pointeurs sur fonctions.

Le C++ permet également de définir des pointeurs attributs membres et des pointeurs sur fonctions membres.

Cela permettra d’écrire de manière élégante des “dispatchers” (appels de différentes fonctions en évitant une forêt de if else)

int main(void)
{
Sample instance;
Sample *instance_ptr = &instance;

// Equivalent pointeur
int Sample::*p = NULL;// pointeur sur un attribut membre de type d'int d'une intance de la classe

// Equivalent pointeur sur fonction
void (Sample::*f)(void) const: // pointeur sur une fonction membre d'une instance de la classe


// POINTEUR SUR ATTRIBUT D'INSTANCE
// Ici on découvre deux nouveaux opérateurs .* et ->*.
// Ils permettent d'indiquer sur quelle instance de la classe le pointeur pointe
instance.*p = 21;//va modifier la valeur de notre attribut foo
instance_ptr->*foo = 42;

// POINTEUR SUR FONCTION D'INSTANCE
f = &Sample::bar();

// Appel de la fonction via le pointeur sur fonction
(instance.*f)();
(instance_ptr->*f)();

return (0);
}

--

--

Mahaut Latinis
Mahaut Latinis

No responses yet