Aprender sobre C++, Motores ECS e IA
Entendiendo los objetos en C++
En C++, todo lo que los programas hacen es manipular objetos: los crean y destruyen, los leen, escriben y operan. Necesitamos comprender y dominar cómo los objetos se representan en memoria y cómo se manejan sus propiedades. Veamos qué es un objeto en C++, qué características tiene, cómo se gestiona su memoria y cómo funciona.
Objetos en C++
- Definición: un objeto es una región de memoria con una semántica asociada.
- Región de memoria: es un término genérico, pero se refiere a un espacio de memoria accesible por la CPU, es decur, RAM, Registros o incluso ROM. Como la unidad mínima es el byte, este espacio tendrá un número entero de bytes.
- Semántica asociada: define qué significan los posibles valores almacenados en el objeto, y qué operaciones están lógicamente permitidas sobre ese objeto (y, por tanto, qué otras cosas no lo están). Es la forma de explicar qué significa cualquier valor concreto de ese objeto y, también, de definir el conjunto de valores permitidos (llamados invariantes de un objeto).
Los programas crean, destruyen, se refieren a, y manipulan objetos.
Nota: para referirse a un objeto en memoria, sin importar cuanto ocupa, se indica siempre la dirección de su primer byte. Como sabemos su tamaño, sabiendo su primer byte, podemos leerlo o modificarlo sin equivocarnos.
Un objeto en C++ tiene:
- Tamaño (size): número de bytes que ocupa. Se puede saber usando el operador
sizeof.- Ejemplo:
int vidas = 25;
std::print("Tamaño de 'vidas' = {}\n", sizeof(vidas));
- Ejemplo:
- Alineamiento requerido (alignment): múltiplo de byte en memoria donde puede estar el inicio del objeto. Podemos saberlo con el operador
alignof. - Duración de almacenamiento (storage duration): es una condición que determina cuánto tiempo será válido el objeto.
- Automático (automatic): se crea en un ámbito (scope) y se destruye al terminar ese ámbito. Solemos llamarlo "local",
- Estático (static): se crea antes de empezar
main()y se destruye tras terminarmain(). Tiene la misma duración que el programa. - Dinámico (dynamic): lo creamos y destruimos manualmente a conveniencia (con
new/mallocydelete/free). - Local a hilo (thread-local): que pertence exclusivamente a un hilo de ejecución, y su creación y destrucción dependerá de la vida del hilo.
- Tiempo de vida (lifetime): intervalo de tiempo desde que un objeto se crea hasta que se destruye. Tiene dos posibles condiciones:
- Está limitado por el tipo de duración de almacenamiento (storage duration) del objeto.
- Puede ser temporal: en objetos que sólo existen durante la evaluación de la expresión en que están involucrados.
- Ejemplo:
// 25 es un objeto temporal. Deja de existir tras ejecutar esta línea int vidas = 25; // Se llama a random(1,10) y eso devuelve un int temporal. // Se suma 100 + el int temporal y eso produce otro int temporal. // El objeto health se inicializa con el último int temporal. // Todos los temporales dejan de existir tras esta línea. float health = 100 + random(1, 10);
- Ejemplo:
- Tipo (type): clase dentro de los posibles tipos de objeto. Determina los invariantes y el conjunto de operaciones válidas.
- Valor (value): el valor que tiene el objeto en un instante, que puede ser indeterminado, por ejemplo, en la inicialización por defecto (default-initialized).
- Nota: indeterminado quiere decir que tiene un valor, pero que no podemos saber cuál es. Si se usa una posición de memoria para el objeto, pero no se escribe en ella un valor inicial (inicializar), tendrá un valor previo que no sabemos cuál es. Es indeterminado.
- Nombre (name) --Opcional--: los objetos pueden tener un nombre y, entonces, pueden ser variables.
Variable: un objeto o referencia que se introduce con una (declaration) y que no es ni un dato miembro, ni un miembro no-estatico.
Nota: un dato miembro es un campo de una estructura o clase, que no es estático. Los datos estáticos, aunque se declaren dentro de una clase, son únicos y globales como cualquier estático. Se crean antes de que empiece el programa y existen hasta su terminación. Por eso son únicos y no dependen de la creación de objetos de estructura o clase. Son, a todos los efectos, datos estáticos globales a los que limitamos su visibilidad.
Ejemplos
Considera el siguiente código:
10 | int main () {
11 | int vidas = 25; // Un objeto tipo int que ocupa 4 bytes
...| //...
56 | }
En este ejemplo, vidas es un objeto en C++ con:
- Nombre:
vidas(al tener nombre, es una variable) - Tamaño: 4 bytes
- Valor: 25
- Tipo:
int(indica sus semánticas asociadas: se representará en binario como un número aritmético estándar, y admitirá operaciones aritméticas como suma, resta, multiplicación, etc.) - Alineamiento: 4 bytes (sólo puede estar en memoria en celdillas múltiplo de 4 bytes)
- Storage duration: Automático (se crea en el stack y se libera al terminar su scope/ámbito)
- Tiempo de vida: Líneas 11 a 56 (se crea al ejecutar la 11, se libera al ejecutar la 56)
Considera ahora este otro código:
04 | // Definición de estructura Entity que ocupará 32 bytes
05 | struct Entity {
06 | int x, y, vx, vy; // 4 ints = 16 bytes
07 | double attrs[2]; // 2 doubles = 16 bytes
08 | };
.. |
40 | int main () {
41 | static Entity* ep = new Entity();
.. | //...
56 | }
En este ejemplo se crean 2 objetos en la línea 41:
- La variable
ep - Un objeto de tipo
Entitysin nombre
El objeto sin nombre se crea en el free-store (Heap) y sólo podemos acceder a él a través de su dirección de memoria. Para eso usamos la variable ep, para almacenar el valor del puntero a Entity que nos devuelve la expresión new y que nos dice dónde está el nuevo objeto sin nombre.
Así pues, hemos creado 2 objetos:
| Propiedad | ep |
Objeto Sin-Nombre (OSN) |
|---|---|---|
| Tamaño | 8 bytes (en CPUs de 64 bits) | 32 bytes |
| Alineamiento | 8 bytes (en CPUs de 64 bits) | 8 bytes (campo más grande, double) |
| Storage duration | Estático (global) | Dinámico (gestión manual) |
| Tiempo de Vida | Desde la línea 41 hasta el final del programa | Desde la línea 41 hasta que se llame a delete |
| Tipo | Entity* |
Entity |
| Nombre | ep |
No tiene nombre |
| Valor | Dirección de memoria del OSN | Indeterminado (Entity no tiene constructor ni valores por defecto) |
Importante: cosas que NO son objetos
Estas "entidades" no son objetos:
- Un valor (value)
- Una referencia (reference)
- Una función (function)
- Un enumerador (enumerator)
- Un tipo (type)
- Un miembro de clase no-estático (non-static class member)
- Una plantilla (template)
- Una clase (class)
- Una especialización de plantilla de función (function template specialization)
- Un espacio de nombres (namespace)
- Un pack de parámetros (parameter pack)
- La expresión
this(this)
Creación y Destrucción de Objetos en C++
Las palabras crear y destruir en C++ suelen dar problemas. Es importante que las conozcamos con precisión.
- Crear: decidir un lugar exacto de memoria donde estará el objeto e inicializarlo (escribir su valor inicial). A partir de ese momento, se dice que el objeto ha sido creado y existe.
- Destruir: dejar de usar la memoria del objeto, quedando libre para otros usos.
Esto es lo básico, pero hay matices y detalles. Vamos a explorarlos con precisión.
Creación de Objetos
La creación de un objeto implica reservar una región de memoria (elegir un sitio, decidir dónde va) e inicializar esa región con un valor específico. A la inicialización la llamamos construcción, y es la labor del constructor. Los objetos que no tienen un constructor por defecto, ni se les asigna un valor inicial, quedan sin inicializar, por lo que su valor es indeterminado.
Existen diversas formas de crear objetos dependiendo de la duración de almacenamiento:
-
Automático: Los objetos se crean automáticamente cuando se entra en su ámbito (scope). Se elige un espacio en la pila (stack, que está en la RAM).
-
Estático: Los objetos con almacenamiento estático se crean antes de la ejecución del
main()y se destruyen después de terminarlo. La región de memoria que usan suele elegirse (reservarse) al cargar el programa en memoria. -
Dinámico: Los objetos se crean cuando se ejecuta
newomalloc. Se elige un sitio para ellos en el free-store (reserva en el Heap). Este espacio permanece disponible hasta que ejecutadeleteofree, indicando que queda libre para otros usos. -
Local a hilo: Se crean cuando un hilo comienza y existen solo mientras el hilo esté activo.
Destrucción de Objetos
La destrucción de un objeto implica dejar de usar su región de memoria, por lo que quedará libre para otros usos. Algunos objetos complejos son responsables de la gestión de otros objetos o recursos, por lo que necesitan ejecutar código antes de destruirse, para liberar los recursos que gestionan. Esta lógica es la que va en su destructor. A estos objetos se les llama no-trivialmente-destruibles.
Según la duración de sus almacenamientos:
-
Automático: se destruyen automáticamente cuando se sale del ámbito (scope) donde fueron creados. Es al final del bloque donde el objeto está definido.
-
Estático: se destruyen después de finalizar
main(). -
Dinámico: deben ser liberados explícitamente usando
deleteofree. De lo contrario, se producirá una fuga de memoria (memory leak), pues sus regiones de memoria permanecen en uso. -
Local a hilo: se destruyen cuando el hilo termina.