lordpanther
Usuario (Colombia)
C++ (Decimocuarta parte) 1.4.4b2 Librerías dinámicas §1 Presentación Antes de conocer los pormenores de la construcción y uso de una librería dinámica (DLL) en un programa C++, es conveniente tener una perspectiva general del mecanismo que rige su funcionamiento. Como se indicó en la introducción ( 1.4.4b), una librería es simplemente un trozo de código que contiene recursos preconstridos; recursos que pueden ser utilizados en un ejecutable. Sin embargo, si nos fijamos un poco, vemos que esta definición es un tanto ambigua, y puede encajar en objetos que no son realmente librerías. En realidad, la utilización de recursos preconstruidos por parte de un ejecutable puede realizarse de tres formas que podríamos resumir del siguiente modo: • Utilización de librerías estáticas. Es el método tradicional. Como hemos señalado, son las clásicas colecciones de ficheros objeto .obj (compilados), que en el momento de la construcción de la aplicación, son incluidos por el "Linker" en el propio ejecutable ( 1.4.4b1). • Utilización de librerías dinámicas. En esta modalidad, los recursos ocupan un fichero independiente del ejecutable, que puede ser utilizado por cualquier aplicación que lo necesite. En algún momento, durante la carga del ejecutable, o posteriormente, en run-time, el ejecutable deberá integrar este bloque de código en su propio espacio, de forma que pueda acceder a los recursos contenidos en él. • Utilización de programas externos. Es también un recurso utilizado desde siempre en informática. Un ejecutable puede llamar a ejecución a otro mediante mecanismos de varios tipos. El ejecutable llamado proporciona alguna funcionalidad antes de su terminación, y dispone de su propio espacio de ejecución independiente del programa que lo invocó. Nota: respecto a la utilización de programas externos "versus" librerías dinámicas, hay que tener en cuenta que las plataformas Windows disponen de un espacio de memoria protegida para cada proceso o programa que es iniciado por el Sistema, lo que es oneroso en término de recursos, y origina una sobrecarga similar a la que suponen los procesos involucrados en la invocación de funciones ( 4.4.6b). Por contra, las librerías dinámicas no necesitan su propio espacio, y corren en el espacio del proceso que las invoca, lo que es mucho más rápido y ligero en término de recursos. Las DLLs son trozos de código capaz de realizar determinadas operaciones, cuya "funcionalidad" puede ser utilizada desde otros ejecutables, y como puede verse, ocupan una posición intermedia (diríamos que una solución de compromiso) entre las posiciones extremas antes citadas. Con las librerías estáticas comparten la característica de que es un trozo de código que acaba siendo incluido en el espacio del ejecutable que las utiliza [1]. A su vez, comparten con los programas externos la característica de que constituyen ficheros distintos y físicamente independientes del ejecutable que los utiliza. Antes de seguir adelante, debemos puntualizar un extremo que es importante para comprender el funcionamiento de las DLLs. En realidad, la DLL no es cargada en el espacio de memoria del ejecutable, sino que tiene su propio espacio. Lo que ocurre es que este espacio es accesible desde el ejecutable, y está "mapeado" en él. Es decir, en el ejecutable existe un cierto "mapa" de cómo está distribuida esa zona de memoria; dónde están sus objetos utilizables desde el exterior (exportables 1.4.4b2a). Como veremos a continuación, existen dos formas de incluir esta información en el ejecutable. Además, el hecho de que la DLL disponga de su propio espacio (code segment), tiene una importante ventaja adicional: si dos o más procesos que se están ejecutando simultáneamente en el Sistema necesitan de la misma DLL, no necesita ser cargada dos veces en memoria. Basta que ambos tengan acceso a ella y cierto conocimiento de su estructura interna. Por esta razón, en los entornos Unix/Linux son conocidas como librerías compartidas [5]. Nota: esta utilización del mismo código por varias aplicaciones es posible porque, como se ha indicado [1], los objetos creados por la DLL no pertenecen a esta, sino al programa usuario. En otras palabras: se comparte el código pero no los datos. En realidad lo que caracteriza a una DLL es la forma en que es traída a ejecución; no directamente desde el shell del Sistema como un ejecutable .exe normal, sino desde otro ejecutable (que puede ser incluso otra DLL), de forma parecida a como se invoca una función (una especie de función externa al programa). Por esta razón no disponen de una función main ( 4.4.4) o de un módulo de inicio en el sentido clásico (ver ejemplo 1.4.4b2a). Al llegar aquí, es pertinente una observación para los que han programado para entornos como MS-DOS y se adentran en el territorio de la programación C++ con librerías dinámicas. Por ejemplo aplicaciones para MS Windows. Algunos enlazadores para DOS [3] permiten que determinadas porciones del ejecutable se sitúen en ficheros independientes (generalmente con terminación .OVL), los denominados "overlays". Estos overlays son traídos a memoria (cargados) automáticamente según convenga, de forma que salvo contadísimas excepciones, su funcionamiento es totalmente transparente para el programador. Por su parte, las librerías dinámicas permiten también que partes del ejecutable se alojen en ficheros independientes. Sin embargo, su comportamiento es mucho (muchísimo) menos flexible que su contrapartida DOS. La razón es que durante la fase de enlazado de la librería dinámica, la tabla de símbolos del que será su ejecutable anfitrión no es accesible (ni siquiera conocido), de forma que la única información que puede ser intercambiada con el exterior por el código de la librería, es la que se recibe a través de los parámetros de sus funciones y los valores devueltos por estas [7]. El resultado es que, a todos los efectos, su funcionamiento se parece más a la invocación de un programa externo, que una vez ejecutado, devuelve el control al programa inicial. El resultado es que si desde una DLL necesitamos utilizar una funcionalidad existente en el cuerpo del programa (una función o clase), no podemos accederla a menos que dicha función sea también incluida en una DLL independiente. Naturalmente esto exige que la separación de partes del programa en DLLs se realice después de un estudio minucioso de las funcionalidades que serán utilizadas desde cada módulo. Como nota complementaria, debemos añadir que en este, como en muchos otros aspectos, los entornos Unix son mucho más flexibles, ya que permiten la existencia de librerías compartidas de enlazado dinámico bidireccional automático. §2 Utilización De lo dicho anteriormente se desprende que la utilización de los recursos contenidos en una DLL requiere dos condiciones: a.- Cargar en memoria el contenido de la DLL utilizando un espacio accesible desde el ejecutable que la utiliza. Esto puede efectuarse de dos formas: a1.- En el mismo momento de la carga inicial del programa. a2.- En el momento en que se necesite alguno de sus recursos (en runtime). b.- Conocer la topografía interna de ese trozo de código para poder acceder a sus objetos. §2a1 En el primer caso (a1), las DLLs requeridas por el ejecutable .EXE son cargadas, y sus objetos inicializados por el módulo de inicio como cualquier otro módulo del programa. Es decir, que serán inicializadas antes que comience la ejecución de main. Cuando la aplicación es cargada por el SO, este mira en el fichero .EXE para ver que DLLs se necesitan, y se encarga de cargarlas también. Veremos que este tipo de utilización, denominada enlazado estático o implícito (librería dinámica enlazada estáticamente), es con diferencia la más usual ( 1.4.4b2b). §2a2 En el caso a2, la librería es cargada en runtime cuando la aplicación lo necesita. Esta forma de uso se denomina enlazado dinámico o explícito (librería dinámica enlazada dinámicamente). Para realizar la carga, el programador dispone de algunas funciones de la API de Windows que se encargan de realizar la tarea cuando él lo decide (de ahí que se denomine enlazado explícito). Cualquiera que sea la forma de carga elegida, implícita o explícita, el orden de búsqueda seguido por el Sistema para localizar el fichero (.dll) a cargar, es siempre el mismo: • En el directorio que contiene el ejecutable (fichero .EXE) Fig. 1 Fig. 2 • El directorio actual de la aplicación [4]. • El directorio de sistema de Windows • El directorio de Windows • Los directorios incluidos en la variable de entorno PATH del Sistema. Si en una carga implícita el Sistema no encuentra el fichero .DLL en ninguno de los sitios anteriores, se muestra un mensaje de error y la aplicación no puede ejecutarse (fig. 1). En caso de que fracase la carga del fichero durante la carga explícita, si el Sistema devuelve un error, es potestad del programador decidir que hacer. La figura 2 es un ejemplo tomado de una aplicación real cuando no aparece la .DLL requerida [6]. El orden de carga mencionado es el que podríamos llamar "clásico"; de las versiones de Windows anteriores a XP SP1. A partir de esta, el orden se modificó, de forma que la búsqueda no comienza en el directorio que contiene el ejecutable, sino en los directorios de Windows. La razón está relacionada con la seguridad http://msdn.microsoft.com/. Incluimos el párrafo más significativo: "DLL Search Order Has Changed No longer is the current directory searched first when loading DLLs! This change was also made in Windows XP SP1. The default behavior now is to look in all the system locations first, then the current directory, and finally any user-defined paths. This will have an impact on your code if you install a DLL in the application's directory because Windows Server 2003 no longer loads the 'local' DLL if a DLL of the same name is in the system directory. A common example is if an application won't run with a specific version of a DLL, an older version is installed that does work in the application directory. This scenario will fail in Windows Server 2003. The reason this change was made was to mitigate some kinds of trojaning attacks. An attacker may be able to sneak a bad DLL into your application directory or a directory that has files associated with your application. The DLL search order change removes this attack vector." §2b Es evidente, que una vez cargado el código de la DLL, el programa anfitrión necesita conocer las direcciones de los recursos contenidos en esa zona de memoria para poder acceder a ellos. El procedimiento es distinto según el método de carga utilizado: En el caso de librería dinámica enlazada estáticamente, se construye una librería tradicional (.LIB) de un tipo especial denominado librería de importación, que es enlazada estáticamente con el ejecutable (formando parte de él). La librería de importación no contiene código, en realidad es un índice o tabla de dos columnas. En la primera están los nombres de las funciones exportables de las DLLs; la segunda está vacía, pero cuando las librerías son cargadas en memoria durante el proceso de carga del ejecutable, el programa cargador ya puede conocer las direcciones de estos recursos, y completa la segunda columna de la tabla con las direcciones adecuadas. De esta forma, el ejecutable puede acceder a los recursos de la DLL con solo conocer los nombres adecuados. En el caso de librería dinámica enlazada dinámicamente, una vez realizada la carga mediante las funciones correspondientes, la API del Sistema dispone de una función específica GetProcAddress(), que permite obtener punteros a las funciones de la DLL que deban utilizarse. Es necesario mencionar que para obtener las direcciones de los recursos dentro del bloque de código de la DLL, tanto el programa cargador como la función GetProcAddress de la API, utilizan a su vez una tabla que acompaña a cualquier librería dinámica, la tabla de entrada ("entry table" 1.4.4b2a). §3 Tabla de inicio Cuando se compila cualquier módulo, en el objeto resultante existe un segmento denominado _INIT_ que contiene una referencia ("Init entry" al constructor de cada objeto global que deba ser inicializado, así como un orden de prioridad. Más tarde, cuando el enlazador construye un ejecutable, estas entradas se agrupan en una tabla, la tabla de inicio ("Init table" que contiene ordenadamente todas las entradas de los constructores de los objetos que existen en los módulos que componen el programa. Finalmente, esta tabla es utilizada por el módulo de inicio cuando el programa es iniciado. En el caso de que este ejecutable sea una librería de enlazado dinámico, el orden de estas entradas en la tabla depende de dos factores: su prioridad, y el orden en que se encuentren los módulos-objeto en la orden de enlazado. Por ejemplo, si la orden de enlazado incluye tres objetos A.o, B.o y C.o, cada uno de los cuales tiene tres objetos globales cuyos constructores tienen entradas en el segmento _INIT_ de su módulo con la misma prioridad (supongamos que 0x20), al construir la librería, puesto que sus prioridades son idénticas, el orden en que serán inicializados dichos objetos dependerá del orden de los módulos A, B y C en la orden de enlazado. Cambiando este orden puede alterarse el orden en que se invocan los constructores de los objetos correspondientes. Las librerías dinámicas enlazadas estáticamente a un ejecutable, son inicializadas junto con el resto de módulos del programa, por el módulo de inicio ( 1.5) del ejecutable antes que comience la ejecución de main (o WinMain). Esto supone que cualquier directiva #pragma startup ( 4.9.10i) existente en los módulos .OBJ que componen la librería será ejecutada, y que todas sus variables estáticas, serán inicializadas según el orden precedente. En el caso de que la DLL tenga enlazado dinámico, la inicialización solo se realiza cuando es cargado el módulo (.DLL). §4 Recordar que para usar las funciones contenidas en una librería (estática o dinámica) se necesitan tres condiciones: • Un prototipo que permita conocer el nombre del fichero que compone la librería, su localización, parámetros y tipo de retorno de la función de librería que queramos utilizar (esto es lo normal para utilizar cualquier función 4.4.1). En el caso de librerías que contienen clases predefinidas, esta condición se sustituye por el conocimiento de la interfaz de la clase. • Disponer de los tipos de datos que pasarán como argumentos (también normal para cualquier función). • En el caso de funciones, poder utilizar la convención de llamada que corresponda a la librería en cuestión. Es decir, que el enlazador C++ utilizado permita usar la convención de llamada adecuada, de forma que estos módulos externos puedan ser llamados a ejecución desde nuestro programa [2]. Inicio. ________________________________________ [1] Esta característica es importante y debe ser tenida en cuenta. Una DLL es un trozo de código inerte que no tiene vida por sí mismo hasta que es incluido en el espacio de la aplicación que la utiliza. No tiene un proceso propio, y cualquier objeto creado por su código pertenece a la aplicación anfitriona. Si los programas tienen vida, seguramente un biólogo compararía a las DLLs con los virus. Materia inerte que solo puede vivir en el interior de una célula anfitriona. [2] Recuerde que los compiladores más usados; Borland C++; MS Visual C++; GNU g++, etc., permiten especificar las convenciones de llamada más usuales en este tipo de librerías ( 4.4.6a). [3] Por ejemplo el magnífico enlazador Blinker. http://blinkinc.com. [4] Generalmente una aplicación "reside" en el directorio donde se encuentra el ejecutable, pero puede cambiar su directorio activo durante la ejecución. [5] Un perfecto ejemplo es el caso de las DLLs que contienen la API de Windows, que son cargadas una sola vez en memoria y utilizadas por casi cualquier aplicación que se corra en el Sistema. [6] Mi recomendación es no utilizar un mensaje tan críptico como el de la figua-2, que sirve de escasa ayuda al usuario. Es preferible mostrar un mensaje como el de la figura-1. De esta forma el usuario tiene una mejor información sobre el problema, incluso la posibilidad de solventarlo (quizás la DLL ha sido borrada accidentalmente; no ha sido instalada; se encuentra en una localización distinta, etc.) [7] Si se trata de aplicaciones "modernas", por ejemplo aplicaciones Windows, entre estos argumentos recibidos y valores devueltos por la librería, se encuentran los mensajes que sus "callbacks" (funciones con el especificador __stdcall 4.4.6a) pueden intercambiar con el sistema.

C++ (Decimonovena parte) 1.6.1a Excepciones en la Librería Estándar §1 Sinopsis Ciertos elementos del lenguaje y algunas utilidades de la Librería Estándar ( 5) pueden lanzan excepciones para señalar condiciones de error. Los objetos lanzados pertenecen a algún miembro de una jerarquía de clases que tiene el siguiente diseño: exception logic_error domain_error invalid_argument length_error out_of_range runtime_error range_error overflow_error underflow_error bad_alloc bad_cast bad_exception bad_typeid Como puede verse, todas ellas derivan de la superclase exception, definida en la cabecera <stdexcept>, que tiene la siguiente interfaz: class exception { public: exception () throw(); exception (const exception& throw(); exception& operator= (const exception& throw(); virtual ~exception () throw(); virtual const char* what () const throw(); }; Esta clase tiene cinco métodos públicos, ninguno de los cuales puede lanzar una excepción. El más interesante es what(), que generalmente devuelve una descripción textual del error causante de la excepción. Un logic_error señala una inconsistencia en la lógica interna del programa, o la violación de cierta precondición en la parte del software cliente (una rutina de usuario). Por ejemplo, el método substr de la clase estándar string, lanza una excepción del tipo out_of_range si se interroga una subcadena (substring) situado más allá del final de la cadena. Interfaz: class logic_error : public exception { public: explicit logic_error (const string& what_arg); }; De esta clase derivan las siguientes: class domain_error : public logic_error { public: explicit domain_error (const string& what_arg); }; class invalid_argument : public logic_error { public: explicit invalid_argument (const string& what_arg); }; class length_error : public logic_error { public: explicit length_error (const string& what_arg); }; class out_of_range : public logic_error { public: explicit out_of_range (const string& what_arg); }; Los runtime_error son aquellos que no pueden ser fácilmente previstos por anticipado, y generalmente se deben a causas externas al programa. Por ejemplo, la ocurrencia de un overflow aritmético como consecuencia de procesar argumentos que son perfectamente legales para una función. class runtime_error : public exception { public: explicit runtime_error (const string& what_arg); }; De aquí derivan las siguientes: class range_error : public runtime_error { public: explicit range_error (const string& what_arg); }; class overflow_error : public runtime_error { public: explicit overflow_error (const string& what_arg); }; class underflow_error : public runtime_error { public: explicit underflow_error (const string& what_arg); }; La excepción bad_alloc se lanza cuando se agota la memoria disponible en el montón ( 4.9.20d). También deriva públicamente de la clase exception. La excepción bad_cast es generada cuando fracasa un modelado dynamic_cast al ser aplicado a una referencia ( 4.9.9c). Responde a la siguiente interfaz: class bad_exception : public exception { public: bad_exception() throw(); bad_exception(const bad_exception& throw(); bad_exception& operator=(const bad_exception& throw(); virtual ~bad_exception() throw(); virtual const char* what() const throw(); }; Cuando desde una función se pretende lanzar una excepción no prevista (de un tipo no incluido en su especificador de excepciones 1.6.4), la excepción se convierte en el tipo bad_exception. La excepción bad_typeid es lanzada cuando se intenta aplicar el operador typeid ( 4.9.14) a una expresión nula. class bad_typeid : public exception { public: bad_typeid() throw(); bad_typeid(const bad_typeid& throw(); bad_typeid& operator=(const bad_typeid& throw(); virtual ~bad_typeid() throw(); virtual const char* what() const throw(); }; Ver ejemplo de bad_typeid ( 4.9.14). §2 Las sentencias en que se puedan recibir excepciones de librería , deben estar inexcusablemente incluidas en un bloque try. En caso contrario, de producirse un error, el programa terminará sin más ceremonial que un mensaje: Abnormal program termination. Como puede verse en sus declaraciones, las clases logic_error, runtime_error y las que derivan de ellas, tienen un constructor explicit ( 4.11.2d1) que acepta la forma genérica: explicit exception_class (const string& what_arg); Aquí se puede incluir como argumento una cadena alfanumérica explicativa del error, que será más tarde devuelta por el método what . La forma normal de usarlo es la siguiente: try { ... if ( condicion ) { throw runtime_error("runtime num. xxxx"; } } catch (const exception& e) { // captura todas las excepciones de la jerarquía cout << "Error: " << e.what(); } §3 Por las razones señaladas al tratar de la captura de excepciones ( 1.6.2), la captura discriminada de este tipo de excepciones requieren comenzar siempre por la clase más derivada. Ejemplo sin discriminación del tipo de error producido: try { // posible lanzamiento ... } catch (...) { // captura ... } Ejemplo con poca discriminación: try { ... } catch (bad_alloc){...} catch (bad_cast){...} catch (bad_exception){...} catch (bad_typeid){...} catch (exception) {...} ... Este otro proporciona más información: try { ... } catch (logic_error) {...} catch (runtime_error) {...} catch (bad_alloc){...} catch (bad_cast){...} catch (bad_exception){...} catch (bad_typeid){...} ... Mejor este otro con la máxima discriminación: try { ... } catch ( domain_error) {...} catch ( invalid_argument) {...} catch ( length_error) {...} catch ( out_of_range) {...} catch ( range_error) {...} catch ( overflow_error) {...} catch ( underflow_error) {...} catch (bad_alloc){...} catch (bad_cast){...} catch (bad_exception){...} catch (bad_typeid){...} ... Ejemplos relacionados: Capturar excepciones en jerarquía de clases ( 1.6.2) Inicio. ________________________________________ Virtualmente en cualquier sentencia. Para que un programa C++ esté correctamente diseñado, debe contar con un sistema de captura de excepciones aunque sea rudimentario. No obstante, el creador del lenguaje nos advierte [1a] que el sistema de excepciones C++ no se ha pensado para hacer cada función tolerante a fallos, sino subsistemas completos. Sin que sea necesario establecer un sistema de detección de excepciones en cada función. [1a] Stroustrup & Ellis: ACRM §15.1.
1.7.1 Tecnicismos - I - §1 Presentación Se incluyen en este capítulo una serie de conceptos no directamente relacionados con el lenguaje C++, aunque si desde luego con la Informática, y que más pronto que después, tendrá que manejar cualquier programador. Se trata de un cajón de sastre que incluye conceptos y definiciones que en su momento me hubiera gustado encontrar explicitadas en algún sitio, y que con demasiada frecuencia se dan "por sabidas", cuando en realidad algunas son de cultura general informática, pero de tal naturaleza que quizás no estén incluidas en ninguna asignatura, de forma que las más de las veces el futuro informático tiene que aprenderlas por su cuenta. Así pues, pido perdón a los "puristas"; a los que opinen que estos contenidos son impropios de un "Manual de C++" (sí, un día de estos cambiaré el título), pues sencillamente... pueden pasar la página. §2 Unidades de medida Permitidme introducir un cuadro recordatorio de los múltiplos y divisores de las unidades de medida; muchas de ellas son de uso frecuentísimo en informática y en ocasiones no recordamos que significan exactamente. Yx Yota 1024 = 1.000...(24 ceros)...000 Millón de trillones Zx Zeta 1021 = 1.000...(21 ceros)...000 Mil trillones Ex Exa 1018 = 1.000.000.000.000.000.000 Trillón español (un millón de Teras) . Px Peta 1015 = 1.000.000.000.000.000 Mil billones (español), un millón de Gigas Tx Tera 1012 = 1.000.000.000.000 Millón de millones; Billón español, Trillón USA . Gx Giga 109 = 1.000.000.000 Mil Millones (mil Megas); el Billón USA. Ej. 1 GB. Mx Mega 106 = 1.000.000 Millón. Ejemplo: 1 MB = 1 Megabyte Mx Miria 104 = 10.000 No usado en informática Kx Kilo 103 = 1.000 Millar (mil). Ejemplo: 1 KB = 1 Kilobyte Hx Hecto 102 = 100 No usado en informática Dx Deca 101 = 10 No usado en informática x unidad 100 = 1 Unidad Ejemplo 1 B = Byte; 1 b = bit dx deci 10-1 = 0.1 décima cx centi 10-2 = 0.01 centésima mx mili 10-3 = 0.001 milésima. Ejemplo: 1 ms = milisegundo μx micro 10-6 = 0.000.001 millonésima. Ejemplo: 1 μ F = microfaradio nx nano 10-9 = 0.000.000.001 milmillonésima. Ejemplo: 1 ns = nanosegundo px pico 10-12 = 0.000.000.000.001 billonésima. Ejemplo: 1 pF = picofaradio fx femto 10-15 = 0.000.000.000.000.001 milbillonésima. ax atto 10-18 = 0.000.000.000.000.000.001 trillonésima zx zepto 10-21 = 0.000...(20 ceros)...001 miltrillonésima yx atto 10-24 = 0.000...(23 ceros)...001 Nota-1: El micro se identifica con la letra griega "μ. Pero puede sustituirse por una "u" si no se dispone de dicho alfabeto. Ejemplo: 1 uF = microfaradio. En la literatura especializada es frecuente leer expresiones como "micrones" o "micras" para referirse a este divisor (lo correcto sería decir micrómetros). Por ejemplo, podemos leer: "La nueva tecnología de conductores de cobre en los procesadores permite rebajar a 0.13 el tamaño de los de los actuales conductores de aluminio de 0.18 micrones". Nota-2: Algunos tratados y libros de informática se empeñan en enseñarnos que 1KByte es igual a 1.024 Bytes, que 64 KB son 65.536 Bytes y que 1 MByte son 1.048.576 Bytes, lo que supongo causa gran consternación entre las personas "normales". Estos autores se saltan las normas de urbanidad y buena conducta (para con el resto de los mortales) e inventan directamente su particular sistema de medidas . Las definiciones utilizadas serían las siguientes: Px Peta 250 = 1.125.899.906.842.624 Tx Tera 240 = 1.099.511.627.776 Gx Giga 230 = 1.073.741.824 Mx Mega 220 = 1.048.576 Kx Kilo 210 = 1.024 Afortunadamente parece que las cosas volverán a su cauce. Aunque todavía no es de general utilización, en Diciembre de 1998 la IEC ha propuesto una estandarización para uso en el mundo digital (afortunadamente un Kilobit vuelve a tener 1000 bits): Ei Exbi 260 = 1.152.921.504.606.846.976 Pi Pebi 250 = 1.125.899.906.842.624 Ti Tebi 240 = 1.099.511.627.776 Gi Gibi 230 = 1.073.741.824 Mi Mebi 220 = 1.048.576 Ki Kibi 210 = 1.024 La razón de esta anormalidad del sistema métrico decimal cuando se refiere a medidas "informáticas", hay que buscarla en las consideraciones ya señaladas al tratar del almacenamiento interno de los Ordenadores Electrónicos Digitales ( 0.1). El hecho de que sean binarios sus dispositivos físicos y su sistema de numeración, hace que esta característica se refleje en muchos detalles, de forma que las potencias de dos (2n) aparecen constantemente . Por ejemplo, en la serie 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, ..., 32.768, 65.536, ..., 524.288, 1.048.576, ... lo más próximo a 1K es 1.024, de forma que como en los sistemas reales la memoria crece en módulos que son múltiplos de dos, lo más "parecido" a 1KB son 1.024 Bytes (justamente de 1000 Bytes no encontraremos nada, no se fabrica). Como complemento a lo anterior, a reproducimos el comentario de Raimond Chen, en su obra "The Old New Thing". Addison-Wesley . In 2003, a lawsuit charging computer manufacturer of misleading consumers over hard drive capacity caused a momentary uproar. The manufacturers use the ISO definition, wherein a "gigabyte" is one billion bytes, even though most people consider a gigabyte to be 1024 megabytes. This is a tricky one. The computer industry is itself inconsistent as to whether the "kilo", "mega", etc. prefixes refer to powers of ten or powers of two. The only place you see powers of two is when describing storage capacity. Everything else is powers of ten: Your 1GHz processor is running at one billion cycles per second, not 1,073,741,824 cycles per second. Your 28.8K modem runs at 28,800 bytes per second, not 29,491. And your 19" monitor measures only 17.4" inches diagonally. (Okay, that last one was a joke, but it's another case where the quoted value in't ncessarily measured the way you expect.) IEC standard designations for power-of-two multipliers. A kibibyte (KiB) is 1024 bytes, a mebibyte (MiB) is 1024 KiB, and a gibibyte (GiB) is 1024 MiB. Good luck finding anybody who actually uses these terms. At least they don't report sized in terms of unformatted capacity any more. §2.1 Al objeto de proporcionar una escala conceptual de lo que significan estos múltiplos y divisores de la unidad, a continuación mostramos una tabla con algunas magnitudes relacionadas con la unidad de longitud. Factor Ud unidad Escala 1024 Ym Yotámetro Escala cósmica (el tamaño del universo observable es de 10.000 Ym). 1015 Pm Petámetro 1012 Tm Terámetro Distancias planetarias (la distancia media de Saturno al Sol es de 1.4 Tm) 109 Gm Gigámetro Escala planetaria (la tierra tiene 0.127 Gm de diámetro, Saturno 2.398 Gm) 106 Mm Megámetro Escala de un país (1.000 Km) 103 Km Kilómetro Escala de un pueblo 100 m metro Escala del hombre (1-2 m) 10-2 cm centímetro Un ratón (5 cm), microondas. 10-3 mm milímetro Un insecto 10-6 μm micrómetro Escala bacteriana (el tamaño típico de una bacteria está entre 1 y 10 μm). Nanotecnología 10-9 nm nanómetro Escala molecular (una molécula de agua mide 1 nm). Longitud de onda de los rayos X. 10-12 pm picómetro Escala atómica 10-15 fm femtómetro Escala subatómica (tamaño del núcleo del átomo) Inicio. ________________________________________ Un Terabyte equivale aproximadamente al contenido de un millón de libros (suponiendo la utilización de un juego de caracteres de 8 bits) . Esta correspondencia entre la forma externa y los principios físico-matemáticos que la sustentan, es justamente lo que caracteriza y hace bellos los diseños ingenieriles de cualquier clase, ya sean un puente, o una memoria de ordenador. Raimond Chen es un programador de Microsoft relacionado con el desarrollo del SO Windows durante más de una década y mantenedor de un conocido e influyente blog. Existe una versión online del comentario en http://blogs.msdn.com/ Según un estudio dirigido por los profesores Peter Lyman y Hal Varian de la School of Information Management and Systems de Berkeley, el total de información nueva que se almacenó mundialmente en papel, película, medios ópticos o magnéticos en 2002, fue de aproximadamente 5 Exabytes (en 1999 fue la mitad). De este total, el 92% fue almacenada en medios magnéticos (discos). En el mismo periodo de 2002, el total de información que fluyó por medios electrónicos, radio, televisión e Internet, fue de unos 18 Exabytes. El Sistema Internacional de Unidades SI, fue aprobado en 1960 en la Onceava Conferencia General de Pesos y Medidas. En 1970 el SI ya había sido adoptado como sistema legal en más de 30 países y recomendado para el uso científico en todo el mundo. En España es adoptado como sistema legal el 8 de Noviembre de 1967, con modificaciones de detalles técnicos el 25 de Abril de 1974. En el artículo segundo de la citada Ley se recogen de forma inequívoca los valores y designación de los diversos múltiplos y divisores (en el resto de países que adoptan el SI se utilizan definiciones análogas). IEC International Electrotechnical Commision http://physics.nist.gov/ Cuando se mide el tiempo, el múltiplo "Mil millones de años" tiene un nombre propio: Eón. Este capítulo de la historia informática ocurrió a finales de los 90 y alguien lo ha denominado la WWI (Web War I)
C++ (VigesimoPrimera parte) 1.6.2 Capturar excepciones §1 Sinopsis Recordemos ( 1.6) que el manejador de excepciones ("handler" es un bloque de código precedido por la palabra catch. Este bloque debe seguir inmediatamente el bloque-intento try o a otro bloque catch según el siguiente esquema: try { // bloque-intento ... // posibles errores } catch (TipoX x) { // capturar errores X ... } catch (TipoY y) { // capturar errores Y ... } ... // sigue aquí La sintaxis es: catch (<tipo_exc> [<nombre>]) { <sentencias> } <tipo_exc> es el tipo de excepción que se capturará en esta sentencia. Ejemplo: try { ... if (x > limit) throw "Overflow"; } catch (char*) { cout << "Recibido error: "; } Eventualmente se puede añadir un identificador <nombre>, que puede ser usado en el cuerpo <sentencias> del bloque, de forma análoga a los argumentos de las funciones. Ejemplo: struct E{ char* msg; }; ... try { ... if (x > limit) { E e = { "Error desconocido" }; throw e; } } catch (E r) { cout << "Recibido: " << r.msg << endl; } Según el objeto capturado sea recibido por valor o por referencia, la forma del bloque catch es alguna de las siguientes: catch(T t) {...} catch(const T t) {...} catch(T& t) {...} catch(const T& t) {...} // esta es la más usual Nota: aunque nos referimos a él como bloque-catch, en realidad su comportamiento y su estructura son muy parecidos al de una función. Aunque con diferencias, para muchos aspectos podemos pensar en él como una auténtica función. Como veremos a continuación, también aquí se establecen ciertas reglas de "congruencia de argumentos" para ver que bloque-catch responde a una excepción determinada. §2 Concordancia Debe existir un manejador para cada excepción distinta que pueda lanzar el programa. El manejador captura una excepción cuando el tipo de esta coincide (según ciertas reglas ) con el tipo de <tipo_exc>. Una vez que se ha producido la concordancia, la pila es descargada hasta el punto del "handler" al que se transfiere el control. Es entonces el manejador el que decide el tratamiento adecuado a la anormalidad detectada. Ejemplo: try { // bloque-intento ... // posibles errores overflow o aritmetic if (...) throw Overflow; ... if (...) throw Aritmetic; ... } catch (Overflow, o) { // capturar errores overflow ... } catch (Aritmetic, a) { // capturar errores aritméticos ... } ... // sigue aquí si no hay errores §2.1 En caso de no existir un manejador adecuado a una excepción determinada, se desencadena un protocolo que, por defecto, produce sin más la finalización del programa ( 1.6.3 Excepciones imprevistas). §2.2 Reglas de concordancia La excepción es capturada por el bloque-catch cuyo argumento coincida con el tipo de objeto lanzado por la sentencia throw. La búsqueda de coincidencia se realiza sucesivamente sobre los bloques catch en el orden en que aparecen en el código hasta que aparece la primera concordancia. Después que se ejecuta este bloque, el programa continúa su ejecución después del último de los manejadores que sigan al bloque try que lanzó la excepción, sin que se realice ulterior evaluación de otros posibles manejadores para la excepción lanzada. Por consiguiente, el orden de colocación de los bloques catch es determinante. Por ejemplo: si se incluye un manejador universal , debería ser el último. La concordancia sigue ciertas reglas . El objeto e lanzado por la sentencia throw E() será capturado por un bloque catch(C c) si se cumple alguna de las siguientes condiciones: • E y C son del mismo tipo. • C es una super-clase de E visible en el punto de lanzamiento de la excepción. Por esta razón, cuando se captura una excepción y esta pertenece a una jerarquía de clases, hay que comenzar por capturar la clase más derivada (ver Ejemplo ). • C y E son punteros a clases de una misma jerarquía, y existe una conversión estándar de E a C ( 4.11.2b1) en el punto de lanzamiento de la excepción. Suponiendo que: D* E; // E es un puntero-a-tipoD S* C; // C es un puntero-a-tipoS Para que exista una conversión estándar de E a C , debe cumplirse alguna de las condiciones siguientes: • C es del mismo tipo que E, aunque puede tener un especificador const o volatile. • C es un puntero-a-void (tipo void*). • B es una superclase de D en la que sus miembros pueden referenciar sin ambigüedad a los de B. (esto solo es de aplicación en los casos de herencia múltiple). Nota: De la segunda condición se deduce que cualquier excepción E capturada por un puntero puede ser también capturada por un puntero-a-void (el tipo void* funciona como un capturador universal de punteros). La norma subyacente bajo las condiciones anteriores es que E y C deben coincidir exactamente, o la excepción E capturada, debe ser derivada del parámetro C del "catcher". El siguiente ejemplo termina sin que se capture la excepción ... class B {}; class C {}; void fun() { throw B; } // se lanza un tipo B main() { // ======== try { fun(); } catch(C) { cout << "Capturada excepción C" << endl; abort(); } } En cambio en el que sigue si es capturada: ... class B {}; class D : public B {}; void fun() { throw D(); } main() { // ========== try { fun(); } catch(B) { cout << "Capturada excepción B" << endl; } } §2.3 El manejador universal: Existe la posibilidad de definir un manejador que capture cualquier excepción mediante la sintaxis siguiente: catch (...) { <sentencias> } Ejemplo: En el programa que sigue la sentencia catch captura cualquier excepción con independencia de su tipo. Solo existe un catch para el bloque try. #include <stdio.h> bool pass; class Out{}; void festival(bool firsttime) { if(firsttime) throw Out(); } void test() try { festival(true); } catch(...){ pass = true; } // puede capturar cualquier excepción } int main() { // ================== pass = false; test(); return pass ? (puts("Excepción capturada",0) : (puts("no hay excepción" ,1); } Salida: Excepción capturada §3 Salto a una etiqueta Se puede utilizar un goto para transferir el control del programa fuera de un manejador. Para ilustrarlo utilizaremos una versión modificada del ejemplo ya comentado ( 1.6.1). #include <stdio.h> bool pass; class Out{}; // L.3: Para instanciar el objeto a lanzar void festival(bool); // L.4: prototipo int main() { // ===================== try { // L.7: bloque intento pass = true; festival(true); } catch(Out o) { // L.11: manejador (captura la excepción) goto fallo } // L.13: return (puts("Acierto!",0); fallo: return (puts("Fallo!",1); } void festival(bool firsttime){ // L.17: definición de festival if(firsttime) throw Out(); // L.18: Lanzar excepción. } §4 Excepciones anidadas El sistema de control de excepciones puede ser anidado a cualquier nivel (pueden existir bloques try dentro de bloques try y de bloques catch) Como debe mantenerse la regla de que un bloque try debe ser seguido inevitablemente por un catch, lo anterior significa que pueden existir secuencias try-catch dentro de bloques try y de bloques catch. Secuencias anidadas en el bloque-intento. class Error { }; void func () { ... try { // I1 Bloque-intento 1 ... try { // I1I1 Bloque-intento en I1 ... } catch { // I1H1 Handler de I1I1: capturar excepciones de I1I1 ... } ... // continúa bloque I1 } catch (Error) { // H1 Handler de I1: capturar excepciones de I1 ... } ... // sigue a bloque I1 } Secuencias anidadas en el bloque-manejador class Error { }; void func () { ... try { // I1 Bloque-intento 1 ... } catch (Error) { // H1 Handler de I1: capturar excepciones de I1 ... try { // H1I1 Bloque-intento en Handler-H1 ... } catch { // H1H1 Handler de H1I1: capturar excepciones de H1I1 ... } ... // continúa handler H1 } ... // sigue a bloque I1 } Ejemplo 1: #include <stdio.h> class festival{}; class Verano : public festival{}; class Primavera: public festival{}; void fiesta(int); int main() { // ==================== try { fiesta(0); } catch(const Verano& { puts("Festival de Verano"; } catch(const Primavera& { puts("Festival de Primavera" ); } try { fiesta(1); } catch(const Verano& { puts("Festival de Verano"; } catch(const Primavera& { puts("Festival de Primavera" ); } return 0; } void fiesta(int i) { if(i==1) throw( Verano() ); else throw( Primavera() ); } Resultado: Festival de Primavera Festival de Verano Ejemplo 2: El ejemplo que sigue muestra que cuando se captura una excepción y esta pertenece a una jerarquía de clases, hay que comenzar por la clase más derivada, pues de lo contrario se pierde capacidad de discriminación del tipo de excepción ocurrido. #include <stdio.h> class festival{}; class Verano : public festival{}; class Primavera: public festival{}; void fiesta(int i) { if (i==1) throw(Verano() ); else if(i==2) throw(Primavera()); else throw(festival() ); } int main() { // ==================== try { fiesta(0); } // estas sentencias están en el orden adecuado catch(const Verano& ) { puts("Festival de Verano"; } catch(const Primavera&{ puts("Festival de Primavera" ); } catch(const festival& ){ puts("Festival" ); } try { fiesta(1); } catch(const Verano& ) { puts("Festival de Verano"; } catch(const Primavera&{ puts("Festival de Primavera" ); } catch(const festival& ){ puts("Festival" ); } try { fiesta(2); } catch(const Verano& ) { puts("Festival de Verano"; } catch(const Primavera&{ puts("Festival de Primavera" ); } catch(const festival& ){ puts("Festival" ); } /* Si se captura la clase base primero se pierde la posibilidad de comprobar la sub-clase de la excepción que ha sido lanzada realmente */ try { fiesta(1); } catch(const festival& ){ puts("Festival (de que tipo??!!)"; } catch(const Verano& ) { puts("Festival de Verano" ); } catch(const Primavera&{ puts("Festival de Primavera" ); } try { fiesta(2); } catch(const festival& ){ puts("Festival (de que tipo?!!!)"; } catch(const Verano& ) { puts("Festival de Verano" ); } catch(const Primavera&{ puts("Festival de Primavera" ); } return 0; } Salida: Festival Festival de Verano Festival de Primavera Festival (de que tipo??!!) Festival (de que tipo?!!!) Ejemplo 3 Una posible alternativa al diseño anterior permitiría capturar solo las excepciones de la clase-base y utilizar las propiedades del polimorfismo ( 4.11.8) para realizar la discriminación: #include <iostream> using namespace std; class Festival { public: virtual void foo() { cout << "Festival" << endl; } }; class Verano : public Festival { public: void foo() { cout << "Festival de Verano" << endl; } }; class Primavera: public Festival { public: void foo() { cout << "Festival de Primavera" << endl; } }; void fiesta(int i) { if (i==1) throw(Verano() ); else if(i==2) throw(Primavera()); else throw(Festival() ); } int main() { // ==================== try { fiesta(0); } catch(Festival& f) { f.foo(); } try { fiesta(1); } catch(Festival& f) { f.foo(); } try { fiesta(2); } catch(Festival& f) { f.foo(); } return 0; } Salida: Festival Festival de Verano Festival de Primavera Inicio. ________________________________________ Esta reglas son diferentes de las que sigue el compilador para encontrar la mejor concordancia en caso de sobrecarga de funciones ( 4.4.1a).

C++ (VigesimoSegunda parte) 1.6.3 Excepciones imprevistas §1 Sinopsis Es innecesario decir que las excepciones también pueden provocar excepciones; desde luego, pueden provocar múltiples errores, pero en general son de alguno de los cinco tipos que se relacionan: • 1.- No existe manejador para la excepción ("No handler for the exception". • 2.- Lanzada una excepción no prevista ("Unexpected exception thrown" • 3.- Una excepción solo puede ser lanzada de nuevo desde un manejador ("An exception can only be re-thrown in a handler" • 4.- Durante la limpieza de la pila un destructor debe manejar su propia excepción ("During stack unwinding, a destructor must handle its own exception". • 5.- Memoria agotada ("Out of memory". En este capítulo presentamos los comportamientos adoptados por el compilador para los dos primeros casos; generalmente debidos a que no hemos diseñado correctamente el sistema de manejo excepciones de nuestro programa. Algo así como el sistema de "protección contra fallos del sistema de emergencia". Las dos primeras situaciones erróneas son independientes. Las causas/medidas-a-adoptar responde al siguiente esquema: • La primera es el caso que se lance una excepción para la que no se ha previsto un manejador adecuado; las denominamos excepciones sin manejador . En esencia el sistema consiste en que puede instalarse un manejador genérico (manejador de terminación) que se haga cargo de la situación si hemos olvidado instalar el "handler" adecuado para una excepción concreta. Incluso veremos que si hemos olvidado instalar este manejador universal, el compilador proporciona uno por defecto. • La segunda situación contempla que una función lance una excepción que no está incluida en su especificador de excepción. Este concepto se explica más adelante ( 1.6.4), por ahora adelantemos que C++ permite especificar de antemano que excepciones (tipos de objetos) podrán ser lanzados desde una función determinada (recordemos que en C++ todo ocurre dentro de funciones). A estas situaciones las denominamos excepciones imprevistas . Veremos que el patrón de actuación es parecido al anterior; es posible definir para estos "imprevistos" un comportamiento (función) que se encargue de la situación. En caso que no hayamos establecido nada concreto, el compilador proporciona un protocolo de actuación por defecto. §2 Excepciones sin manejador Recordemos ( 1.6.1) que si durante la ejecución de un bloque try se lanza una excepción y no se encuentra ningún manejador adecuado se adoptan las siguientes medidas: Se invoca la función terminate() a: Se ha establecido una función t_func por defecto con set_terminate(). terminate invoca t_func (que debe terminar el programa). b: No se ha establecido ninguna función por defecto con set_terminate() terminate invoca la función abort(). El siguiente ejemplo muestra lo que ocurre cuando el programa encuentra una excepción no soportada. #include <except.h> #include <process.h> #include <stdio.h> bool pass; class Out{}; void final(); // prototipo void festival(bool); // ídem. void test(); // ídem. int main() { // ============= set_terminate(final); // M.1: añade final a la lista test(); // M.2: test lanza la excepción Out sin manejador return pass ? (puts("Salir del test",0) : (puts("Seguir el test",1); } void final(){ puts("*** Nadie captura la excepción ***"; abort(); } void festival(bool firsttime){ if(firsttime) throw Out(); } void test() {festival(true); } Salida: *** Nadie captura la excepción *** Comentario: La sentencia M.1 registra la función final como manejador por defecto , de forma que a partir de este momento si se lanza una excepción que no encuentra manejador adecuado, se invocará esta función. M.2 invoca a test que invoca a su vez a festival con true como argumento. Lo que hace que esta última lance una excepción con una instancia de Out. En el programa no existe ningún manejador específico previsto para esta excepción (ni para ninguna otra). En realidad no se ha previsto ningún dispositivo para manejar excepciones, no existe ningún bloque try, por lo que es invocada la función terminate, que invoca a su vez a la función final que se había instalado al principio. Esta última es la responsable de la salida obtenida y de terminar el programa. §2.1 terminate Esta función de la Librería Estándar (except.h), es invocada cuando se lanza una excepción que no encuentra el manejador adecuado. Sintaxis: void terminate(); Descripción: La misión de esta función es simplemente verificar si existe alguna función de usuario definida como reserva para el caso de no encontrarse un manejador adecuado para la excepción lanzada (esta función de reserva se denomina manejador de terminación y se instala como se indica a continuación). Si la función existe, terminate la invoca; si no existe, termnate realiza una llamada a abort ( 1.5.1), lo que origina la terminación inmediata del programa. En otras palabras: es posible modificar la forma en que termina el programa cuando se genera una excepción que no tiene un "handler" adecuado. Si se desea terminar con algo distinto que la llamada a abort, se puede definir cualquier otra. Esta función, manejador de terminación, será llamada por la función terminate si ha sido registrada mediante set_terminate . §2.2 set_terminate set_terminate es una función de Librería Estándar <except.h>, que permite instalar una función que determina el comportamiento del programa cuando se lanza una excepción que no encuentra ningún "handler" específico. Podríamos decir que instala un manejador por defecto (el manejador de terminación). Sintaxis: typedef void (*terminate_handler)(); terminate_handler set_terminate(terminate_handler t_func); Ejemplo: set_terminate(final); ... void final(){ puts("*** Nadie captura la excepción ***"; abort(); } Descripción Vemos que set_terminate es una función que devuelve un objeto terminate_handler y recibe un argumento del mismo tipo. A su vez, terminate_hadler es un puntero a función que no recibe argumentos y devuelve void. La acción a ejecutar está definida por t_func, este argumento debe ser el nombre de la función que queremos invocar en caso de que una excepción no encuentre un manejador adecuado. Evidentemente t_func debe responder a las expectativas, es decir: Ser una función que no reciba argumentos y devuelva void. Debe ser definida de forma que termine el programa. Cualquier intento de volver a su invocadora, la función terminate, conduce a un comportamiento indefinido del programa. Tampoco se puede lanzar una excepción desde t_func. Si no se ha previsto ningún manejador, entonces el programa llama a la función terminate, que a su vez termina con una llamada a la función abort ( 1.5.1), y el programa termina con el mensaje: Abnormal program termination. Si se desea que se llame cualquier otra función distinta de abort desde terminate entonces debemos instalar nuestra propia t_func e instalarla con set_terminate, lo que nos permitiría implementar cualquier acción que deseemos que no sea cubierta por abort. §3 Excepciones imprevistas Si una función lanza una excepción que no está incluida en su especificador de excepción ( 1.6.4), se produce una llamada a la función unexpected , que a su vez invoca a cualquier función establecida por set_unexpected . Caso de no haberse establecido ninguna función, unexpected llama a terminate . §3.1 unexpected Esta función de la Librería Estándar <except.h> es invocada cuando una función lanza una excepción que no está incluida en su especificador de excepción ( 1.6.4). Sintaxis: void unexpected(); Descripción: A su vez unexpected invoca a cualquier función establecida por set_unexpected . Si no existe ninguna función registrada, unexpected llama a la función terminate . Como puede verse en su definición, unexpected no devuelve nada, aunque a su vez puede lanzar una excepción. Ver ejemplo ( 1.6.4) §3.2 set_unexpected Función de Librería Estándar <except.h>. Sintaxis typedef void ( * unexpected_handler )(); unexpected_handler set_unexpected(unexpected_handler unexp_func); unexp_func define la función que se pretende instalar. Descripción Esta función permite instalar una función que será ejecutada en caso que una función invoque una excepción que no esté incluida en su especificador de excepción ( 1.6.4). Como puede verse, el argumento a utilizar es un objeto tipo unexpected_handler, es decir: un puntero a función que no recibe argumentos y devuelve void. En la práctica esto significa que se puede utilizar directamente el nombre de la función que se desea instalar, y que esta función debe ser del tipo adecuado (una función que no reciba argumentos y no devuelva nada). La función instalada debe ser tal que termine el programa. No debe intentar volver a su invocadora (unexpected), ya que un intento de esta índole produciría un resultado indefinido. Por contra, unexp_func puede llamar a las funciones abort ( 1.5.1), exit ( 1.5.1) o terminate ( ). §4 Corolario El sistema C++ de tratamiento de errores ofrece infinitas combinaciones posibles. Cada circunstancia requiere una estrategia distinta, pero siempre deberíamos instalar un sistema, aunque fuese mínimo y rudimentario, para el tratamiento de excepciones. Es mucho más elegante salir del programa de forma controlada con un mensaje adecuado, y quizás escribiendo un fichero con el estatus y tipo de error, que terminar con un mensaje del Sistema. Como hemos visto, el compilador establece por defecto un sistema que obedece al siguiente esquema: cuando una función lanza una excepción que no está incluida en su especificador de excepción se lanza unexpected(). Si no se ha previsto otra cosa unexpected() invoca a terminate(). A su vez la acción por defecto de terminate() es invocar a abort(). Generalmente los programas tienen una vida larga y sujeta a cambios; revisiones sucesivas que los van mejorando. No existe inconveniente para que el sistema de control se vaya afinando y sofisticando a partir de un diseño inicial más o menos rudimentario, en función de la experiencia obtenida con su explotación. Como punto de partida podría servir el siguiente esquema: #include <signal.h> #include <except.h> ... void noHandler(); // excepciones sin manejador void imprevistas(); // excepciones imprevistas int main() { // ================= set_terminate(noHandler); // añade noHandler set_unexpected(imprevistas); // añade imprevistas ... // nuestro proceso... return 0; // Ok. el programa concluye correctamente } void noHandler() { // definición cerr << "Excepción sin manejador. Programa terminado"; raise(SIGABRT); // El programa termina con error } void imprevistas() { // definición cerr << "Excepción imprevista. Programa terminado"; abort(); // El programa termina con error } 1.6.4 Especificación de excepciones §1 Sinopsis En C++ existe una opción denominada especificación de excepción que permite señalar que tipo de excepciones puede lanzar una función directa o indirectamente (en funciones invocadas desde ella). Este especificador se utiliza en forma de sufijo en la declaración de la función y tiene la siguiente sintaxis: throw (<lista-de-tipos> // lista-de-tipos es opcional La ausencia de especificador indica que la función puede lanzar cualquier excepción. El mecanismo de excepciones fue introducido en el lenguaje en 1989, pero la primitiva versión adolecía de un problema que podemos resumir como sigue: Supongamos que tenemos una función de librería cuya definición, contenida en un fichero de cabecera, es del tipo: void somefuncion (int); Lo normal es que las "tripas" de la función queden ocultas al usuario, que solo dispone de la información proporcionada por el prototipo ( 4.4.1), pero es evidente que en estas circunstancias es imposible saber si la función puede lanzar una excepción y en consecuencia, decidir si de deben tomar (o no) las medidas apropiadas para su captura. Años después, y ante la confusión creada, el Comité de Estandarización decidió incluir la especificación de excepciones que comentamos en este capítulo. Como puede verse es un modo de incluir en el prototipo información suficiente para que el usuario conozca que tipo de excepciones pueden esperarse de una función (si es que las hay). §2 Ejemplos de funciones con especificadores de excepción: void f1(); // f1 puede lanzar cualquier excepción void f2() throw(); // f2 no puede lanzar excepciones void f3() throw(BETA); // f3 solo puede lanzar objetos BETA void f4() throw(A, B*); /* f4 puede lanzar excepciones derivadas públicamente de A o un puntero a derivada públicamente de B */ Nota: La sintaxis utilizada con f2 es la forma estándar C++ para especificar que una función no puede lanzar excepciones, y que salvo indicación en contrario (§4 ), tampoco las funciones que puedan ser invocadas desde ella. No obstante, los compiladores Borland C++ y MS Visual C++ disponen de otra posibilidad sintáctica para el mismo propósito ( 4.4.1b). §3 Tenga en cuenta que las funciones con especificador de excepción no son susceptibles de sustitución inline ( 4.4.6b). Por ejemplo, la sentencia: inline void f1() throw(int) { ... } daría lugar a una advertencia del compilador: Warning: Functions with exception specifications are not expanded inline §4 Las excepciones señaladas para una función no afectan a otras funciones que pudieran ser llamadas durante su ejecución. Por ejemplo: func1() throw() { // func1 no puede lanzar excepciones ... // en esta zona no se lanzarán excepciones func2(); } func2() throw(A); // func2 puede lanzar un objeto A ... try { ... func1 } Durante la ejecución del bloque de código de func1 no pueden lanzarse excepciones de ningún tipo, pero si ocurren circunstancias adecuadas mientras se está ejecutando la invocación a func2, desde esta sí pueden lanzarse objetos tipo A. Todos los prototipos y definiciones de estas funciones deben tener un especificador de excepción conteniendo la misma <lista-de-tipos>. Si una función lanza una excepción cuyo tipo no está incluido en su especificación, el programa llama a la función unexpected ( 1.6.3Excepciones imprevistas). §5 El sufijo no es parte del tipo de la función; en consecuencia, un puntero a función no se verá afectado por el especificador de excepción que pueda tener la función. Este tipo de punteros solo comprueba el tipo de valor devuelto y los argumentos ( 4.2.4a). Por consiguiente, lo siguiente es legal: void f2(void) throw(); void f3(void) throw(BETA); void (* fptr)(); // Puntero a función devolviendo void fptr = f2; // fptr se puede asignar a cualquiera fptr = f3; // de las funciones f2 y f3 §6 Hay que prestar atención cuando se sobrecontrolan funciones virtuales, porque la especificación de excepción no se considera parte del tipo de función y existe el riesgo de violaciones en el diseño del programa. §7 Ejemplo 1 En el siguiente ejemplo la definición de la clase derivada BETA::vfunc se hace de forma que no puede lanzar ninguna excepción; se trata de una variación de la definición original en la clase base. class ALPHA { public: struct ALPHA_ERR {}; virtual void vfunc(void) throw (ALPHA_ERR) {} // Especificador de excepción }; class BETA : public ALPHA { void vfunc(void) throw() {} // Se cambia el especificador de excepción }; §8 Ejemplo 3 Este ejemplo especifica que excepciones pueden lanzar las funciones festival y test. Ninguna otra excepción podrá ser lanzadas desde ambas. #include <stdio.h> bool pass; class Out{}; // festival solo puede lanzar excepciones Out void festival(bool firsttime) throw(Out) { if(firsttime) throw Out(); } void test() throw() { // test no puede lanzar ninguna excepción try { festival(true); } catch(Out& e){ pass = true; } } int main() { pass = false; test(); return pass ? (puts("Excepción manejada por test",0) : (puts("Sin excepción!!" ,1); } Salida: Excepción manejada por test Si festival generase una excepción distinta de Out, se consideraría una excepción imprevista, y el control del programa sería transferido a la función prevista para estos casos (ver al respecto el ejemplo siguiente). §9 Ejemplo 4 Se muestra que test no puede lanzar ninguna excepción. Si alguna función (por ejemplo el operador new) en el cuerpo de test lanza una excepción, la excepción debe ser capturada y manejada dentro del propio cuerpo de test. En caso contrario, la excepción representaría una violación de la especificación de no-excepciones establecida para dicha función. Es posible establecer que set_unexpected() acepte un manejador diferente, pero en cualquier caso, será invocada la función que se haya previsto para estos casos. #include <except.h> #include <process.h> #include <stdio.h> bool pass; class Out{}; void imprevisto(){ puts("*** Fallo ***"; exit(1); } void festival(bool firsttime) throw(Out) { // festival solo puede lanzar if(firsttime) throw Out(); // excepciones Out } void test() throw() { // test no puede lanzar ninguna excepción try { festival(true); } catch(Out& e){ pass = true; throw; } // Error: intenta ralanzar Out } int main() { // ============ set_unexpected(imprevisto); pass = false; test(); return pass ? (puts("Excepción manejada por test",0) : (puts("Sin excepción !!" ,1); } Salida: *** Fallo *** Inicio. ________________________________________ Si en C++Builder coexisten simultanea e independientemente, un prototipo y una definición de la función, la especificación de excepción debe incluirse en ambos, de lo contrario se produciría un error de compilación.

C++ (VigesimoTercera parte) 1.7 Programación actual §1 Introducción Loque podíamos llamar "programación tradicional", por ejemplo la que se utilizaba (utiliza?) en la confección de programas para los primitivos PCs bajo MS-DOS, o en los actuales Win-32 bajo una "ventana" DOS, es un concepto un tanto ambiguo, pero podemos intentar una definición diciendo que se basa en algunas premisas y características bastante definidas. Estas características pueden coexistir juntas o faltar alguna, pero en general se dan simultáneamente. En este capítulo intentaremos mostrar una visión sinóptica de las diferencias entre esta y la programación "moderna" , a la que seguramente tendrá que adaptarse el programador C++. Sin embargo, advertiremos desde ahora que la mentada "programación tradicional" mezcla conceptos distintos e independientes, cuyas diferencias es importante tener claras antes de adentrarnos en la programación actual. Por ejemplo, la programación para Windows-32; asunto este que desde luego va más allá que la simple utilización de "objetos". Este capítulo se ha redactado teniendo en mente la multitud de problemas con que se enfrenta el programador "tradicional" que se ve abocado a trabajar para un SO moderno (Windows inevitablemente?). Intentaremos dentro de lo posible clarificar algunas ideas de ese cúmulo de nuevos conceptos que con demasiada frecuencia producen más de una "indigestión" inicial . §2 Sinopsis Para situarse correctamente en el asunto, es fundamental entender que las diferencias entre la programación tradicional y la actual tienen su origen inmediato en la evolución de los Sistemas Operativos, evolución que ha sido posible a su vez por la evolución del hardware. Por ejemplo, máquinas capaces de direccionar más memoria. También por la evolución de las herramientas de programación (lenguajes). Las características y diferencias de la programación tradicional frente a la actual pueden resumirse en el siguiente cuadro: Programación tradicional Representación en modo texto Trabajo en mono-programa Trabajo en mono-tarea Ejecución controlada por el programa Programación de tipos fijos Programación actual Representación en modo gráfico Trabajo en multi programación Trabajo en multi-tarea Ejecución controlada por el Sistema Programación orientada a objetos §3 Características de la "Programación Tradicional" §3.1 Representación en modo texto Se trabaja en un entorno de texto (no gráfico), el programa en ejecución controla la información representada en la totalidad de la pantalla (no hay "ventanas"; el control de esta se realiza en término de filas y columnas (generalmente 24 x 80) y un surtido muy limitado de 256 caracteres (ASCII char set 2.2.1a). Los únicos atributos que pueden tener los caracteres suelen ser: Color de tinta y de papel (trazo y fondo); subrayado y parpadeo. Cuando se trabaja en entornos gráficos, este tipo de aplicaciones no-gráficas se denominan "de Consola". En la nomenclatura Windows a este tipo de aplicaciones se las conoce como CUI ("Console User Interface" en contraposición con las de interfaz gráfica GUI ("Graphical User Interface" que comentamos más adelante ( §4.1) . §3.2 Trabajo en mono-programa El SO. no admite multiprogramación, es decir, solo corre una aplicación cada vez. Cuando ejecutamos nuestro programa no tiene que compartir recursos con ningún otro (por ejemplo, nuestras órdenes de impresión pueden ser dirigidas directamente al "puerto" de impresora). Esto hace que en general podamos utilizar rutinas y llamadas de "bajo nivel" sin peligro alguno de interferir con nada. La totalidad de los recursos, tales como el procesador, la memoria, los puertos E/S etc. están a nuestra disposición exclusiva. §3.3 Trabajo en mono-tarea El programa solo tiene una vía, hilo o hebra ("Thread" de ejecución, decimos que es mono-hebra. Coloquialmente podemos decir que solo hace una cosa cada vez. §3.4 Ejecución controlada por el programa Desde su concepción, el programador decide que ocurre exactamente en cada momento de la ejecución del programa, de forma que las vías de actuación pueden ser previstas de antemano. Desde un punto de vista funcional, esto significa que, por ejemplo, se puede decidir en que momento se atenderá una llamada del teclado o en que momento atenderá la UART del puerto serie para atender la llegada o envío de datos. Desde el punto de vista del código, el programa (suponemos que es C) se inicia en la función main y termina cuando esta termina. main puede llamar a otras funciones (que pueden llamar a su vez a otras funciones), pero siempre es el programa el que decide cual es la vía de acción en cada caso; que funciones se invocan y cuando. En el fondo, esta característica es consecuencia de que el programa controla más o menos directamente sus propios procesos de entrada/salida. Puede por ejemplo "leer" el teclado o "escribir" directamente en el puerto de impresora, por lo que las decisiones pueden tomarse "desde dentro" del programa. §3.5 Programación "procedural" Aquí utilizamos el término "procedural" para indicar que generalmente no se utilizan lenguajes orientados a objetos (POO). Los lenguajes empleados adoptan una fuerte compartimentación entre los tipos de datos disponibles y las operaciones posibles con ellos, que son fijas e incluidas en el propio lenguaje. No existen las clases como nuevos tipos que encapsulan el dato y sus operaciones ( 1.1), las entidades de mayor nivel de abstracción que pueden utilizarse son las estructuras o uniones del C clásico y similares. §4 Características de la Programación "Moderna" Debemos recalcar de nuevo que se trata de conceptos generales e independientes. Por ejemplo, un programa "moderno" puede ser multiprogramación pero en modo texto, o no orientado a objetos; sin embargo, la mayoría de las característica se dan juntas. En especial si se trata de programas que utilizan la interfaz gráfica de los SOs más conocidos. §4.1 Representación en modo gráfico El usuario dispone de una interfaz gráfica GUI ("Graphical User Interface" para trabajar en la aplicación. Este tipo de programas controlan la información representada en su "canvas" , un trozo (ventana) de la pantalla cuyo tamaño puede controlar el usuario la mayoría de las veces. La representación se realiza en pixels , y se dispone de un amplísimo surtido de herramientas y parámetros de representación; no solo un amplio juego caracteres ("Fonts" con todos sus atributos, también un pincel ("Brush", una pluma ("Pen", así como iconos e imágenes preconstruidas de todo tipo. La posibilidad de multiprograma reseñada a continuación , junto con las nuevas posibilidades gráficas, hacen que la pantalla pueda contener múltiples "ventanas", representativas de otros tantos procesos en ejecución. El resultado es lo que se ha dado en llamar "Metáfora de un escritorio" ("Desktop metaphor". A su vez la pantalla se convierte en otro dispositivo de entrada (no solo el teclado); puede arrastrarse, cortarse y copiarse información de un punto a otro. Incluso entre aplicaciones diferentes. Ni que decir tiene que la programación de un entorno gráfico es muchísimo más compleja que para un entorno de texto, aunque afortunadamente los entornos de desarrollo actuales ofrecen multitud de soluciones preconstruidas (clases) y librerías que facilitan la labor, de forma que si un programador quiere insertar, por ejemplo, un botón en un formulario, no tiene que preocuparse en "dibujar" el botón y su posible etiqueta. Simplemente indicar su tamaño; el texto o dibujo de su etiqueta; su posición en el "canvas", y las acciones a tomar cuando en dicho objeto se produzcan determinados "eventos": que pase por encima el cursor; que reciba foco; que se haga clic sobre él, etc.). Desde el punto de vista de la interfaz que percibe el usuario, existen dos tipos de aplicaciones: SDI y MDI. SDI significa "Single Document interface". Son aplicaciones que se desarrollan en una sola ventana. Por su parte, MDI ("Multiple Document Interface" supone que la interfaz de la aplicación es una ventana maestra ("Frame window", que puede contener múltiples ventanas hijas o "documentos" abiertos simultáneamente. Estas ventanas descendientes coinciden con la ventana madre cuando son maximizadas, y se cierran cuando se termina el proceso principal (que abrió la ventana maestra). §4.2 Trabajo en multi-programa El SO admite multiprogramación ("multiprogramming" es decir, se ejecutan múltiples aplicaciones a la vez. De hecho, incluso en un sencillo ordenador personal, el propio Sistema puede mantener en ejecución simultanea seis u ocho aplicaciones para sí mismo, además de los programas de aplicación del usuario. Esta operación se ejecuta de forma preemptiva. Lo que significa que el SO tiene un control continuo sobre el procesador y los diversos programas en ejecución. Por ejemplo, el SO puede abortar, suspender o asignar tiempos de ejecución a cualquiera de ellos. Nota: La ejecución no preemptiva se denomina cooperativa, y es propia de sistemas multiprograma antiguos. El control es copado por el programa en ejecución, que debe terminar por sí mismo para que pueda ejecutarse otra aplicación. En estas condiciones, si una aplicación no termina por sí misma puede bloquear el Sistema, razón por la que se necesita la "cooperación" de la aplicación para que el Sistema funcione correctamente . El usuario puede estar ejecutando diversos programas simultáneamente, siendo muy fácil saltar de uno a otro. Por ejemplo, puede estar escribiendo un documento con un procesador de texto y simultáneamente, estar consultando ciertos datos que necesita en el Navegador de Internet o en una hoja de cálculo. Incluso puede estar ejecutando al mismo tiempo diversas activaciones de un mismo programa. En estas condiciones, cuando ejecutamos nuestro programa, debe compartir recursos con todas las demás aplicaciones en ejecución. Por ejemplo, nuestras órdenes de impresión no están ya directamente dirigidas al "puerto" de impresora; en cambio se "lanza" una tarea a un programa (monitor de tareas) que recibe la petición y la encola en un proceso batch que es el que en realidad gobierna la impresión. Esto hace que en general no sea ya posible utilizar rutinas y operaciones E/S de "bajo nivel", pues estos asuntos son controlados directamente por el Sistema Operativo, todo lo más que se puede hacer es realizar "peticiones" al sistema de determinados servicios siguiendo las convenciones (a veces muy complejas) establecidas en cada caso. Tenga en cuenta que por ejemplo, en un programa Windows, los datos de la propia ventana en la que corre el programa están en una zona de memoria gobernada por el Sistema, y que esta información queda oculta al programa, que debe limitarse a manejar los mensajes que le llegan desde aquel. Nota: En el capítulo dedicado al SO Windows ( 0.7) puede verse un punto de vista complementario, como maneja este Sistema las aplicaciones que corren en él. La totalidad de los recursos, tales como el procesador, la memoria, los puertos E/S etc. no están ya a nuestra disposición. Es el SO el que los controla, procurando que nuestro programa no haga algo equivocado que pueda dañar el resto de las aplicaciones. Cuando por error un programa intenta salir del ámbito que se le ha asignado (por ejemplo escribiendo en una zona de memoria equivocada), es el SO el que nos alerta. Puede tratarse del clásico mensajito: Este programa ha realizado una operación no válida y será interrumpido. Si el problema persiste consulte con el proveedor del programa (Horror, si soy yo mismo...). Como puede suponerse, las implicaciones para el programador que trabaja sobre uno de estos entornos son enormes, puesto que además del conocimiento de un lenguaje de programación adecuado (C++ por ejemplo), se exige el conocimiento de la interfaz; lo que se denomina la API (Application Programmer Interface) del sistema ( 1.7.1). El resultado de que sea el propio SO el que controla las E/S del programa, es que el funcionamiento de este es en realidad un diálogo continuo con el Sistema, del que recibe determinada información (entradas), y del que solicita determinados servicios (salidas). §4.3 Trabajo en multi-tarea: El programa puede tener más de una vía, hilo o hebra (thread) de ejecución secuencial; es multi-hebra ("multithread". Coloquialmente decimos que puede hacer varias cosas al mismo tiempo. Esto significa que el programa puede recorrer diversas vías de ejecución simultanea (a cada una de estas vías o caminos de ejecución lo denominamos una "tarea". El programador debe controlar dos o más vías de ejecución paralela que pueden estar o no sincronizadas entre si ( 1.7.2). En rigor solo los equipos multiprocesador son capaces de realizar una auténtica multitarea; los dotados de un solo procesador son capaces de realizar una suerte de simulación de tiempo compartido, siempre que el SO y el lenguaje utilizado están habilitados para ello, aunque desde el punto de vista de la lógica de la aplicación este detalle sea inapreciable. Aunque el hardware sea adecuado, no todos los Sistemas Operativos son capaces de soportar este tipo de ejecución. Por ejemplo, Windows 95 solo simula la multitarea, incluso si el hardware es adecuado. Ver algunos comentarios a estos conceptos: Multiprograma & multitarea ( 1.7b). §4.4 Control de ejecución orientado a "Eventos" Aparte de los otros tópicos que se mencionan en este capítulo, la forma en que se controla la ejecución de un programa para un entorno "moderno" (tipo Windows-32) es bastante distinta de la utilizada en los entornos "clásicos". Este cambio es el que suele provocar mayor desconcierto inicial, y sobre el que quizás encuentre menos información (existen muchos libros sobre Programación Orientada a Objetos y comparativamente pocos sobre Programación Orientada a Eventos). Es en mi opinión, la adaptación mental más trabajosa. El programador clásico se ve obligado a "cambiar el chip" por otro radicalmente distinto. En contra de lo que ocurre en la programación "tradicional", en la que es el propio programa el que decide que se hace y cuando, en la programación orientada a eventos la ejecución está controlada por el SO. El programa se parece a un bucle que espera continuamente la recepción de mensajes del Sistema y responde en consecuencia ejecutando determinados procesos. Los procesos pueden incluir peticiones de nueva información, o solicitud de determinados servicios. Los mensajes del Sistema tienen su origen en eventos (sucesos) de etiología muy distinta. Por ejemplo, una interrupción del teclado; del ratón, que puede hacer clic o doble clic en cualquier área de la ventana de la aplicación, o simplemente pasar sobre una zona determinada. Incluso la terminación del programa ocurre cuando se recibe una petición del sistema en este sentido (porque se ha hecho clic en el botón de cerrar la aplicación, o por cualquiera de los otros procedimientos típicos en las aplicaciones gráficas). Ver algunas matizaciones a estos conceptos en "Controlar un programa" ( 1.7a). §4.5 Programación Orientada a Objetos Los lenguajes empleados utilizan los recursos de esta técnica de programación ( 1.1). Las principales ventajas, aparte de un mejor encapsulamiento de los datos y sus operaciones, son la herencia; la sobrecarga, y el polimorfismo. Virtualmente no existe límite a la complejidad de los "nuevos" tipos de datos que pueda crear el programador, ni de sus operaciones. Aparte de las ventajas genéricas antes enunciadas, la POO está especialmente indicada para la programación en los nuevos entornos operativos, porque los nuevos paradigmas de programación para entornos distribuidos (Redes) conciben las aplicaciones, y sus relaciones con el mundo exterior, como un mundo de objetos que dialogan y transaccionan ( 1.7.1). Un mundo del que solo es necesario conocer las reglas de diálogo y transacción, y este es justamente uno de los paradigmas de la POO. Inicio. ________________________________________ La verdad es que el término "moderno" me produce un cierto rubor. Al paso que van la ciencia y tecnología informáticas, lo "moderno" de hoy seguramente nos producirá una sonrisa de conmiseración dentro de muy pocos años. Se trata solo de una "pincelada" sobre el tema, dado que para programar en un entorno actual como Windows-32, no solo es necesario conocer un lenguaje dotado de un compilador para 32 bits (a ser posible orientado a objetos, como C++), también un montón de otros conceptos que desgraciadamente no están en un solo libro. El resultado es que el programador proveniente de un entorno "tradicional" (modo texto naturalmente), se enfrenta a algo que no se parece a nada que haya visto anteriormente. Aunque se utiliza con frecuencia el término "procedural" para referirse a la programación clásica (no orientada a objetos) y no encontramos una descripción mejor, ni nos gusta ni nos parece acertada la palabra. Preferiríamos decir Programación orientada a tipos (de datos) predefinidos y operaciones fijas (Fixed Type Oriented Programming FTOP?). Evidentemente la MFC no es la única librería para programar en Windows; solo tiene la ventaja de ser del mismo fabricante (que no es poco) y de que es constantemente actualizada para recoger las implementaciones introducidas en estos SOs. Actualmente comprende unas 200 clases que encapsulan la práctica totalidad de la API de Windows; además va más allá de ser una simple colección de clases preconstruidas. Constituye en sí misma un completo entorno de desarrollo. "Canvas": Espacio gráfico que representa un objeto en pantalla. Es una abstracción utilizada en la programación de aplicaciones de interfaz gráfica (como Windows) que encapsula el contenido del dispositivo gráfico. El dibujo más pequeño que puede representarse en pantalla: un punto. Puede ser monocromo (un color o ausencia de él); de escala de grises, desde el blanco a negro pasando por diversos tonos de gris, o en color. En este último caso el pixel está realmente constituido de tres puntos, cada uno de un color básico (rojo verde y azul). En los entornos Windows de 16 bits (W-95) quedan vestigios de este funcionamiento cooperativo; son los momentos en que el cursor se transforma en un reloj de arena y no podemos hacer nada hasta que termina cierto proceso. En los entornos Windows de 32 bits, en los que el funcionamiento es preemptivo, sigue saliendo el reloj de arena cuando un proceso está ocupado, pero puede darse foco a otra aplicación con un clic de ratón.
C++ (VigesimoCuarta parte) Programacion actual Controlar un programa §1 Comentario Los conceptos sinópticamente expuestos en la página anterior ( 1.7) no deben ser tomados de forma absoluta. Por ejemplo, cuando decimos que "en la programación clásica es el propio programa el que decide que se hace y cuando", mientras que en la orientada a eventos "la ejecución está controlada por el SO", puede inducir a confusión al principiante, así que la cuestión quizás merezca un comentario. §2 Una arquitectura de capas superpuestas En un ordenador actual, el software está construido por capas (tanto física como conceptualmente). Las capas más profundas son en realidad mitad software, mitad hardware. Son el firmware; programas construidos sobre silicio (precableados como en las primeras computadoras). Este "soft" no puede ser alterado, está incluido en el propio procesador y en el juego de circuitería asociada (el denominado "chipset". A continuación se sitúan los "drivers", controladores de dispositivos. Estas piezas de software, verdaderas interlocutoras entre el software y el hardware, son específicas para cada SO y hardware particular. Una capa más arriba se encuentra el Sistema Operativo (SO); conjunto de programas encargado de gobernar y controlar el funcionamiento del ordenador. La parte inferior del SO se apoya en los "drivers", a los que traslada diversas peticiones y de los que recibe determinadas señales. Su parte superior sirve a su vez de soporte a la última capa de Software, los programas de aplicación. Estos son los que hacen tareas directamente útiles para el usuario; en esta última categoría se incluyen, por ejemplo un Procesador de textos, un programa de Contabilidad, un Navegador para la Web, etc. En realidad, el SO es el auténtico director de la "orquesta", el que dirige el funcionamiento del conjunto de forma ordenada y armónica. Todas las decisiones importantes pasan por sus manos; por supuesto también las Entradas y Salidas (E/S). Todos los programas están gobernados por entradas/salidas (es justamente lo que hace posible que el usuario los controle). Así pues, tanto un programa tradicional (bajo MS DOS por ejemplo) como un programa "moderno" (bajo Windows), están gobernados por E/S, y estas pasan siempre por el control del Sistema. La diferencia entre ambos tipos de sistemas (antiguos y modernos), es en realidad una cuestión de cantidad y de elaboración. En un entorno tradicional, esta información de E/S pasa por el SO sin apenas intervención. Con raras excepciones las E/S pasan directamente "tal cual" a la aplicación (en realidad no pueden interesar a nadie más, ya que la aplicación es única). Sin embargo, aunque mínimo, este control del Sistema es omnipresente. Por ejemplo, incluso en un entorno MS DOS, la pulsación Ctrl+Break detiene el programa en ejecución, ya que la señal producida por esta combinación de teclas es interceptada por el Sistema que obliga al programa a terminar. En el caso de los Sistemas multi-programa tipo Windows o Linux, las señales de entrada sufren una mayor elaboración. El Sistema las preprocesa y las envía a la aplicación adecuada. Por ejemplo, si se trata de un clic de ratón, se envía un mensaje al proceso correspondiente a la ventana sobre la que se ha pulsado . Si además (supongamos) el clic se ha efectuado sobre el botón de cerrar la aplicación, este clic es transformado en un mensaje al programa para que termine . A su vez, si la aplicación no sabe que hacer con el mensaje, la devuelve al Sistema para que este ejecute cierta acción por defecto. Nota: Esta redirección de las señales de entrada puede ser puesta de manifiesto fácilmente. Por ejemplo, ponemos en marcha un procesador de texto en una ventana y observamos como, mientras tiene foco (es la aplicación activa ), las sucesivas pulsaciones de teclas son registradas; parece que "escribimos" directamente en la ventana, ya que las señales son enviadas por el Sistema a la aplicación. Cuando esta pierde foco, por ejemplo, pulsado con el ratón en un área del escritorio no ocupada por ninguna otra aplicación, la ventana deja de recibir los mensajes de las teclas. Aunque sigamos "escribiendo" en el teclado parece que las pulsaciones se pierden. En realidad siguen siendo recibidas por el Sistema, pero no las redirecciona a ninguna aplicación. A menos que tengan un sentido específico (sea alguna combinación de teclas de uso definido, un atajo, "short-cut", estas señales se pierden. Son descartadas si el Sistema no sabe que hacer con ellas. Inicio. ________________________________________ Si se trata de una aplicación Windows este mensaje corresponde a una constante manifiesta: WM_QUIT (petición de que termine la aplicación). Generalmente la barra de título de la ventana adopta otro color mientras que la aplicación está activa (tiene foco). La "ventana" que vemos en pantalla, es la representación gráfica y parte de la interfaz de un proceso. Multiprograma & Multitarea §1 Comentario Los conceptos someramente expuestos en la página anterior pueden ser desde luego objeto de controversia. Además, desgraciadamente la nomenclatura informática no es demasiado consistente al respecto, y la cuestión se complica un poco más si consideramos la traducción del inglés aplicada en cada caso . Así pues, los conceptos, programa, proceso ("Process" y hebra ("Thread", y sus respectivos "plurales" (multi-programa; multrproceso y multihebra) distan mucho de estar tan claros como sería deseable. A veces, las diferencias entre los conceptos son sutiles, y dependen de la perspectiva o punto de vista adoptado para considerar la cuestión. Como botón de muestra incluimos un par de párrafos (respetando el original inglés). El primero, del libro "Programming Windows"; un clásico de Charles Petzold ( 7). Paj. 1197 Multitasking is the ability of an operating system to run multiple programs concurrently. Basically, the operating system uses a hardware clock to allocate "time slices" for each currently running process. If the time slices are small enough—and the machine is not overloaded with too many programs trying to do something—it appears to a user as if all the programs are running simultaneously. Multitasking is nothing new. On large mainframe computers, multitasking is a given. These mainframes often have hundreds of terminals attached to them, and each terminal user should get the impression that he or she has exclusive access to the whole machine. In addition, mainframe operating systems often allow users to "submit jobs to the background," where they are then carried out by the machine while the user can work on something else. Multitasking on personal computers has taken much longer to become a reality. But we now often seem to take PC multitasking for granted. As I'll discuss shortly, to some extent the earlier 16-bit versions of Microsoft Windows supported multitasking but in a somewhat limited capability. The 32-bit versions of Windows all support both true multitasking and—as an extra bonus—multithreading. Multithreading is the ability for a program to multitask within itself. The program can split itself into separate "threads" of execution that also seem to run concurrently. This concept might at first seem barely useful, but it turns out that programs can use multithreading to perform lengthy jobs in the background without requiring the user to take an extended break away from their machines. Of course, sometimes this may not be desired: an excuse to take a journey to the watercooler or refrigerator is often welcome! But the user should always be able to do something on the machine, even when it's busy doing something else. El segundo, del libro "Secure Programming for Linux and Unix HOWTO" de David A. Wheeler www.dwheeler.com: In Unix-like systems, user-level activities are implemented by running processes. Most Unix systems support a "thread'' as a separate concept; threads share memory inside a process, and the system scheduler actually schedules threads. Linux does this differently (and in my opinion uses a better approach): there is no essential difference between a thread and a process. Instead, in Linux, when a process creates another process it can choose what resources are shared (e.g., memory can be shared). The Linux kernel then performs optimizations to get thread-level speeds; see clone(2) for more information. It's worth noting that the Linux kernel developers tend to use the word "task'', not "thread'' or "process'', but the external documentation tends to use the word process (so I'll use the term "process'' here). When programming a multi-threaded application, it's usually better to use one of the standard thread libraries that hide these differences. Not only does this make threading more portable, but some libraries provide an additional level of indirection, by implementing more than one application-level thread as a single operating system thread; this can provide some improved performance on some systems for some applications. §2 En rigor, todo el Software que se ejecuta en el ordenador puede ser considerado un "programa", más o menos complicado, cuya finalidad es una u otra. De forma general, entendemos un "Programa" como un conjunto de instrucciones organizadas de forma que dirigen al hardware para realizar una tarea determinada. En este sentido, tanto un "Driver" como el propio Sistema Operativo, y los programas de aplicación son "programas" ; se trata por tanto de un concepto muy general. A los efectos que nos ocupan asimilaremos el concepto de programa con un fichero (ejecutable) que cuando es llamado a ejecución origina un proceso (en los sistemas Unix el concepto de proceso está ligado a la actividad de un usuario). §3 Un procesador actual solo puede ejecutar un programa cada vez. Esto, con independencia de que su arquitectura interna permita un cierto grado de "paralelismo" en las operaciones. Por ejemplo, que pueda estar realizando una operación con los registros y al mismo tiempo estar accediendo a una posición de memoria para traer el próximo dato. Así pues, en sentido estricto un ordenador con un solo procesador será siempre monoprograma. Cuando decimos que un SO como Windows es multiprograma, aunque se esté ejecutando en una máquina con un solo procesador, en realidad se habla en sentido figurado. No se ejecutan diversos programas de forma "simultanea" sino alternada; todo lo más diríamos: de forma "concurrente". En estos casos, lo que ocurre en realidad es que el procesador está asignando tiempo de ejecución a diversos programas según un determinado criterio (que puede ser muy sofisticado). La percepción es que varios programas se ejecutan de forma simultanea, aunque la verdadera naturaleza de la operación es lo que se denomina tiempo compartido ("time sharing" . Para que exista una verdadera multiprogramación se necesita un hardware con dos o más procesadores, y un SO que sepa sacar partido de esta circunstancia. En estas condiciones si podemos afirmar con propiedad que se ejecutan simultáneamente varios programas. Nota: en lo que respecta al hardware, esta última es la situación actual en aquellas máquinas con dos procesadores. Por ejemplo las placas-base Intel dual Xeon, o bien con un solo procesador "dual". Por ejemplo, los Intel Core™ Duo. §4 El concepto multihebra ("multithreading" es de tipo lógico. Significa que un programa puede tener varias vías de ejecución que "pueden" ser independientes. Por ejemplo: Un procesador de texto puede tener una hebra que es utilizada por el usuario para escribir y otra, que a periodos de tiempo determinados, salva a disco el contenido de la memoria, obteniendo así una copia de seguridad. Según hemos visto (Wheeler ), desde el punto de vista físico, lo que caracteriza a las diversas hebras de un proceso es que comparten ciertos recursos dentro del mismo; por ejemplo la memoria, los ficheros abiertos y las variables estáticas, mientras que mantienen independencia en los recursos que les permiten una ejecución independiente, por ejemplo su propia pila ("Stack" 2.2.6) y el estado de los registros del procesador, que es salvado y restituido cada vez que la hebra es desactivada o activada. Lo importante aquí es que desde un punto de vista lógico, las hebras corren de forma simultanea e independiente (lo que no impide que pueda existir cierta coordinación entre ellas, de forma que, por ejemplo, una puede esperar hasta que otra ha terminado cierto trabajo). Que esta "simultaneidad" sea o no real no es lo más importante. En cualquier caso, la percepción del usuario es que todas corren simultanea e independientemente. El resultado de enfocar una aplicación como multiprograma o multihebra puede ser muy parecido, aunque cada solución tiene sus propias características. A título de ejemplo incluimos las explicaciones correspondientes al servidor Apache que existe en dos versiones: la versión 1.3 lanza un proceso por cada servicio que debe atender; la versión 2.0 utiliza un solo proceso con una hebra por cada servicio. Tomado de: Apache Overview HOWTO de Daniel Lopez Ridruejo www.tldp.org Apache 1.3 on Unix is a process-based Web server. The Apache program forks several children at startup. Forking means that a parent process makes identical copies of itself, called children. Each one of the children can serve a request independent of the others. This approach has the advantage of improved stability: If one of the children misbehaves (runs out of control or has memory leaks) it can be terminated without affecting the others. The stability comes with a performance penalty. In most Unix operating systems, creating processes and context switching (assigning processor time to each process) are expensive operations. Since processes are isolated from each other, they cannot easily share code and data, consuming system resources. Apache 2.0 abstracts the request processing architecture in special server modules, called Multi Processing modules (MPMs). This means that Apache can be configured to be a pure process-based server, a purely threaded server or a mixture of those models. Threads are contained inside processes and run simultaneously. Unlike processes, threads can share data and code. Threads are thus more "lighweight" than processes, and in most cases threaded servers scale better than process based servers. The disadvantage is that the server is less reliable, since if a thread misbehaves it can corrupt data or code belonging to other threads. §5 En realidad, en un ordenador de un solo procesador con un sistema operativo tipo Windows, solo corre un programa, el kernel del sistema; este programa reside permanentemente en memoria y es multihebra, puede tener varias vías de ejecución . Unas son internas del propio sistema e imprescindibles; otras comienzan y terminan a voluntad del usuario, son los programas de aplicación. Como decíamos, cada una de estas vías de ejecución recibe tiempo de proceso según determinados criterios. Desde la óptica del usuario, algunas de ellas son "programas". A su vez, desde la óptica del programador y del usuario, una de estas aplicaciones o "hebras" del sistema, puede ser multihebra. Inicio. ________________________________________ No olvidemos que el origen de estas denominaciones es casi siempre inglés; la mayoría de los desarrollos de la informática de los últimos 40 años tienen su origen en USA e Inglaterra. En cualquier caso, y en lo referente a este epígrafe, no pretendemos entrar en discusiones semánticas, sino todo lo contrario, fijar y clarificar algunas ideas al respecto. En el caso del SO (Windows o Linux por ejemplo), en realidad no es un solo programa, sino multitud de ellos que interactúan y se llaman unos a otros. Los primeros intentos de algo parecido a la multiprogramación actual fueron precisamente sistemas de tiempo compartido bastante rudimentarios, en los que la asignación de tiempos a cada una de las "tareas" se efectuaba a intervalos de tiempo fijos. El primer sistema serio, el CTSS (Compatible Time Sharing System), fue desarrollado en el MIT sobre la base de un equipo IBM 7094 convenientemente adaptado. Los autores ingleses utilizan "multitasking" y "multiprogramming" casi como sinónimos. Por ejemplo, refiriéndose al SO Windows, Petzold nos dice: "Windows 98 and Windows NT are 32-bit preemptive multitasking and multithreading graphical operating systems" (Petzold PW 5E paj. 6). Creemos que sería más afortunado decir "preemptive multiprogramming and multithreading". Nosotros preferimos utilizar los términos multiprograma y multitarea, empleando este último término como sinónimo de multihebra. El Kernel o nucleo del sistema se ocupa básicamente de tres tareas primordiales: Gestión de memoria; gestión de E/S a disco y control de las tareas en ejecución.Respecto a este último punto, dejemos que sean las autorizadas palabras de Thomas West, el que fuera director de investigación y desarrollo de Data General, el que nos lo explique: Very few people are delivering symmetric multiprocessing at this time (N. del A: en 1990). It's allowing us to get a wide range of functionality out of a single computer. You can add as many processors as you want, but unless you make them work symmetrically, two processors perform no faster than one. A server acting on the behalf of lots of clients is performing a lot of parallel tasks. The role of the operating system is to make sure those tasks get dispatched to the first available processor, but also to decide if they are to be preempted by a priority task. In the end, it's an exercise in scheduling. Todo el capítulo 20 de la mencionada obra está dedicada al "multitasking" y "multithreading". Aunque se refieren exclusivamente a los aspectos de programación multitarea y multihebra para Windows, es sin duda una buena fuente de información, con ejemplos detallados que incluyen la sincronización de hebras.
El lenguaje C++ "Learn not just the hows, but the whys too". Greg Comeau . §1 Generalidades C++ es un lenguaje imperativo orientado a objetos derivado del C . En realidad un superconjunto de C, que nació para añadirle cualidades y características de las que carecía. El resultado es que como su ancestro, sigue muy ligado al hardware subyacente, manteniendo una considerable potencia para programación a bajo nivel, pero se la han añadido elementos que le permiten también un estilo de programación con alto nivel de abstracción. Nota: estrictamente hablando, C no es un subconjunto de C++; de hecho es posible escribir código C que es ilegal en C++. Pero a efectos prácticos, dado el esfuerzo de compatibilidad desplegado en su diseño, puede considerarse que C++ es una extensión del C clásico. La definición "oficial" del lenguaje nos dice que C++ es un lenguaje de propósito general basado en el C, al que se han añadido nuevos tipos de datos, clases, plantillas, mecanismo de excepciones, sistema de espacios de nombres, funciones inline, sobrecarga de operadores, referencias, operadores para manejo de memoria persistente, y algunas utilidades adicionales de librería (en realidad la librería Estándar C es un subconjunto de la librería C++). Respecto a su antecesor, se ha procurando mantener una exquisita compatibilidad hacia atrás por dos razones : poder reutilizar la enorme cantidad de código C existente, y facilitar una transición lo más fluida posible a los programadores de C clásico, de forma que pudieran pasar sus programas a C++ e ir modificándolos (haciéndolos más "++" de forma gradual. De hecho, los primeros compiladores C++ lo que hacían en realidad era traducir (preprocesar) a C y compilar después (las consecuencias se dejan sentir todavía en el lenguaje 1.4.2). Por lo general puede compilarse un programa C bajo C++, pero no a la inversa si el programa utiliza alguna de las características especiales de C++. Algunas situaciones requieren especial cuidado. Por ejemplo, si se declara una función dos veces con diferente tipo de argumentos, el compilador C invoca un error de "Nombre duplicado", mientras que en C++ quizás sea interpretado como una sobrecarga de la primera función (que sea o no legal depende de otras circunstancias). Como se ha señalado, C++ no es un lenguaje orientado a objetos puro (en el sentido en que puede serlo Java por ejemplo), además no nació como un ejercicio académico de diseño. Se trata simplemente del sucesor de un lenguaje de programación hecho por programadores (de alto nivel) para programadores, lo que se traduce en un diseño pragmático al que se le han ido añadiendo todos los elementos que la práctica aconsejaba como necesarios, con independencia de su belleza o purismo conceptual ("Perfection, in some language theoretical sense, is not an aim of C++. Utility is" ). Estos condicionantes tienen su cara y su cruz; en ocasiones son motivo de ciertos "reproches" por parte de sus detractores, en otras, estas características son precisamente una cualidad. De hecho, en el diseño de la Librería Estándar C++ ( 5.1) se ha usado ampliamente esta dualidad (ser mezcla de un lenguaje tradicional con elementos de POO), lo que ha permitido un modelo muy avanzado de programación extraordinariamente flexible (programación genérica). Aunque C++ introduce nuevas palabras clave y operadores para manejo de clases, algunas de sus extensiones tienen aplicación fuera del contexto de programación con objetos (fuera del ámbito de las clases), de hecho, muchos aspectos de C++ que pueden ser usados independientemente de las clases . Del C se ha dicho: "Por naturaleza, el lenguaje C es permisivo e intenta hacer algo razonable con lo que se haya escrito. Aunque normalmente esto es una virtud, también puede hacer que ciertos errores sean difíciles de descubrir" ( Shildt). Respecto al C++ podríamos decir otro tanto, pero hemos de reconocer que su sistema de detección de errores es mucho más robusto que el de C, por lo que algunos errores de este serán rápidamente detectados. Desde luego, C++ es un lenguaje de programación extremadamente largo y complejo; cuando nos adentramos en él parece no acabar nunca. Justo cuando aprendemos un significado descubrimos que una mano negra ha añadido otras dos o tres acepciones para la misma palabra. También descubrimos que prácticamente no hay una regla sin su correspondiente excepción. Cuando aprendemos que algo no se puede hacer, hay siempre algún truco escondido para hacerlo, y cuando nos dicen que es un lenguaje fuertemente tipado ("Strong type checking", resulta completamente falso. A pesar de todo, ha experimentado un extraordinario éxito desde su creación. De hecho, muchos sistemas operativos , compiladores e intérpretes han sido escritos en C++ (el propio Windows y Java). Una de las razones de su éxito es ser un lenguaje de propósito general que se adapta a múltiples situaciones. Para comprobar el éxito e importancia de los desarrollos realizados en C++ puede darse una vuelta por la página que mantiene el Dr. Stroustrup al respecto: www.research.att.com. Tanto sus fervientes defensores como sus acérrimos detractores han hecho correr ríos de tinta ensalzando sus cualidades o subrayando sus miserias, aunque todo el mundo parece estar de acuerdo en que es largo y complejo. Ha servido de justificación para el diseño de otros lenguajes que intentan eliminar sus inconvenientes al tiempo que mantener sus virtudes (C# y Java por ejemplo), y una de sus última incorporaciones, las plantillas ( 4.12), ha sido origen de un nuevo paradigma de programación (metaprogramación). En mi opinión, cualquier lenguaje de propósito general que como C++, permita tocar ambos mundos, la programación de bajo nivel y altos niveles de abstracción, resultará siempre e inevitablemente complejo. Ocurre lo mismo con los lenguajes naturales que son también extraordinariamente complejos (esto lo saben bien los gramáticos). Cualquier comunicación entre humanos presupone una ingente cantidad de conocimientos y suposiciones previas entre los interlocutores. A pesar de lo cual, la comunicación exacta y sin ambigüedades entre dos personas no resulta fácil. §2 Consejos para mejorar el rendimiento Lo mismo que en su ancestro, en el diseño del C++ primó sobre todo la velocidad de ejecución del código . Tanto uno como otro representan los ejecutables más rápidos que se pueden construir para una máquina y circunstancias determinadas. En este sentido, la única alternativa de mejora es la codificación manual, el "pulido" de determinadas rutinas (o de todo el código) en ensamblador, aunque evidentemente esto es impracticable para aplicaciones medianamente grandes, a no ser que se disponga de todos los recursos y tiempo del mundo. Con todo, a pesar de ser un lenguaje intrínsecamente rápido, y de que los compiladores modernos son bastante "inteligentes" en este sentido (adoptan automáticamente las decisiones que resultan en el código de ejecución más eficiente), es mucho lo que puede hacer el programador para favorecer esta rapidez con solo adoptar algunas sencillas precauciones. Estos son los consejos: • Use enteros (int) con preferencia sobre cualquier otro tipo de variable numérica. En especial en los contadores de bucles. Las operaciones con enteros son del orden de 10 a 20 veces más rápidas que las de números en coma flotante. • Use operadores incremento y decremento ++/-- ( 4.9.1) • Use variables de registro, en especial en los bucles críticos, sobre todo si son anidados ( 4.1.8b). • Use aritmética de punteros frente a subíndices de matrices ( 4.2.2). • En problemas de computación numérica recuerde que el cálculo de funciones trascendentes es por lo general muy lento. • Use referencias para argumentos y valores devueltos en funciones, antes que objetos "por valor" ( 4.2.3) • Al definir clases utilice al mínimo las funciones virtuales ( 1.4.4; 4.11.8a), así como los punteros a funciones-miembro ( 4.2.1g) o Tenga en cuenta lo señalado respecto al rendimiento al tratar de: o Sustituciones inline en funciones definidas por el usuario ( 4.4.6b) • Preste atención al modo de uso de aquellas funciones de librería que se presentan en dos versiones ( 5.1 Funciones y macros) Los compiladores modernos permiten fijar que criterio de optimización será dominante: La velocidad de ejecución o el tamaño. Tanto Borland C++ ( 1.4.3) como MS Visual C++ utilizan la misma convención de llamada para este propósito (opciones -O2 o -O1 respectivamente). Por su parte, GNU gcc dispone de varias opciones de optimización. En particular, la opción -Os adopta las medidas tendentes a reducir el tamaño del código resultante. Nota: Aparte de las decisiones de optimización que puedan adoptar automáticamente los compiladores y las reglas de precaución anteriores, las modernas "suites" ofrecen herramientas de análisis que permiten conocer de forma objetiva como es la utilización de recursos dentro del programa, de forma que se puedan adoptar precauciones en las zonas que resulten más costosas, y concentrar nuestros esfuerzos de optimización en la zonas donde resulten más provechosos. El compilador GNU cpp dispone de la utilidad gcov, que ofrece estadísticas para analizar el rendimiento del código. Entre otros datos: • Cómo se ejecuta cada línea de código • Qué línea se está ejecutando actualmente. • Cuanto tiempo consume cada sección de código • Posibilidad de análisis a nivel de fichero o de función Inicio. ________________________________________ Los lenguajes imperativos son aquellos en los que se especifica cómo conseguir los objetivos que se persiguen. C; C++; Javascript y Perl, entre otros muchos, pertenecen a esta categoría. Por contra, los lenguajes declarativos son aquellos en los que se especifica que objetivo se persigue, sin preocuparse por el cómo. SQL y HTML son quizás los ejemplos más representativos de esta categoría. El propio Stroustrup reconoce en el prólogo de su primer libro sobre C++: "Se ha dado gran importancia a mantener compatibilidad con C .... C++ debe la mayor parte a C. C es mantenido como un subconjunto" ( Stroustrup 1987). De hecho, el mentado compilador Zortech C++, se ofrecía como: "The World's first 'TRUE' C++ compiler for MS-DOS". Stroustrup señala que una de las razones que llevaron a K&R a diseñar el C fue no tener que programar en ensamblador (*), y que en el diseño de C++ se cuidó no perder las ventajas ganadas en este sentido. Precisamente comenzó a desarrollar C++ como un lenguaje que le ayudara en el trabajo de diseño de un Sistema Operativo distribuido. (*) En ocasiones, C ha sido considerado como un "lenguaje ensamblador de alto nivel". "You are not doing OO just because you are compiling with C++" Ian Joyner "C++??" ( 7). Bjarne Stroustrup. Entrevista con Al Stevens en Dr. Dobb's Journal Sept. 1992. Entrevista en "C++ Pointers and Dynamic Memory Management" de Michael C. Daconta. Edit. John Wiley & Sons, Inc. ISBN 0-471-04998-0 1.2.1 Léxico y conceptos fundamentales "Este ha sido mi último curso. Me acabo de jubilar después de cerca de 40 años dedicado a la enseñanza. A pesar de la poca consideración que mi profesión ha tenido económica y socialmente, acabo con la misma ilusión con la que comencé. E igualmente convencido de la trascendencia de la tarea de educar. Sólo me duele un poco el comprobar la menor eficacia que ésta ha tenido estos últimos años. ¿Cuáles han sido las causas? Ni la mentalidad hedonista que rehuye cualquier tipo de sacrificio, ni el permisivismo de los padres que satisfacen todos los caprichos de los hijos, ni los planes de estudios que se fundamentan en una visión lúdica de la educación, ni las políticas educativas con pretensiones de progresismo que priman el uniformismo sobre la búsqueda de la excelencia han favorecido en absoluto la cultura del esfuerzo, imprescindible en cualquier proceso personal de mejora, como es la educación". Federico Gómez Pardo en "La voz del Lector" de "El Confidencial Digital" (22-06-2006) ECD. §1 Presentación Creo que buena parte de la dificultad del principiante respecto a algunos conceptos del lenguaje C++, y de muchas otras áreas de la ciencia informática, proviene de un conocimiento incompleto o vago de algunos conceptos fundamentales. En realidad se trata de una cuestión semántica; de conocer el significado exacto de algunas palabras; de disponer de un vocabulario mínimo que sirva de soporte para entender el resto. Ocurre con frecuencia que los textos informáticos están plagados de términos que supuestamente son de uso común, pero que en realidad no lo son tanto y cuyo significado tampoco está claramente explicado en ningún sitio. Se dan por sabidos, pero las más de las veces el estudiante es incapaz de verbalizar correctamente su significado. Es importante que este "corpus" mínimo sea conocido sin ambigüedades, de forma que los conceptos construidos sobre él no tengan fisuras y resulten de una solidez conceptual a toda prueba. Estas cuestiones semánticas son evidentemente el objeto fundamental de los gramáticos y de los puristas del lenguaje (aunque sean lenguajes artificiales como los de programación), de forma que en el caso del C++, la primera preocupación del Estándar es definir sin ambigüedad una serie de términos. Introduciré aquí algunos de los que creo más importantes, en la seguridad de que su conocimiento atañe no solo al lenguaje C++, sino al acerbo cultural de cualquier interesado en esta disciplina. §2 Algunos conceptos RTFM Siglas por las que se conoce un principio universal y de gran interés en la ciencia informática actual . Escribir un programa es establecer el comportamiento de una máquina mediante una serie de algoritmos que definirán su funcionamiento ( 1.4). Según el DRAE (Diccionario de la Real Academia Española de la Lengua) un algoritmo es un conjunto ordenado y finito de operaciones que permite hallar la solución de un problema . En informática se utiliza en el sentido de un conjunto ordenado y finito de instrucciones que gobiernan el comportamiento de una máquina para conseguir un comportamiento determinado. Las instrucciones se expresan en un lenguaje artificial (inventado conscientemente por el hombre) denominado lenguaje de programación. Al llegar a cierto grado de madurez y universalidad algunos lenguajes son estandarizados, lo que significa que un comité internacional ( 1) se encarga de establecer sus reglas de uso. En el caso de los lenguajes (naturales o artificiales) estas reglas constituyen lo que se denomina su gramática. El lenguaje C++ alcanzó oficialmente esta madurez y universalidad en 1989, de forma que a partir de entonces, el Estándar C++ establece la gramática del lenguaje que nos ocupa. Aunque todos los lenguajes de programación tienen su "Gramática", el enfoque respecto a la misma varía grandemente de unos a otros. En ciertos casos, como en C++, estas reglas son tremendamente estrictas, lo que tiene sus pros y sus contras. Por ejemplo, una gramática rígida permite afinar mucho en los matices de lo que deseamos hacer; manipular los conceptos (principalmente los datos) con gran precisión, y ser advertidos de posibles errores involuntarios. Pero hay que estar muy atento a los detalles del código, y hasta que no se tiene cierta práctica, los mensajes de aviso y errores del compilador pueden desesperar a cualquiera. Otro extremo está representado por los lenguajes de sintaxis más o menos "borrosa". Por ejemplo JavaScript o Perl, en los que el compilador, o intérprete, pretende adivinar qué se supone que queremos hacer. Este comportamiento se conoce como DWIM ("Do what I mean" . La gramática se concreta en una serie de reglas y condiciones, aunque puede haber otras. Por ejemplo, de estilo. Las que imprescindiblemente debe cumplir un programa C++ para ser correcto, son de dos clases: semánticas (de significado) y sintácticas (de forma 1.3.1a). Justamente es el contenido de estas normas lo que hace que un programa C++ sea distinto de un programa Java o Fortran por ejemplo. Las reglas sintácticas son muchas, pero comienzan por las que definen el alfabeto del lenguaje. Es decir: el conjunto de grafos que pueden utilizarse para escribir su código. C++ permite utilizar un alfabeto de 96 caracteres ASCII en la escritura de sus programas. De ellos 91 son los caracteres gráficos (representables por un grafo) que se incluyen en la tabla; los cinco restantes son caracteres no representables: Espacio; tabulación horizontal; tabulación vertical; salto de formato y nueva línea. Caracteres gráficos del alfabeto C++: a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9 _ { } [ ] # ( ) < > % : ; . ? * + ¬/ ^ & | ~ ! = , " ’ La norma C++ establece que cuando en un programa no se respetan las formas (reglas sintácticas) el compilador debe lanzar un mensaje de error o indicación de que no entiende lo que queremos decir. Por ejemplo, el caso de que olvidemos el punto y coma al final de una sentencia. Sin embargo, con las reglas semánticas no ocurre siempre así. El estándar distingue dos tipos de reglas de significado: diagnosticables y no diagnosticables. En cuanto a las primeras, denominadas reglas de significado diagnosticable ("Diagnosable semantic rules", la Norma establece que en caso de infracción, el compilador enviará al menos un mensaje de error. En el caso de las segundas no se requiere esta exigencia. Nota: en principio, todo el conjunto de reglas semánticas definidas en el Estándar pueden considerarse de significado diagnosticable, a excepción de aquellas que tienen una indicación explícita de que no necesitan diagnóstico. Las reglas están definidas de modo que cuando se establece una condición diagnosticable y la forma del programa no cumple este requisito, deberá generarse un mensaje de error correspondiente. Sin embargo, cuando se establece un requisito diagnosticable para un dato y este no se cumple durante la ejecución, el Estándar no establece ningún comportamiento especial para el programa. Comportamiento dependiente de la implementación: Cuando el comportamiento de un programa correcto con datos correctos, depende de la implementación. Este comportamiento debe estar documentado en cada implementación. Comportamiento indefinido: Es el comportamiento de un programa en aquellas circunstancias o situaciones para las que el Estándar no especifica un comportamiento específico. Sería el caso del comportamiento en runtime de un programa que recibe datos incorrectos. Observe que, según lo indicado al principio, muchos casos de programas erróneos no generan un comportamiento indefinido, sino un aviso de diagnóstico. Implementación: Un compilador concreto para una plataforma determinada. Límites de implementación: Restricciones impuestas a los programas por una implementación. Programa correcto ("Well-formed": Programa C++ construido de acuerdo con las reglas sintácticas de este lenguaje, con reglas de semántica diagnosticable, y que sigue la regla de una sola definición ( 4.1.2). Un programa incorrecto ("Ill-formed" es el que no sigue los condicionantes anteriores. Un identificador es un conjunto de caracteres alfanuméricos de cualquier longitud que sirve para identificar las entidades del programa. Los identificadores pueden ser combinaciones de letras y números, y cada lenguaje tiene sus propias reglas que definen como pueden estar construidos ( 3.2.2 Identificadores C++). Nota: algunas combinaciones de caracteres, las denominadas palabras-clave ("Keywords", tienen un significado específico para el lenguaje y no pueden ser utilizadas en otro contexto ( 3.2.1). Generalmente los identificadores se introducen en el programa para designar una entidad; entonces se dice de ellos que son un nombre. C++ permite utilizar el mismo identificador para designar entidades diferentes a condición de que estén en ámbitos ( 4.1.3) diferentes, o que sean funciones con distinto número y/o tipo de argumentos. Nota: otro tipo especial de identificadores lo constituyen las etiquetas; en vez de representar entidades sirven para significar sentencias . Se utilizan para identificar el destino en sentencias de salto ( 4.10.4). La introducción de un nombre en el programa puede efectuarse mediante alguna de estas formas: • Si el nombre representa a una entidad, mediante una declaración ( 4.1.2). • Si el nombre representa a una sentencia (es una etiqueta), mediante una sentencia etiquetada ( 4.10.1) o una instrucción de salto goto . Que un mismo nombre utilizado en dos unidades de compilación designe o no a una misma entidad, depende del tipo de enlazado que tenga el identificador en cada una de ellas ( 1.4.4). Cuando compilador encuentra un identificador en un programa, supone que es un nombre o una etiqueta y antes de realizar el "parsing" ( 1.4), intenta determinar a que entidad representa. Esta operación, que asocia sin ambigüedad cada nombre con una declaración de dicho nombre, se denomina búsqueda de nombres ("Name-lookup". Si al final del proceso no ha aparecido la declaración correspondiente entonces el programa es incorrecto ("Ill-formed" ). Cuando el identificador corresponde a una función, puede ocurrir que el "Name-lookup" asocie un mismo nombre con más de una declaración. Es el caso de funciones sobrecargadas, en las que un mismo nombre corresponde a varias definiciones de la "misma" función. En estos casos, después de esta primera búsqueda tiene lugar otro proceso denominado de resolución de sobrecarga ("Overload resolutión" 4.4.1a) en el que se intenta averiguar a que definición concreta corresponde el identificador. El proceso de búsqueda de nombres sigue un conjunto relativamente extenso de reglas que cubren todas las posibilidades que pueden presentarse; además existen modificadores que alteran la forma de esta búsqueda, los denominados especificadores o modificadores de acceso. Ver en la hoja adjunta una somera descripción del proceso ( Name-lookup). El concepto de entidad es muy amplio; corresponde a: un valor; clase; elemento de una matriz; variable; función; miembro de clase; instancia de clase; enumerador; plantilla, o espacio de nombres del programa (más detalles en 4.1.1). Un objeto es una entidad a la que corresponde una zona de almacenamiento , razón por la que se dice de ellos que tienen existencia real o física. Los objetos son introducidos en el programa mediante una declaración, y creados mediante una definición ( 4.1.2). Gozan de una serie de propiedades que vienen definidas desde el momento de su creación, entre ellas está la duración de su almacenamiento, que tiene influencia en su ciclo vital ("Lifetime", y puede ser de tres tipos: estática, dinámica y automática ( 4.1.5). Otra propiedad no menos importante de los objetos es su tipo, que también viene determinado desde el momento de su creación ( 2.2). Las entidades del programa pueden tener otros atributos o propiedades cuya comprensión es igualmente importante: dirección (Lvalue) y valor (Rvalue). Sus características se detallan en el apartado correspondiente ( 2.1), pero adelantemos aquí que Rvalue se asigna al concepto de "valor"; algo que tenga valor es un Rvalue (o tiene esta propiedad). Por su parte Lvalue se asimila al concepto de dirección; espacio de almacenamiento que pueda recibir un valor . Una entidad puede tener uno o ambos de estos atributos. Por ejemplo: char func () { ... } char c = '5'; const int k = 2 + 3; La función func puede ser considerada un Rvalue en el sentido que devuelve un valor: también la variable c; la constante k, o la expresión 2 + 3 pueden ser consideradas Rvalues porque tienen o representan un valor. Tanto la variable c como la constante k son también Lvalues, en el sentido que ambas disponen de una dirección y un espacio de almacenamiento correspondiente (que en el primer caso puede ser alterado y en segundo no). Sin embargo, ni la función func, ni las expresións '5' o 2 + 3 pueden considerarse como tales porque no les corresponde una dirección o espacio de almacenamiento donde pueda colocarse un Rvalue. En este contexto tiene sentido decir que un Lvalue modificable es aquel que puede cambiar el valor contenido en su almacenamiento. Una expresión es un conjunto de operadores, operandos y signos de puntuación que especifican una computación (suponen una secuencia de instrucciones concretas para la máquina); generalmente, como resultado de esta computación producen un valor, aunque también, como consecuencia de ella pueden producirse otros efectos denominados efectos laterales ( 1.3.1). Inicio. ________________________________________ Cuando a principios de los 70 la programación no-estructurada cayó en descrédito, las etiquetas, que en forma de número de sentencia acompañaba a estas en algunos lenguajes, desaparecieron. Como a pesar de todo en algunas ocasiones persiste la necesidad de acudir al denostado GOTO, se introdujeron estos identificadores para señalar el destino de estos saltos imprescindibles (las sentencias catch del mecanismo de excepciones son otro vestigio de aquello). El lenguaje C++ establece que antes de utilizar un identificador hay que declararlo, excepto en el caso de etiquetas ("Labels". Observe que esta definición es más amplia que la tradicional de la POO, en la que un objeto es la instancia de una clase. El término algoritmo y la propia ciencia del álgebra, son otra aportación cultural que debemos al Islám. Esta ciencia debe mucho a Muhammad Ibn Musa, que fue miembro de la “Casa de la Sabiduría”, una notable academia científica de Bagdad en época del califa Al-Ma'amun (813-833). El propio vocablo "algoritmo" deriva del sobrenombre "Al-Khwarizmi" de Ibn Musa. Los algoritmos informáticos siguen de cerca el concepto matemático de función. Aunque estas últimas pueden exigir un número infinito de pasos para llegar a la solución. Acrónimo inglés de "Read the fucking manual". El creador del lenguaje nos informa que generalmente es una buena idea y que la "F" es muda (no se pronuncia). Glosario de términos C++ de Bjarne Stroustrup www.research.att.com Formalmente un Lvalue es una expresión que se refiere a un objeto o a una función (una invocación a función es un Lvalue solo si el valor devuelto es una referencia). Un Lvalue es modificable si no se refiere a una función, una matriz, o un objeto constante. Desde luego, lo ideal sería un comportamiento DWIW ("Do What I Want", pero ya hemos señalado que los lenguajes actuales son del tipo DWIS ("Do What I Say". Quizás lo contrario sería extremadamente peligroso... 1.3 Estructura de un programa "The input of computation is energy and information; the output is order, structure, extropy" Kevin Kelly: "God Is the Machine" en Wired Magazine www.wired.com §1 Sinopsis La estructura de un programa es una cuestión que puede ser abordada desde varios puntos de vista, en este capítulo consideramos solo dos: Componentes lógicos: se refiere a los diversos elementos que componen una aplicación, desde los más complejos hasta los más simples. Si comparamos un programa con un edificio, los elementos irían desde el edificio como un todo, a los ladrillos (sus elementos más pequeños) 1.3.1. Almacenamiento: se refiere a como están alojadas sus partes en la máquina que lo ejecuta. Esta cuestión puede ser abordada desde dos perspectivas: • Organización lógica. Comprende las características de los diversos tipos de almacenamiento que se distinguen en un programa C++ ( 1.3.2). • Organización física. Toma en consideración los diversos dispositivos físicos utilizados por el programa para alojarse él mismo y sus datos ( 2.2.6). 1.3.1 Estructura lógica. Part of the problem is that programming is hard to teach. “Programming is a mixture of a highly technical skill and an aesthetic art. And that’s a very difficult combination.” James Maguire en su artículo "The 'Anti-Java' Professor and the Jobless Programmers". Basado en una entrevista con Robert Dewar, profesor emérito de Ciencia de la Computación en New York. §1 Sinopsis Desde el punto de vista lógico, puede considerarse que los programas comprenden dos tipos de elementos diferentes: estructuras de datos y algoritmos. O dicho en otras palabras: datos, e instrucciones para su manipulación. Su representación codificada adopta dos formas: una entendible por la máquina (ejecutable y ficheros de datos) y otra entendible por el humano (fuente). Para el conjunto de ambas puede considerarse una escala conceptual que, si vamos de lo general a lo particular, podemos representarla como sigue: Nota: clasificaciones como la que aquí proponemos solo tiene una finalidad didáctica; son un vehículo para introducir al lector en la comprensión global del concepto y no un asunto dogmático e inamovible, ya que este tipo de asuntos depende grandemente del punto de vista que se adopte. Como botón de muestra, vaya por delante la definición de lo que es un programa en palabras de Ellis y Stroustrup [6 §3.3], que por lo demás ofrece un punto de vista muy interesante: "Un programa consiste en uno o más ficheros enlazados juntos. Un fichero consiste en una secuencia de declaraciones" (ver Declaraciones y definiciones en 4.1.2). A continuación admiten que esta afirmación puede parecer extraña a aquellos acostumbrados a pensar en un programa como una serie de sentencias ejecutables acompañadas de las declaraciones de las variables correspondientes. Pero recuerdan que en C++, las sentencias están contenidas en funciones, y que en realidad, el cuerpo de una función es en sí mismo una declaración (de la propia función). En consecuencia, puesto que las declaraciones pueden contener inicializadores, también deben ser consideradas como "ejecutable" (lo que hemos denominado algoritmo). §2 Aplicación Comprende ejecutables y datos. Puede haber múltiples ficheros de ambos tipos (ficheros de datos y ejecutables). §3 Programa Parte de una aplicación (código) que puede cargarse y ejecutarse independientemente. §4 Fichero fuente: Se llaman así (abreviadamente) los ficheros que contienen el código fuente (ficheros .C / .CPP) escrito por el programador. Un "fuente" se compila de una vez, cuando recibe la acción del preprocesador ( 1.4.1), dando lugar a lo que técnicamente se denomina unidad de compilación ( 1.4.2). Un solo fuente puede ser dividido en múltiples ficheros, cada uno de los cuales puede contener varias funciones, aunque la inversa no es cierta: una función no puede ser dividida entre varios fuentes. La mayoría de las aplicaciones de cierto porte ocupan más de un fuente. Los diversos ficheros son creados y mantenidos por distintos programadores. Después, los ficheros son compilados y enlazados para producir una aplicación final. Es conveniente recordar que C y C++ se desarrollaron en ambientes UNIX, por lo que desde su cuna son lenguajes sensibles a las mayúsculas/minúsculas ("case sensitive" . Esto no representa inconveniente para los programadores acostumbrados a entornos Unix/Linux, pero suele ser fuente de errores para los que han desarrollado en Sistemas MS-DOS y Windows donde esta distinción no es tan importante. Por ejemplo, en C++ la declaración int X, x; produce dos variables distintas. §5 Función: Una parte de un programa (subrutina) con un nombre, que puede ser invocada (llamada a ejecución) desde otras partes tantas veces como se desee. Opcionalmente puede recibir valores (argumentos); se ejecuta y puede devolver un valor ( 4.4). main ( 4.4.4) es la primera función en cualquier programa C++; es llamada desde unas rutinas especiales "de inicio" que se incluyen automáticamente en todo programa C++. Esta función es el punto de inicio del programa desde el punto de vista del programador (donde este toma el control). §6 Bloque Lista, que puede estar vacía, de sentencias delimitadas por corchetes { } ( 3.2.6). Desde el punto de vista sintáctico, un bloque puede ser considerado como una sola sentencia (sentencia compuesta 4.10). Dentro de las posibilidades de memoria, los bloques pueden ser anidados a cualquier nivel (los bloques pueden contener otros bloques). El aspecto de los bloques "anidados" es como sigue: ... // espacio global del fichero main { // comienzo del bloque main .... // espacio del bloque main { // bloque anidado ... // espacio del bloque anidado } // fin de bloque .... } // fin del bloque main §7 Sentencia Si establecemos una analogía entre un lenguaje natural y un lenguaje computacional como C++, podemos afirmar que las sentencias ("Statements" juegan en C++ el mismo papel que las oraciones en el lenguaje natural, y del mismo modo que una oración gramatical es una palabra o conjunto de ellas, con que se expresa un sentido gramatical completo , las sentencias se componen de una o varias expresiones y tienen sentido computacional completo. La sentencia es la unidad lógica completa más simple en un programa; en C/C++ terminan en punto y coma ;. Un caso especial es la expresión nula ( ; aislado). En el apartado 1.3.1a se describe su sintaxis y en el capítulo 4.10 se ofrece su definición formal y una clasificación de los distintos tipos que existen en C++. Un caso especial lo componen las directivas de preprocesado ( 4.9.10), que suponen transformaciones en el fuente por parte del preprocesador ( 1.4.1). §8 Expresión Siguiendo en sentido ascendente de simplicidad en el lenguaje natural, después de las oraciones están las frases, Conjunto de palabras que basta para formar sentido, especialmente cuando no llega a constituir una oración cabal . Su equivalente en el lenguaje C++ serán las expresiones. Las expresiones son secuencias de tokens (operadores, operandos y elementos de puntuación) que especifican una computación; tienen sentido computacional en sí mismas. Son los bloques de computación más simples con los que se construye un programa [6 §5] aunque no pueden ejecutarse separadamente sino cuando forman una sentencia. Como verá el lector, la diferencia entre sentencia y expresión es algo arbitraria. Una o varias expresiones terminadas en punto y coma constituyen una sentencia. Desde el punto de vista lógico, cada sentencia se ejecuta de una vez (aunque sus expresiones sean evaluadas individualmente siguiendo ciertas reglas ). En el programa suele estar muy claro cual es el orden de ejecución de sus sentencias, no así de sus expresiones dentro de ellas, toda vez que el Estándar es permisivo en algunos aspectos de detalle. Es importante señalar que, en contra de lo que ocurre en otros lenguajes, en C/C++, el final de línea no significa necesariamente el fin de la expresión, de forma que esta puede ocupar más de una línea (en realidad el compilador ignora los pares de caracteres CR/LF que añaden los editores al final de línea). Por ejemplo: la expresión int x = 5*(4+(3/2-1)) puede ser escrita como: int x = 5* ( 4+ (3/2-1) ); Los conocedores de otros lenguajes advertirán que en C++ no tiene sentido la barra inclinadas con que otros lenguajes indican que la siguiente línea de código es en realidad una continuación de la anterior. Se exceptúa naturalmente el caso de constantes de cadena, donde si es preciso indicar al compilador que, en su caso, la cadena continúa en la línea siguiente. Ejemplo, aunque es correcto escribir: int x = 5; En cambio es incorrecto poner char* string = "Las constantes de cadena pueden tener cualquier longitud"; En este caso es necesario indicar al compilador que la cadena continúa en la línea siguiente: char* string = "Las constantes de cadena pueden tener cualquier longitud"; Del mismo modo que los gramáticos distinguen varios tipos de frases (hecha, musical, proverbial, etc), los informáticos distinguen también varios tipos de expresiones. Los ejemplos que siguen son meramente ilustrativos y no pretenden ser una relación completa: • De asignación: Si tienen uno, o varios, operadores de asignación ( 4.9.2). Ejemplos: x += 3; z = y = x/2; • Declarativas: Si dan a conocer un identificador al compilador ( 4.1.2). Ejemplos: extern int funcint x, char c); class MiClase; • Definiciones: Establecen la creación de un objeto reservándole espacio físico. Ejemplo: int x = 2; MiClase c1 (x, y, z); • Condicionales: Definen alternativas de acción en el programa. Ejemplos: int y = 6 ? 7: 8; if (salida = 'S') break; • De salto: Obligan al programa a seguir en un sitio distinto de la secuencia natural (la siguiente sentencia). Ejemplos: break; continue; • Bitwise. De manejo y comparación de bits ( 4.9.3). Ejemplos: x | y; x << 2; Las expresiones pueden producir un Lvalue ( 2.1.5), un Rvalue ( 2.1.6) o ningún valor. Ejemplo: int x; // no produce ningún valor x = 2; // produce un valor (resultado) Además de producir un valor, las expresiones pueden tener efectos laterales (tanto si producen un valor como si no lo producen). Ejemplo: x = ++a; Además de producir un valor (un resultado 4.9.2), esta expresión tendría dos efectos laterales: 1º: Incrementar en 1 el valor de a, 2º: Asignar el valor modificado de a a la variable x. El "resultado" de la expresión coincide precisamente con el valor de x . Las expresiones se evalúan siguiendo ciertas reglas de conversión , agrupamiento, asociatividad y precedencia ( 4.9). Estas reglas dependen de varios factores: el operador; la naturaleza de los operandos utilizados en cada caso, y de la presencia de paréntesis en la expresión ( 4.9.0a). En el apartado 1.3.1b se describe completamente su sintaxis formal así como su precedencia y asociatividad. Su sintaxis muestra que las expresiones son definidas recursivamente, y que las subexpresiones pueden ser anidadas sin ningún límite formal. §9 Token Los tokens ( 3.2) son los elementos en que el preprocesador desmenuza el código fuente. En un lenguaje de programación, los tokens son el equivalentes al conjunto de las palabras y signos de puntuación en el lenguaje natural escrito. En el lenguaje C++ estas "palabras" pueden ser de varios tipos: palabras clave, identificadores, constantes, operadores, signos de puntuación (puntuadores) y comentarios (son eliminados). Los tokens están separados por elementos de separación que reciben el nombre genérico de separadores ("Whitespaces". Inicio. ________________________________________ Los métodos utilizados en las conversiones aritméticas estándar se han detallado en 2.2.5 Esta característica: producir un resultado, de las expresiones de asignación, es lo que posibilita la existencia en C++, de expresiones como: x = y = z+1; Me encantaría conocer una versión española de esta palabra inglesa. Este es el sentido gramatical de la palabra oración según el Diccionario Usual de la Real Academia de la Lengua Española. Según el Diccionario de Uso del Español de María Moliner, la oración es la unidad más pequeña de lenguaje organizado gramaticalmente. Si nos referimos al lenguaje natural, conceptualmente la oración consta de dos partes: un sujeto del que se dice o afirma algo, y un predicado, que es lo que se dice del sujeto, y puede ser de dos clases, nominal y verbal. Es nominal cuando lo que se dice del sujeto está contenido en un nombre o adjetivo, es verbal cuando lo que se dice del sujeto está contenido fundamentalmente en un verbo. Este es el sentido de la palabra "frase" según el Diccionario de la Real Academia de la Lengua Española. Stroustrup & Ellis: ACRM. 1.3.1a. Sentencias §1. Introducción Al igual que los lenguajes naturales , los lenguajes computacionales tienen una gramática, es decir, un conjunto de reglas que describen los elementos que componen el lenguaje y la forma correcta de utilizarlos ( 1.2.1).. Una parte de la gramática, la sintaxis, se ocupa de las reglas para combinar adecuadamente los elementos del lenguaje de forma que tengan un sentido.. En el caso de los lenguajes naturales, la sintaxis enseña a formar las oraciones y expresar conceptos.. En el caso de los lenguajes computacionales, enseña a construir sentencias que describan operaciones correctas de un computador. §2. Sintaxis de las sentencias C++ Si nos referimos concretamente a la sintaxis, sus reglas pueden ser expresadas en forma de una lista, que adecuadamente interpretada, describe estas reglas sintácticas.. A continuación se expone la sintaxis permitida en C++ para las sentencias ("Statements". . Junto a una traducción, más o menos afortunada al español , hemos incluido el original inglés para que no quepa la más mínima ambigüedad al respecto, ya que el rigor y el "purismo" son imprescindibles al tratar estos aspectos gramaticales del lenguaje. Una lista de este tipo describe la sintaxis de lo que se denomina una Gramática Independiente del Contexto de Chomsky . En este caso define la sintaxis de una sentencia (primera entrada de la lista).. Nos describe que una sentencia está compuesta por los elementos que siguen en la lista (sangrados).. Aquí tendríamos que interpretar:. Una sentencia está compuesta por una sentencia etiquetada, o una sentencia compuesta, o una sentencia-expresión, Etc.. A su vez, las entradas sucesivas de la lista (en azul), describen la sintaxis de cada elemento.. Por ejemplo, una sentencia compuesta está formada a su vez por una lista-de-declaraciones que puede ir seguida de una lista-de-sentencias; todo ello entre corchetes { } (el símbolo <xxx> indica que el componente xxx es opcional).. A su vez, la lista-de-declaraciones está formada por ... Etc. Etc. Resulta evidente que muchas de estas definiciones son recursivas. . Por ejemplo, vemos que una lista-de-declaraciones está formada por una declaración o una lista-de-declaraciones seguida de una declaración. Es decir, una lista-de-declaraciones puede contener a su vez otra lista de declaraciones. La lista indica que una sentencia-expresión está constituida por una expresión seguida de un punto y coma (, aunque la expresión es opcional.. Quiere esto decir que un punto y coma aislado es una sentencia-expresión sintácticamente correcta en C++; es lo que se denomina sentencia-expresión nula. Nota: Como este "libro" está dirigido principalmente a estudiantes de C++ que necesiten del lenguaje para sus aplicaciones prácticas, debemos advertir aquí que estas cuestiones "gramaticales" son estrictamente formales, lo que dicho sencilla y llanamente, significa que de una de estas listas no es posible sacar ninguna idea concreta de qué es una sentencia, ni la menor indicación de para qué sirven.. Son simplemente una indicación exquisitamente exacta de cómo se usan desde el punto de vista formal, lo que para el programador de a pie tiene escaso o nulo valor . sentencia: sentencia etiquetada (labeled-statement) sentencia compuesta (compound-statement) sentencia-expresión (expression-statement) sentencia de selección (selection-statement) sentencia de iteración (iteration-statement) sentencia de salto (jump-statement) sentencia ensamblador (asm-statement) declaración simple (declaration) bloque-intento ( 1.6) (try-block) sentencia etiquetada: identificador : sentencia case expresión constante : sentencia default : sentencia sentencia compuesta: ( 3.2.6) { <lista-de-declaraciones> <lista-de-sentencias> } (<declaration-list> <statement-list> lista-de-declaraciones: declaración lista-de-declaraciones. declaración lista-de-sentencias: sentencia lista-de-sentencias. sentencia sentencia-expresión: <expresión> ; sentencia ensamblador ( 4.10): asm tokens nueva-línea (newline) asm tokens; asm { tokens; <tokens;>= <tokens;>} sentencia de selección ( 4.10.2): if ( expresión ) statement if ( expresión ) statement else statement switch ( expresión ) statement sentencia de iteración ( 4.10.3): while ( expresión lógica ) sentencia do sentencia while ( expresión ) ; for (sentencia de inicio-for <expresión> ; <expresión> sentencia sentencia de inicio-for: sentencia-expresión declaración simple sentencia de salto ( 4.10.4): goto identificador ; continue ; break ; return <expresión> ; Inicio. ________________________________________ . Debo advertir que he visto algunas traducciones del original inglés, principalmente realizadas en Latinoamérica, que a los Españoles nos resultan chocantes y poco afortunadas.. Supongo que ocurrirá exactamente lo mismo a la inversa. . Lenguaje utilizado por los humanos para comunicarse entre sí de forma "natural":. Español, Japonés, Inglés, Alemán, etc. . Noam Chomsky, nacido en 1.928 en Philadelphia (Pennsylvania) USA, hijo de un inmigrante ruso, estudió lingüística, matemáticas y filosofía, interesándose también por la política, pero es más reconocido por sus trabajos en gramática generativa, que basada en la lógica y en la matemática moderna, fue posteriormente aplicada a la descripción de los lenguajes naturales. 1.3.1b Sintaxis de las expresiones C++ §1 Sinopsis: En este epígrafe se incluye una descripción formal de la gramática permitida a las expresiones en C++. Como puede verse, las expresiones son definidas recursivamente, de forma que las subexpresiones pueden ser anidadas sin ningún límite formal, aunque quizás el compilador pueda reportar un error de límite de memoria si no puede compilar una expresión muy compleja. Como en el caso de las sentencias ( 1.3.1a), en los casos que pudieran resultar dudosos, junto a la traducción al español se ha incluido el original inglés. Expresiones primarias: (primary-expression): literal this :: identificador :: nombre de función-operador (operator-function-name) :: nombre cualificado (expresión) expresión-de-identificación literal: constante entera (integer-constant) constante carácter (character-constant) constante fraccionaria (floating-constant) cadena de caracteres (string-literal) expresión-de-identificación: nombre (no cualificado) nombre-cualificado nombre (no cualificado): identificador nombre de función-operador conversion-function-name ~ nombre-de-clase nombre-cualificado nombre-cualificado: (qualified-name) nombre-cualificado-de-clase :: nombre expresiones: (postfix-expression) primary-expression postfix-expression [ expression ] postfix-expression ( <expression-list> ) simple-type-name ( <expression-list> ) postfix-expression . name postfix-expression -> name postfix-expression ++ postfix-expression -- const_cast < identificación-de-tipo > ( expresión ) dynamic_cast < identificación-de-tipo > ( expresión ) reinterpret_cast < identificación-de-tipo > ( expresión ) static_cast < identificación-de-tipo > ( expresión ) typeid ( expresión ) typeid ( nombre-de-tipo ) lista de expresiones: (expression-list) expresión-de-asignación lista-de-expresiones , expresión-de-asignación expresión-unitiaria: (unary-expression) postfix-expression ++ expresión unitaria - - expresión-unitaria operador-unario expresión-de-modelado sizeof expresión-unitaria sizeof ( nombre-de-tipo ) expresión de asignación-de-memoria (allocation-expression) expresión-de-desasignación operador-unario: (alguno de los siguientes) & * + - ! ~ expresión-de-asignación-de-memoria: (allocation-expression) <::> new <placement> new-type-name <inicializador> <::> new <placement> (nombre-de-tipo) <inicializador> placement: (lista-de-expresiones ) new-type-name: especificadores-de-tipo <declarador-new> declarador-new: (new-declarator) ptr-operator <new-declarator> declarador-new [ <expresión> ] expresión de desasignación: (deallocation-expression) <::> delete expresión-de-modelado <::> delete [ ] expresión-de-modelado expresión-de-modelado: (cast-expression) expresión-unitaria ( nombre-de-tipo ) expresión-de-modelado expresión-pm: (pm-expression) expresión-de-modelado pm-expresion .* expresión-de-modelado pm-expresion ->* expresión-de-modelado expresión-multiplicativa: (multiplicative-expression) expresión-pm expresión-multiplicativa * pm-expression expresión-multiplicativa / pm-expression expresión-multiplicativa % pm-expression expresión-aditiva: (additive-expression) expresión-multiplicativa expresión-aditiva + expresión-multiplicativa expresión-aditiva - expresión-multplicativa expresión-de-desplazamiento: (shift-expression): expresión-aditiva expresión-de-desplazamiento << expresión-aditiva expresión-de-desplazamiento >> expresión-aditiva expresión relacional: (relational-expression) expresión-de-desplazamiento expresión-relacional < expresión-de-desplazamiento expresión-relacional > expresión-de-desplazamiento expresión-relacional <= expresión-de-desplazamiento expresión-relacional >= expresión-de-desplazamiento expresión-de-igualdad: (equality-expression) expresión-relacional expresión-de-igualdad == expresión-relacional expresión-de-igualdad != expresión-relacional expresión-AND: (AND-expression) expresión-de-igualdad expresión-AND & expresión-de-igualdad expresión-de-OR-exclusivo: (exclusive-OR-expression) expresión-AND expresión-de-OR-exclusivo ^ expresión-AND expresión-de-OR-inclusivo: (inclusive-OR-expression) expresión-de-OR-exclusivo expresión-de-OR-inclusivo | expresión-de-OR-exclusivo expresión-lógica-AND: (logical-AND-expression) expresión-de-OR-inclusivo expresión-lógica-AND && expresión-de-OR-inclusivo expresión-lógica-OR: (logical-OR-expression) expresión-lógica-AND expresión-lógica-OR || expresón-lógica-AND expresión condicional: expresión-lógica-OR expresión-logica-OR ? expresión : expresión-condicional expresión-de-asignación: (assignment-expression) expresión condicional expresión-unitiaria operador-de-asignación expresión-de-asignación operador-de-asignación (alguno de los siguientes): = *= /= %= += -= << => >= &= ^= |= expresión: expresión-de-asignación expresión, expresión-de-asignación expresión-constante: expresión-condicional
C++ (Cuarta parte) 1.4.0 Construir un ejecutable (compilación) "For all intent and purpose, any description of what the codes are doing should be construed as being a note of what we thought the codes did on our machine on a particular Tuesday of last year. If you're really lucky, they might do the same for you someday. Then again, do you really feel *that* lucky?". R. Freund. "Readme" de una subrutina matemática (tomado de las FAQ de gnuplot). §1 Sinopsis En el presente Curso C++ nos referimos infinidad de veces al "Compilador". Sin embargo, hablando en propiedad no existe realmente tal cosa; en realidad los procesos anteriormente descritos ( 1.4) se ejecutan por una serie de aplicaciones distintas: El preprocesador, el analizador sintáctico, el generador de código, el enlazador y algunas otras auxiliares, aunque de forma genérica nos referimos a ellas como "el compilador". Además, los "compiladores" ofrecen una serie de herramientas adicionales que no son propiamente para construir ejecutables, sino para labores auxiliares como puede ser la construcción de librerías. Estas herramientas son conocidas colectivamente como binutils. Nota: en el caso del "Compilador" Borland C++, se dispone de los módulos que siguen, cada uno de los cuales realiza una labor concreta: ILINK32 EXE, BCC32 EXE, BRC32 EXE, BRCC32 EXE, COFF2OMF EXE, CPP32 EXE, FCONVERT EXE, GREP EXE, IMPDEF EXE, IMPLIB EXE, MAKE EXE, TDUMP EXE, TLIB EXE, TOUCH EXE, TRIGRAPH EXE, TD32 EXE (ver detalles 1.4.0w1). En el caso del compilador GNU para C++, además del compilador propiamente dicho g++, están el enlazador ld; el intérprete de comandos make, y una serie de utilidades auxiliares (binutils) tales como ar, nm, objcopy, objdump, ranlib, readelf, size, windres, etc. Para construir el ejecutable es necesario cubrir sucesivamente diversas etapas utilizando los módulos adecuados, y los resultados de unos como entradas de los siguientes. Sin embargo, para facilitar el proceso, los "compiladores" cuentan con una utilidad a la que se conoce genéricamente como compilador pero que en realidad es un programa supervisor ("Front-end" que se encarga de invocar los módulos sucesivos en el orden correcto y con los parámetros adecuados al fin que se persigue. En el caso del "Compilador" Borland C++ es BCC32.exe y CL.exe en MS Visual C++. En el caso del "Compilador" GNU C++, el programa supervisor es gcc, que actúa como front-end del preprocesador (cpp), que actúa a su vez como front-end generador de código y este del enlazador (ld). Lo corriente es que el programa supervisor acepte una larga fila de parámetros que controlan su funcionamiento, dado que él mismo debe controlar una serie de procesos (en el caso de Bcc32, la lista asciende a más de 135 opciones distintas). Viene a ser para el programador una especie de navaja suiza, la herramienta utilizada habitualmente. Solo en casos excepcionales se utilizan directamente el preprocesador, el generador de código, el enlazador o cualquiera de las utilidades necesarias para la construcción de ejecutables y librerías. Nota: en general es posible comprobar la secuencia de actuación del "compilador" invocándolo con la opción -v ("verbose". Por ejemplo, de esta forma es posible comprobar que durante su operación, el compilador GNU cc invoca al enlazador, de nombre ld. §2 El proceso de construcción de un ejecutable (o librería) depende de la complejidad del proyecto. Consideramos tres casos: • Compilaciones unitarias: Cuando se trata de un solo fuente que produce un ejecutable. Veremos que en estos casos puede mandarse una orden directa al programa supervisor . • Compilaciones medianas: Cuando el proyecto involucra unos pocos fuentes que también producen un ejecutable. En estos casos no es corriente que el programa supervisor sea invocado directamente mediante un solo comando. Se recurre a ficheros auxiliares (de configuración), que contienen los parámetros; los ficheros de configuración son leídos e interpretados por el supervisor . • Proyectos grandes: En ocasiones los proyectos software implican centenares de fuentes que producen decenas de ejecutables de diversos tipos, incluidas librerías estáticas y dinámicas. En estos casos no se puede hablar de simple compilación; más bien de "construir" la aplicación. Mantener tales proyectos es cuestión complicada, generalmente se realiza mediante herramientas auxiliares. En el apartado correspondiente ( 1.4.0a) reseñamos una utilidad específica para controlar estas "construcciones". Nota: precisamente uno de los puntos fuertes de las "Suites" RAD ( 1.8) de programación del tipo C++Builder o MS Visual C++, es la cómoda gestión de proyectos grandes. Sin embargo, abrir un "proyecto" para una aplicación de un par de fuentes o un sencillo ejemplo, supone matar pulgas a cañonazos. §3 Compilaciones unitarias En estos casos puede mandarse una orden directa al supervisor indicándole cual es el fuente (fichero .c ó .cpp) que hay que compilar, y él se encarga de realizar todo el proceso, invocando en último extremo al enlazador que construye el ejecutable. Por ejemplo: en el caso de Borland C++ 5.5, Bcc32.exe está alojado en el directorio ...BorlandCPPbin. Suponiendo que queremos compilar un fuente denominado hola.c con las opciones por defecto ( 1.4.3), podemos lanzar una orden directa desde el CLI : bcc32 hola.c Si realizamos muchas de estas compilaciones y/o utilizamos opciones, puede ser cómodo construir un fichero de proceso por lotes (.BAT en los entornos MS-DOS y Windows) que facilite la labor. A continuación se muestra el contenido de uno de estos ficheros (compi.BAT), que utilizo habitualmente para este tipo de compilaciones: ECHO OFF rem compi.BAT Compilar UN solo .C como C++ Normas Relajadas bcc32 -IE:BorlandCPPInclude -LE:BorlandCPPLib -Vd -P -j5 %1%.C rem -P Perform C++ compile regardless of source extension rem -Vd for loop variable scoping rem -j5 Errors: stop after n messages (Default = 25) if errorlevel 1 goto ERROR IF NOT ERRORLEVEL 1 ECHO !! OK. Compilacion C++ (-Vd) GOTO END : ERROR ECHO !! ERROR en la compilacion.... Pause : END echo BORRADO fichero .TDS!! erase %1%.TDS Puede comprobarse que el fichero no necesita que se le indique el nombre completo del objeto a compilar (él incluye la terminación .c). En consecuencia, solo hay que escribir: compi hola En este caso, los comandos -IE:BorlandCPPInclude y -LE:BorlandCPPLib, instruyen al compilador de los directorios donde debe buscar los ficheros de cabecera (comando -I) y las librerías (comando -L), que están en el directorio BorlandCPP de la unidad lógica E: Si se realizan frecuentemente compilaciones de tipo distinto (con diversas opciones en la línea de comando), puede ser buena idea preparar los ficheros .BAT correspondientes, por ejemplo: compil.bat; compi1.bat; etc. §4 Compilaciones medianas Salvo en programitas de ejemplo y verificación, es muy raro que un programa C++ exista en un solo fichero fuente. En estos casos el proceso se complica un poco más, porque hay que instruir al compilador de todos los fuentes que debe procesar. Por supuesto, en uno de ellos debe existir una función main ( 4.4.4). Ilustraremos la operativa con un sencillo programa de dos módulos pA.c y pB.c, y un fichero de cabecera específico <Cabe-1.h> que suponemos en el mismo directorio que los fuentes (no es conveniente mezclar nuestras propias cabeceras con las del compilador). Fichero Cabe-1.h: // Cabe-1.h #include <iostream.h> #include <conio.h> #define Salida(msg) cout << #msg << endl; # define PAUSA for( ; ; ) if(getch()!=0) break Fichero pA.c: #include <Cabe-1.h> // pA.c Prueba MODULOS-1 extern void func(float); extern var1; namespace { // Anonimo float pi = 3.14; // identificador conocido solo en este fichero } int main() { // ======= float pi = 0.1; cout << "pi = " << pi << endl; func(pi); // invocada función externa con pi local cout << "Variable es " << var1 << endl; Salida(Pulse una tacla para terminar); PAUSA; Salida("el programa ha terminado :-)"; return 0; } Fichero pB.c: #include <Cabe-1.h> // pB.c Prueba MODULOS-2 int var1 = 33; namespace { // subespacio anonimo float pi = 10.0001; // conocido solo en este fichero void func(void) { std::cout << "Invocada func-1: pi = " << pi << endl; } } void func(float f) { std::cout << "Invocada func-2: f = " << f << endl; } La salida, después de construido el ejecutable es: pi = 0.1 Invocada Segunda func(): f = 0.1 Variable es 33 Pulse una tacla para terminar "el programa ha terminado :-)" El comando necesario para construir el ejecutable es: bcc32 -ID:;E:BorlandCPPInclude -LE:BorlandCPPLib -Vd -P -Q pa.C pb.c Observe que en este caso se ha alterado ligeramente el comando -I, para que el preprocesador encuentre la cabecera <Cabe-1.h>, que está en el directorio actual (el directorio de trabajo está en la unidad lógica D. Para automatizar el proceso, en estos casos utilizo un fichero compiv.bat del siguiente aspecto: ECHO OFF rem Compiv.BAT Compilar 2 fuentes (pa.c & pb.c) como C++ bcc32 -ID:;E:BorlandCPPInclude -LE:BorlandCPPLib -Vd -P -Q pa.C pb.c rem -Q extended compiler error information if errorlevel 1 goto ERROR IF NOT ERRORLEVEL 1 ECHO !! OK. Compilacion C++ (-Vd) GOTO END : ERROR ECHO !! ERROR en la compilacion.... Pause : END echo Fichero .TDS BORRADO!! erase %1%.TDS §5 Ficheros de configuración Ocurre que en ocasiones la línea de comando para invocar el supervisor es demasiado larga aún en proyectos pequeños. El motivo puede ser doble: por un lado puede contener muchas opciones de compilación y enlazado, hemos indicado que el programa Bcc32 puede contener más de 135 opciones distinta. De otro lado (y esto es lo más frecuente), puede contener una larga serie de nombres de ficheros fuente y/o librerías que deben ser enlazadas juntas. El resultado es que incluso en un fichero de proceso por lotes .BAT como el anterior resultaría una línea larga y farragosa. Para simplificar el proceso se utilizan ficheros auxiliares que contienen los parámetros y/o opciones de compilación. Estos ficheros son leídos e interpretados por el supervisor. Existen dos opciones al respecto : Los ficheros de configuración ("Configuration files" y los ficheros de réplica ("Computer response files". Además de simplificar la utilización repetitiva de largos comandos de compilación, ocurre que existe una limitación en cuanto a la longitud máxima que puede tener la línea de comando en los Sistemas Operativos, y estos ficheros representan una forma de soslayar dicha limitación. §5.1 Ficheros de configuración Los ficheros de configuración son ficheros de texto ASCII de terminación .CFG que contienen opciones para el programa supervisor; cada opción debe ir separada por un espacio o nueva línea. Cuando se invoca Bcc32.exe, busca un fichero de configuración por defecto con el nombre Bcc32.CFG en el directorio actual, y si no lo encuentra, en el directorio donde reside el compilador. El programa de instalación del compilador Borland crea un fichero Bcc32.CFG con el siguiente contenido (suponemos que se ha instalado en el directorio E:BorlandCPP): -I"e:BorlandCPPinclude" -L"e:BorlandCPPlib" Además del fichero por defecto, es posible utilizar varios otros en la misma línea de comando. Para invocar el programa supervisor con otro fichero de configuración, además del estándar, se utiliza la siguiente sintaxis: +[path]nombre-de-fichero Por ejemplo, la línea de comando que sigue invoca al supervisor con un fichero denominado Pro-1.CFG: BCC32 +CROYECTOPro-1.CFG fuente.cpp En cualquier caso, las opciones de la línea de comando pueden coexistir con un fichero de configuración, pero tienen precedencia sobre las indicaciones contenidas en aquel. Por ejemplo: BCC32 +CROYECTOPro-1.CFG -P fuente.c §5.2 Ficheros de réplica Los ficheros de réplica ("Response files" pueden contener opciones para el supervisor y/o nombres de ficheros (los de configuración solo pueden contener opciones para el supervisor). Un fichero de respuesta es también un fichero de texto ASCII donde cada entrada debe estar separada por un espacio o nueva línea. Los ficheros de respuesta pueden tener cualquier terminación (los ficheros de réplica incluidos con el compilador BC++ tienen la extensión .RSP). §5.2.1 Contenido El contenido es exactamente el mismo que se incluiría en la línea de comando si la invocación se hiciera manualmente y la línea pudiera ser lo suficientemente larga, pero teniendo en cuenta que en el fichero de réplica se puede partir una línea y seguir en la siguiente si se termina la primera con +. Por ejemplo, un fichero RespFile.RSP: /c c0ws+ myprog,myexe + mymap + mylib cws pasado al enlazador con el comando: Ilink32 @RespFile.RSP (ver invocación ), equivale a la línea de comando: ILINK32 /c c0ws myprog,myexe,mymap,mylib cws Como puede verse, a diferencia de la línea de comando, el fichero de réplica sí puede tener varias líneas. Observe que si una línea debe seguir en la siguiente pero la opción termina en el carácter +, por ejemplo la opción /v+, la línea del fichero de respuesta debe terminar en: ...../v+ +. Observe también que las opciones que deben ir separadas por comas en la línea de comando, deben seguir separadas por comas en el fichero de réplica (línea myprog,myexe + del ejemplo). §5.2.2 Invocación La sintaxis para invocar un ficheros de réplica con el compilador es: BCC32 @[path]fichero-replica.txt Para invocar varios al mismo tiempo se emplea la siguiente sintaxis: BCC32 @[path]fichero-1.txt @[path]fichero-2.txt También en este caso las opciones de la línea de comando tienen precedencia sobre las indicadas en los ficheros de réplica. Recuerde que no solo el compilador (BCC32) acepta este tipo de ficheros como parte del comando de entrada; el enlazador (ILINK32) y otras utilidades también puede aceptar este tipo de fichero de órdenes (por ejemplo la utilidad TLIB 1.4.0w1). Inicio. ________________________________________ CLI: Command Line Interpreter; intérprete de líneas de comando. El nombre concreto cambia de un SO a otro, pero se refiere a un programa que interpreta las órdenes directas del teclado. En el viejo MS-DOS es command.com. En los sistemas Windows se suelen referir a él como el "Shell" del DOS. En los sistemas Windows 9x todavía es accesible mediante las opciones "Abrir una ventana MS-DOS" o en: Menú de Inicio Ejecutar. Los "Linuxeros" no necesitan este tipo de aclaración, lo conocen bien; en su mundo todavía es normal funcionar a golpe de tecla. Nuevamente nos referimos al caso de Borland C++; los demás compiladores tienen opciones análogas.

C++ (Decimotercera parte) 1.4.4a1 Ejemplo de fichero de definición .DEF §1 Sinopsis A continuación se incluye un fichero .DEF junto con un comentario detallado de sus diversas secciones. ; fichero .DEF de ejemplo NAME HOLA DESCRIPTION 'C++ Windows Hola Mundo' EXETYPE WINDOWS CODE MOVEABLE DATA MOVEABLE MULTIPLE HEAPSIZE 1024 STACKSIZE 5120 EXPORTS MainWindowProc Comentario NAME Indica el nombre de un ejecutable normal. Si se desea construir una DLL, debe utilizarse la sección LIBRARY en sustitución de esta. Cualquier fichero DEF debe tener alguna de estas dos secciones, pero no ambas al mismo tiempo. Recuerde que este nombre debe coincidir con el del ejecutable. DESCRIPTION Es una cadena que permite especificar libremente determinadas características del ejecutable o librería. EXETYPE es WINDOWS . CODE determina los atributos por defecto del segmento de código. El argumento MOVEABLE significa que este segmento puede ser movido en tiempo de ejecución. DATA determina los argumentos por defecto del segmento de datos. En este caso, MOVEABLE significa que puede ser movido en memoria en tiempo de ejecución. Por su parte, MULTIPLE garantiza que cada instancia de la aplicación dispondrá de su propio segmento de datos . HEAPSIZE especifica el tamaño del montón ( 1.3.2). STACKSIZE especifica el tamaño de la pila ( 1.3.2). Recuerde que no debe utilizar esta variable si pretende crear una DLL. EXPORTS relaciona aquellas funciones de la aplicación HOLA que pueden ser invocadas por otras aplicaciones o por el mismo Windows (las "callbacks". Recuerde que los compiladores BC++ y MSVC proveen de los especificadores __declspec(dllexport) y __export, de forma que las funciones declaradas con ellos no necesitan ser incluidas en esta sección. Observe que esta aplicación no tiene sección IMPORTS, porque suponemos que las únicas funciones que utiliza de otros módulos son de la API de Windows ( 1.7.1) y estas funciones son importadas mediante la inclusión de IMPORT32.LIB (esta librería es incluida automáticamente por BC++ en el proceso de construcción de un ejecutable Windows). Recuerde que cuando una aplicación necesita invocar funciones externas (situadas en librerías dinámicas), dichas funciones deben ser incluidas en la sección IMPORTS, o en una librería de importación ( 1.4.4b2c) enlazada estáticamente con la aplicación. La aplicación presentada no incluye la sección STUB. Suponemos que se compila con BC++ y no incluimos nada en esta opción, ya que esta plataforma de desarrollo dispone de un "Stub" preconstruido que es utilizado por defecto si no se especifica explícitamente otro ejecutable. Nota: Este stub por defecto es el responsable de que si pretendemos correr una aplicación desarrollada para Windows32 en una máquina MS DOS, aparezca un mensaje: This program must be run under Win32. Inicio. ________________________________________ La documentación que acompaña al compilador Borland C++ 5.5 indica que esta versión solo soporta WINDOWS como parámetro de la variable EXETYPE. No obstante, esta se mantiene solo por cuestiones de compatibilidad, y debe ser sustituida por NAME o LIBRARY. Recuerde que Windows es un sistema operativo que permite multiprogramación ( 1.7). Cada programa en ejecución se denomina "instancia" y estas instancias pueden ser del mismo ejecutable. 1.4.4b Librerías: generalidades Nota: tenga en cuenta que este capítulo no trata de características que puedan considerarse estándar del lenguaje C++, sino de peculiaridades (aunque muy extendidas) de la construcción de aplicaciones. Tenga en cuenta también que en informática, el concepto "Librería" es muy general, y no está asociado a ningún lenguaje concreto (aunque C y C++ las utilizan ampliamente). De hecho, es posible y frecuente, utilizar en un lenguaje librerías que han sido escritas en otro. Por ejemplo, buena parte de las librerías de C++Builder, la denominadas VCL "Visual Component Library", han sido desarrolladas en Pascal ( 4.11.8b). §1 Sinopsis Al tratar de la construcción de un programa ( 1.4) señalamos que en ocasiones no se desea construir un ejecutable, al menos no en el sentido tradicional del término, sino una librería, y que estas librerías son trozos de código que contienen alguna funcionalidad pre-construida que puede ser utilizada por un ejecutable. Por supuesto, las librerías contienen en su interior variables y funciones. Si como suponemos son librerías C++, lo más probable es que estas variables y funciones estén encapsuladas en forma de clases ( 4.11). Observe que la idea central de librería es precisamente la de ser un módulo de software preconstruido -generalmente por terderos- para cuya utilización no es necesario conocer los detalles íntimos de su funcionamiento, sino su interfaz. Es decir, que respuestas nos puede dar y cómo hay que preguntar -a la librería- para obtenerlas. En general, el término librería se utiliza para referirse a un conjunto de módulos objeto .obj / .o (resultados de compilación) agrupados en un solo fichero que suele tener las extensiones .lib, .bpl .a, .dll, etc. Estos ficheros permiten tratar las colecciones de módulos como una sola unidad, y representan una forma muy conveniente para el manejo y desarrollo de aplicaciones grandes, además de ser un concepto muy fértil para la industria del software, ya que permiten la existencia de las librerías de los propios compiladores ( 5) y de un mercado de utilidades y componentes adicionales. Son las denominadas librerías 3pp (de terceras partes), en referencia a que no son incluidas de forma estándar con los compiladores ni creadas por el programador de la aplicación. En este sentido el software se parece a cualquier otro mercado de componentes. Además de las librerías más o menos extensas que acompañan a los compiladores, pueden adquirirse otras, que permiten añadir a nuestros programas las funcionalidades más diversas sin necesidad de ser un experto en cada área de la programación y sin necesidad de que tengamos que estar reinventando la rueda constantemente. Si quiere una opinión autorizada -en inglés- sobre la filosofía de uso e importancia de las librerías en C++, puede consultar este documento del Sr. Stroustrup: Abstraction, libraries, and efficiency in C++ §2 Tipos En lo que respecta al lenguaje C++, existen dos tipos fundamentales de librerías: estáticas y dinámicas, que aunque comparten el mismo nombre genérico "librería", utilizan mecanismos distintos para proporcionar su funcionalidad al ejecutable. En ambos casos es costumbre, que junto a las librerías propiamente dichas (ficheros .lib, .a, .dll etc), se incluya un fichero .h denominado "de cabecera" ( 4.4.1), porque es tradición utilizar las primeras líneas del programa para poner las directivas #include ( 4.9.10g) que los incluirán en el fuente durante la fase de preproceso ( 1.4). Este fichero contiene las declaraciones de las entidades contenidas en la librería, así como las macros y constantes predefinidas utilizadas en ella, de forma que el programador solo tiene que incluir el correspondiente fichero .h en su aplicación para poder utilizar los recursos de la librería en cuestión (recuerde que en C/C++ es imprescindible incluir la declaración de cualquier función o clase antes de su utilización 4.1.2). Este sistema tiene la ventaja adicional de que proporciona al usuario la información mínima para su uso. Es decir, la "interfaz" de las funciones o clases que utilizará. En el caso de funciones esto se concreta en el prototipo ( 4.4.1); en el caso de clases, en la especificación de sus métodos y propiedades públicas. §2.1 Librerías estáticas Denominadas también librerías-objeto, son colecciones de ficheros objeto (compilados) agrupados en un solo fichero de extensión .lib, .a, etc. junto con uno o varios ficheros de cabecera (generalmente .h). Nota: una posición extrema la constituyen aquellas librerías en las que toda la funcionalidad se ha incluido en el fichero de cabecera .h, en cuyo caso no existen los módulos compilados .lib, .a, etc. Es el caso de la Librería Estándar de Plantillas STL ( 5.1) que está compuesta casi exclusivamente por ficheros de cabecera. No obstante, lo anterior representa un caso extremo que suele ser evitado, ya que por lo general, los autores incluyen en los ficheros de cabecera la información mínima indispensable para utilizar la librería (la interfaz), incluyendo la operatoria en forma de ficheros compilados. La razón no suele ser otra que proteger la propiedad intelectual (el "know how". Durante la construcción de la aplicación, el preprocesador incluye en los fuentes los ficheros de cabecera. Posteriormente, durante la fase de enlazado, el linker incluye en el ejecutable los módulos correspondientes a las funciones y clases de librería que hayan sido utilizadas en el programa, de forma que el conjunto entra a formar parte del ejecutable. De ahí su nombre: Librerías enlazadas estáticamente . Dejando aparte consideraciones de comodidad y rapidez, el resultado de utilizar una de tales librerías no se diferencia en nada al que puede obtenerse escribiendo en al fuente las funciones o clases correspondientes y compilándolas como un módulo más de nuestra aplicación. Nota: genralmente los compiladores disponen de herramientas específicas para la creación de librerías estáticas. Por ejemplo, la del compilador Borland C++ es el ejecutable TLIB.EXE ( 1.4.0w1); las de GNU se denominan ar y ranlib. Como tendremos ocasión de ver en los ejemplos, también pueden crearse mediante opciones específicas en la orden de compilación. §2.1.1 Diccionario Junto con los módulos .obj que las componen, las librerías estáticas incluyen una especie de índice o diccionario con información sobre su contenido. Este índice contiene los nombres de los recursos públicos de los distintos módulos (que pueden ser accedidos desde el exterior) y su dirección. Estos nombres deben ser distintos para evitar ambigüedades durante el enlazado, y sirven para incrementar la velocidad de enlazado cuando el "Linker" debe incluir alguno en un ejecutable. Nota: cuando se crea una librería estática a partir de uno o varios ficheros relocalizables (objetos), el proceso de incluir esta tabla o diccionario de símbolos puede ejecutarse en un solo paso o en dos, aunque siempre en el momento de crear la librería. Por ejemplo, tlib de Boland crea la librería y la tabla en un solo proceso. En cambio, ar de GNU puede crear la librería y posteriormente añadir la tabla (esto último puede también hacerse con ranlib). Cuando se añade un nuevo módulo a una librería existente, la misma herramienta que añade el contenido, se encarga de actualizar el índice. §2.2 Librerías dinámicas Otra forma de añadir funcionalidad a un ejecutable son las denominadas librerías de enlazado dinámico (repasar en 1.4.4 el significado de "enlazado dinámico", generalmente conocidas como DLLs, acrónimo de su nombre en inglés ("Dynamic Linked Library". Estas librerías se utilizan mucho en la programación para el SO Windows. Este Sistema contiene un gran número de tales librerías de terminación .DLL, aunque en realidad pueden tener cualquier otra terminación .EXE, .FON, .BPI, .DRV etc. Cualquiera que sea su terminación, de forma genérica nos referiremos a ellas como DLLs, nombre por el que son más conocidas. Nota: la programación tradicional de aplicaciones Windows utilizando la API del Sistema ( 1.7.1) es en realidad una sucesión de invocación a funciones contenidas en librerías de este tipo. En realidad este Sistema Operativo está constituido por un conjunto de DLLs; la mayoría de los ficheros de disco asociados con el Sistema son de este tipo, y se ha llegado a afirmar que escribir una DLL es escribir una extensión del propio Windows ( PW2E Petzold p.878). §3 Diferencias: librería Estática "versus" Dinámica Las diferencias más relevantes de las librerías dinámicas respecto a las estáticas son fundamentalmente dos: • Las librerías estáticas quedan incluidas en el ejecutable, mientras las dinámicas son ficheros externos, con lo que el tamaño de la aplicación (nuestro ejecutable) es mayor en el primer caso que en el segundo. Esto puede ser de capital importancia en aplicaciones muy grandes, ya que el ejecutable debe ser cargado en memoria de una sola vez . • Las librerías dinámicas son ficheros independientes que pueden ser invocados desde cualquier ejecutable, de modo que su funcionalidad puede ser compartida por varios ejecutables. Esto significa que solo se necesita una copia de cada fichero de librería (DLL) en el Sistema. Esta característica constituye la razón principal de su utilización, y es también origen de algunos inconvenientes, principalmente en sistemas como Windows en los que existen centenares de ellas. Como consecuencia de las diferencias citadas se derivan otras. Por ejemplo: • Si se realizan modificaciones en los módulos de una librería estática, es necesario recompilar todos los ejecutables que la utilizan, mientras que esto no es necesario en el caso de una librería dinámica, siempre que su interfaz se mantenga. • Como consecuencia de lo anterior, generalmente es más difícil la depuración y mantenimiento de aplicaciones que utilizan librerías dinámicas que las estáticas, ya que en el primer caso, es necesario controlar qué versiones de los ejecutables (.EXE) son compatibles con qué versiones de las DLLs y de estas entre sí, de forma que el usuario no utilice un versiones incompatibles de los ficheros que componen la aplicación. • Durante la ejecución de un ejecutable, las librerías estáticas que hubiesen intervenido en su construcción no necesitan estar presentes, en cambio las dinámicas deben estar en el mismo directorio o en el camino de búsqueda "Path" . • Las librerías estáticas solo se utilizan en la fase de construcción del ejecutable. Las dinámicas se utilizan durante la ejecución. • Los ejecutables que utilizan librería estáticas solo incorporan los módulos de aquellas que necesitan para resolver sus símbolos externos. Por contra, las librerías dinámicas deben ser cargadas en su totalidad aunque no solo se utilice una parte de su funcionalidad (no son divisibles). • Las librerías estáticas, que entran a formar parte indivisible del ejecutable, son cargadas con el proceso de carga de este. Las librerías dinámicas no necesariamente tienen que cargarse con la carga inicial (aunque pueden serlo). De hecho, una librería dinámica puede ser cargada bajo demanda en el momento en que se necesita su funcionalidad, e incluso puede ser descargada cuando no resulta necesaria. • El mecanismo de enlazado estático depende del compilador. El de enlazado dinámico depende del SO, de forma que manteniendo ciertas precauciones, las DLLs construidas con un lenguaje y un compilador pueden ser utilizadas por cualquier aplicación. §4 Utilizar Librerías Desde la óptica del programador C++, el manejo de librerías comprende dos aspectos totalmente diferenciados: su utilización y quizás la construcción de alguna de ellas si nuestras aplicaciones son medianamente grandes. En cuanto al primer punto, es seguro que cualquier aplicación por pequeña que sea, utilice algunas de la Librería Estándar ( 5). Por ejemplo, cada vez que en su código aparece una sentencia del tipo cout << "Hola mundo" << endl; está utilizando una librería estática, y cada vez que en la programación de una aplicación Windows utiliza un mensaje del tipo MessageBox(NULL, "Hola mundo!", "Mi primer programa", MB_OK); está usando una librería dinámica. En cuanto a su construcción, si se dedica a esto de programar en C++, antes o después pondrá manos a la obra. Por cierto: existen empresas de software cuya principal actividad es precisamente fabricar y vender librerías (ya hemos indicado que el mercado de las 3pp es todo un "mundillo" dentro de la informática). Cualquiera que sea el caso, tanto la utilización como la construcción, son diferentes según se trate de librerías estáticas o dinámicas. En las páginas que siguen se describen en detalle ambas situaciones. Empezaremos por una descripción general de su funcionamiento, para continuar con la descripción de los pasos necesarios para construirlas. A continuación exponemos los detalles de su utilización, incluyendo un ejemplo de construcción de un ejecutable que utiliza los recursos de una librería. Inicio. ________________________________________ Recordemos que en C++, uno de los significados del término "estático" es algo que ha sido resuelto en tiempo de compilación ( 1.4.4). Existen utilidades que permiten compactar ("Squeeze" un ejecutable disminuyendo su tamaño como fichero, lo que puede ser de utilidad a la hora de transportarlo (por redes por ejemplo). Pero incluso en estos casos, después de cargados en memoria deben ser "expandidos" a su tamaño normal y reacomodados en memoria según un patrón definido por el SO. BPI; acrónimo de Borland Package Import Library. Un tipo especial de librería dinámica del citado compilador, que utiliza enlazado estático con el ejecutable que las usa. En el caso del compilador BC++, durante la construcción de un programa o librería, este "path" puede ser controlado mediante el comando de compilación -L o de enlazado /L. Más tarde durante la ejecución (runtime) el "path" depende de las variables de entorno del sistema. En los programas para Windows caben tres opciones: poner las librerías específicas en el mismo directorio que el ejecutable (o un subdirectorio del anterior); en nuestra opinión esto sería lo recomendado. Otra opción es ponerlas junto con el resto de librerías del sistema (WindowsSystem) o en un directorio particular cualquiera (no es aconsejable). 1.4.4b1 Librerías estáticas §1 Sinopsis Como se indicó en la introducción ( 1.4.4b), las librerías estáticas, denominadas también librerías-objeto (en relación a que sus componentes o módulos incluyen ficheros de este tipo), son colecciones de ficheros-objeto agrupados en un solo fichero, generalmente de extensión .lib o .a., acompañados de ficheros de cabecera, generalmente .h, que contienen las declaraciones de los objetos definidos en la librería. Posteriormente, durante la fase de enlazado, el linker incluye en el ejecutable los módulos correspondientes a las funciones y clases de librería que hayan sido utilizadas en la aplicación. Como resultado, tales módulos entran a formar parte del ejecutable, de forma exactamente igual que cualquier otra función o clase que hubiese sido escrita en el cuerpo de la aplicación. Para el programador C/C++, el manejo de librerías estáticas puede tener una doble vertiente: su utilización y eventualmente la creación de alguna de ellas. Ambos son procesos distintos e independientes. El primero es prácticamente inevitable en C++, dado que como señalábamos, la Librería Estándar C++ ( 5) está constituida en su totalidad por librerías estáticas y la mera inclusión de una sentencia del tipo cout << "Hola mundo" << endl; supone la utilización de una de ellas. En lo que respecta a la creación, aunque no es usual en el caso de ejecutables triviales o pequeños, resulta en cambio un recurso habitual cuando el programador constata que algunos trozos de su código (funciones y clases), puedan ser utilizados por distintas aplicaciones. En consecuencia, es frecuente que tanto los programadores individuales como los departamentos de software de las empresas, construyan sus propios juegos de herramientas en forma de librerías estáticas y de otro tipo, que entran así a formar parte del arsenal de recursos de desarrollo. Esto sin contar con que en algunas compañías dedicadas a la fabricación de software, las librerías constituyen justamente el "producto final" de la empresa. En el presente capítulo abordaremos ambos procesos. En primer lugar el creación de una librería estática. A continuación su uso en una aplicación. La explicación la haremos sobre un ejemplo muy sencillo pero que muestra claramente el proceso a seguir en todos los casos. El ejemplo se muestra en dos versiones. Suponemos que ambas se ejecutan sobre Windows32. La primera utilizando el compilador GNU C++ de MinGW, tal como aparece en en el entorno de desarrollo Dev-C++ (ver recuadro en 1.4.0a1). De esta forma, los ejemplos pueden ser reproducidos en Linux sin modificación. La segunda tal como se efectuaría en Borland C++ 5.5. §2 Creación de una Librería estática Suponemos que queremos utilizar en nuestras aplicaciones ciertas funciones que deseamos estén incluidas en una librería. Además dispondremos de un fichero de cabecera que contenga la interfaz necesaria para la utilización de la mentada librería. Las funciones se encuentran en tres ficheros fuente y uno de cabecera: planet1.cpp; planet2.cpp; planet3.cpp y planets.h. Los ficheros están en el directorio D:LearnCplanetslibs, y responden al siguiente diseño: // planet1.cpp #include <iostream> void showMercury () { std::cout << "Primer planeta: Mercurio" << std::endl; } // planet2.cpp #include <iostream> void showVenus () { std::cout << "Segundo planeta: Venus" << std::endl; } // planet3.cpp #include <iostream> void showEarth () { std::cout << "Tercer planeta: Tierra" << std::endl; } // planets.h #ifndef _PLANETS #define _PLANETS void showMercury(); void showVenus(); void showEarth(); #endif // _PLANETS De la inspección de los ficheros es inmediato deducir que se trata de código C++ absolutamente normal, en el sentido de que es indistinguible del de cualquier otro módulo que formara parte de una aplicación C++. La razón es la ya señalada, de que estos módulos entrarán finalmente a formar parte del ejecutable que los usa, y que los detalles del proceso dependen exclusivamente del compilador. La consecuencia es que no es necesario tomar precauciones especiales respecto a asuntos tales como el planchado de nombres, o la convención de llamada de las funciones ( 4.4.6a) en los objetos de la librería. Nota: como tendremos ocasión de ver ( 1.4.4b2a) , este no es el caso del diseño de módulos de librerías dinámicas, ya que los detalles del enlazado dinámico dependen del SO, y es frecuente que librerías DLLs escritas en un lenguaje, sean utilizadas por aplicaciones escritas en otro. Como resultado, tanto las convenciones de llamada de las funciones como el planchado de nombres, pueden ser diferentes entre los diversos módulos de la aplicación, por lo que el mantenimiento de la compatibilidad exige un acuerdo en la convención a utilizar. En lo que respecta a las aplicaciones para las plataformas Windows, la convención es no utilizar planchado para los nombres de funciones exportables (que será utilizados por otros módulos de la aplicación), y la convención de llamada __pascal para las funciones que serán invocadas por el Sistema ("callbacks". §2.1 Construir una librería estática con GNU Make Para la construcción de la librería utilizamos el siguiente makefile ( 1.4.0a1) al que denominamos makefile.gnu . # Makefile.GNU para GNU make CXXFLAGS = -I"C:/DEV-CPP/lib/gcc/mingw32/3.4.2/include" -I"C:/DEV-CPP/include/c++/3.4.2/backward" -I"C:/DEV-CPP/include/c++/3.4.2/mingw32" -I"C:/DEV-CPP/include/c++/3.4.2" -I"C:/DEV-CPP/include" all: planets.a planets.a: planet1.o planet2.o planet3.o ar r planets.a planet1.o planet2.o planet3.o ranlib planets.a planet1.o: planet1.cpp g++.exe -c planet1.cpp -o planet1.o $(CXXFLAGS) planet2.o: planet2.cpp g++.exe -c planet2.cpp -o planet2.o $(CXXFLAGS) planet3.o: planet3.cpp g++.exe -c planet3.cpp -o planet3.o $(CXXFLAGS) Recordemos que la macro CXXFLAGS señala los directorios donde el compilador g++.exe debe buscar los ficheros de cabecera. Las tres últimas reglas sirven para obtener los ficheros-objeto que serán posteriormente utilizados para construir la librería. g++.exe es el compilador C++ GNU en su versión para Windows. Por su parte, ar es la utilidad GNU que agrupa los tres módulos objeto en un solo fichero planets.a, que es la librería. A continuación, la utilidad ranlib incluye en el anterior un índice o diccionario con los símbolos definidos en los ficheros que componen la librería ( 1.4.4b). Para invocar make utilizamos el procedimiento estándar para nuestro entorno. Es decir, nos situamos en el directorio correspondiente, incluimos el directorio con los binarios de Dev-Cpp en nuestra variable de entorno PATH , e invocamos la utilidad de forma que utilice nuestro fichero: C:Windows>D: D:>cd LearnCplanetslibs D:LearnCplanetslibs>set PATH=Cev-Cppbin;%path% D:LearnCplanetslibs>make -f makefile.gnu Después de unos instantes tenemos en nuestro directorio D:LearnCplanetslibs cuatro nuevos ficheros: la librería planets.a y los ficheros-objeto planet1.o, planet2.o y planet3.o necesarios para su construcción. Estos últimos pueden ser borrados, ya que en adelante, solo son necesarios la librería propiamente dicha y el fichero de cabecera planets.h. Observe que los ficheros resultantes tienen las terminaciones usuales de los entornos Linux/Unix. §2.2 Construir la librería con Borland C++ 5.5 Make La operatoria para construir una librería con el Make de Borland es análoga a la del caso anterior, aunque aquí utilizamos un makefile makefile.bor, adecuado a las particularidades de dicho compilador y de nuestro entorno: # Makefile.bor para Make de Borland C++ 5.5.1 CXXFLAGS = -IE:BorlandCPPInclude LIBS = -LE:BorlandCPPLib all: planets.lib planets.lib: planet1.obj planet2.obj planet3.obj tlib /C planets -+planet1.obj -+planet2.obj -+planet3.obj planet1.obj: planet1.cpp bcc32 $(CXXFLAGS) $(LIBS) -c -P -Q planet1.cpp planet2.obj: planet2.cpp bcc32 $(CXXFLAGS) $(LIBS) -c -P -Q planet2.cpp planet3.obj: planet3.cpp bcc32 $(CXXFLAGS) $(LIBS) -c -P -Q planet3.cpp # -c Compile to .OBJ, no link # -P Perform C++ compile regardless of source extension # -Q Extended compiler error information (Default = OFF) La única particularidad digna de mención es que la utilidad tlib desempeña las funciones que en GNU están encomendada a las utilidades ar y ranlib . Los signos -+ delante de los nombres de los objetos tienen por misión que, si el fichero .LIB existiera previamente, se descargue la versión previa del módulo correspondiente antes de incluir la nueva. En caso de que el módulo no exista previamente, se obtiene un mensaje de aviso. La invocación es similar a la de GNU Make, aunque en este caso, la variable de entorno PATH corresponde a la situación de los binarios de Borland. C:Windows>D: D:>cd LearnCplanetslibs D:LearnCplanetslibs>set PATH=E:BORLAN~1BIN;%path% D:LearnCplanetslibs>make -f makefile.bor Ahora los ficheros resultantes tienen las terminaciones habituales de los entornos Windows32: planets.LIB para la librería, y planet1.obj, planet2.obj y planet3.obj para los ficheros-objeto. §3 Usar una Librería Estática Para completar la descripción del proceso, incluiremos sendos ejemplos de uso de la librería anterior en un ejecutable C++, representado por un fuente en el directorio D:LearnCplanets, al que denominaremos main.cpp: // main.cpp #include <cstdlib> // ver nota #include "libs/planets.h" int main(int argc, char *argv[]) { showMercury(); showVenus(); showEarth(); system("PAUSE"; return EXIT_SUCCESS; } Se trata de una aplicación de consola (no gráfica) muy sencilla, que utiliza las tres funciones de nuestra librería. Para ello, la primera medida es incluir el fichero de cabecera correspondiente (planets.h) junto con el resto de includes. A continuación solo queda construir la aplicación siguiendo los procedimientos estándar de la plataforma utilizada. Como se verá en los ejemplos que siguen, la única precaución especial es indicar al compilador que debe incluir la librería correspondiente. §3.1 Construir la aplicación con GNU Make Para construir la aplicación utilizamos un makefile, al que denominamos makefile2.gnu, situado en el mismo directorio que el fuente main.cpp: # Makefile2.gnu Construir la aplicación planets.exe (GNU g++) LIBS = -L"C:/DEV-CPP/lib" CXXFLAGS = -I"C:/DEV-CPP/lib/gcc/mingw32/3.4.2/include" -I"C:/DEV-CPP/include/c++/3.4.2/backward" -I"C:/DEV-CPP/include/c++/3.4.2/mingw32" -I"C:/DEV-CPP/include/c++/3.4.2" -I"C:/DEV-CPP/include" planets.exe: main.o g++.exe main.o -o "planets.exe" $(LIBS) libs/planets.a main.o: main.cpp g++.exe -c main.cpp -o main.o $(CXXFLAGS) El proceso no tiene nada especial; después de obtenido el objeto main.o en la última línea, se ordena al enlazador (invocado a través de g++.exe) que lo enlace para producir el ejecutable. El único punto a destacar respecto al makefile utilizado para crear la librería , es la inclusión de la macro LIBS, que indica al enlazador donde encontrar las librerías estándar, y la indicación de que incluya en la compilación nuestra libs/planets.a. Esto último es importante, pues de lo contrario se obtendrían errores en el enlazado señalando que algunas referencias no han podido ser resueltas: main.o(.data+0x0):main.cpp: undefined reference to `showMercury()' main.o(.data+0x4):main.cpp: undefined reference to `showVenus()' main.o(.data+0x8):main.cpp: undefined reference to `showEarth()' Suponiendo las condiciones señaladas antes para la confección de la librería, la invocación de este makefile solo exige situarse en el directorio e invocar el fichero: D:LearnCplanetslibs>cd .. D:LearnCplanets>make -f makefile2.gnu La respuesta es la creación del ejecutable planets.exe y del fichero-objeto main.o. Como cabría esperar, la ejecución del primero produce la siguiente salida: Primer planeta: Mercurio Segundo planeta: Venus Tercer planeta: Tierra Presione cualquier tecla para continuar . . . §3.2 Construir la aplicación con Borland C++ 5.5 Make Para construir la aplicación que utiliza nuestra librería estática, mediante el Make de Borland, utilizamos un makefile makefile2.bor con el siguiente diseño: # Makefile2.bor construir la aplicación planets.exe (Borland C++ 5.5.1) LIBS = -LE:BorlandCPPLib -LD:LearnCplanetslibs CXXFLAGS = -IE:BorlandCPPInclude all: planets.exe planets.exe: main.cpp bcc32 -eplanets.exe $(CXXFLAGS) $(LIBS) -WC -P -Q main.cpp planets.LIB # -P Perform C++ compile regardless of source extension # -Q Extended compiler error information (Default = OFF) # -WC Console aplication La única particularidad es que hemos optado por construir la aplicación en mediante un solo comando, de forma que el compilador Borland gcc32.exe, se encarga de invocar sucesivamente los módulos correspondientes. Observe que en la línea de comando indicamos que debe incluirse la librería planets.LIB. A su vez, mediante la macro LIBS, señalamos las direcciones donde deben buscarse las librerías necesarias. La invocación es análoga a la anterior: D:LearnCplanetslibs>cd .. D:LearnCplanets>make -f makefile2.bor En esta ocasión, el resultado incluye el fichero main.obj además del ejecutable planets.exe. Inicio. ________________________________________ Recordemos que en los makefiles GNU, las líneas de comando deben estar precedidas de una tabulación (TAB), mientras que en los de Borland basta con un espacio. El lector debe realizar los ajustes necesarios en los comandos para adecuarlos a las condiciones particulares de su entorno. Para la compilación con Borland C++ 5.5 este include debe ser cambiado por la versión tradicional de la cabecera: #include <stdlib.h> Las "binutils" de Borland incluyen tlib.exe, una herramienta que combina la funcionalidad de ar y ranlib de GNU.