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
Suivez les instructions de compilation: Installation des outils de compilation
afin de vérifier que votre environnement est bien configuré.

Programme minimaliste

Code: 01_first_program/a_introduction
L'organisation des fichiers du répertoire est la suivante:
Le fichier src/main.cpp contient le code suivant:
#include <iostream>
#include <cmath>

int main()
{
    std::cout << "Hello world" << std::endl;
    return 0;
}
On rappelle les éléments suivants:
Remarques générales:

Fonctions

Les fonctions suivent la syntaxe suivante
typeRetour nomFonction(type nomArgument1, type nomArgument2, etc.)
{
    ... code de la fonction
    return value;
}
Exemple:
int addition(int a, int b)
{
    return a+b;
}
Il faut nécessairement que la signature d'une fonction soit déclarée avant son utilisation. Sinon il y aura une erreur de compilation.
Exemples:
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;
}
> Ajoutez une fonction appelée "norm" qui prend en paramètre trois valeurs de type "float" assimilés aux coordonnées d'un vecteur \((x,y,z)\) et renvoie la norme de ce vecteur. Vérifiez votre fonction en affichant les résultats de quelques cas particuliers. La fonction aura la signature suivante:
float norm(float x, float y, float z)
Quelques fonctions utiles
// 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:
Vous rencontrerez également les types suivants:
Rem. Le type char possède la garantie d'être encodé sur 1 octet. Il peut être utilisé également à ce titre pour définir finement des valeurs en mémoires à l'octet près.
Remarques:
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
auto a = 5;    // a est un int
auto b = 8.4f; // b est un float
auto c = 4.2;  // c est un double

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.

  • 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.
Exemple d'utilisation d'un std::vector
#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;
}
> Implémentez une fonction qui génère et renvoie un std::vector de taille \(N\geq 2\) passée en paramètre, et dont le contenu varie entre 0 et 1 suivant un pas d'incrémentation de \(1/(N-1)\).
La signature de la fonction doit être
std::vector<float> generate_vector(int N);
Vérifiez certaines valeurs de votre vecteur
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;
}
> Implémentez une fonction qui calcule la norme d'un vecteur de taille générique passé en paramètre. La signature de la fonction sera
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` :
* 
*    *                                *
*   ***                              ***
*  *****                            *****
* *******                          *******
*   |||                           *********
*                                    |||
*/
Hints :
> Modifiez ensuite votre fonction de manière à ce qu'elle retourne le contenu ASCII sous forme d'une 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;
}
> Créez une fonction qui exporte un sapin dans un fichier texte. On pourra considérer la signature de fonction suivante:
// 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 utilise std::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 fonction build_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);
Etapes:
> Implémentez la fonction 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);
Pour chaque entrée de la map, affichez le mot, puis le caractère |, puis autant d'astérisques que le nombre d'occurrences.
Exemple de sortie (extrait):
a | ************
and | *********
children | ******
the | ******************
Hints:

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:
La fonction 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 ...
...

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:
> Complétez la fonction suivante pour exporter une image image_structure au format PPM ASCII (P3). Signature:
void export_ppm(const image_structure &img, const std::string &filename);
Contraintes:
Indications:
// Création d’un fichier
std::ofstream out(filename);

// Écriture
out << "du texte" << "\n";

Tests rapides

Une fois export_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: Le fichier 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:
assets/gradient.jpg
> Créez une image en damier (checkerboard):
assets/checkerboard.jpg
> Créez une image en bruit (random_noise):
float rand_float = static_cast <float> (rand()) / static_cast <float> (RAND_MAX);
assets/random_noise.jpg
> Créez un disque coloré centré dans l'image:
assets/disk.jpg