Cuando printf es una dirección de una variable, ¿por qué usar void*?

12 minutos de lectura

avatar de usuario
Alcott

Vi algún uso de (void*) en printf().

Si quiero imprimir la dirección de una variable, ¿puedo hacerlo así?

int a = 19;
printf("%d", &a);
  1. Creo, &a es ala dirección de que es solo un número entero, ¿verdad?
  2. Muchos artículos que leí usan algo como esto:

    printf("%p", (void*)&a);
    
  1. Que hace %p ¿representar? (¿Un puntero?)
  2. Por que usar (void*)? no puedo usar (int)&a ¿en lugar de?

  • Lo hace sizeof(void*) == sizeof(int) en cada plataforma?

    –Keith Layne

    3 de septiembre de 2011 a las 3:18

  • @keith: Absolutamente no. El código en esta pregunta es un caso clásico de hacer suposiciones injustificadas sobre el puntero y la longitud de int.

    – Nicolás Caballero

    3 de septiembre de 2011 a las 3:21

  • @Alcott: Lo cual, por ningún tramo de la imaginación, significa que serán iguales en cualquier otro lugar. Tan pronto como intente ejecutar su código en la mayoría de los sistemas de 64 bits, se romperá.

    – Nicolás Caballero

    3 de septiembre de 2011 a las 3:26

  • @Alcott: No, los punteros son no números enteros Los punteros son punteros. Los enteros son enteros.

    –Keith Thompson

    3 de septiembre de 2011 a las 3:56


  • Incluso si los diferentes tipos de punteros son iguales Tallason de diferentes tipos. printf requiere los tipos de argumento correctos exactos que coincidan con la cadena de formato. Otros tipos mayo funciona, dependiendo de los detalles de implementación, pero es un comportamiento indefinido.

    – R.. GitHub DEJA DE AYUDAR A ICE

    3 de septiembre de 2011 a las 4:46

avatar de usuario
keith thompson

Los punteros no son números. A menudo se representan internamente de esa manera, pero son conceptualmente distintos.

void* está diseñado para ser un tipo de puntero genérico. Cualquier valor de puntero (que no sea un puntero de función) puede convertirse en void* y viceversa sin pérdida de información. Esto típicamente significa que void* es al menos tan grande como otros tipos de punteros.

printfs "%p" formato requiere un argumento de tipo void*. Por eso un int* debe ser echado a void* en ese contexto. (No hay conversión implícita porque es una función variable; no hay un parámetro declarado, por lo que el compilador no sabe a qué convertirlo).

Prácticas descuidadas como imprimir punteros con "%d"o pasando un int* para printf con un "%p" format, son cosas con las que probablemente pueda salirse con la suya en la mayoría de los sistemas actuales, pero hacen que su código no sea portátil. (Tenga en cuenta que es común en los sistemas de 64 bits para void* y int ser de diferentes tamaños, por lo que la impresión de punteros con %d" es De Verdad no portátil, no solo teóricamente).

Por cierto, el formato de salida para "%p" está definida por la implementación. El hexadecimal es común, (en mayúsculas o minúsculas, con o sin un principio "0x" o "0X"), pero no es la única posibilidad. Todo lo que puede contar es que, suponiendo una implementación razonable, será una forma razonable de representar un valor de puntero en forma legible por humanos (y que scanf entenderá la salida de printf).

El artículo que leíste es totalmente correcto. La forma correcta de imprimir un int* el valor es

printf("%p", (void*)&a);

No tome la salida perezosa; no es nada difícil hacerlo bien.

Lectura sugerida: Sección 4 de la Preguntas frecuentes sobre comp.lang.c. (Lectura sugerida adicional: Todas las demás secciones.

EDITAR:

En respuesta a la pregunta de Alcott:

Todavía hay una cosa que no entiendo muy bien. int a = 10; int *p = &a;, entonces el valor de p es la dirección de a en mem, ¿verdad? Si es correcto, entonces el valor de p variará de 0 a 2 ^ 32-1 (si la CPU es de 32 bits), y un número entero es de 4 bytes en un sistema operativo de 32 bits, ¿verdad? entonces ¿Cuál es la diferencia entre el valor de p y un número entero? ¿Puede el valor de p salirse del rango?

La diferencia es que son de diferentes tipos.

Suponga un sistema en el que int, int*, void*y float son todos de 32 bits (esto es típico para los sistemas actuales de 32 bits). ¿El hecho de que float es de 32 bits implica que su rango es de 0 a 232-1? O -231 a 231-1? Ciertamente no; el rango de flotación (asumiendo la representación IEEE) es de aproximadamente -3.40282e+38 a +3.40282e+38, con una resolución muy variable en todo el rango, además de valores exóticos como cero negativo, números subnormalizados, números desnormalizados, infinitos y NaN (no -un número). int y float ambos son de 32 bits, y usted lata tomar los 32 bits de un float objeto y tratarlo como un int representación, pero el resultado no tendrá ninguna relación directa con el valor de la float. El segundo bit de orden inferior de un int, por ejemplo, tiene un significado específico; aporta 0 al valor si es 0, y 2 al valor si es 1; el bit correspondiente de un float tiene un significado, pero es bien diferente (aporta un valor que depende del valor del exponente).

La situación con los punteros es bastante similar. Un valor de puntero tiene un significado: es la dirección de algún objeto (o cualquiera de varias otras cosas, pero dejaremos eso de lado por ahora). En la mayoría de los sistemas actuales, interpretar los bits de un objeto puntero como si fuera un número entero le da algo que tiene sentido a nivel de máquina. Pero el lenguaje en sí mismo no garantiza, ni siquiera insinúa, que ese sea el caso.

Los punteros no son números.

Un ejemplo concreto: hace algunos años, me encontré con un código que intentaba calcular la diferencia en bytes entre dos direcciones convirtiéndolas en números enteros. Era algo como esto:

unsigned char *p0;
unsigned char *p1;
long difference = (unsigned long)p1 - (unsigned long)p0;

Si supone que los punteros son solo números, que representan direcciones en un espacio de direcciones monolítico lineal, entonces este código tiene sentido. Pero esa suposición no está respaldada por el lenguaje. Y, de hecho, había un sistema en el que se pretendía ejecutar ese código (el Cray T90) en el que simplemente no habría funcionado. El T90 tenía punteros de 64 bits que apuntaban a palabras de 64 bits. Los punteros de byte se sintetizaron en el software almacenando un desplazamiento en los 3 bits de orden superior de un objeto puntero. Restar dos punteros de la manera anterior, si ambos tuvieran 0 desplazamientos, le daría el número de palabras, no bytes, entre las direcciones. Y si tuvieran compensaciones distintas de 0, le daría basura sin sentido. (La conversión de un puntero a un número entero simplemente copiaría los bits; pudo han hecho el trabajo para darle un índice de bytes significativo, pero no fue así).

La solución fue simple: elimine los moldes y use la aritmética de punteros:

long difference = p1 - p0;

Son posibles otros esquemas de direccionamiento. Por ejemplo, una dirección puede consistir en un descriptor que (quizás indirectamente) haga referencia a un bloque de memoria, más un desplazamiento dentro de ese bloque.

Puede suponer que las direcciones son solo números, que el espacio de direcciones es lineal y monolítico, que todos los punteros son del mismo tamaño y tienen la misma representación, que un puntero se puede convertir de forma segura a into para long, y viceversa sin pérdida de información. Y el código que escriba en base a esas suposiciones probablemente funcionará en la mayoría de los sistemas actuales. Pero es muy posible que algunos sistemas futuros vuelvan a usar un modelo de memoria diferente y su código se rompa.

Si evita hacer suposiciones más allá de lo que realmente garantiza el lenguaje, su código estará mucho más preparado para el futuro. E incluso dejando de lado los problemas de portabilidad, probablemente será más limpio.

  • Todavía hay una cosa que no entiendo muy bien. “int a = 10; int *p = &a;”, entonces el valor de p es la dirección de a en mem, ¿verdad? Si es correcto, entonces el valor de p variará de 0 a 2 ^ 32-1 (si la CPU es de 32 bits), y un número entero es de 4 bytes en un sistema operativo de 32 bits, ¿verdad? entonces ¿Cuál es la diferencia entre el valor de p y un número entero? ¿Puede el valor de p salirse del rango?

    – Alcott

    4 de septiembre de 2011 a las 12:46

  • La gente debería aprender a programar en DOS de 16 bits, donde sizeof(int) es 2, el valor de un puntero, ya sea un int de 16 bits o 2 de 16 bits, donde 4096 punteros diferentes pueden apuntar exactamente a la misma dirección y donde el puntero de función y el puntero de datos pueden tener diferentes tamaños. Eso les enseñaría. 😉

    – Patrick Schlüter

    04/09/2011 a las 19:30

  • @PatrickSchlüter Dios mío, eso fue un viaje por el callejón de las pesadillas. Nunca quiero volver a eso (pero ciertamente me recuerda a diario lo bien que lo tenemos ahora).

    – WhozCraig

    6 de agosto de 2016 a las 4:17


Tanta locura presente aquí…

%p es generalmente el especificador de formato correcto para usar si solo desea imprimir una representación del puntero. Nunca, nunca uses %d.

La longitud de un int y la longitud de un puntero (void* o de otra manera) tienen sin relación. La mayoría de los modelos de datos en i386 tienen 32 bits ints Y punteros de 32 bits — ¡otras plataformas, incluyendo x86-64, no son lo mismo! (Esto también se conoce históricamente como “todo el mundo es un síndrome VAX”.) http://en.wikipedia.org/wiki/64-bit#64-bit_data_models

Si por alguna razón desea mantener una dirección de memoria en una variable integral, ¡use los tipos correctos! intptr_t y uintptr_t. Están en stdint.h. Ver http://en.wikipedia.org/wiki/Stdint.h#Integers_wide_enough_to_hold_pointers

avatar de usuario
Hogan

C ª void * es un puntero sin tipo. void no significa vacío… significa cualquier cosa. Así echando a void * sería lo mismo que convertir a “puntero” en otro idioma.

Utilizando (int *)&a debería funcionar también… pero el punto estilístico de decir (void *) es decir, no me importa el tipo, solo que es un puntero.

Nota: es posible que una implementación de C provoque que esta construcción falle y aun así cumpla con los requisitos de los estándares. No conozco ninguna implementación de este tipo, pero es posible.

  • no funciona si void* y int* tienen diferentes representaciones.

    –Keith Thompson

    22 de enero de 2015 a las 18:21

  • quieres decir si void * es de diferente tamaño que int *? ¿Qué no funciona?

    – Hogan

    22 de enero de 2015 a las 18:39


  • Me refiero a lo que dije: si void* y int* Tiene diferente representaciones, incluso si tienen el mismo tamaño. En la mayoría de los sistemas modernos, todos los punteros tienen el mismo tamaño y representación, pero el estándar C no garantiza eso. La conversión de int* para void* podría no ser trivial printf("%p", ...) espera y argumento de tipo void*; si le pasas un int* en cambio, no se convertirá, y printf tratará a su int* objeto como si era un void* objeto. N1570 7.21.6.1 párrafo 9: el comportamiento no está definido

    –Keith Thompson

    22 de enero de 2015 a las 18:51

  • Estoy de acuerdo, no tenía la intención de decir void * es portable o es una buena idea, solo trato de explicar (mal) el concepto.

    – Hogan

    22 de enero de 2015 a las 19:02

  • Tu respuesta implica que int a; printf("%p", &a); es seguro. no lo es

    –Keith Thompson

    22 de enero de 2015 a las 19:06

Aunque la gran mayoría de las implementaciones de C almacenan punteros a todo tipo de objetos usando la misma representación, el Estándar C no requiere que todas las implementaciones lo hagan, ni siquiera proporciona ningún medio por el cual un programa que aproveche la similitud de las representaciones podría probar si una implementación sigue la práctica común y negarse a ejecutar si una implementación no lo hace.

Si en alguna plataforma en particular, un int* mantuvieron una dirección de palabra, mientras que ambos char* y void* combinar una dirección de palabra con una palabra que identifica un byte dentro de una palabra, pasando un int* a una función que espera recuperar un argumento variádico de tipo char* o void* daría como resultado que esa función intente obtener más datos de la pila (una dirección de palabra más la palabra complementaria) de los que se habían enviado (solo la dirección de palabra). Esto podría causar que el sistema funcione mal de manera impredecible.

Muchos compiladores para plataformas comunes que usan la misma representación para todos los punteros procesarán una acción que pasa un puntero no vacío exactamente de la misma manera que procesarían una acción que convierte el puntero en void* antes de pasarlo. Por lo tanto, no tienen motivos para preocuparse por si el tipo de puntero que se pasa como argumento variádico coincidirá con precisión con el tipo de puntero esperado por el destinatario. Aunque el estándar podría haber especificado que tales implementaciones que no tendrían motivos para preocuparse por los tipos de punteros deberían comportarse como si los punteros se hubieran convertido en void*, los autores de C89 Standard evitaron describir cualquier cosa que no fuera común a todos los compiladores conformes. La terminología estándar para una construcción que el 99 % de las implementaciones debería procesar de manera idéntica, pero el 1 % podría procesar de manera impredecible, es “Comportamiento indefinido”. Las implementaciones pueden, ya menudo deberían, ampliar la semántica del lenguaje especificando cómo tratarán dichas construcciones, pero ese es un problema de Calidad de Implementación fuera de la jurisdicción del Estándar.

¿Ha sido útil esta solución?