lenguaje - ¿Imprimir punteros nulos con% p es un comportamiento indefinido?
punteros void lenguaje c (3)
¿Es un comportamiento indefinido imprimir punteros nulos con el especificador de conversión
%p
?
#include <stdio.h>
int main(void) {
void *p = NULL;
printf("%p", p);
return 0;
}
La pregunta se aplica al estándar C y no a las implementaciones de C.
La respuesta corta
Si
La impresión de punteros nulos con el especificador de conversión
%p
tiene un comportamiento indefinido.
Dicho esto, no estoy al tanto de ninguna implementación conforme existente que pueda comportarse mal.
La respuesta se aplica a cualquiera de los estándares C (C89 / C99 / C11).
La larga respuesta
El especificador de conversión
%p
espera que se anule un argumento de tipo puntero, la conversión del puntero a caracteres imprimibles está definida por la implementación.
No indica que se espera un puntero nulo.
La introducción a las funciones de la biblioteca estándar establece que los punteros nulos como argumentos para las funciones (de la biblioteca estándar) se consideran valores no válidos, a menos que se indique explícitamente lo contrario.
C99
/
C11
§7.1.4 p1
[...] Si un argumento para una función tiene un valor no válido (como [...] un puntero nulo, [...] el comportamiento es indefinido).
Ejemplos de funciones (biblioteca estándar) que esperan punteros nulos como argumentos válidos:
-
fflush()
usa un puntero nulo para vaciar "todos los flujos" (que apliquen). -
freopen()
usa un puntero nulo para indicar el archivo "actualmente asociado" con la secuencia. -
snprintf()
permite pasar un puntero nulo cuando ''n'' es cero. -
realloc()
usa un puntero nulo para asignar un nuevo objeto. -
free()
permite pasar un puntero nulo. -
strtok()
usa un puntero nulo para llamadas posteriores.
Si tomamos el caso de
snprintf()
, tiene sentido permitir pasar un puntero nulo cuando ''n'' es cero, pero este no es el caso para otras funciones (biblioteca estándar) que permiten un cero similar ''n''.
Por ejemplo:
memcpy()
,
memmove()
,
strncpy()
,
memset()
,
memcmp()
.
No solo se especifica en la introducción a la biblioteca estándar, sino también una vez más en la introducción a estas funciones:
C99 §7.21.1 p2
/
C11 §7.24.1 p2
Cuando un argumento declarado como
size_t
n especifica la longitud de la matriz para una función, n puede tener el valor cero en una llamada a esa función. A menos que se indique explícitamente lo contrario en la descripción de una función particular en esta subcláusula, los argumentos de puntero en dicha llamada aún tendrán valores válidos como se describe en 7.1.4.
¿Es intencional?
No sé si la UB de
%p
con un puntero nulo es de hecho intencional, pero dado que el estándar establece explícitamente que los punteros nulos se consideran valores no válidos como argumentos para las funciones de biblioteca estándar, y luego va y especifica explícitamente los casos donde un puntero nulo es un argumento válido (snprintf, free, etc.), y luego desaparece y una vez más repite el requisito de que los argumentos sean válidos incluso en casos n ''n'' (
memcpy
,
memmove
,
memset
), entonces creo que es razonable suponer que el comité de estándares C no está demasiado preocupado por tener tales cosas indefinidas.
Los autores del Estándar C no hicieron ningún esfuerzo por enumerar exhaustivamente todos los requisitos de comportamiento que una implementación debe cumplir para ser adecuados para un propósito particular. En cambio, esperaban que las personas que escriben compiladores ejercieran una cierta cantidad de sentido común tanto si el Estándar lo requiere como si no.
La cuestión de si algo invoca a UB rara vez es en sí misma útil. Las verdaderas preguntas de importancia son:
-
¿Debería alguien que intenta escribir un compilador de calidad hacer que se comporte de manera predecible? Para el escenario descrito, la respuesta es claramente sí.
-
¿Deberían los programadores tener derecho a esperar que los compiladores de calidad para algo parecido a las plataformas normales se comporten de manera predecible? En el escenario descrito, diría que la respuesta es sí.
-
¿Podrían algunos escritores compiladores obtusos estirar la interpretación del Estándar para justificar hacer algo extraño? Espero que no, pero no lo descartaría.
-
¿Deberían los compiladores de desinfección graznar sobre el comportamiento? Eso dependería del nivel de paranoia de sus usuarios; un compilador desinfectante probablemente no debería predeterminar a graznar sobre tal comportamiento, pero tal vez proporcionar una opción de configuración para hacer en caso de que los programas puedan ser portados a compiladores "inteligentes" / tontos que se comportan de manera extraña.
Si una interpretación razonable del Estándar implicaría que se define un comportamiento, pero algunos escritores de compiladores extienden la interpretación para justificar hacer lo contrario, ¿realmente importa lo que dice el Estándar?
Este es uno de esos casos extraños en los que estamos sujetos a las limitaciones del idioma inglés y la estructura inconsistente en el estándar. Entonces, en el mejor de los casos, puedo hacer un contraargumento convincente, ya que es imposible demostrarlo :) 1
El código en la pregunta exhibe un comportamiento bien definido.
Como [7.1.4] es la base de la pregunta, comencemos allí:
Cada una de las siguientes afirmaciones se aplica a menos que se indique explícitamente lo contrario en las descripciones detalladas que siguen: Si un argumento a una función tiene un valor no válido ( como un valor fuera del dominio de la función, o un puntero fuera del espacio de direcciones del programa, o un puntero nulo , [... otros ejemplos ...] ) [...] el comportamiento es indefinido. [... otras declaraciones ...]
Este es un lenguaje torpe.
Una interpretación es que los elementos de la lista son UB para todas las funciones de la biblioteca, a menos que las descripciones individuales lo anulen.
Pero la lista comienza con "como", lo que indica que es ilustrativa, no exhaustiva.
Por ejemplo, no menciona la terminación nula correcta de cadenas (crítica para el comportamiento de, por ejemplo,
strcpy
).
Por lo tanto, está claro que la intención / alcance de 7.1.4 es simplemente que un "valor no válido" conduce a UB (a menos que se indique lo contrario ). Tenemos que mirar la descripción de cada función para determinar qué cuenta como un "valor no válido".
Ejemplo 1 -
strcpy
[7.21.2.3] solo dice esto:
La función
strcpy
copia la cadena a la que apuntas2
(incluido el carácter nulo de terminación) en la matriz a la que apuntas1
. Si la copia se lleva a cabo entre objetos que se superponen, el comportamiento es indefinido.
No hace mención explícita de punteros nulos, pero tampoco hace mención de terminadores nulos.
En cambio, se infiere de "cadena señalada por
s2
" que los únicos valores válidos son cadenas (es decir, punteros a matrices de caracteres con terminación nula).
De hecho, este patrón se puede ver en todas las descripciones individuales. Algunos otros ejemplos:
-
[7.6.4.1 (fenv)] almacena el entorno de punto flotante actual en el objeto al que apunta
envp
-
[7.12.6.4 (frexp)] almacena el número entero en el objeto int al que apunta
exp
-
[7.19.5.1 (fclose)] la secuencia apuntada por
stream
Ejemplo 2 -
printf
[7.19.6.1]
dice esto acerca de
%p
:
p
- El argumento será un puntero avoid
. El valor del puntero se convierte en una secuencia de caracteres de impresión, de una manera definida por la implementación.
Nulo es un valor de puntero válido, y esta sección no menciona explícitamente que nulo es un caso especial, ni que el puntero tiene que apuntar a un objeto. Por lo tanto, se define el comportamiento.
1. A menos que se presente un autor de normas, o a menos que podamos encontrar algo similar a un documento de rationale que aclare las cosas.