Mostrando las entradas con la etiqueta Programación básica. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Programación básica. Mostrar todas las entradas

domingo, 20 de febrero de 2022

Apuntadores y arreglos, estructuras, objetos y clases (más bien en C y C++)

 En el principio era la nada.

Luego, a mediados del siglo XIX, se empezó a volver un tema recurrente la idea de construir una máquina de cálculo general, o sea, que pudiera realizar todas las operaciones aritméticas realizables. Esos esfuerzos no conocieron un final feliz hasta mediados del siglo XX cuando surgieron las primeras computadoras electrónicas realmente programables.

Como se expuso en un artículo anterior, la memoria tiene un papel clave en el funcionamiento de estas máquinas, pues es el componente clave donde se almacenan tanto las instrucciones para trabajar, como el resultado del trabajo realizado.

En este artículo, vamos a analizar las abstracciones más frecuentes que se suelen usar para administrar la memoria.

Arreglos.

C / C++

En estos lenguajes un arreglo suele representar un conjunto de valores del mismo tipo que están de alguna manera, y a juicio del programador, relacionados. Internamente, un arreglo indica el inicio de una serie de bloques consecutivos de memoria del mismo tamaño, y cuando hablamos de "tamaño" aquí, nos referimos a bytes. Por ejemplo, en la declaración "int arr[3];" se indica que "arr" es un apuntador al inicio de tres bloques consecutivos de 4 bytes cada uno (por lo general, el tipo con signo "int" suele consumir 4 bytes). Es decir que ese arreglo es el punto de entrada a un bloque de memoria de 12 bytes.

En C y C++ los arreglos son muy rígidos: se espera que el programador tenga una idea clara del tamaño que va a necesitar desde la declaración, no permiten crecimiento o reducción de ese tamaño una vez declarados y para los ojos de programadores más jóvenes que vienen de lenguajes más flexibles son bastante incómodos de manejar. C++, por tanto, integra un tipo conocido como "vector" que permite un manejo más dinámico de los arreglos.

¿Sirve de algo que sean tan estáticos? En casos muy específicos de uso, sí. Pero es probable que la mayor parte del tiempo prefieras usar un vector en lugar de un arreglo.

Así mismo, los arreglos en estos lenguajes permiten algunas expresiones peculiares:

  • Dado que un arreglo es en el fondo un apuntador, pueden ser frecuentemente intercambiadas sus sintaxis.
  • Si a la posición de un arreglo le agregas o restas un número entero, el compilador va a interpretarlo como un desplazamiento de tamaño de tipo bytes en la memoria.
  • ¿Se pueden hacer apuntadores a apuntadores? ¡por supuesto!
  • ¿Son arreglos de apuntadores? ¡claro! y sus sintaxis siguen siendo de alguna manera intercambiables. Y es en el fondo lo que se hace cuando se hacen arreglos de tipos complejos como estructuras u objetos (C++).
  • Una cadena de caracteres es en realidad un arreglo de n+1 posiciones (tantas como sean necesarias) de tipo char y en el byte extra se suele guardar 0x00
  • Por lo anterior, una cadena de caracteres se puede navegar usando un ciclo "for" para ir avanzando de posición en posición (o sea, bytes, o letras).
  • También por lo anterior, y si no sé el largo específico de una cadena, puedo declararlo como un apuntador de tipo char y pelearme con el tamaño luego.

 Otros lenguajes de tipo estricto.

Otros lenguajes no son tan estrictos con el tamaño de los arreglos, teniendo instrucciones específicas para modificar sus dimensiones, pero siguen siendo estrictos acerca de que todos los elementos que los componen tengan el mismo tipo. Por ejemplo, en Visual Basic se conserva la instrucción "redim" que nos permite cambiar el tamaño de un arreglo, y preservar su información previa.

El manejo de los arreglos, entonces, se vuelve más bien una abstracción de una lista que una relación tan íntima con la memoria como lo podría ser en C y C++, y eso hace que su manejo sea mucho más "natural" en términos humanos.

Sin embargo, en todos los lenguajes se comparte esa jerga común del indicador de posición sobre el arreglo. Algunos con el operador "[n]", otros con "(n)", para indicarnos con un número entero en qué parte de la lista estamos leyendo o escribiendo.

Lenguajes de tipo dinámico.

En este caso el arreglo se confunde ya directamente con la estructura, por que no se exige que todos sus miembros sean del mismo tipo, pudiendo generar estructuras verdaderamente arbóreas de organización. El tipo de gestión que se hace en estos casos se podría representar a un nivel más bajo como un arreglo de apuntadores, sin embargo, la gestión de la memoria será siempre un reto y es por eso que suelen ser más lentos en su funcionamiento.

En un lenguaje de tipo dinámico la estructura carece de sentido ya que su manejo se confunde directamente con el arreglo, y no suelen existir expresiones sintácticas para definir y manipular estructuras, sino que todo se basa en arreglos.

Estructuras.

Fuera de los lenguajes de tipo estricto, las estructuras no suelen tener mucho sentido, como se mencionó anteriormente, sin embargo, en lenguajes más tradicionales suelen ser muy útiles a la hora de generar abstracciones amigables dentro de un contexto de programación y de agrupar diferentes tipos de variables dentro de un mismo campo semántico.

C/C++.

Podemos pensar en las estructuras como una agrupación similar a un registro de una base de datos, que contiene columnas de diferente tipo, pero que toda la información se refiere a una sola entidad. El ejemplo más clásico es pensar que un usuario tiene nombre (arreglo de caracteres), edad (entero), estatura (punto flotante), activo (binario), grupo (caracter), y esa estructura puede llamarse "alumno".

Si necesitamos tener referencias a varios alumnos, pues podemos hacer un arreglo de alumnos y cada posición va a hacer referencia a la posición de memoria donde se almacenan las referencias a los datos de ese alumno.

En general son poco socorridas las estructuras por lo programadores novatos, por que objetivamente hablando, no representan una necesidad funcional y eso hace que terminen haciendo soluciones peculiares como hacer un arreglo de nombres, otro de edades, otro de estaturas, etc. y usar la posición del arreglo para hacer la correlación de los datos de cada alumno. A nivel de funcionalidad ambos pueden ser sinónimos, pero resultaría mucho más simple darle mantenimiento a un código basado en estructuras.

Objetos y clases.

Sin querer entrar a definir todo el paradigma de la programación orientada a objetos en este punto, hay algunas notas que son pertinentes hacer respecto al uso de la memoria y su relación con los apuntadores.

En un sumarísimo resumen podemos definir como "clase" una plantilla de programa que soluciona un problema muy específico. Una clase como tal no suele ser muy útil a no ser que sea expresada en forma de objeto, y a través de interacciones entre objetos solucionará un problema más complejo o abstracto. Y aunque hay sus excepciones, por lo pronto vamos a priorizar el uso de clases a través de sus instancias (objetos).

Internamente, al momento de compilar o interpretar un programa basado en este paradigma, todo el código íntegro de la clase se cargará a la memoria (recordemos que en última instancia cualquier programa es solamente un conjunto de instrucciones expresadas en forma numérica) y cada que se invoque la creación de una nueva instancia se hará una copia de ese código (o casi, como se verá más adelante) a una nueva sección de memoria y se regresará un apuntador al punto donde inicia la memoria donde está alojada esa copia. Mientras más veces invoquemos "new" más veces se va a copiar ese código y más memoria va a consumir el programa. Y aunque hay recolectores de basura y otros mecanismos, hay que tenerlo presente.

El misterioso "this".

Pensemos que el código de ejecución de una clase mida (por decir) 1 KB. Si cada vez que generamos una instancia de esa clase copiamos la totalidad del código de ejecución que la compone, estaremos gastando siempre 1 KB de memoria, más el necesario para almacenar todas las variables públicas y privadas de esa clase. Podemos llegar fácilmente a la conclusión de que quizás no sea necesario gastar ese kilobyte de memoria por cada instancia, ya que su contenido es, al fin y al cabo, inmutable; entonces nos podemos ahorrar esa memoria si solamente generamos un espacio suficiente para almacenar las variables de la instancia y creamos un mecanismo que nos permita hacer referencia a esos espacios particulares.

Bueno, pues lo anterior es lo que se hace con el apuntador a "this", que es nada más que una forma muy simple de decirle a una función dentro de una clase que use el apuntador de instancia para alterar una variable o invocar un procedimiento. El mecanismo de generación de la instancia entonces guardará una variable especial donde esté esa posición de memoria, y los métodos usarán esa referencia como punto clave. ¿confuso? muchísimo: cuando se escribe el código de una clase es fácil perderse y olvidarnos que ese código será usado a través de instancias. Tenerlo en cuenta nos ayuda a sacarle el máximo provecho, y eso implica usar correctamente el contexto estático de las clases.

El contexto estático.

La intencionalidad de la existencia de las clases es que sus instancias puedan tener sus espacios de memoria aislados y se comporten de manera estable a través de las instancias. Sin embargo, a veces es útil poder trabajar fuera del contexto "personalizado" de la instancia, ya sea por que las operaciones a realizar no requieren usar información de instancia, o por que se requiera establecer valores comunes a todas las instancias de la clase. Para eso se inventó el contexto estático.

En pocas palabras, simplemente no podremos usar el apuntador a "this" dentro de una función estática, y eso implica que no vamos a poder acceder a variables de instancia (que son, básicamente, todas las que no fueron declaradas como estáticas).

Lenguajes de tipo estricto.

Manejar instancias de clases en lenguajes de tipo estricto nos permite comparar si dos apuntadores (o sea, variables) hacen referencia al mismo tipo de clase, usualmente a través de "typeof", y también es posible establecer comparadores entre clases. En esto el premio se lo lleva C++ por que nos permite definir (por sobrecarga) incluso los operadores que podemos usar con nuestras clases.

Un dato importante es que si una clase no define comparador explícito, lo más probable es que el intérprete detecte la operación como ilegal, o bien compare las direcciones de memoria de ambos apuntadores, y al ser obligadamente distinta, devuelva siempre falso.

Lenguajes de tipo dinámico.

Suelen ser mucho más permisivos con la comparación entre instancias de objetos, o bien entre instancias y valores. PHP, por ejemplo, suele evaluar como verdadero si el apuntador simplemente hace referencia a algo.

Existe también el operador "typeof", sin embargo, se pueden generar instancias de una super clase llamada simplemente "Object" que, en Javascript, fue muy socorrido antes de que se pudieran declarar explícitamente clases.

Conclusiones.

Sin importar mucho qué contenga el bloque al que se apunta, un apuntador sigue siendo un número entero positivo.

domingo, 13 de febrero de 2022

Notas sobre apuntadores, memoria y referencias.

En el principio era la nada.

Luego, en algún punto de la década de los cincuentas del siglo XX, una matemática soviética establece los fundamentos teóricos de la programación de sistemas informáticos en base a referencias de memoria. Aproximadamente una década después otro investigador sugiere una metodología similar en los Estados Unidos; entre ambos establecen el origen de uno de los elementos de programación que ha aterrado más a los programadores de finales del siglo XX, y sigue subyaciendo en las pesadillas del siglo XXI: los apuntadores, o punteros, o pointers en inglés.

Quizás la más famosa de las implementaciones de la aritmética de apuntadores se establece en el lenguaje de programación C. En ese lenguaje el manejo de los apuntadores es explícito en la sintaxis del mismo, es decir, que existen elementos lexicográficos claros para su manipulación, C++ mantiene ese manejo explícito, sin embargo, pocos lenguajes mantienen un manejo tan claro de los mismos, y eso ha sido terreno fértil para la confusión.

Bases teóricas.

Antes de ir más allá, vamos a establecer algunos términos indispensables para poder seguir adelante:


  • bit o "dígito binario", que es 0 o 1, verdadero o falso, el todo o la nada, o sea, la unidad mínima de información que se puede almacenar dentro de una computadora digital.
  • byte o "palabra lógica", que es un grupo de 8 bits, que no siempre ha sido así a lo largo de la historia, y que puede representar un número decimal entre 0 y 255 (o sea, 256 valores posibles). Tradicionalmente, un byte podía almacenar una letra legible al humano, como una "A", "b", "1", o "ñ". En nuestro contexto esta información es irrelevante.
  • Memoria, que es el lugar donde las computadoras almacenan información. La memoria está compuesta por (actualmente) miles de millones de casillas individuales, cada una de las cuales puede almacenar solamente un byte.
  •  Apuntador, puntero o pointer, que es un número entero que indica la posición en la memoria (la cinta enorme donde las computadoras digitales escriben toda su información) de una casilla en específico en la memoria. En el argot, se le suele representar como un valor hexadecimal: 0x00F8B310, que representaría a la 16'298,768-va casilla de memoria (algún punto después de los 16 megabytes). Un dato importante es que el puntero hace referencia al inicio de donde se guarda el valor.

Una vez definido lo anterior, vamos a retomar el concepto de la memoria de la computadora, que al final es el espacio de trabajo de donde el procesador va a leer el trabajo a realizar (programa), la información con la cual realizarlo (datos) y en donde va a almacenar el resultado obtenido. Cabe señalarse aquí que en términos teóricos es indistinto que la memoria se almacene de manera volátil, o permanente: en ambos casos la memoria sigue siendo una cinta enorme, dividida en casillas del mismo tamaño, que pueden ser referenciadas a través de un número entero.

Cuando almacenamos información en la memoria, por ejemplo, al asignarle el valor a una variable, la computadora intentará hacer lo siguiente:


  1. Evaluar el número de casillas necesarias para guardar la información, según el tipo de dato especificado
  2. Buscar una posición disponible en la cinta con las casillas contiguas necesarias para guardar la información.
  3. Escribir, uno por uno, los bytes de información en cada una de las casillas, leídos desde un espacio temporal de trabajo.
  4. Guardar la posición de la primera casilla en la cual guardó la información.

Si lo anterior tuvo éxito, podremos usar esa variable para hacer referencia al valor guardado, ahora bien, para leer el valor de una variable, los pasos serían los siguientes:

  1. Ubicar el punto de lectura en la posición de memoria a la que hace referencia esa variable.
  2. Evaluar el número de casillas según el tipo de dato.
  3. Abrir un espacio en el área de trabajo para poner el valor.
  4. Copiar los bytes contenidos en las casillas al área de trabajo.

Y por último, cuando modificamos los valores, simplemente vuelve a escribir en las casillas a las que hace referencia esa variable. Bueno, pues esa referencia es como tal un apuntador o puntero.

¿Qué pasa si no hay casillas suficientes en la memoria? pues vamos a tener un error de Out of memory o memoria insuficiente; ¿qué pasa, en cambio, si queremos escribir más bytes que casillas disponibles en la variable? lo más posible es que perdamos información, o que tengamos una señal de aborto de ejecución (SIGABRT en el estándar POSIX). Así mismo, si la variable hace referencia a una posición de memoria fuera  del ámbito de nuestro programa, vamos a tener una señal de segmentación (SIGSEGV en el estándar POSIX).

La pregunta clave entonces ¿Qué guardaste (el valor) y en dónde lo guardaste (el apuntador)?

Más allá de los básicos

Antes de seguir adelante, es necesario diferenciar la "naturaleza" de los tipos de datos que los lenguajes de programación nos dejan manipular. Cabe aquí una importante aclaración: internamente TODOS los tipos de datos son en realidad bytes (números entre 0 y 255 en decimal, 0x00 al 0xFF) organizados en casillas de memoria; sin embargo, los lenguajes de programación hacen una abstracción de ésto generando "tipos" de datos, como enteros, letras, cadenas de texto, o incluso estructuras, instancias de objetos y una larga lista de etcéteras.

Sin embargo, en este texto nos vamos a enfocar en dos naturalezas básicas: tipos primitivos y tipos complejos.

  • Tipos primitivos son los que típicamente se relacionan con datos que son representables directamente en bytes, como enteros, o letras: ambos se pueden guardar en la memoria en sus representaciones binarias.
  • Tipos complejos son, en cambio, aquellos tipos que requieren una interpretación posterior para volverse valores útiles, como por ejemplo, una cadena de caracteres (string), una estructura de datos (struct), o referencias a objetos.

A ambos tipos se pueden hacer apuntadores, sin embargo, los tipos primitivos pueden ser valores manipulados directamente, mientras que los tipos complejos van a ser manipulados preferentemente a través de un apuntador, y esto es especialmente relevante cuando hablamos de mandar valores a funciones.

Cuando invocamos una función con parámetros, y esos parámetros son variables de tipos primitivos, lo más probable es que la computadora haga una copia del valor a manejar, y esa copia sea con la que trabaje la función. Sin embargo, cuando son tipos complejos, puede ser que intente enviar una referencia de dónde está almacenada la información necesaria.

Lenguajes donde el manejo de apuntadores es explícito nos permiten decidir qué hacer al momento de invocar las funciones, pudiendo elegir entre si enviar copias de los valores, o referencias a los mismos, pero lenguajes modernos como Javascript lo van a decidir automáticamente. PHP, en cambio, está un poco en medio de ambos.

Por último, es  importante recordar que para una computadora, no solamente lo que nosotros entendemos por "valores" son valores, sino que también los programas están expresados en una serie de números que representan instrucciones al procesador. O sea, al final TODO son números, entre 0 y 255...

¿Javascript y apuntadores?

Debido a que es un lenguaje muy usado actualmente, y que además tiene una sintaxis muy dependiente de apuntadores, a la par que oscura al respecto, vamos a hablar a detalle aquí de cómo se implementan.

Lo primero a considerar es que Javascript es un lenguaje de tipo flexible o dinámico, es decir, que no es necesario decir qué tipo de valor vamos a almacenar en una variable o parámetro al momento de declararlo, haciendo que sea especialmente oscura su manipulación.

Lo siguiente es mencionar que, dada su orientación a objetos, es también fuertemente codependiente del uso de apuntadores, pero no tiene ningún tipo de manejo explícito al respecto, lo cual, una vez más, lo vuelve especialmente oscuro.

En términos generales, sin embargo, podemos pensar que todo aquello que es reducible a un tipo primitivo (básicamente los enteros y los flotantes, y forzadamente las cadenas de caracteres) se manejan como copias de valores, mientras que todo aquello que resulta ser una instancia de algo (arreglos y objetos) son manejados como referencias.


Eso nos importa por que quiere decir que podemos tener una variable conteniendo un entero, enviarla a una función, cambiar el valor dentro de esa función, y al retornar al contexto original su valor no debería de haber cambiado. Sin embargo, si tenemos un objeto que es enviado a una función, cambiamos alguna de las propiedades de ese objeto en la función, al regresar al contexto original, ese valor cambió. Saber ésto es fundamental.

Otra nota relevante es que podemos declarar un arreglo como constante, y modificar los valores del arreglo sin problemas, ya que lo que se vuelve constante es la ubicación en memoria a donde está almacenado el inicio del arreglo, pero no los valores contenidos. Lo mismo sucede con las instancias de los objetos: una variable que haga referencia a una instancia de objeto, puede ser considerada como constante, pues mientras no se cambie la referencia en memoria de la instancia, no va a ocasionar ningún problema, sin embargo, si sobre esa constante intentamos crear una nueva instancia (usando "new") entonces dará problemas.

Usar constantes es importante en lenguajes interpretados por que permiten que se haga una optimización del uso de la memoria, haciendo nuestros programas más ligeros y eficientes.

Conclusiones.

Para más referencias, recomiendo la lectura de "El lenguaje de programación C" de Kernigan y Ritchie, y para adentrarse un poco de manera amigable al funcionamiento de las computadoras de cálculo general "La nueva mente del emperador" de Roger Penrose

Por mucho que nos digan que los lenguajes de programación avanzan, en el fondo todos mantienen principios teóricos similares y es importante poder relacionarlos con nuestro trabajo del día a día. La correcta comprensión de la memoria es vital a la hora de planificar la ejecución de una tarea y el cómo vamos a manejar la información durante ese desarrollo.

Si bien es cierto, la tendencia se inclina por oscurecer y alejar a los programadores nóveles de estos conceptos, por considerarlos "demasiado complejos" para ellos, no quiere decir que sea positivo para el desarrollo de sus carreras que se mantengan ignorantes al respecto, sino que más bien los tornan idiots savants de un oficio altamente complejo.

sábado, 5 de febrero de 2022

Introducción al flujo de ejecución y estructura de los programas.

 En el principio, era la nada.

Mucho tiempo después, cuando se inventó el proceso de programar máquinas de cálculo general se estableció la primera estructura de los programas. Esa primera aproximación se le conoce coloquialmente (y seguramente tiene un nombre más formal) como "caída de piedra". Tal cual, las instrucciones del programa se van ejecutando desde el inicio hasta el final, una a la vez, hasta llegar al final del programa. Este mecanismo de ejecución subyace hasta nuestros días y el olvidarnos de eso suele causar algunos de los errores más frecuentes de los programadores novatos. Después, con la creciente complejidad de los programas, surgió un nuevo paradigma de organización que se conoce como "programación estructurada", que nos permite fragmentar nuestros programas en secciones más pequeñas, idealmente aisladas en unidades funcionales ("funciones" les decimos) que determinan fragmentos de código ejecutable; en el fondo, la programación estructurada nos permite "aventar" la piedra a caer en diferentes secciones del programa, y nos proporciona mecanismos para obtener resultados de esos "saltos" en la ejecución. Después, cuando las unidades funcionales se volvieron cada vez más complejas, surgió el paradigma de "orientación a objetos", donde la programación pasa a centrarse en la definición de "clases" que no son otra cosa mas que machotes de ejecución, con sus valores, unidades funcionales, y sus reacciones aisladas y bien definidas, que una vez más, vuelven a la caída de piedra; con la orientación a objetos surge también el concepto de "Excepción", que se puede traducir algo libremente como "error". A continuación vamos a analizar a detalle cada uno de los tres paradigmas.

"Caída de piedra"


Es quizás la aproximación más simple que puede hacer a la ejecución de un programa, y equivale a tener una lista de instrucciones enumerada. Es tan fácil como pensar en:

  1. Inicia la ejecución.
  2. Declara una variable.
  3. Asigna el valor "hola, mundo" a esa variable.
  4. Vuelca el valor de esa variable a la salida estándar.
  5. Termina la ejecución.

Las cinco instrucciones serán ejecutadas desde la 1 a la 5, manteniendo un ámbito acumulativo, es decir, que no se puede usar una variable antes de ser declarada. Dentro de este paradigma se consideran acciones de modificación del flujo, como por ejemplo condicionales ("if" o "switch"), o ciclos ("for", "while"), sin embargo, en algún momento después de estas estructuras (y dentro de ellas) se volverá a la ejecución secuencial hasta el final del programa (aunque todos sabemos que se podría encerrar esa ejecución en un ciclo infinito, que esto volverá a mencionarse).

Para programas simples, y en el fondo, todo los lenguajes de programación mantienen esta estructura, pues todas las funciones siguen ejecutándose de manera secuencial en última instancia.

Programación estructurada.


En algún punto los problemas a resolver se fueron complicando cada vez más, y los programas se fueron haciendo cada vez más largos. Unas cien líneas de código pueden ser fácilmente leídas y entendidas por un programador promedio, pero arriba de las 300 líneas se pueden volver prácticamente imposibles de analizar. La solución fue dividir el programa en secciones de funcionalidad específica, llamadas "funciones" (y a veces subrutinas), y esas funciones, entonces, son invocadas una a una; la lectura se simplifica al volverse más expresiva al humano, a la par que surge el concepto de reutilización de código, es decir, que una función puede ser invocada en reiteradas ocasiones.

Aquí entra en juego dos tipos de funciones: las que requieren regresar un valor al punto donde fueron invocadas, y aquellas que no lo hacen. Algunos lenguajes, sobre todo más viejos, hacen una diferencia explícita entre ambas: si no regresan un valor, se les llama "subrutina" o "procedimiento", mientras que las que regresan valores, se les llama "funciones" simplemente.

Un punto adicional a mencionar, es que cuando una función regresa un valor, en ese momento se interrumpe la ejecución de la misma, por lo que el resto de las líneas de código después de la instrucción "return" serán ignoradas.

El estándar POSIX considera, así mismo, que un programa "regresa" un valor al terminar su ejecución, por lo que es importante dejar un "return 0;" al final de las funciones "main()" para indicar al sistema operativo que la ejecución del proceso fue exitosa (también, por eso, en C y C++ la función "main()" es declarada como "int"). Otros valores serán leídos como códigos de error por el sistema operativo.

Programación orientada a objetos.

El tema es bastante amplio y en este texto nos concentraremos en las particularidades del flujo de ejecución.

Cuando se habla de un programa orientado a objetos debemos pensar en una extensión de la programación estructurada, permitiendo una mayor reutilización de código, niveles más altos de abstracción y en algún punto, hacer más realista el modelado de los problemas.

Se basa en "clases", una clase será la definición de una estructura de código que busca agrupar variables, procedimientos y referencias necesarias para realizar una tarea específica. Generalmente, esas clases serán utilizadas a través de sus instancias, que son conocidos como "objetos". Un punto a tener en cuenta es que la gran mayoría de los lenguajes de programación no permiten modificar la definición misma de un objeto, sino que el programador trabaja a partir de la "clase". Javascript es una salvedad, que permite la modificación dinámica de los objetos, que además, no persistirá en subsecuentes ejecuciones del mismo código.

Cuando hablamos de clases, surge un nuevo concepto que es el de "ciclo de vida" de un objeto: como tal entenderemos la secuencia de eventos de creación (generación de la instancia), preparación, vida útil y destrucción (finalización de la instancia). Las clases que gestionan la funcionalidad de formas o ventanas (Java, C++ con Qt, etc.) suelen especificar en su documentación eventos adicionales en su ciclo de vida, que pueden incluir la minimización, el cerrado, el redimensionado, etc.

Un concepto surge con este paradigma de trabajo es el de "excepción", que puede ser un error ocurrido durante la ejecución de un código. En el caso de C++, sin embargo, los errores suelen ser generados como señales de sistema operativo, y solamente cuando una clase explícitamente lo genera, excepciones. Otros lenguajes suelen, en cambio, suelen disparar excepciones para cualquier error. Una excepción suele ser una instancia de un objeto que hereda directamente la clase abstracta "Exception" (Java, sobre todo).


Existe, además, un mecanismo específico para evitar que una excepción interrumpa la ejecución global del código, y ese mecanismo son los bloques "try" y "catch"; esa estructura está especialmente diseñada para contener secciones de código que sospechamos que pueden generar un error. Si el error llegase a ocurrir, se interrumpe la ejecución del código en el punto donde se dispara, y el control de ejecución pasa al bloque "catch", que pueden configurarse para recibir diferentes tipos de excepciones; una vez que se concluye el bloque "catch" se continua ejecutando el código hasta el final de la función o procedimiento.


Se considera que el mecanismo de manejo de excepciones como demasiado costoso, prefiriéndose siempre evitar la generación de las mismas. Si una excepción no cae dentro de un bloque "try" dentro de la actual función, "burbujeará" a la siguiente función en la pila de llamadas, y si no existe ningún bloque "try" en la pila de llamadas, el programa entero interrumpirá su ejecución reportando un error al sistema operativo.

Extra: La pila de llamadas.

Tanto en programación estructurada, como en orientación a objetos, existe un control de la secuencia en que se han invocado las funciones. En inglés se le conoce como el "stack". La pila de llamadas suele mostrar funciones fuera de nuestro control, relacionadas más bien con la infraestructura del lenguaje que estamos trabajando. No es para espantarse, pero es importante poder detectar qué partes de esa pila podemos controlar, resultando bastante útil a la hora de depurar un código.

Conclusiones.

Aún el lenguaje de programación más moderno sigue manejando internamente el viejo "caída de piedra", pero suelen dar la ilusión de otras maneras. Generalmente, un proceso de Java, por ejemplo, ejecutará un programa muy simple que generará una instancia de la clase principal de la aplicación y pasará el control de ejecución a un ciclo infinito donde se ejecutará el hilo de eventos de esa clase... pero en el fondo, seguirá siendo una secuencia lineal.