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 environment est bien configuré.
Programme minimaliste
Code: 01_first_program/a_introduction
-
- Si vous êtes sous Windows: executez le script du fichier
scripts/visual-studio-generate.batafin de générer un projet Visual Studio, puis ouvez Visual Studio depuis le fichiera_introduction/a_introduction.sln. -
- Si vous êtes sous Mac/Linux: ouvez 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éja existante en C. Les bibliothèques issues du language 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'identation 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 et 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 couteuse 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'utilisez 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 contigues 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é au cours de l'execution 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é 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 couteuse 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);
Export ASCII
Code: 01_first_program/b_ascii_tree
> 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` : * * * * * *** *** * ***** ***** * ******* ******* * ||| ********* * ||| */
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
> Implémentez une fonction qui affiche un histogramme horizontal à partir d'un vecteur de valeurs entières.
La signature de la fonction sera:
void print_histogram(const std::vector<int>& values);
values = {3, 7, 2, 5}:
0 | *** 1 | ******* 2 | ** 3 | *****
|, puis autant d'astérisques que la valeur.
- Utilisez une boucle imbriquée pour afficher les astérisques.
> Modifiez votre fonction pour normaliser l'affichage: le plus grand élément doit toujours correspondre à une largeur fixe (par ex. 40 caractères). Les autres barres sont mises à l'échelle proportionnellement.
void print_histogram_normalized(const std::vector<int>& values, int max_width = 40);
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 pouvez 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 noir (ou fond bleu, etc.)