C++/CLI en 10 min, partie 1: Rappels C++

Avant de rentrer dans les détails du C++/CLI, quelques rappels sont effectués sur la manipulation des objets en .NET et en C++. Si vous êtes à l’aise avec le C++, allez directement à la partie suivante.

Généralités sur les pointeurs et les références

D’une façon générale, un pointeur et une référence possèdent tous les deux, l’adresse mémoire d’un objet. Toutefois ils n’exposent pas les mêmes informations et il n’est pas possible d’effectuer le même type d’opération sur les deux.

Un pointeur est une variable contenant une adresse mémoire. Cette adresse peut pointer vers n’importe quel objet en mémoire, y compris vers des objets dont le type est différent. Le pointeur n’a pas connaissance du type de l’objet vers lequel il pointe.

De même que les pointeurs, les références contiennent une adresse mémoire, toutefois cette adresse n’est pas directement accessible par la code. D’autre part, la référence a une connaissance du type d’objet vers lequel elle pointe. Par construction, il n’est pas possible de modifier le type d’une référence.

En C#, en dehors des objets de type valeur, on manipule des références. Dans un contexte purement managé, on ne manipule jamais directement des pointeurs.
En C++, on peut manipuler les pointeurs définis avec "*" et les références définies avec "&".

Par exemple, on déclare un pointeur:

int *customPointer; 
Point *pointPointer; 

Une référence se déclare avec "&":

int &customPointer = ... ; 
Point &pointReference = ... ;

On peut instancier ces objets:

int value = 5; 
int &valueRef = value; 

Point *pointPointer = new Point(2, 7); 

L’utilisation du mot clé new nécessite d’utiliser delete pour supprimer l’objet:

delete pointPointer; 

Plus directement:

CustomObject &pointReference = Point(7, 3); 

Dans le cas de constructeur par défaut:

Point point; // le constructeur par défaut est appelé 
Point point(); // le constructeur par défaut n'est pas appelé. 

Opérateur “address-of” &

En C++, l’opérateur “&” permet d’obtenir l’adresse d’un objet:


int value1 = 5; 
int *valuePointer = &value1;

Opérateur de déférence “*”

L’opérateur de déférence (deference operator) permet d’obtenir l’objet pointé par un pointeur. Si on écrit *point, on souhaite accéder à l’objet pointé par la variable point.

Par exemple:

int value = 65; 
int *valuePointer; // déclaration 
valuePointer = &value; // initialisation du pointeur 
*valuePointer = 54; // on affecte 54 à l'objet pointé par le pointeur donc on affecte 54 à value 

Objets de type valeur

En C#, les objets de type valeur correspondent au struct et au enum. La plupart des types primitifs sont des struct et donc sont des objets de type valeur:

  • Les types intégraux: sbyte (signed byte), byte, char, short, ushort (unsigned short), int, uint (unsigned int), long et ulong (unsigned long).
  • Les types à virgule flottante: decimal, float et double (voir nombres à virgule flottante en C# pour plus de détails)
  • Le booléen bool.

Il faut noter que ces types primitifs sont des alias qui sont mappés vers des types dans le namespace System. Par exemple, int est un alias pour System.Int32.

Par défaut, en C# les objets de type valeur sont passés en argument de fonction par valeur. La valeur de l’objet est copiée et l’objet est dupliqué.

On peut passer des objets de type valeur par référence en utilisant les mots clé ref et out:

  • ref: il est obligatoire d’initialiser la variable à l’extérieur de la fonction. La fonction n’est pas obligée d’initialiser ou de modifier la valeur de l’argument précédé de ref.
  • out: il n’est pas obligatoire d’initialiser la variable avant d’appeler la fonction. En revanche, la fonction doit au moins affecter une valeur à l’argument précédé de out.

Objets de type référence

En C#, les objets de type référence sont les classes, les interfaces et les delegates. Ces objets dérivent de System.Object. Par suite, object et string sont des objets de type référence.
Lorsqu’on manipule des objets de type référence, en C# on manipule des références d’objet. 2 variables peuvent contenir la même référence et une modification sur une variable aura pour effet de modifier directement l’objet vers lequel pointe la référence.

Par défaut en C#, les objets de type référence sont passés par valeur c’est-à-dire que c’est la valeur du pointeur vers le type qui est passée en paramètre de la fonction. Si on modifie la référence dans le corps de la fonction, il n’y aura pas d’incidence sur la référence à l’extérieur de la fonction. En revanche, on peut modifier directement l’objet pointé par la référence.

On peut passer des objets de type référence par référence en utilisant les mots clé ref et out avec les mêmes conditions que pour les objets de type valeur.

Objets en C++

En C++, il n’y a pas de distinctions entre objets de type valeur et d’objets de type référence. Par défaut tous les objets sont passés par valeur en paramètre de fonction.

En C++, les struct sont comme les “classes”, la différence concerne la portée par défaut des membres (c’est-à-dire s’il n’y a pas d’opérateur de portée devant le membre):

  • Les membres des struct sont par défaut publiques
  • Les membres des class sont par défaut privés.

Passage d’arguments par valeur

Sans indications, les objets sont passés en argument de fonction ou sont instanciés sur la pile. Par exemple, si on définit une classe:

class Point 
{ 
    private: 
        int x, y; 
    public: 
        Point(int x, int y) 
        { 
            this->x = x; 
            this->y = y; 
        } 
};

Et si on instancie la classe de cette façon:

Point point1(9, 3);

Par défaut, Point est un objet de type valeur. Elle est allouée sur la pile (stack) et sa durée de vie se limite à la fonction dans laquelle elle est définie.

Dans un passage d’objets en argument de fonctions par valeur, la valeur de l’objet est copiée et l’objet est dupliqué. On limite le passage par valeur au type simple comme int, float, bool, char, double, wchar_t, etc…

Par exemple, si on exécute:

#include <iostream> 
using namespace std; 
  
// function declaration 
void Swap(int a, int b) 
{ 
    int c = a; 
    a = b; 
    b = c; 
} 
 
int main () 
{ 
   int a = 100; 
   int b = 200; 
  
   cout << "Avant a = " << a << endl; 
   cout << "Avant b = " << b << endl; 
  
   Swap(a, b); 
  
   cout << "Apres a = " << a << endl; 
   cout << "Apres b = " << b << endl; 
  
   return 0; 
}

L’exécution donnera:

Avant a = 1 
Avant b = 2 
Apres a = 1 
Apres b = 2

Pour des classes, on peut passer des objets en argument de fonction par valeur mais il faut qu’un constructeur de copie (copy constructor) soit implémenté:

class Point 
{ 
    private: 
        int *x_value, *y_value; 
    public: 
        Point(int x, int y) 
        { 
            x_value = new int; 
            y_value = new int; 
            *x_value = x; 
            *y_value = y; 
        } 
 
 
        Point(const Point &obj) 
        { 
            x_value = new int; 
            y_value = new int; 
            *x_value = obj->x_value; 
            *y_value = obj->y_value; 
        } 
};

Passage d’arguments par référence

Pour passer des objets en argument de fonction par référence, on utilise l’opérateur "&". Par exemple, si on définit la méthode:

void Swap(int &a, int &b) 
{ 
    int c = a; 
    a = b; 
    b = c; 
}

En exécutant la méthode précédente, on aura bien une inversion des valeurs:

Avant a = 1 
Avant b = 2 
Apres a = 2 
Apres b = 1

Allocation des objets en mémoire

En C#, c’est le type des objets qui indique où ils seront alloués:
Les objets de type valeur sont alloués dans la pile (stack). Leur durée de vie se limite aux fonctions dans lesquelles ils sont définis.
Les objets de type référence sont alloués dans le tas managé (managed heap). C’est le garbage collector qui gère la destruction de ces objets.

En C++, on a plus de flexibilité pour choisir où un objet sera alloué suivant la façon dont on le déclare.

Si on déclare un objet sur la pile avec une déclaration du type:

Point point(5, 8); 
int intValue = 32;

Comme en C#, leur durée de vie se limite aux fonctions dans lesquelles ils sont définis.

Par exemple, si on définit la classe:

class PointConsumer 
{ 
    private: 
        Point *pointAsMember; 
    public: 
        void DefinePoint() 
        { 
            Point point(9, 23); 
            this->pointAsMember = &point; 
        } 
 
        void UsePointAsMember() 
        { 
            cout << "pointAsMember->x = " << this->pointAsMember->x << endl; 
            cout << "pointAsMember->y = " << this->pointAsMember->y << endl; 
        } 
};

Et si on exécute les méthodes:

PointConsumer pointConsumer; 
pointConsumer.DefinePoint(); 
pointConsumer.UsePointAsMember();

On aura une erreur de type:

An unhandled exception of type "System.AccessViolationException" occurred in ....dll 
Additional information: Attempted to read or write protected memory. 
This is often an indication that other memory is corrupt.

Cette erreur est due au fait que la variable point est alloué sur la pile dans la méthode DefinePoint(). Même si on garde un pointeur sur cet objet, lorsqu’on quitte DefinePoint(), point est supprimée de la pile à la sortie de la fonction. Quand on essaie d’utiliser le pointeur pointAsMember dans UsePointAsMember(), il ne pointe plus vers une instance de l’objet en mémoire puisqu’il en a été supprimé.

En C++, pour déclarer un objet sur le tas (heap), on utilise le mot clé new. L’objet restera dans le tas tant qu’on ne le supprime pas en utilisant le mot clé delete.

Par exemple:

Point *pointInstance = new Point(5, 32);

Leave a Reply