Types et classes en C++

Déclaration de variables

Les variables doivent se déclarer avec leur type
type nomVariable;
// ex. 
// int maVariable;
// float y;
Il est possible d'affecter une valeur directement lors de la déclaration
type nomVariable = valeur;
// ou similairement:
//  type nomVariable(valeur);
//  type nomVariable = type(valeur);
Il est possible de spécifier qu'une variable va être constante par le mot clé "const"
type const nomVariable = valeur;
// ou similairement
const type nomVariable = valeur;
Une variable qualifiée de "const" ne peut plus voir sa valeur modifiée.
int const a = 5;
a = 6; // erreur de compilation
Remarque: L'utilisation de "const" ne modifie généralement rien à l'exécution du programme, mais il permet d'aider le programmeur à éviter de modifier la valeur d'une variable qui ne le devrait pas par inadvertance. Il s'agit principalement d'un mot clé permettant de préserver une bonne qualité de code et d'éviter des bugs.

Durée de vie des variables

La durée de vie des variables standard ("scope") est celle du bloc d'instruction dans laquelle elle est créée ("bloc scope") - c-a-d depuis sa déclaration jusqu'à l'accolade fermante "}" du bloc où elle a été créée.
int main()
{
    if(someCondition) {
        int x = 5; // x est défini dans le bloc "if"
        ...
        // x existe jusqu'à la fin du bloc
    }
    // x n'existe plus ici 
}
int main()
{
    int x = 5; // x est défini dans le bloc
               //  de la fonction main()
    if(someCondition) {
        ...
        // x peut être utilisé ici (sous-bloc)
    }
    // x existe toujours jusqu'à la fin de main()
}
Remarque:

Classes

Considérez le code du répertoire scenes_inf443/03a_cpp_classes/ qui définit un objet appelé vec3 qui est composé de trois nombres flottants: x,y,z.
#include <iostream>
#include <cmath>

struct vec3 {
    float x, y, z;
};

int main()
{
    // create an un-initialized vec3
    vec3 p1; 

    // create and initialize vec3
    vec3 p2 = {1.0f, 2.0f, 5.0f}; 

    // Access attributes of the struct
    p2.y = -4.0f;

    std::cout<<p2.x<<","<<p2.y<<","<<p2.z<<std::endl;

    return 0;
}
Les objets en C++ se déclarent de la manière suivante en utilisant le mot clé struct ou class:
struct/class nomObject {
    public:
    // parametres et méthodes accessible publiquement
    private:
    // parametres et méthodes privées de l'objet.
};
Exemple de déclaration:
struct vec3 {
    float x, y, z;
};
class vec3 {
    public:
    float x, y, z;
};
Utilisation:
int main()  {
    // Creation d'une instance de l'objet
    vec3 p = {5.3f, 1.1f, 3.2f};

    // Accès à un membre public
    p.y = -4.5f;
}
Remarques:

Méthodes

Les classes peuvent porter des méthodes -- également appelée fonction membres. Exemple
struct vec3 {
    float x, y, z;

    float norm() const;
    void display() const;
    void normalize();
};

float vec3::norm() const {
    return std::sqrt(x * x + y * y + z * z);
}
void vec3::normalize() {
    float n = norm();
    x /= n;
    y /= n;
    z /= n;
}
void vec3::display() const {
    std::cout <<"(" << x << "," << y << "," << z << ")" << std::endl;
}

int main()
{
    vec3 p2 = { 1.0f, 2.0f, 5.0f };

    // Display the norm (doesn't change p2)
    std::cout << p2.norm() << std::endl;

    // modifies p2
    p2.normalize();

    // Display the values on the command line
    p2.display();

    return 0;
}
returnType objectName::methodName(arguments ...) [const] {
  ...
}
// Define norm to be a non-member function
float norm(vec3 const& p) {
    return std::sqrt(p.x*p.x + p.y*p.y + p.z*p.z);
}
// note: the use of const& avoids to copy the argument of the vec3
//   however float norm(vec3 p) would works too.

int main() {
  ...
  vec3 p = ...
  // Use norm as a function (and not a member function)
  float n = norm(p);
}
Testez votre implémentation sur ce type d'utilisation
int main()
{
    vec3 p1 = { -1.0f, 1.0f, 2.0f };
    vec3 p2 = { 1.0f, 2.0f, 5.0f };

    // Expected display: 11 ; 11
    std::cout << p1.dot(p2) <<" ; "<<dot(p1,p2) << std::endl; 

    vec3 p3 = p1.cross(p2);
    vec3 p4 = cross(p1, p2);

    p3.display(); // Expected display: (1,7,-3)
    p4.display(); // Same

    return 0;
}
Solution possible

Opérateurs

C++ permet d'utiliser des opérateurs (/symbols) entre classes tels que * / + -, ce qui permet de simplifier l'écriture d'opérations vectorielles.
La plupart des opérateurs peuvent s'écrire comme des fonctions simples. Exemple de définition de l'opérateur + entre deux vec3
// Enable to write: c = a+b with vec3
vec3 operator+(vec3 const& a, vec3 const& b) {
    return { a.x + b.x, a.y + b.y, a.z + b.z };
}
// First argument a -> variable before the operator
// Second argument b -> variable after the operator

int main()
{
    vec3 a = { -1.0f, 1.0f, 2.0f };
    vec3 b = { 1.0f, 2.0f, 5.0f };

    vec3 c = a + b;

    c.display();

    return 0;
}
> Définissez l'opérateur *, de manière à pouvoir écrire le code suivant
int main()
{
    vec3 a = { -1.0f, 1.0f, 2.0f };

    vec3 b = 2.0f * a;
    vec3 c = a * 2.0f;

    b.display(); // Expect (-2,2,4)
    c.display(); // Same as before

    return 0;
}
Aide: Vous devez définir deux fonctions dont l'ordre des arguments est différent.
Solution possible

Constructeurs et destructeurs

Les informations suivantes sont à destination des étudiants qui souhaitent en savoir plus sur le C++. Il n'est pas nécessaire de connaitre ces détails pour réaliser les TP d'INF443.
Les objets peuvent être associés à des méthodes particulières qui sont appelées automatiquement.
Considérez le code présent du répertoire scenes_inf443/03a_cpp_classes/b_constructor/.
#include <iostream>
#include <cmath>

struct vec3 {
    float x, y, z;

    // Constructor (without argument)
    vec3();

    // Constructor with arguments
    vec3(float x, float y);

    // Copy constructor
    vec3(vec3 const& v);

    // Destructor
    ~vec3();

    void display() const;
};

// This is another operator: <<, that is used to be able to write cout<<vec3
std::ostream& operator<<(std::ostream& s, vec3 const& v)
{
    s << "(" << v.x << "," << v.y << "," << v.z << ")";
    return s;
}
void vec3::display() const
{
    std::cout<< "(" << x << "," << y << "," << z << ")";
}

vec3::vec3() {
    std::cout << "> I am the empty constructor. I set everything to 1." << std::endl;

    // This is an unusual construction for a vector
    // only for the sake of learning c++
    x = 1; y = 1; z = 1;
}

vec3::vec3(float x_arg, float y_arg)
{
    std::cout << "> I am the constructor with two argument (" <<x_arg<<", "<<y_arg <<")"<< std::endl;
    x = x_arg;
    y = y_arg;
    z = 0;
}

vec3::~vec3()
{
    std::cout << "> I am the destructor of this vector: ";
    display();
    std::cout << std::endl;
    
    // The destructor is useless in this example
}

vec3::vec3(vec3 const& to_be_copied)
{
    std::cout << "> I am the copy constructor. I am going to copy this vector: ";
    to_be_copied.display();
    std::cout << std::endl;

    x = to_be_copied.x;
    y = to_be_copied.y;
    z = to_be_copied.z+1 ;//this is a very bad idea, but we can do it anywhere!
    // the "copied" version has now (x,y,z+1) as coordinates
}



int main()
{
    vec3 a; // Empty constructor call
    std::cout << "a=" << a << std::endl;

    vec3 b = { 4,7 }; // Constructor with two arguments
    std::cout << "b=" << b << std::endl;

    {
        std::cout << "-- Beginning of block --" << std::endl;

        vec3 c = { 8,1 }; // Constructor with two arguments
        std::cout << "c=" << c << std::endl;

        std::cout << "-- End of block --" << std::endl;    
    } // Call of the destructor on c

    vec3 d = b; // Call of the copy constructor
    std::cout << "d=" << d << std::endl;


    
    return 0; // destroy a and b
}
Les constructeurs permettent d'initialiser des données au moment de la création d'un l'objet.
vec3::vec3(float x_arg, float y_arg)
    :x(x_arg), y(y_arg), z(z_arg)
{ 
}
qui aura le même effet que d'écrire x=x_arg, y=y_arg, etc. dans le cas des types de bases.
Le constructeur dit "par copie" prenant un objet de même type en paramètre (vec3(vec3 const& ))est particulier, et est utilisé automatiquement lorsque l'on initialize un objet à partir d'un autre et lors d'un passage d'argument "par copie" dans une fonction.
Le destructeur est une méthode automatiquement appelée à la fin de vie d'une variable: généralement en fin de bloc (ou après appel à delete si la variable a été initialisé par new).
Remarque générale:

Exercice possible

(Ne pas passer de temps sur cet exercice sauf si vous êtes vraiment en avance: passez à la partie Modélisation avant) > Créez deux variables globales (variable définit au début du fichier) stockant respectivement:
Testez votre résultat sur le code suivant:
// Note: these variables could also be defined as static variables of the class vec3
int nbr_total_vec3  = 0; // Total number of instanciation of vec3
int nbr_active_vec3 = 0; // Number of currently vec3 that are alive in the program

struct vec3 {
    float x, y, z;
    ... // your code here
}

void display_vec3_status() {
    std::cout << "Number of vec3: (total=" << nbr_total_vec3 << " ; active=" << nbr_active_vec3 << ")" << std::endl;
}

int main()
{
    vec3 a; 
    vec3 b = vec3(4,8,1);
    
    display_vec3_status(); // Expect (total=2, active=2)

    for (int k = 0; k < 3; ++k) {
        vec3 temp = vec3(k, 1, 0); // creation of vec3

        std::cout << "Loop, step=" << k << std::endl;
        display_vec3_status(); // Expect (total=3+k, active=3)
    }// at every new loop, the previous vec3 is destroyed

    display_vec3_status(); // Expect (total=5, active=2)

    {
        vec3 d = a;
        display_vec3_status(); // Expect (total=6, active=3)
    }

    display_vec3_status(); // Expect (total=6, active=2)

    return 0; // destroy a and b
}
Solution possible