Si defino una variable de cierto tipo (que, hasta donde yo sé, solo asigna datos para el contenido de la variable), ¿cómo hace un seguimiento de qué tipo de variable es?
c++
variables
data-types
Finn McClusky
fuente
fuente
Respuestas:
Las variables (o más generalmente: "objetos" en el sentido de C) no almacenan su tipo en tiempo de ejecución. En lo que respecta al código de máquina, solo hay memoria sin tipo. En cambio, las operaciones en estos datos interpretan los datos como un tipo específico (por ejemplo, como un flotante o como un puntero). Los tipos solo los utiliza el compilador.
Por ejemplo, podríamos tener una estructura o clase
struct Foo { int x; float y; };
y una variableFoo f {}
. ¿Cómo seauto result = f.y;
puede compilar un acceso de campo ? El compilador sabe quef
es un objeto de tipoFoo
y conoce el diseño de losFoo
objetos. Dependiendo de los detalles específicos de la plataforma, esto podría compilarse como "Tome el puntero al inicio def
, agregue 4 bytes, luego cargue 4 bytes e interprete estos datos como flotantes". En muchos conjuntos de instrucciones de código de máquina (incl. X86-64 ) hay diferentes instrucciones de procesador para cargar flotadores o ints.Un ejemplo en el que el C ++ sistema de tipo no puede perder de vista el tipo para nosotros es una unión como
union Bar { int as_int; float as_float; }
. Una unión contiene hasta un objeto de varios tipos. Si almacenamos un objeto en una unión, este es el tipo activo de la unión. Solo debemos tratar de sacar ese tipo de la unión, cualquier otra cosa sería un comportamiento indefinido. O bien "sabemos" mientras programamos cuál es el tipo activo, o podemos crear una unión etiquetada donde almacenamos una etiqueta de tipo (generalmente una enumeración) por separado. Esta es una técnica común en C, pero debido a que debemos mantener la unión y la etiqueta de tipo sincronizadas, esto es bastante propenso a errores. Unvoid*
puntero es similar a una unión, pero solo puede contener objetos de puntero, excepto punteros de función.C ++ ofrece dos mejores mecanismos para tratar objetos de tipos desconocidos: podemos usar técnicas orientadas a objetos para realizar el borrado de tipos (solo interactuar con el objeto a través de métodos virtuales para que no necesitemos saber el tipo real), o podemos uso
std::variant
, una especie de unión de tipo seguro.Hay un caso en el que C ++ almacena el tipo de un objeto: si la clase del objeto tiene algún método virtual (un "tipo polimórfico", también conocido como interfaz). El objetivo de una llamada a método virtual es desconocido en el momento de la compilación y se resuelve en tiempo de ejecución en función del tipo dinámico del objeto ("despacho dinámico"). La mayoría de los compiladores implementan esto almacenando una tabla de funciones virtuales ("vtable") al comienzo del objeto. El vtable también se puede usar para obtener el tipo de objeto en tiempo de ejecución. Entonces podemos hacer una distinción entre el tipo estático conocido de tiempo de compilación de una expresión y el tipo dinámico de un objeto en tiempo de ejecución.
C ++ nos permite inspeccionar el tipo dinámico de un objeto con el
typeid()
operador que nos da unstd::type_info
objeto. O bien el compilador conoce el tipo de objeto en el momento de la compilación, o el compilador ha almacenado la información de tipo necesaria dentro del objeto y puede recuperarla en tiempo de ejecución.fuente
void*
).typeid(e)
Introspecta el tipo estático de la expresióne
. Si el tipo estático es un tipo polimórfico, se evaluará la expresión y se recuperará el tipo dinámico de ese objeto. No puede apuntar typeid a la memoria de tipo desconocido y obtener información útil. Por ejemplo, typeid de una unión describe la unión, no el objeto en la unión. El typeid de avoid*
es solo un puntero vacío. Y no es posible desreferenciar avoid*
para llegar a su contenido. En C ++ no hay boxeo a menos que se programe explícitamente de esa manera.La otra respuesta explica bien el aspecto técnico, pero me gustaría agregar un poco de "cómo pensar sobre el código de máquina".
El código de la máquina después de la compilación es bastante tonto, y realmente asume que todo funciona según lo previsto. Digamos que tienes una función simple como
Se necesita un int y escupe un bool.
Después de compilarlo, puede considerarlo como algo así como este exprimidor automático de naranjas:
Toma naranjas y devuelve jugo. ¿Reconoce el tipo de objetos en los que se mete? No, se supone que son naranjas. ¿Qué sucede si se obtiene una manzana en lugar de una naranja? Quizás se rompa. No importa, ya que un propietario responsable no intentará usarlo de esta manera.
La función anterior es similar: está diseñada para recibir entradas, y puede romperse o hacer algo irrelevante cuando se alimenta con otra cosa. (Generalmente) no importa, porque el compilador (generalmente) verifica que nunca suceda, y de hecho nunca sucede en un código bien formado. Si el compilador detecta la posibilidad de que una función obtenga un valor de tipo incorrecto, se niega a compilar el código y en su lugar devuelve errores de tipo.
La advertencia es que hay algunos casos de código mal formado que el compilador pasará. Ejemplos son:
void*
aorange*
cuando hay una manzana en el otro extremo de la aguja,Como se dijo, el código compilado es como la máquina exprimidora: no sabe lo que procesa, solo ejecuta instrucciones. Y si las instrucciones son incorrectas, se rompe. Es por eso que los problemas anteriores en C ++ provocan bloqueos no controlados.
fuente
void*
coacciona afoo*
las promociones aritméticas habituales,union
escribir tipos,NULL
vs.nullptr
incluso tener un puntero malo es UB, etc. Pero no creo que enumerar todas esas cosas mejoraría materialmente su respuesta, por lo que probablemente sea mejor irse tal como es.void*
no se convierte implícitamentefoo*
, y elunion
punteo de tipos no es compatible (tiene UB).Una variable tiene varias propiedades fundamentales en un lenguaje como C:
En su código fuente , la ubicación, (5), es conceptual, y esta ubicación se conoce por su nombre, (1). Entonces, una declaración de variable se usa para crear la ubicación y el espacio para el valor, (6), y en otras líneas de origen, nos referimos a esa ubicación y al valor que contiene al nombrar la variable en alguna expresión.
Simplificando solo un poco, una vez que su programa es traducido al código de máquina por el compilador, la ubicación, (5), es alguna ubicación de registro de memoria o CPU, y cualquier expresión de código fuente que haga referencia a la variable se traduce en secuencias de código de máquina que hacen referencia a esa memoria o ubicación del registro de la CPU.
Por lo tanto, cuando se completa la traducción y el programa se está ejecutando en el procesador, los nombres de las variables se olvidan efectivamente dentro del código de la máquina y las instrucciones generadas por el compilador se refieren solo a las ubicaciones asignadas de las variables (en lugar de a sus nombres). Si está depurando y solicitando depuración, la ubicación de la variable asociada con el nombre, se agrega a los metadatos para el programa, aunque el procesador todavía ve las instrucciones del código de la máquina utilizando ubicaciones (no esos metadatos). (Esta es una simplificación excesiva, ya que algunos nombres están en los metadatos del programa con el fin de vincular, cargar y buscar dinámicamente; sin embargo, el procesador solo ejecuta las instrucciones del código de máquina que se le indica para el programa, y en este código de máquina los nombres tienen sido convertido a ubicaciones)
Lo mismo también es cierto para el tipo, el alcance y la vida útil. Las instrucciones del código de máquina generado por el compilador conocen la versión de máquina de la ubicación, que almacena el valor. Las otras propiedades, como tipo, se compilan en el código fuente traducido como instrucciones específicas que acceden a la ubicación de la variable. Por ejemplo, si la variable en cuestión es un byte de 8 bits con signo versus un byte de 8 bits sin signo, entonces las expresiones en el código fuente que hacen referencia a la variable se traducirán, por ejemplo, en cargas de byte firmado versus cargas de byte sin signo, según sea necesario para satisfacer las reglas del lenguaje (C). El tipo de la variable se codifica así en la traducción del código fuente en instrucciones de la máquina, que le indican a la CPU cómo interpretar la memoria o la ubicación del registro de la CPU cada vez que usa la ubicación de la variable.
La esencia es que tenemos que decirle a la CPU qué hacer a través de instrucciones (y más instrucciones) en el conjunto de instrucciones de código de máquina del procesador. El procesador recuerda muy poco sobre lo que acaba de hacer o se le dijo: solo ejecuta las instrucciones dadas, y es tarea del compilador o del programador de lenguaje ensamblador proporcionarle un conjunto completo de secuencias de instrucciones para manipular adecuadamente las variables.
Un procesador admite directamente algunos tipos de datos fundamentales, como byte / palabra / int / largo firmado / sin signo, flotante, doble, etc. El procesador generalmente no se quejará ni se opondrá si trata alternativamente la misma ubicación de memoria como firmada o sin firmar, por ejemplo, aunque eso normalmente sería un error lógico en el programa. Es el trabajo de la programación instruir al procesador en cada interacción con una variable.
Más allá de esos tipos primitivos fundamentales, tenemos que codificar cosas en estructuras de datos y usar algoritmos para manipularlos en términos de esas primitivas.
En C ++, los objetos involucrados en la jerarquía de clases para el polimorfismo tienen un puntero, generalmente al comienzo del objeto, que se refiere a una estructura de datos específica de la clase, que ayuda con el despacho virtual, la conversión, etc.
En resumen, el procesador no conoce ni recuerda el uso previsto de las ubicaciones de almacenamiento: ejecuta las instrucciones del código de máquina del programa que le indican cómo manipular el almacenamiento en los registros de la CPU y la memoria principal. La programación, entonces, es el trabajo del software (y los programadores) para usar el almacenamiento de manera significativa y presentar un conjunto consistente de instrucciones de código de máquina al procesador que ejecute fielmente el programa en su conjunto.
fuente
useT1(&unionArray[i].member1); useT2(&unionArray[j].member2); useT1(&unionArray[i].member1);
, clang y gcc son propensos a suponer que el punterounionArray[j].member2
no puede accederunionArray[i].member1
a pesar de que ambos se derivan de lo mismounionArray[]
.Aquí hay dos fases relevantes:
El compilador de C compila el código de C en lenguaje de máquina. El compilador tiene toda la información que puede obtener de su archivo fuente (y bibliotecas, y cualquier otra cosa que necesite para hacer su trabajo). El compilador de C realiza un seguimiento de lo que significa qué. El compilador de C sabe que si declaras que una variable es
char
, es char.Lo hace utilizando una llamada "tabla de símbolos" que enumera los nombres de las variables, su tipo y otra información. Es una estructura de datos bastante compleja, pero se puede considerar como un seguimiento de lo que significan los nombres legibles por humanos. En la salida binaria del compilador, ya no aparecen nombres de variables como este (si ignoramos la información de depuración opcional que puede solicitar el programador).
La salida del compilador, el ejecutable compilado, es lenguaje de máquina, que su sistema operativo carga en la RAM y ejecuta directamente su CPU. En lenguaje de máquina, no existe la noción de "tipo" en absoluto, solo tiene comandos que operan en alguna ubicación en la RAM. De hecho, los comandos tienen un tipo fijo con el que operan (es decir, puede haber un comando en lenguaje de máquina "agregue estos dos enteros de 16 bits almacenados en las ubicaciones de RAM 0x100 y 0x521"), pero no hay información en ningún lugar del sistema que indique que Los bytes en esas ubicaciones en realidad representan números enteros. No hay protección de errores de tipo en absoluto aquí.
fuente
char *ptr = 0x123
en C). Creo que mi uso de la palabra "puntero" debería ser bastante claro en este contexto. Si no, no dudes en avisarme y agregaré una oración a la respuesta.Hay un par de casos especiales importantes en los que C ++ almacena un tipo en tiempo de ejecución.
La solución clásica es una unión discriminada: una estructura de datos que contiene uno de varios tipos de objetos, más un campo que dice qué tipo contiene actualmente. Una versión con plantilla está en la biblioteca estándar de C ++ como
std::variant
. Normalmente, la etiqueta sería unenum
, pero si no necesita todos los bits de almacenamiento para sus datos, podría ser un campo de bits.El otro caso común de esto es la escritura dinámica. Cuando
class
tiene unavirtual
función, el programa almacenará un puntero a esa función en una tabla de funciones virtual , que se inicializará para cada instancia declass
cuando se construya. Normalmente, eso significará una tabla de funciones virtuales para todas las instancias de clase, y cada instancia con un puntero a la tabla apropiada. (Esto ahorra tiempo y memoria porque la tabla será mucho más grande que un solo puntero). Cuando llame a esavirtual
función a través de un puntero o referencia, el programa buscará el puntero de la función en la tabla virtual. (Si conoce el tipo exacto en el momento de la compilación, puede omitir este paso). Esto permite que el código invoque la implementación de un tipo derivado en lugar de la clase base.Lo que hace que esto sea relevante aquí es: cada uno
ofstream
contiene un puntero a laofstream
tabla virtual, cada unoifstream
a laifstream
tabla virtual, etc. Para las jerarquías de clases, el puntero de tabla virtual puede servir como la etiqueta que le dice al programa qué tipo de objeto tiene una clase.Aunque el estándar de lenguaje no le dice a las personas que diseñan compiladores cómo deben implementar el tiempo de ejecución bajo el capó, así es como puede esperar
dynamic_cast
ytypeof
trabajar.fuente