Séance 1: Introduction
Téléchargement du code
Téléchargez le code des séances depuis la page github: Rem. Si vous êtes sous Windows: placez votre code dans un répertoire sans espaces ni accents dans le chemin.- Par ex. C:\Cours\2A\43043\csc43043ep-lab-code, et non pas C:\Céline\Mes Cours\csc43043ep-lab-code
afin de vérifier que votre environnement est bien configuré.
Programme minimaliste
Code: 01_first_program/a_introduction
-
- Si vous êtes sous Windows: exécutez le script du fichier
scripts/visual-studio-generate.batafin de générer un projet Visual Studio, puis ouvrez Visual Studio depuis le fichiera_introduction/build/project.sln(ouproject.slnx). Pensez à compiler en mode "RelWithDebInfo" pour avoir les options de debug. -
- Si vous êtes sous Mac/Linux: ouvrez VS Code depuis le fichier
a_introduction/vscode.code-workspace(relwithDebInfo). Choisissez la configuration "RelWithDebInfo" pour compiler avec les options de debug.
-
CMakeLists.txt: Fichier de configuration CMake décrivant le projet et les fichiers sources à compiler. -
CMakePresets.json: Fichier de configuration CMake uniquement utile pour VS Code. -
vscode.code-workspace: Fichier workspace pour ouvrir le projet dans VS Code (Mac/Linux). -
script/visual-studio-generate.bat: Script de génération du projet Visual Studio (Windows uniquement). -
src/: Répertoire contenant les fichiers sources C++ (.cpp et .hpp). -
src/main.cpp: Le fichier source principal contenant la fonctionmain().
#include <iostream> #include <cmath> int main() { std::cout << "Hello world" << std::endl; return 0; }
- #include <iostream>
-
- - #include permet d'inclure le code d'un autre fichier. Il permet le mécanisme d'inclusion de bibliothèque externe en C++.
-
- #include <FileName> permet l'inclusion du fichier nommé
FileName. Les chevrons indiquent que ce fichier doit être préférentiellement cherché dans les fichiers système (ce qui est le cas pour iostream). Le mécanisme d'inclusion est très minimaliste car le contenu du fichier est littéralement copié/collé lors de la compilation. - - iostream correspond à la bibliothèque standard C++ d'entrée sortie (iostream = Input Output Stream). Elle permet en particulier l'affichage sur la ligne de commande.
- - cmath correspond à la bibliothèque de fonction mathématique déjà existante en C. Les bibliothèques issues du langage C (cLibName).
- int main()
-
- Correspond au point d'entrée, ou "de départ", de l'exécutable, c'est-à-dire la fonction qui va être appelée automatiquement lorsque vous lancez l'exécutable.
- Tout programme C++ doit nécessairement contenir une (et une seule) fonction appelée main.
- std::cout << "Hello World" << std::endl;
-
- std :: cout << syntaxe standard pour afficher du texte sur la ligne de commande (ici "Hello World")
-
- - cout signifie Common Output. (réfère par défaut à la ligne de commande sauf si l'on a redirigé la sortie standard)
- - std::cout est le nom complet de l'objet cout qui est présent dans l'espace de nommage (namespace) std (standard library).
- - :: est appelé l'opérateur de résolution de portée (scope resolution operator). Ici il permet de trouver l'objet cout dans l'espace de nom std
- - << est un opérateur C++ (c-a-d un symbole utilisé en tant que fonction). Ici l'opérateur << est utilisé pour envoyer la chaine de caractère (string) à l'objet std::cout afin de l'afficher en ligne de commande.
- <<std::endl Ajout un saut de ligne à la fin de l'affichage. endl réfère à end of line.
- return 0; Valeur de retour de l'exécutable (renvoi au processus appelant - par exemple la ligne de commande). Par convention une valeur de retour de 0 indique que le programme s'est terminé sans erreurs.
- - Chaque instruction de code C++ doit nécessairement terminer par un point virgule ;
- - Contrairement à Python, mais similairement à Java, l'indentation et les sauts de lignes n'ont pas d'importance sur l'exécution du code.
Fonctions
Les fonctions suivent la syntaxe suivantetypeRetour nomFonction(type nomArgument1, type nomArgument2, etc.) { ... code de la fonction return value; }
int addition(int a, int b) { return a+b; }
- - Une fonction qui ne renvoie pas de valeur aura pour typeRetour void.
- - Une fonction qui ne prend pas d'argument aura simplement des parenthèses vides.
- - La première ligne décrivant le nom et les types de la fonction est appelée la signature ou l'en-tête de la fonction.
- - Le reste est appelé le corps ou l'implémentation de la fonction.
int addition(int a, int b) { return a+b; } int main() { // OK // La signature (et le corps) // est déclarée avant. int c = addition(5,3); }
int addition(int a, int b); int main() { // OK // La signature est déclarée avant. int c = addition(5,3); } int addition(int a, int b) { return a+b; }
int main() { // KO - ne compile pas, // la fonction "addition" // n'est pas déclarée. int c = addition(5,3); } int addition(int a, int b) { return a+b; }
float norm(float x, float y, float z)
// Le carré d'une valeur float x2 = x*x; // La racine carré d'une valeur float y = std::sqrt(x); // La puissance p quelconque d'une valeur x (puissance pas forcément entière, fonction plus générique mais plus coûteuse que les deux précédentes). float y = std::pow(x, p); // Attention: N'utilisez pas les symboles ^ ou ** qui ne sont pas un calcul de puissance en C++.
Types fondamentaux C++ (Rappels)
Vous utiliserez principalement deux types fondamentaux dans vos codes:- int: qui correspond à un nombre entier (integer). Sur nos machines, un int est encodé sur 4 octets.
-
- ex. int = 325;
- float: qui correspond à un nombre à virgule flottante dit à "simple précision". Sur nos machines, un float est encodé également sur 4 octets.
-
- ex. float = 3.2f;
- bool: Valeur booléenne qui peut prendre la valeur true/false.
-
- Ce type est introduit en C++ (n'existe pas en C), et permet un code plus expressif que l'utilisation d'un entier.
- double: Un nombre à virgule flottante dit à "double précision". Sur nos machines, un double est encodé sur 8 octets.
-
- ex. double = 3.2;
- Ce type est celui utilisé par défaut (nombre à virgule sans extension), mais nous utiliserons dans notre cas davantage les "float" qui sont compatible avec la carte graphique.
- char: Un caractère dont la correspondance entre valeur/caractère est donnée par la table ASCII.
- 1) Attention lorsque vous effectuez une division entre deux entiers: vous obtenez le quotient de la division euclidienne en entier et non pas un nombre flottant.
int a = 5/2; // vaut 2 (division Euclidienne) int b = 5%2; // vaut 1 (reste de la division Euclidienne) // Si vous souhaitez obtenir 2.5, il est nécessaire qu'au moins l'un des deux argument soit un flottant float c = 5/2.0f; // OK - 2.5 float d = 5.0f/2; // OK - 2.5 float e = float(5)/2; // OK - 2.5 // ... etc
- 2) Lorsque vous ne souhaitez/connaissez pas le type, il est possible d'utiliser auto. Le compilateur complétera alors le type automatiquement.
auto a = 5; // a est un int auto b = 8.4f; // b est un float auto c = 4.2; // c est un double
-
- Pour simplifier la lisibilité et compréhension de votre code, évitez d'utiliser auto pour des types simples. Ce mot clé est surtout pratique pour des fonctions générique ou pour éviter d'avoir à écrire des types trop long.
- 3) En C++, les types fondamentaux ne sont pas initialisés à de valeurs spécifiques par défaut.
-
- ex. "int a;" ne vaudra donc pas nécessairement 0 si on ne le définit pas explicitement avec "int a=0;".
- \(\Rightarrow\) Pour éviter les "comportements indéterminés", il est donc préférable d'initialiser ces valeurs au moment de leur déclaration.
Arrays/Vectors
La bibliothèque standard C++ (appelée STL) définit des types génériques de vecteurs/tableaux de valeurs stockés de manière contiguë en mémoire.- std::vector<T> est un tableau de valeur de type "T" (T doit être explicité à la compilation) dont la taille peut être modifiée au cours de l'exécution du programme. On parle de tableau "dynamique".
- std::array<T, N> est un tableau de valeur de type "T" et de taille entière "N". N doit être connu à la compilation et ne peut plus être modifiée plus tard. On parle de tableau "statique".
- Les valeurs d'un std::array sont stockés sur une mémoire dite de la pile (stack memory) qui est plus efficace, mais limitée en taille (typiquement 2Mo). Les valeurs d'un std::vector sont stockés sur la mémoire dite du tas (heap memory) qui peut être légèrement plus coûteuse lors de l'allocation et utilisation intensive, mais qui donne accès à l'intégralité de votre mémoire RAM.
#include <iostream> #include <cmath> // Add inclusion for std::vector and std::array #include <vector> #include <array> // this one is optional here int main() { // Create an empty vector containing integers std::vector<int> vec; // the vector is empty at this state, its size is 0 // Can add elements one after the other (the vector is resized automatically) vec.push_back(5); vec.push_back(6); vec.push_back(2); // Check the size of the vector std::cout << "Vector has " << vec.size() << " elements" << std::endl; // Elements of the vector can be accessed via // vec[0], ..., vec[k], ..., vec[vec.size()-1]. std::cout <<"first element: " << vec[0] << std::endl; // You can write over an indexed element vec[1] = 12; // now the second element will be 12 // We can loop over all elements of a vector for (int k = 0; k < vec.size(); ++k) { std::cout << "Element " << k << " : " << vec[k] << std::endl; } // You should not access/write over an element beyond the size of the vector // Try to uncomment these two lines and see the result (the program may crash, but this is actually un undefined behavior that depends on your system). // vec[8568] = 12; // std::cout<<vec[8568]<<std::endl; // You can resize (or allocate an initial size) for the vector using .resize(N) vec.resize(10000); // previously existing elements (the 3 first values) are kept, and the new elements are initialized to 0 std::cout << "New vector size: " << vec.size() << std::endl; // Now it is legit to access/write on element vec[8568] vec[8568] = 12; std::cout << "Element 8568 : " << vec[8568] << std::endl; return 0; }
std::vector<float> generate_vector(int N);
int main() { std::vector<float> vec = generate_vector(101); std::cout<<vec[0]<<std::endl; // should be 0 std::cout<<vec[100]<<std::endl; // should be 1 std::cout<<vec[50]<<std::endl; // should be 0.5 std::cout<<vec[25]<<std::endl; // should be 0.25 return 0; }
float norm(std::vector<float> vec); // or // float norm(const std::vector<float>& vec);
Notez qu'il s'agit ici d'une surcharge de fonction (_function overloading_): deux fonctions portent le même nom
norm, mais avec des signatures différentes (types et/ou nombre de paramètres). Le compilateur choisit automatiquement la bonne version à appeler en fonction des arguments passés.
La première signature
norm(std::vector<float> vec) passe le vecteur par copie: le contenu entier du vecteur est dupliqué en mémoire à chaque appel. La seconde signature norm(const std::vector<float>& vec) passe le vecteur par référence constante: aucune copie n'est effectuée, seule une référence vers le vecteur original est transmise. Le mot-clé const garantit que la fonction ne modifiera pas le contenu du vecteur. Le passage par référence constante est à privilégier pour les objets de taille importante (vecteurs, chaînes, etc.) car il évite une copie coûteuse en mémoire et en temps.
Export ASCII
Code: 01_first_program/b_ascii_tree
On se place désormais dans le contexte du répertoire 01_first_program/b_ascii_tree. Ouvrez le code de ce répertoire et compilez-le en suivant les mêmes démarches que précédemment.
> Dans le fichier main.cpp, complétez la fonction void print_tree(int levels) afin d'afficher sur la ligne de commande un sapin en ASCII dont la hauteur est donnée en paramètre.
Le sapin doit ressembler à l'exemple ci-dessous:
/* * Exemple pour `levels = 4` , `levels = 5` : * * * * * *** *** * ***** ***** * ******* ******* * ||| ********* * ||| */
- - Chaque ligne contient un certain nombre d'espaces puis d'astérisques.
- - Le nombre d'étoiles augmente de 2 à chaque niveau.
- - La largeur maximale correspond à la dernière ligne de feuillage.
- - Le tronc peut être codé séparément après la boucle principale.
std::string. Notez que l'on peut concaténer des std::string avec l'opérateur + et +=, ou ajouter en fin de chaine avec la fonction [string].append([to_add]).
L'affichage dans la fonction main se réalisera alors avec l'appel suivant:
int main() { std::cout<< print_tree(4) <<std::endl; return 0; }
// filename: nom du fichier de sortie // level: hauteur du sapin void export_tree_file(const std::string& filename, int level);
Histogramme ASCII
Code: 01_first_program/c_ascii_histogram
Dans cet exercice, on souhaite compter les occurrences de chaque mot dans un fichier texte, puis afficher le résultat sous forme d'histogramme ASCII. Le fichier src/input.txt contient un texte qui servira d'entrée.
std::map
Pour associer chaque mot à son nombre d'occurrences, on utilise le conteneur std::map<Key, Value> de la bibliothèque standard (nécessite#include <map>). Une std::map est un dictionnaire qui associe une clé à une valeur (similaire à un dictionnaire Python).
#include <map> #include <string> std::map<std::string, int> histogram; // Insertion / mise à jour d'une entrée histogram["hello"] = 1; // Incrémentation : si la clé n'existe pas encore, // elle est créée automatiquement avec la valeur 0, puis incrémentée. histogram["hello"]++; // vaut maintenant 2 histogram["world"]++; // créé avec la valeur 1 // Parcours de la map (les clés sont triées par ordre alphabétique) for (const auto& it : histogram) { std::cout << it.first << " : " << it.second << std::endl; // it.first = la clé (std::string) // it.second = la valeur (int) }
Lecture d'un fichier texte
Pour lire un fichier, on utilisestd::ifstream (nécessite #include <fstream>). L'opérateur >> permet de lire les mots un par un (séparés automatiquement par les espaces et retours à la ligne).
#include <fstream> #include <string> std::ifstream file("src/input.txt"); // Vérification de l'ouverture if (!file) { std::cerr << "Erreur: impossible d'ouvrir le fichier" << std::endl; } // Lecture mot par mot std::string word; while (file >> word) { std::cout << word << std::endl; // affiche chaque mot }
Exercices
> Implémentez la fonctionbuild_word_histogram qui lit un fichier texte et construit une std::map comptant les occurrences de chaque mot.
La signature de la fonction est:
std::map<std::string, int> build_word_histogram(const std::string& filename);
-
- Créer une
std::map<std::string, int>vide. -
- Ouvrir le fichier avec
std::ifstream. -
- Lire les mots un par un avec
while (file >> word). -
- Pour chaque mot lu, incrémenter son compteur dans la map:
histogram[word]++. - - Retourner la map.
print_histogram qui affiche le contenu de la map sous forme d'histogramme ASCII horizontal.
La signature de la fonction est:
void print_histogram(const std::map<std::string, int>& hist);
|, puis autant d'astérisques que le nombre d'occurrences.
Exemple de sortie (extrait):
a | ************ and | ********* children | ****** the | ******************
-
- Parcourez la map avec une boucle:
for (const auto& it : hist) { ... } -
- Accédez à la clé avec
it.firstet à la valeur avecit.second. - - Utilisez une boucle imbriquée pour afficher les astérisques.
Export d'images PPM
Code: 01_first_program/d_ppm_image
Dans cette partie, on souhaite exporter une image simple au format PPM (Portable PixMap). C’est un format d’image extrêmement minimaliste, pratique pour débuter car il ne nécessite aucune bibliothèque externe.
Nous allons utiliser la variante ASCII du format, appelée P3 (les valeurs de pixels sont écrites sous forme de nombres dans un fichier texte).
Structure d'image en mémoire
On utilise la structure suivante:- - Nx, Ny : largeur et hauteur (en pixels)
-
- data : tableau contigu de flottants de taille
3*Nx*Ny -
- Chaque pixel stocke 3 composantes (R,G,B) dans cet ordre.
- Les valeurs sont supposées être dans [0,1].
-
L'index du pixel
(x,y)est:offset = 3 * (y*Nx + x)
set_pixel(x,y, r,g,b) écrit les composantes au bon endroit dans le tableau.
Format PPM (P3)
Un fichier PPM ASCII suit la forme:P3 <Nx> <Ny> 255 r g b r g b r g b ... ...
-
- La première ligne
P3indique PPM ASCII. - - La deuxième ligne indique les dimensions.
-
- La troisième ligne
255indique la valeur maximale d’une composante. - - Ensuite on écrit tous les pixels, sous forme d’entiers entre 0 et 255, séparés par des espaces (les retours à la ligne sont libres, mais on les met souvent en fin de ligne de pixels pour la lisibilité).
Conversion float -> int
Nos pixels sont stockés en flottants entre 0 et 1, alors que le fichier PPM demande des entiers entre 0 et 255. On pourra utiliser une conversion du type:-
- clamp:
x = max(0, min(1, x)) -
- scale:
x*255 -
- arrondi:
int(x*255 + 0.5)
image_structure au format PPM ASCII (P3).
Signature:
void export_ppm(const image_structure &img, const std::string &filename);
-
- Le fichier doit être ouvert en écriture avec
std::ofstream. - - Écrire l’en-tête PPM (P3, dimensions, 255).
- - Écrire ensuite les pixels sous forme d’entiers.
-
- Parcourir l’image ligne par ligne (par exemple
ydeNy-1à0, puisxde0àNx-1). -
- Terminer chaque ligne
ypar un saut de ligne.
// Création d’un fichier std::ofstream out(filename); // Écriture out << "du texte" << "\n";
Tests rapides
Une foisexport_ppm implémentée, vous pourrez créer de petites images (par ex. 32x32) et vérifier visuellement le résultat.
> Faites un premier test:
- - Image blanche (valeurs initialisées à 1)
- - Un pixel rouge à (10,10)
output.ppm doit pouvoir être ouvert par certains logiciels d'image (ou converti facilement).
Rem. Si votre visionneuse ne lit pas le PPM directement, vous pouvrez utiliser des outils tels que GIMP.
Génération d'images procédurales simples
> Créez une fonction qui génère un dégradé horizontal noir → blanc:-
-
r=g=b = x/(Nx-1) -
- Pour tout
y, pour toutx
-
- Choisir une taille de case
s(par ex.s=4) -
- Une case noire si
(x/s + y/s) % 2 == 0, blanche sinon
-
- Pour chaque pixel, choisir
r,g,baléatoirement entre 0 et 1 - - On pourra utiliser la syntaxe suivante pour générer un flottant aléatoire entre 0 et 1:
float rand_float = static_cast <float> (rand()) / static_cast <float> (RAND_MAX);
-
- Centre
(cx, cy) = (Nx/2, Ny/2) -
- Rayon
R -
- Si
(x-cx)^2 + (y-cy)^2 < R^2alors pixel rouge, sinon blanc.