42. Objetos, tipos y valores en C. Discusión general
Este post contiene una discusión que terminó resultándome demasiado filosófica.
Yo no perdería tiempo en tal cosa si no lo considerara absolutamente necesario.
El dilucidar la correcta interpretación de los términos que aquí estudiaremos, me ha permitido a mí sentirme sólido en la comprensión del manejo de los datos que se producen en un programa en
C.
Definición informal de: objetos, valores y tipos Un
objeto es una porción de memoria, que contiene unos bits determinados allí.
Típicamente, un
objeto ocupa una cantidad entera de
bytes, y no de
bits individuales o sueltos.
En la práctica, los
objetos se consideran, pues, "una porción específica de la memoria RAM", en la que los bytes están contiguos, en bloque.
En ese caso, el
objeto consta de una
posición de memoria, un
tamaño medido en bytes contiguos, y un
contenido formado por los
bits presentes en esa región de memoria.
Nota técnica: No debemos confundir esta noción de
objeto con los tipos de datos de la
Programación Orientada a Objetos que puede haber en lenguajes como
C++.
En nuestro contexto,
objeto es un término que sólo se refiere a un dato guardado en una posición específica de memoria, y "ocupando" una cantidad determinada de bytes.
Para vislumbrar mejor esto, conviene tener en cuenta la representación típica de los datos en un chip de memoria. Se considera a la memoria RAM como un conjunto de bytes alineados en secuencia y en forma contigua.
Los elementos de la memoria se pueden indexar desde el 1ero, que lleva número 0, hasta el último, que típicamente es alguna potencia de 2 (menos uno).
En cada byte de la memoria hay unos bits, que alojan 0's y 1's.
0 \( \longrightarrow \) 01101101
1 \( \longrightarrow \) 10101100
2 \( \longrightarrow \) 00011001
3 \( \longrightarrow \) 11100010
4 \( \longrightarrow \) 11111110
5 \( \longrightarrow \) 01000001
... ... ... ... ... ... ... ... ... ... ...
Un
objeto podría ser el ocupado por las posiciones contiguas de memoria de la 2 a la 5, otro podría ser el ocupado por las posiciones de la 11 a la 33, y así por el estilo.
Puede haber
objetos que se superpongan, ocupando algunos bytes en común.
Para la definición formal de
objeto, el estándar
C no asume ningún modelo específico de memoria RAM, aunque asume que tiene una cierta "posición", la cual es compatible con operaciones aritméticas de los tipos enteros, un "tamaño" que es un entero positivo de bytes, y un "contenido".
Para
C, el
contenido exacto (bit a bit) del objeto no es algo "importante" per se, en el sentido de que la codificación que se usa para representar una determinada información puede variar de un compilador a otro, o de un entorno a otro, y esto no es algo de lo que
C se preocupe mayormente (en realidad sí, pero sólo en relación a ciertos detalles, y por razones lejanas a la presente discusión).
Lo importante es que ese
contenido codifique, de alguna manera, los "valores" de algún "tipo" de datos dado.
Un
valor de un cierto
tipo es algo que tiene "significado" para nosotros como seres humanos. Este "valor" se exhibirá al usuario del programa de alguna manera. Pero no se trata de un concepto del todo nítido.
Lo que el estándar
C intentará es que las operaciones y comportamiento de los "valores" sea consecuente con lo que esperamos de ellos, sin importar el modo en que estén representados como bits en memoria.
Así, cuando calculemos 3 + 5, nos tiene que dar 8, sin importar qué codificación usemos en memoria para los "valores" 3, 5 y 8.
En resumen, lo que quiero decir aquí es que no tenemos que preocuparnos de la
representación interna en bits de los
valores, sino que, independientemente de dicha representación, el estándar
C asegura un comportamiento bien definido.
Es más:
distintos compiladores y entornos de ejecución pueden dar lugar a representaciones internas diferentes de los mismos valores y tipos. Bueno, pero, si se almacena "contenido crudo en bits" y no "valores", ¿cómo es que se logra un comportamiento bien definido al operar sobre los "valores"?
Esto es responsabilidad del diseñador del compilador, quien debe elegir con cuidado una codificación coherente de los datos.
La "interpretación" del
contenido de un
objeto ocurre cuando se le mira como de cierto
tipo de datos (ya veremos cómo hacer esto).
Si "enmascaramos" al
objeto con cierto
tipo, resulta tener un determinado
valor.
El
tipo lo podemos elegir nosotros (no quiere decir que siempre dé algo con sentido), y el
valor resultante queda "ahí en el limbo" esperando que alguien lo vea.
Posteriormente hay que buscar la manera de que ese
valor sea realmente visualizado por el usuario.
Sin embargo, cuando se realizan operaciones aritméticas entre
objetos, lo que ocurre es más o menos lo siguiente:
\( \bullet \) Se toman los
objetos sobre los que se quiere operar.
\( \bullet \) Se considera a esos objetos como de cierto
tipo, según reglas que luego veremos.
\( \bullet \) Se efectúan operaciones entre los
valores de los objetos, respecto el
tipo de datos elegido para cada uno.
\( \bullet \) Se obtiene un resultado que es otro
valor, que tiene también su propio
tipo.
\( \bullet \) Se codifica este resultado en algún
objeto en memoria.
Así, en memoria no se guarda ni el valor ni el tipo, pero para acceder a un objeto, debemos hacerlo mediante un tipo e interpretando su contenido como un valor de dicho tipo. Ejemplus:
El programador escribe la constante
33, cuyo tipo es
int según las normas de
C. Esto quiere decir que, en un compilador que interpreta enteros de 16 bits y bytes de 8 bits, el "valor"
33 se codificará en memoria como un
objeto que ocupa 2 bytes, así:
00000000 00100001.
El objeto resultante es un bloque contiguo de 2 bytes de memoria RAM, con los bits antes indicados como
contenido.
Cuando queremos leer el dato, asumiendo que es un
int, esos bits se "interpretan" como el número entero matemático y abstracto 33.
Esa "abstracción" no queda almacenada en ninguna parte, ni se "ve" en ninguna parte de la computadora.
Todo lo que podemos asegurar es que, operando con ese
objeto, se comportará como si fuera el número 33.
Por ejemplo, si le sumamos 1, el resultado será otro objeto en memoria, cuya codificación coincidirá con la elegida para el valor
34 de tipo
int.
Si tenemos ahora el valor negativo
-33, ya ni siquiera voy a poner su formato binario como ejemplo.
Se elegirá alguna codificación en bits, como objeto en la memoria, que "provino" del valor
-33, de tipo
int.
Ese objeto, interpretado ahora como de tipo
unsigned int, puede darnos un
valor sin sentido para nosotros, tal como
65503.
Es que ahora el tipo
unsigned int impide que un objeto en memoria sea interpretado como un número negativo.
¿Qué ocurre entonces cuando
printf() nos muestra un "valor"?
Ahí está el meollo del problema. Una función que nos muestra información como
printf(), no nos muestra un dato que está "almacenado por ahí en memoria". En realidad lo que hace es: "tomar un
objeto alojado en memoria, e interpretar su
contenido según un tipo que se habrá seleccionado, y al
valor resultante lo exhibe en pantalla con un
formato específico".
De nuevo, el "valor" sigue dando vueltas en abstractilandia, y es algo de lo que la computadora no se entera ni le concierne.
Así, hay una sutil relación entre "contenido de un objeto" y "valor interpretado" de ese contenido, que depende de las subjetividades del intelecto humano hasta cierto punto.
El que estas subjetividades humanas sean consecuentes con operaciones de objetos en una computadora, se debe a la combinación de varios elementos de diseño: una elección de codificación para los valores de un tipo dado, unas reglas de operación con dichos valores que son firmes y predecibles, y una elección del formato de visualización de los datos.
La visualización de los datos no es algo trivial, sino que involucra una compleja cadena de operaciones internas.
Por ejemplo, para que
printf() nos muestre el valor
33, antes tiene que hacerse una conversión de binario a decimal, tras varias operaciones de división. Luego los dígitos resultantes se transforman en caracteres que representan dígitos, obligando a hacer nuevas operaciones.
Los objetos son modificables. Si bien las posiciones de memoria están fijas en una computadora, así como el tamaño de cada byte, en cambio el "contenido" de 1 byte de memoria puede alterarse, cambiando sus bits por otros.
Es decir, el
contenido de un
objeto puede variar.
Como resultado, un mismo
objeto puede representar diferentes
valores a lo largo de un programa.
Hay ocasiones en que se habla de posiciones de memoria de "sólo lectura", o de "objetos no modificables" (o "constantes").
Esto quiere decir que hay
objetos que, una vez que tienen determinado
contenido, éste no varía a lo largo del programa.
Sin embargo, estas son convenciones dentro del mismo programa, o del sistema operativo en donde se ejecuta, y no un impedimento de la computadora, cuya memoria RAM siempre es físicamente alterable.
Así, para impedir que un
objeto varíe su
contenido a lo largo de un programa, es necesario que el programa mismo efectúe algunas comprobaciones extra todo el tiempo.
Esta tarea no la debe realizar el programador, sino que es el compilador el que ha de producir un programa ejecutable diesñado de esa manera. Por lo tanto, no debemos preocuparnos por tales detalles.
Es interesante darse cuenta de cuánto hay de "engaño" cuando usamos una computadora. Creemos que vemos números y datos, y en realidad sólo obtenemos el cínico reflejo de nuestras convenciones sociales sobre números e información.
Es decir, mucho del trabajo computacional lo termina haciendo nuestro cerebro, sin que nos demos siquiera cuenta de ello.
La máquina sólo transforma unas cosas en otras, unos bits de un lugar en otros bits en otro lugar.
En el ejemplo del número
33, cuando un programa finalmente nos muestra ese "valor", no nos muestra ni el "contenido" en bytes del objeto en memoria que lo tiene almacenado, ni tampoco el número 33 propiamente dicho, que es una abstracción matemática que la computadora ni sabe que existe. Lo que nos muestra
printf() es un par de caracteres ASCII
3 (cuyo código ASCII es 51...), que es lo mismo que exhibir la cadena de caracteres
"33".
El "valor"
33, ¿dónde está? En el imaginario social.
¿Y qué es un
tipo? Es también una entidad abstracta: es el nombre de un cierto conjunto de cosas.
Típicamente los conjuntos a que nos referiremos son "finitos".
Si bien un conjunto está unívocamente determinado por sus "elementos", resulta que podemos darle más de un "nombre" distinto.
Esos "nombres" son lo que, a grandes rasgos, consideramos un
tipo.
Sin embargo, un
tipo especifica también propiedades globales de los elementos del conjunto a que se refiere: qué operaciones aritméticas son válidas entre elementos del conjunto, qué relaciones hay entre ellos, y cómo se vinculan con elementos de otros
tipos.
Los elementos de un conjunto, nombrado por un
tipo, son lo que se entiende por
valores posibles para ese
tipo.
Así, un
valor está determinado por dos propiedades: el elemento que representa, y el
tipo al que pertenece.
Un mismo elemento, considerado de
tipos distintos, representa
valores distintos.
Un
33 de tipo
int es un
valor distinto que un
33 de tipo
long long int.
A pesar de eso, las cosas están diseñadas en
C para que todas las versiones "distintas" de
33 funcionen como si fueran equivalentes aritméticamente, y esto hace transparentes los cálculos para un programador.
En todo caso, a fin de no pifiarle, tendremos que estudiar luego las reglas que permiten pasar de un tipo a otro, y qué consecuencias hay para los valores considerados.
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCIntento formal de definición de: objetos, valores y tipos En
C se le dice
objeto a una región de almacenamiento de datos en el
entorno de ejecución cuyos contenidos pueden representar
valores.
Un
valor es el significado preciso de los contenidos de un objeto cuando se interpretan teniendo un
tipo específico.
Se supone que esas frases definen el significado de los términos
objeto,
valor y
tipo.
A mí me parece todo circular y mal definido.
A fin de definir las cosas con más certeza, podríamos intentar subsumir estos términos en una fraseología matemática.
Lo que sigue son definiciones mías, y no del estándar
C99:
Digo que un
tipo es un nombre dado a un conjunto finito de elementos, sujetos a ciertas relaciones y operaciones y reglas.
Esto del "nombre" es importante, porque un mismo conjunto puede tener diferentes nombres, o sea, ser designado mediante diferentes tipos.
Digo que un
valor es uno de los elementos del conjunto a que se refiere un
tipo dado. Pero no es un elemento "suelto", sino que un
valor dado lleva atado siempre un
tipo al cual pertenece.
Un
objeto es una entidad formada por dos cosas: una región de almacenamiento de datos (típica, aunque no necesariamente, un bloque contiguo de 1 o más bytes de memoria RAM), y unos contenidos alojados en dicho región de almacenamiento, susceptibles de modificación a lo largo del tiempo.
En la práctica, los contenidos de un
objeto son sólo unos bits puestos en fila, sin significado alguno.
Sólo tienen un significado concreto cuando se les interpreta con un tipo de datos específico.
Ese significado es uno de los valores aceptados para el tipo en cuestión.
El "significado" es una noción que atañe al intelecto humano. Se refiere al sentido subjetivo último que una mente humana le da a los entes manejados y exhibidos por computadoras.
Si el
tipo es algo "abstracto", ¿cómo se entera la máquina de que le estamos indicando un tipo y no otro, en determinadas operaciones aritméticas, o cuando le pedimos que nos muestre un resultado?
Ahí es la máquina la que debe interpretar correctamente el
tipo, y no nuestra psique.
Hay algunos detalles de los
tipos que se almacenan como información en el programa.
Por ejemplo, a un
tipo se le asocia comunmente una determinada cantidad fija de bytes, que será su
tamaño.
Hay relaciones concretas entre los
tipos y la codificación de sus
valores como
objetos en memoria, que permiten un trabajo coherente.
Estas relaciones se almacenan como parte del programa ejecutable, y entonces todo comienza a funcionar, como un engranaje.
No voy a discutir aquí cómo se hace esto, porque en realidad lo que acabo de decir es incluso más alegórico que cierto.
Como sea, parece que estoy ampliando aquí la definición de
tipo. (En matemática no me dejan hacer eso de "ampliar luego un poquito más la definición").

En cierto modo, un
tipo tiene dos caras: una serie de operaciones y relaciones entre objetos de los que se almacenan en memoria, y una serie de operaciones y relaciones entre los elementos de un conjunto abstracto.
Se estipula una correspondencia uno a uno entre dichos objetos de memoria con los elementos abstractos, de modo que se conserven operaciones y relaciones en forma coherente.
En particular, esto obliga a que los conjuntos resultantes se comporten de un modo algo "trabado", siguiendo las limitaciones que una computadora impone.
Por ejemplo, el tipo
int no puede representar todos los números enteros, sino sólo una porción de ellos. Las operaciones con números grandes darán resultados no claramente definidos, o con significado inútil.
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCUna nota sobre el documento estándar Para mi gusto, las nociones presentadas en el estándar son poco claras, algo circulares, y sólo con experiencia y reflexión uno termina logrando una comprensión exacta de los términos allí "definidos".
Salvando este defecto, parece ser que el estándar ha intentado definir los términos
objeto,
tipo y
valor de forma "operacional", es decir, indicando más bien "cómo interactúan entre ellos", o qué efectos producen.
En tal caso, se deja la puerta abierta a más sutiles abstracciones.
Esto puede ser preferible, dado que el concepto de
máquina en el estándar
C es la más indefinida de las nociones que he visto allí nombrar.
Dejando algo de flexibilidad para el significado de los valores y objetos asociados, puede resultar fecundo en algún caso.
Los ejemplos que yo dí, con bytes de 8 bits, y con modelos de memoria RAM concretos, aún cuando lo hice de forma simplificada, no dejan de ser una elección concreta, un modelo de almacenamiento de datos que no necesariamente hay que asumir.
Cuando estudiemos los punteros, veremos qué es lo máximo que el estándar
C asume como propiedades ciertas de un dispositivo de almacenamiento de datos.
Ejemplificar con la RAM sirve más para "fijar ideas" y concretar en un ejemplo, cosa útil pedagógicamente, pero que quita generalidad. Ha de tenerse en cuenta que el estándar intenta ser lo más general que sea posible, es decir, va en la dirección contraria. Esto es así debido a que, a fin de cuentas,
C es un lenguaje de
alto nivel, y tanto mejor es si puede ser ejecutado en la más variada gama de sistemas y máquinas (incluyendo dispositivos móviles, máquinas virtuales, microcontroladores, placas de robots, y un largo etcétera).
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCOrganizaciónComentarios y Consultas