¿Está bien definido usar un puntero que apunte a uno-pasado-malloc?


En C, está perfectamente bien hacer un puntero que apunte a uno más allá del último elemento de una matriz y usarlo en aritmética de punteros, siempre y cuando no lo desreferencie:

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

Sin embargo, estos son UBs:

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

Ahora tengo una pregunta: ¿Esto se aplica a la memoria dinámicamente asignada? Supongamos que solo estoy usando un puntero que apunta a uno más allá del último en aritmética de punteros, sin desreferenciarlo, y malloc() tiene éxito.

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?
Author: iBug, 2017-12-20

4 answers

¿Está bien definido usar un puntero que apunte a uno-pasado-malloc?

Está bien definido si p está apuntando a uno más allá de la memoria asignada y no está desreferenciado.

N1570 - §6.5.6 (p8):

[...] Si el resultado apunta uno más allá del último elemento del objeto array, no se utilizará como operando de un operador unario * que se evalúe.

Restar dos punteros solo es válido cuando apuntan a elementos del mismo objeto de matriz o uno más allá del último elemento del objeto de matriz, de lo contrario resultará en un comportamiento indefinido.

(p9):

Cuando se restan dos punteros, ambos apuntarán a elementos del mismo objeto de matriz, o uno más allá del último elemento del objeto de matriz [...]

Las comillas anteriores son muy aplicables tanto para la memoria asignada dinámica como estáticamente.

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

Otra razón que esto es válido para la memoria dinámicamente asignada, como señala Jonathan Leffler en un comentario es:

§7.22.3 (p1):

El orden y la contigüidad del almacenamiento asignados por sucesivas convocatorias a la aligned_alloc, calloc, malloc, y realloc funciones no se especifica. El puntero devuelto si la asignación tiene éxito está alineado adecuadamente para que pueda asignarse a un puntero a cualquier tipo de objeto con un requisito de alineación fundamental y luego se utiliza para acceder a dicho objeto o una matriz de dichos objetos en el espacio asignado (hasta que el espacio se desasigne explícitamente).

El puntero devuelto por malloc en el fragmento de código anterior se asigna a d y la memoria asignada es una matriz de 5 objetos int.

 22
Author: haccks,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-22 00:02:13

El borrador n4296 para C11 es explícito que apuntar uno más allá de un array está perfectamente definido: 6.5.6 Operadores de lenguaje / Expresiones / Aditivos:

§ 8 Cuando una expresión que tiene tipo entero se agrega o se resta de un puntero, el el resultado tiene el tipo del operando puntero. ... Además, si la expresión P apunta a la última elemento de un objeto de matriz, la expresión (P)+ 1 apunta uno más allá del último elemento de la objeto array, y si la expresión Q apunta a uno pasado el último elemento de un objeto array, la expresión (Q) -1 apunta al último elemento del objeto array... Si el resultado apunta uno más allá del último elemento del objeto array, no se utilizará como operando de un operador unario * que se evalúe.

Como el tipo de memoria nunca se especifica en la subcláusula, se aplica a cualquier tipo de memoria, incluida la asignada.

Eso significa claramente que después de:

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

Ambos

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

Son perfectamente definido y como se aplican las reglas aritméticas usuales del puntero, diff recibirá el valor 5.

 26
Author: Serge Ballesta,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-20 07:25:29

Sí, las mismas reglas se aplican a las variables con duración de almacenamiento dinámica y automática. Incluso se aplica a una solicitud malloc para un solo elemento (un escalar es equivalente a una matriz de un elemento en este sentido).

La aritmética de punteros solo es válida dentro de los arrays, incluyendo uno pasado el final de un array.

Al desreferenciar, es importante tener en cuenta una consideración: con respecto a la inicialización int a[5] = {0};, el compilador no debe intentar desreferir a[5] en el expression int* p = &a[5]; debe compilar esto como int* p = a + 5; De nuevo, lo mismo se aplica al almacenamiento dinámico.

 7
Author: Bathsheba,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-20 08:19:35

¿Está bien definido usar un puntero que apunte a uno-pasado-malloc?

Sí, sin embargo, existe un caso de esquina donde esto es no bien definido:

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;
  ...
}

Funciones de gestión de memoria ... Si el tamaño del espacio solicitado es cero, el comportamiento está definido por la implementación: o se devuelve un puntero nulo, o el comportamiento es como si el tamaño fuera un valor distinto de cero, excepto que el puntero devuelto no se utilizará para acceder a un objeto. C11dr §7.22.3 1

foo(0) --> malloc(0) puede devolver un NULL o non-NULL. En la primera implementación un retorno de NULL no es un "fallo de asignación de memoria". Esto significa que el código está intentando int *p = NULL + 0; con int *p = a+n; lo que falla las garantías sobre la matemática del puntero, o al menos pone en duda dicho código.

El código portátil se beneficia al evitar asignaciones de tamaño 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
Author: chux,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-20 13:20:26