Est-il bien défini d’utiliser un pointeur pointant vers one-past-malloc?

En C, il est parfaitement judicieux de créer un pointeur qui pointe sur 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érenciez pas:

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

Cependant, ce sont des UB:

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

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

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

Est-il bien défini d’utiliser un pointeur pointant vers one-past-malloc?

Il est bien défini si p pointe vers un passé au-delà de la mémoire allouée et qu’il n’est pas déréférencé.

n1570 – §6.5.6 (p8):

[…] Si le résultat pointe le dernier élément du tableau, il ne doit pas être utilisé comme opérande d’un opérateur unaire * évalué.

La soustraction de deux pointeurs n’est valide que lorsqu’ils pointent sur des éléments du même object de tableau ou sur un élément du dernier object du tableau, sinon cela entraînera un comportement indéfini.

(p9) :

Lorsque deux pointeurs sont soustraits, les deux doivent pointer vers des éléments du même object tableau, ou vers le dernier élément de l’object tableau […]

Les citations ci-dessus sont bien applicables à la fois pour la mémoire allouée dynamicment 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 ceci est valable pour la mémoire allouée dynamicment, 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 aux fonctions aligned_alloc , calloc , malloc et realloc ne sont pas spécifiés. Le pointeur est retourné si l’atsortingbution réussit est correctement alignée de sorte qu’il puisse être assigné à un pointeur sur n’importe quel type d’object avec une exigence d’alignement fondamental et utilisé ensuite pour accéder à un tel object ou à un tableau de ces objects dans l’espace alloué l’espace est explicitement désalloué).

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

Le brouillon n4296 pour C11 est explicite: le fait de pointer un passé devant un tableau est parfaitement défini: 6.5.6 Langage / Expressions / Opérateurs additifs:

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

Le type de mémoire n’étant jamais précisé dans la sous-clause, il s’applique à tout type de mémoire, y compris celui alloué.

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 = pa; 

sont parfaitement définis et que les règles arithmétiques de pointeur habituelles s’appliquent, diff reçoit la valeur 5 .

Oui, les mêmes règles s’appliquent aux variables avec une durée de stockage dynamic et automatique. Elle 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 de pointeur n’est valide que dans les tableaux, y compris un passé après la fin d’un tableau.

En 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érencer a[5] dans l’expression int* p = &a[5] ; il doit comstackr ceci comme int* p = a + 5; Encore une fois, la même chose s’applique au stockage dynamic.

Est-il bien défini d’utiliser un pointeur pointant vers one-past-malloc?

Oui, il existe un cas de figure où ce n’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 = pa; ... } 

Fonctions de gestion de la mémoire … Si la taille de l’espace demandé est zéro, le comportement est défini par l’implémentation: un pointeur nul est renvoyé ou le comportement est comme si la taille présentait une valeur différente de zéro, sauf que le pointeur renvoyé ne devait pas être utilisé pour accéder à un object. C11dr §7.22.3 1

foo(0) -> malloc(0) peut renvoyer une 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; qui ne répond pas aux garanties sur les maths de pointeur – ou du moins, remet en question ce code.

Le code portable profite 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 = pa; } else { a = p = NULL; diff = 0; } ... }