PARTIE 1 | PARTIE 2 | PARTIE 3 | PARTIE 4 | ||
---|---|---|---|---|---|
Préambule | Rappels C++ | Caractéristiques générales | Syntaxe de base | Syntaxe détaillée | Références |
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
Opérateur “address-of” &
Opérateur de déférence “*”
Objets en C++
Passage d’arguments par valeur
Passage d’arguments par référence
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
etulong
(unsigned long
). - Les types à virgule flottante:
decimal, float
etdouble
(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é deref
.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é deout
.
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);