2: Construir y usar objetos

Tabla de contenidos

2.1. El proceso de traducción del lenguaje
2.2. Herramientas para compilación modular
2.3. Su primer programa en C++
2.4. Más sobre iostreams
2.5. Introdución a las cadenas
2.6. Lectura y escritura de ficheros
2.7. Introducción a los vectores
2.8. Resumen
2.9. Ejercicios

Este capítulo presenta la suficiente sintaxis y los conceptos de construcción de programas de C++ como para permitirle crear y ejecutar algunos programas simples orientados a objetos. El siguiente capítulo cubre la sintaxis básica de C y C++ en detalle.

Leyendo primero este capítulo, le cogerá el gustillo a lo que supone programar con objetos en C++, y también descubrirá algunas de las razones por las que hay tanto entusiasmo alrededor de este lenguaje. Debería ser suficiente para pasar al Capítulo 3, que puede ser un poco agotador debido a que contiene la mayoría de los detalles del lenguaje C.

Los tipos de datos definidos por el usuario, o clases es lo que diferencia a C++ de los lenguajes procedimentales tradicionales. Una clase es un nuevo tipo de datos que usted o alguna otra persona crea para resolver un problema particular. Una vez que se ha creado una clase, cualquiera puede utilizarla sin conocer los detalles de su funcionamiento, o incluso de la forma en que se han construído. Este capítulo trata las clases como si sólo fueran otro tipo de datos predefinido disponible para su uso en programas.

Las clases creadas por terceras personas se suelen empaquetar en librerías. Este capítulo usa algunas de las librerías que vienen en todas las implementaciones de C++. Una librería especialmente importante es FIXME:iostreams, que le permite (entre otras cosas) leer desde ficheros o teclado, y escribir a ficheros o pantalla. También verá la clase string, que es muy práctica, y el contenedor vector de la Libreria Estándar de C++. Al final del capítulo, verá lo sencillo que resulta utilizar una librería de clases predefinida.

Para que pueda crear su primer programa debe conocer primero las herramientas utilizadas para construir aplicaciones.

2.1. El proceso de traducción del lenguaje

Todos los lenguajes de programación se traducen de algo que suele ser fácilmente entendible por una persona (código fuente) a algo que es ejecutado por una computadora (código máquina). Los traductores se dividen tradicionalmente en dos categorías: intérpretes y compiladores.

2.1.1. Intérpretes

Un intérprete traduce el código fuente en actividades (las cuales pueden comprender grupos de instrucciones máquina) y ejecuta inmediatamente estas actividades. El BASIC, por ejemplo, fue un lenguaje interpretado bastante popular. Los intérpretes de BASIC tradicionales traducen y ejecutan una línea cada vez, y después olvidan la línea traducida. Esto los hace lentos debido a que deben volver a traducir cualquier código que se repita. BASIC también ha sido compilado para ganar en velocidad. La mayoría de los intérpretes modernos, como los de Python, traducen el programa entero en un lenguaje intermedio que es ejecutable por un intérprete mucho más rápido [33].

Los intérpretes tienen muchas ventajas. La transición del código escrito al código ejecutable es casi inmediata, y el código fuente está siempre disponible, por lo que el intérprete puede ser mucho más específico cuando ocurre un error. Los beneficios que se suelen mencionar de los intérpretes es la facilidad de interacción y el rápido desarrollo (pero no necesariamente ejecución) de los programas.

Los lenguajes interpretados a menudo tienen severas limitaciones cuando se construyen grandes proyectos (Python parece ser una excepción). El intérprete (o una versión reducida) debe estar siempre en memoria para ejecutar el código e incluso el intérprete más rápido puede introducir restricciones de velocidad inaceptables. La mayoría de los intérpretes requieren que todo el código fuente se les envíe de una sola vez. Esto no sólo introduce limitaciones de espacio, sino que puede causar errores difíciles de detectar si el lenguaje no incluye facilidades para localizar el efecto de las diferentes porciones de código.

2.1.2. Compiladores

Un compilador traduce el código fuente directamente a lenguaje ensamblador o instrucciones máquina. El producto final suele ser uno o varios ficheros que contienen código máquina. La forma de realizarlo suele ser un proceso que consta de varios pasos. La transición del código escrito al código ejecutable es significativamente más larga con un compilador.

Dependiendo de la perspicacia del escritor del compilador, los programas generados por un compilador tienden a requerir mucho menos espacio para ser ejecutados, y se ejecutan mucho más rápido. Aunque el tamaño y la velocidad son probablemente las razones más citadas para usar un compilador, en muchas situaciones no son las más importantes. Algunos lenguajes (como el C) están diseñados para admitir trozos de programas compilados independientemente. Estas partes terminan combinando en un programa ejecutable final mediante una herramienta llamada enlazador (linker). Este proceso se conoce como compilación separada.

La compilación separada tiene muchos beneficios. Un programa que, tomado de una vez, excedería los límites del compilador o del entorno de compilación puede ser compilado por piezas. Los programas se pueden ser construir y probar pieza a pieza. Una vez que una parte funciona, se puede guardar y tratarse como un bloque. Los conjuntos de piezas ya funcionales y probadas se pueden combinar en librerías para que otros programadores puedan usarlos. Como se crean piezas, la complejidad de las otras piezas se mantiene oculta. Todas estas características ayudan a la creación de programas grandes, [34].

Las características de depuración del compilador han mejorado considerablemente con el tiempo. Los primeros compiladores simplemente generaban código máquina, y el programador insertaba sentencias de impresión para ver qué estaba ocurriendo, lo que no siempre era efectivo. Los compiladores modernos pueden insertar información sobre el código fuente en el programa ejecutable. Esta información se usa por poderosos depuradores a nivel de código que muestran exactamente lo que pasa en un programa rastreando su progreso mediante su código fuente.

Algunos compiladores solucionan el problema de la velocidad de compilación mediante compilación en memoria. La mayoría de los compiladores trabajan con ficheros, leyéndolos y escribiéndolos en cada paso de los procesos de compilación. En la compilación en memoria el compilador se mantiene en RAM. Para programas pequeños, puede parecerse a un intérprete.

2.1.3. El proceso de compilación

Para programar en C y en C++, es necesario entender los pasos y las herramientas del proceso de compilación. Algunos lenguajes (C y C++, en particular) empiezan la compilación ejecutando un preprocesador sobre el código fuente. El preprocesador es un programa simple que sustituye patrones que se encuentran en el código fuente con otros que ha definido el programador (usando las directivas de preprocesado). Las directivas de preprocesado se utilizan para ahorrar escritura y para aumentar la legilibilidad del código (posteriormente en este libro, aprenderá cómo el diseño de C++ desaconseja en gran medida el uso del preprocesador, ya que puede causar errores sutiles). El código preprocesado se suele escribir en un fichero intermedio.

Normalmente, los compiladores hacen su trabajo en dos pasadas. La primera pasada consiste en analizar sintácticamente el código generado por el preprocesador. El compilador trocea el código fuente en pequeñas partes y lo organiza en una estructura llamada árbol. En la expresión FIXME:«A+B», los elementos «A», «+», «B» son hojas del árbol.

A menudo se utiliza un optimizador global entre el primer y el segundo paso para producir código más pequeño y rápido.

En la segunda pasada, el generador de código recorre el árbol sintáctico y genera lenguaje ensamblador o código máquina para los nodos del árbol. Si el generador de código crea lenguaje ensamblador, entonces se debe ejecutar el programa ensamblador. El resultado final en ambos casos es un módulo objeto (un fichero que típicamente tiene una extensión de .o o .obj. A veces se utiliza un optimizador de mirilla en esta segunda pasada para buscar trozos de código que contengan sentencias redundantes de lenguaje ensamblador.

Usar la palabra «objeto» para describir pedazos de código máquina es un hecho desafortunado. La palabra comenzó a usarse antes de que la programación orientada a objetos tuviera un uso generalizado. «Objeto» significa lo mismo que «FIXME:meta» en este contexto, mientras que en la programación orientada a objetos significa «una cosa con límites».

El enlazador combina una lista de módulos objeto en un programa ejecutable que el sistema operativo puede cargar y ejecutar. Cuando una función en un módulo objeto hace una referencia a una función o variable en otro módulo objeto, el enlazador resuelve estas referencias; se asegura de que todas las funciones y los datos externos solicitados durante el proceso de compilación existen realmente. Además, el enlazador añade un módulo objeto especial para realizar las actividades de inicialización.

El enlazador puede buscar en unos archivos especiales llamados librerías para resolver todas sus referencias. Una librería contiene una colección de módulos objeto en un único fichero. Una librería se crea y mantiene por un programa conocido como bibliotecario (librarian).

Comprobación estática de tipos

El compilador realiza una comprobación de tipos durante la primera pasada. La comprobación de tipos asegura el correcto uso de los argumentos en las funciones y previene muchos tipos de errores de programación. Como esta comprobación de tipos ocurre se hace la compilación y no cuando el programa se está ejecutado, se conoce como comprobación estática de tipos.

Algunos lenguajes orientados a objetos (Java por ejemplo) realizan comprobaciones en tiempo de ejecución (comprobación dinámica de tipos). Si se combina con la estática, la comprobación dinámica es más potente que sólo la estática. Sin embargo, añade una sobrecarga a la ejecución del programa.

C++ usa la comprobación estática de tipos debido a que el lenguaje no puede asumir ningún soporte particular durante la ejecución. La comprobación estática de tipos notifica al programador malos usos de los tipos durante la compilación, y así maximiza la velocidad de ejecución. A medida que aprenda C++, comprobará que la mayoría de las decisiones de diseño del lenguaje están tomadas en favor de la mejora del rendimiento, motivo por el cual C es famoso en la programación orientada a la producción.

Se puede deshabilitar la comprobación estática de tipos en C++, e incluso permite al programador usar su propia comprobación dinámica de tipos - simplemente necesita escribir el código.



[33] Los límites entre los compiladores y los intérpretes tienden a ser difusos, especialmente con Python, que tiene muchas de las caractéristicas y el poder de un lenguaje compilado pero también tiene parte de las ventajas de los lenguajes interpretados.

[34] Python vuelve a ser una excepción, debido a que permite compilación separada.