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 environment 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 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;
}
> 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 renvoit la norme de ce vecteur. Vérifiez votre fonction en affichant le 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 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:
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êt.
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 contigues 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é 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.
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 renvoit un std::vector de taille \(N\) 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);

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` :
* 
*    *                                *
*   ***                              ***
*  *****                            *****
* *******                          *******
*   |||                           *********
*                                    |||
*/
Hints : - 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.
> 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
> 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);
Exemple d'affichage attendu pour values = {3, 7, 2, 5}:
0 | ***
1 | *******
2 | **
3 | *****
Hints: - Parcourez le vecteur avec une boucle. - Pour chaque valeur, affichez l'indice, puis le caractère |, 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:
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 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:
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