64 votes

La réutilisation d'un emplacement mémoire est-elle sûre ?

Cette question est basée sur un code C existant porté en C++. Je souhaite simplement savoir s'il est "sûr". Je sais déjà que je ne l'aurais pas écrit comme ça. Je suis conscient que le code ici est essentiellement du C plutôt que du C++, mais il a été compilé avec un compilateur C++ et je sais que les normes sont parfois légèrement différentes.

J'ai une fonction qui alloue de la mémoire. J'utilise le retour void* à un int* et commencez à l'utiliser.

Plus tard, j'ai lancé le retour void* à un Data* et commencez à l'utiliser.

Est-ce sûr en C++ ?

Exemple :-

void* data = malloc(10000);

int* data_i = (int*)data;
*data_i = 123;
printf("%d\n", *data_i);

Data* data_d = (Data*)data;
data_d->value = 456;
printf("%d\n", data_d->value);

Je n'ai jamais lu de variables utilisées via un type différent de celui où elles ont été stockées mais je m'inquiète que le compilateur puisse voir que data_i et data_d sont de types différents et ne peuvent donc pas légalement s'aliaser l'un l'autre et je décide de réorganiser mon code, par exemple en mettant le magasin à data_d avant le premier printf . Ce qui casserait tout.

Cependant, c'est un modèle qui est utilisé tout le temps. Si vous insérez un free et malloc entre les deux accès, je ne pense pas que cela modifie quoi que ce soit puisqu'il ne touche pas la mémoire concernée elle-même et peut réutiliser les mêmes données.

Mon code est-il défectueux ou est-il "correct" ?

52voto

Niall Points 6133

C'est "OK", cela fonctionne comme vous l'avez écrit (en supposant des primitives et des types de données simples et anciens (POD)). Il est sûr. C'est effectivement un gestionnaire de mémoire personnalisé.

Quelques notes :

  • Si des objets avec des destructeurs non triviaux sont créés à l'emplacement de la mémoire allouée, assurez-vous qu'il est appelé

    obj->~obj();
  • Si vous créez des objets, tenez compte de la placement d'une nouvelle syntaxe sur un plâtre ordinaire (fonctionne aussi avec les POD)

    Object* obj = new (data) Object();
  • Vérifiez si un nullptr (ou NULL ), si malloc échoue, NULL est retourné

  • L'alignement ne devrait pas être un problème, mais il faut toujours en tenir compte lors de la création d'un gestionnaire de mémoire et s'assurer que l'alignement est approprié.

Étant donné que vous utilisez un compilateur C++, à moins que vous ne souhaitiez conserver la nature "C" du code, vous pouvez également vous tourner vers l'option globale de l'interface utilisateur. operator new() .

Et comme toujours, une fois fait, n'oubliez pas les free() (ou delete si vous utilisez new )


Vous indiquez que vous n'allez pas convertir le code pour l'instant, mais si vous l'envisagez, il y a quelques fonctionnalités idiomatiques en C++ que vous pourriez utiliser par rapport à l'architecture de l'entreprise. malloc ou même le global ::operator new .

Vous devriez regarder le pointeur intelligent std::unique_ptr<> ou std::shared_ptr<> et leur permettre de s'occuper des questions de gestion de la mémoire.

25voto

DevSolar Points 18897

Selon la définition de Data votre code pourrait être brisé. C'est mauvais code, de toute façon.

Si Data est un simple type de données (POD, c'est-à-dire un typedef pour un type de base, une structure de types POD, etc,) et la mémoire allouée est correctement alignée pour le type (*), alors votre code est bien défini ce qui signifie qu'il "fonctionnera" (à condition que vous initialiser chaque membre de *data_d avant de l'utiliser), mais ce n'est pas une bonne pratique. (Voir ci-dessous.)

Si Data est un type non-POD, vous allez avoir des problèmes : L'affectation du pointeur n'aurait pas invoqué de constructeurs, par exemple. data_d qui est du type "pointeur vers Data ", serait effectivement mentir parce qu'il pointe vers quelque chose, mais ce quelque chose n'est pas de type Data parce qu'un tel type n'a pas été créé / construit / initialisé. Un comportement indéfini ne sera pas loin à ce moment-là.

La solution pour bien construire un objet à un emplacement mémoire donné est appelé nouveau placement :

Data * data_d = new (data) Data();

Ceci indique au compilateur de construire un Data objet à l'endroit data . Cela fonctionne aussi bien pour les types POD que non POD. Vous devrez également appeler le destructeur ( data_d->~Data() ) pour s'assurer qu'il est exécuté avant delete de la mémoire.

Veillez à ne jamais mélanger les fonctions d'allocation et de libération. Tout ce que vous malloc() doit être free() d, ce qui est alloué avec new besoins delete et si vous new [] vous devez delete [] . Toute autre combinaison est UB.


Dans tous les cas, l'utilisation de pointeurs "nus" pour la propriété de la mémoire est déconseillée en C++. Vous devez soit

  1. mettre new dans un constructeur et les delete dans le destructeur d'une classe, ce qui rend l'élément objet le propriétaire de la mémoire (y compris la désaffectation correcte lorsque l'objet sort de sa portée, par exemple dans le cas d'une exception) ; ou

  2. utiliser un pointeur intelligent qui fait effectivement ce qui précède pour vous.


(*) : Les implémentations sont connues pour définir des types "étendus", dont les exigences d'alignement ne sont pas prises en compte par malloc(). Je ne suis pas sûr que les juristes du langage les appelleraient encore "POD", en fait. MSVC, par exemple, fait Alignement sur 8 octets sur malloc() mais définit le type étendu SSE __m128 comme ayant un Alignement sur 16 octets exigence.

16voto

Matthieu M. Points 101624

Les règles entourant l'aliasing strict peuvent être assez délicates.

Voici un exemple d'aliasing strict :

int a = 0;
float* f = reinterpret_cast<float*>(&a);
f = 0.3;
printf("%d", a);

C'est une violation stricte de l'aliasing car :

  • la durée de vie des variables (et leur utilisation) se chevauche
  • ils interprètent le même morceau de mémoire à travers deux "lentilles" différentes.

Si vous ne faites pas les deux en même temps, alors votre code ne viole pas l'aliasing strict.


En C++, la durée de vie d'un objet commence à la fin du constructeur et s'arrête au début du destructeur.

Dans le cas des types intégrés (pas de destructeur) ou des PODs (destructeur trivial), la règle est plutôt que leur durée de vie se termine lorsque la mémoire est soit écrasée, soit libérée.

Note : ceci est spécifiquement destiné à supporter l'écriture des gestionnaires de mémoire ; après tout malloc est écrit en C et operator new est écrit en C++ et ils sont explicitement autorisés à mettre la mémoire en commun.


J'ai utilisé spécifiquement objectifs au lieu de types car la règle est un peu plus difficile.

Le C++ utilise généralement saisie nominale : si deux types ont un nom différent, ils sont différents. Si vous accédez à une valeur de type dynamique T comme s'il s'agissait d'un U alors vous violez l'aliasing.

Il existe un certain nombre d'exceptions à cette règle :

  • accès par la classe de base
  • dans les PODs, accès en tant que pointeur vers le premier attribut

Et la règle la plus compliquée est liée à union où le C++ passe à typage structurel : vous pouvez accéder à un morceau de mémoire par deux types différents, si vous n'accédez qu'aux parties du début de ce morceau de mémoire dans lesquelles les deux types partagent une séquence initiale commune.

§9.2/18 Si une union standard-layout contient deux ou plusieurs structures standard-layout qui partagent une séquence initiale commune, et si l'objet union standard-layout contient actuellement une de ces structures standard-layout, il est permis d'inspecter la partie initiale commune de n'importe laquelle d'entre elles. Deux structs standard-layout partagent une séquence initiale commune si les membres correspondants ont des types compatibles avec la mise en page et si aucun des membres n'est un champ de bits ou si les deux sont des champs de bits avec la même largeur pour une séquence d'un ou plusieurs membres initiaux.

Étant donné :

  • struct A { int a; };
  • struct B: A { char c; double d; };
  • struct C { int a; char c; char* z; };

Dans un délai de union X { B b; C c; }; vous pouvez accéder x.b.a , x.b.c et x.c.a , x.c.c en même temps ; toutefois, l'accès x.b.d (respectivement x.c.z ) est une violation de l'aliasing si le type actuellement stocké n'est pas B (respectivement pas C ).

Remarque : de manière informelle, le typage structurel revient à ramener le type à un tuple de ses champs (en les aplatissant).

Note : char* est spécifiquement exemptée de cette règle, vous pouvez visualiser n'importe quel morceau de mémoire à travers char* .


Dans votre cas, sans la définition de Data Je ne peux pas dire si la règle des "lentilles" pourrait être violée, cependant puisque vous le faites :

  • en écrasant la mémoire avec Data avant d'y accéder par le biais de Data*
  • ne pas y accéder par int* ensuite

alors vous êtes en conformité avec la règle de la durée de vie, et donc il n'y a pas d'aliasing en ce qui concerne le langage.

8voto

Joachim Pileborg Points 121221

Tant que la mémoire n'est utilisée que pour une seule chose à la fois, elle est sûre. En fait, vous utilisez les données allouées comme un union .

Si vous voulez utiliser la mémoire pour des instances de classes et pas seulement pour de simples structures ou types de données de style C, vous devez vous rappeler de faire ce qui suit nouveau placement pour "allouer" les objets, car cela appellera en fait le constructeur de l'objet. Le destructeur doit être appelé explicitement lorsque vous en avez fini avec l'objet, vous ne pouvez pas delete il.

6voto

Bo Persson Points 42821

Tant que vous ne manipulez que des types "C", tout va bien. Mais dès que vous utilisez des classes C++, vous aurez des problèmes d'initialisation. Si nous supposons que Data serait std::string par exemple, le code serait très erroné.

Le compilateur ne peut pas vraiment déplacer le magasin à travers l'appel à printf parce que c'est un effet secondaire visible. Le résultat doit être comme si les effets secondaires étaient produits dans l'ordre prescrit par le programme.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X