web-dev-qa-db-fra.com

Est-il bien défini d'utiliser un pointeur pointant vers un-passé-malloc?

En C, il est parfaitement bien de créer un pointeur qui pointe vers un dernier le dernier élément d'un tableau et de l'utiliser dans l'arithmétique du pointeur, tant que vous ne le déréférencez pas:

int a[5], *p = a+5, diff = p-a; // Well-defined

Cependant, ce sont des UB:

p = a+6;
int b = *(a+5), diff = p-a; // Dereferencing and pointer arithmetic

Maintenant, j'ai une question: cela s'applique-t-il à la mémoire allouée dynamiquement? Supposons que j'utilise uniquement un pointeur pointant vers un avant-dernier dans l'arithmétique des pointeurs, sans le déréférencer, et malloc() réussit.

int *a = malloc(5 * sizeof(*a));
assert(a != NULL, "Memory allocation failed");
// Question:
int *p = a+5;
int diff = p-a; // Use in pointer arithmetic?
47
iBug

Est-il bien défini d'utiliser un pointeur pointant vers un-passé-malloc?

Il est bien défini si p pointe vers un passé la mémoire allouée et il n'est pas déréférencé.

n157 - §6.5.6 (p8):

[...] Si le résultat pointe au-delà du dernier élément de l'objet tableau, il ne doit pas être utilisé comme l'opérande d'un unaire * opérateur évalué.

La soustraction de deux pointeurs n'est valide que lorsqu'ils pointent vers des éléments du même objet tableau ou un après le dernier élément de l'objet tableau, sinon cela entraînera un comportement indéfini.

(p9) :

Lorsque deux pointeurs sont soustraits, les deux pointent vers des éléments du même objet tableau, ou un au-delà du dernier élément de l'objet tableau [...]

Les citations ci-dessus s'appliquent bien à la mémoire allouée dynamiquement et statiquement.

int a[5];
ptrdiff_t diff = &a[5] - &a[0]; // Well-defined

int *d = malloc(5 * sizeof(*d));
assert(d != NULL, "Memory allocation failed");
diff = &d[5] - &d[0];        // Well-defined

Une autre raison pour laquelle cela est valable pour la mémoire allouée dynamiquement, comme indiqué par Jonathan Leffler dans un commentaire est:

§7.22.3 (p1) :

L'ordre et la contiguïté du stockage alloué par appels successifs au aligned_alloc, calloc, malloc et realloc les fonctions ne sont pas spécifiées. Le pointeur renvoyé si l'allocation réussit est convenablement aligné de sorte qu'il peut être affecté à un pointeur sur n'importe quel type d'objet avec une exigence d'alignement fondamentale puis utilisé pour accéder à un tel objet ou un tableau de ces objets dans l'espace alloué (jusqu'à ce que l'espace soit explicitement désalloué).

Le pointeur renvoyé par malloc dans l'extrait ci-dessus est affecté à d et la mémoire allouée est un tableau de 5 int objets.

23
haccks

Le brouillon n4296 pour C11 indique explicitement que le fait de pointer un tableau est parfaitement défini: 6.5.6 Opérateurs de langage/expressions/additifs:

§ 8 Lorsqu'une expression de type entier est ajoutée ou soustraite d'un pointeur, le résultat a le type de l'opérande du pointeur. ... De plus, si l'expression P pointe vers le dernier élément d'un objet tableau, l'expression (P) +1 pointe un après le dernier élément de l'objet tableau, et si l'expression Q pointe un après le dernier élément d'un objet objet tableau, l'expression (Q) -1 pointe vers le dernier élément de l'objet tableau ... Si le résultat pointe au-delà du dernier élément de l'objet tableau, il ne doit pas être utilisé comme l'opérande d'un opérateur unaire * qui est évalué.

Comme le type de mémoire n'est jamais précisé dans la sous-clause, il s'applique à tout type de mémoire, y compris celle allouée.

Cela signifie clairement qu'après:

int *a = malloc(5 * sizeof(*a));
assert(a != NULL, "Memory allocation failed");

tous les deux

int *p = a+5;
int diff = p-a;

sont parfaitement définis et comme les règles arithmétiques habituelles du pointeur s'appliquent, diff recevra la valeur 5.

26
Serge Ballesta

Oui, les mêmes règles s'appliquent aux variables avec une durée de stockage dynamique et automatique. Il s'applique même à une requête malloc pour un seul élément (un scalaire est équivalent à un tableau à un élément à cet égard).

L'arithmétique du pointeur n'est valide que dans les tableaux, y compris un après la fin d'un tableau.

Concernant le déréférencement, il est important de noter une considération: en ce qui concerne l'initialisation int a[5] = {0};, le compilateur ne doit pas tenter de déréférencementa[5] dans l'expression int* p = &a[5]; il doit le compiler comme int* p = a + 5; Encore une fois, la même chose s'applique au stockage dynamique.

7
Bathsheba

Est-il bien défini d'utiliser un pointeur pointant vers un-passé-malloc?

Oui, pourtant il existe un cas d'angle où ceci est pas bien défini:

void foo(size_t n) {
  int *a = malloc(n * sizeof *a);
  assert(a != NULL || n == 0, "Memory allocation failed");
  int *p = a+n;
  intptr_t diff = p-a;
  ...
}

Fonctions de gestion de la mémoire ... Si la taille de l'espace demandé est nulle, le comportement est défini par l'implémentation: soit un pointeur nul est renvoyé, soit le comportement est comme si la taille était une valeur non nulle, sauf que le pointeur renvoyé ne doit pas être utilisé pour accéder à un objet. C11dr §7.22.3 1

foo(0) -> malloc(0) peut renvoyer un NULL ou non-NULL. Dans la première implémentation, un retour de NULL n'est pas un "échec d'allocation de mémoire". Cela signifie que le code tente int *p = NULL + 0; Avec int *p = a+n;, Ce qui échoue aux garanties sur les mathématiques du pointeur - ou au moins remet en question ce code.

Les avantages du code portable en évitant les allocations de taille 0.

void bar(size_t n) {
  intptr_t diff;
  int *a;
  int *p;
  if (n > 0) {
    a = malloc(n * sizeof *a);
    assert(a != NULL, "Memory allocation failed");
    p = a+n;
    diff = p-a;
  } else {
    a = p = NULL;
    diff = 0;
  }
  ...
}
7
chux