Enlazando librerías en C con Linux y otras curiosidades

¡Buenas a todos! Hoy os traigo un tip bastante sencillo que os puede ayudar si alguna vez se os ocurre programar en C utilizando NetBeans en GNU/Linux. Más que un tip de gran interés, es una curiosidad que tiene que ver con el tema de las librerías y el compilador. Así que si alguna vez os habéis preguntado cómo funciona la magia del #include<libreria.h>, esto os resultará interesante.

Todo este asunto viene porque hasta ahora, había realizado las prácticas de mi asignatura Sistemas Informáticos Industriales utilizando como IDE el Dev-C++ (bajo Windows). Todos los programas funcionaban a la perfección y para mí las librerías eran utilidades que estaba usando sin saber cómo se comportaban realmente, a un nivel más profundo.

Para refrescar algo de conceptos, cuando alguien programa en C suele utilizar un IDE que genera archivos loquesea.c con una plantilla determinada, que incluye todo lo necesario para que no tengamos que importar librerías y escribir toda la sintaxis de la función principal cada vez. Por defecto, en todos los entornos de desarrollo que he utilizado, se importa automáticamente la librería stdio (standard input-output) a nuestro proyecto. Tiene sentido, pues en la mayoría de ocasiones necesitamos que nuestro programa tenga entradas y salidas. Tomar datos por teclado o mostrar resultados en pantalla son ejemplos claros de la necesidad de importar esta librería.

Pues bien, uno de los ejercicios propuestos para la práctica de esta asignatura consistía en realizar un programa que obtuviera una aproximación del seno de un ángulo (cercano a 0, para que esta aproximación fuera más válida) a través de una serie finita de términos, que incluía números factoriales y potencias, entre otros. Creo que sería excesivo incluir en este artículo el código que forma ese programa, pero no me importaría explicarlo a través de otro artículo. Podéis escribir en los comentarios si queréis ese tutorial sobre el código.

Antes de comenzar a hablar de las librerías, cabe destacar que en GNU/Linux no existe:

system("PAUSE");

Esa es la forma que me sugirieron en las prácticas de mi asignatura para poder visualizar el output del programa antes de que este finalice (usando Windows, claro). La alternativa más obvia es la instrucción:

getchar();

Comento este detalle porque investigando por qué en Ubuntu no es posible utilizar el primero, he descubierto que la segunda forma consume muchísimos menos recursos. La descripción de usar system("PAUSE"); que leí en Internet durante mi investigación era la siguiente:

Es como quemar tus muebles para calentarte, cuando tienes una estufa justo al lado.

Para que lo entendáis mejor, la instrucción system("PAUSE"); realiza la siguiente serie de acciones:

  1. Suspende el programa.
  2. Llama al sistema operativo.
  3. Abre una terminal.
  4. Busca el comando PAUSE.
  5. Asigna la memoria para ejecutar el comando.
  6. Ejecuta el comando y espera a que el usuario pulse una tecla.
  7. Libera la memoria.
  8. Sale del sistema operativo.
  9. Como última acción, permite que tu programa siga ejecutándose.

Es bastante obvio que consume muchos más recursos que el resto de alternativas. Este hecho, sumado a que no permite la portabilidad de un programa desde Windows a GNU/Linux, creo que merece que jamás lo utilicemos en nuestros programas. Obviamente, para el ámbito de mis prácticas, utilizar system("PAUSE"); es una opción completamente válida y no es necesario tener en cuenta todo lo comentado anteriormente, porque no tenemos problemas de espacio y los programas son relativamente sencillos.

Pasamos al tip y las curiosidades sobre las librerías en C. Para ello incluyo el siguiente código, que básicamente es un programa que permite calcular n elevado a m:

/* * File:   main.c * Author: javierlight * Created on 28 de febrero de 2015 */ #include<stdio.h> #include<stdlib.h> #include<math.h> /* * Programa que calcula la potencia de un número. */ int main(int argc, char** argv) { int num, exp, potencia;   //declaración de las variables a utilizar printf("Por favor, introduce el numero: "); scanf("%d",&num); printf("Por favor, introduce el exponente: "); scanf("%d",&exp); potencia = pow(num, exp); printf("El resultado es %d",potencia); getchar(); //el usuario debe pulsar una tecla para finalizar return (0); }

Como he comentado anteriormente, todos los entornos de desarrollo importan la librería stdio nada más crear un nuevo proyecto, forma parte de la plantilla. ¿Qué pasa si queremos utilizar funcionalidades que no estén incluidas en esa librería tan básica? En el caso del ejemplo de mi ejercicio, ¿cómo calculamos potencias de un número, o el seno de un ángulo? La respuesta es trivial: importamos otra librería que nos permita aprovechar esas funciones. En nuestro caso, solo tenemos que incluir en nuestro proyecto la librería math.

Lo indicamos en nuestro programa, en la segunda línea:

#include<stdio.h> #include<math.h>

¿Y ya está? Pues sí. Si estamos utilizando el IDE Dev-C++ en Windows, podremos utilizar las instrucciones sin(angle) o pow(n, m) para realizar las operaciones nombradas anteriormente. Pero no es tan sencillo si nos encontramos en otra posible situación: Netbeans en Ubuntu.

El output que muestra el programa después de compilar y ejecutar es el siguiente:

build/Debug/GNU-Linux-x86/main.o: En la función `main': /home/javierlight/NetBeansProjects/CProjects/Potencias/main.c:20: referencia a `pow' sin definir collect2: error: ld returned 1 exit status make[2]: *** [dist/Debug/GNU-Linux-x86/potencias] Error 1 make[2]: se sale del directorio «/home/javierlight/NetBeansProjects/CProjects/Potencias» make[1]: *** [.build-conf] Error 2 make[1]: se sale del directorio «/home/javierlight/NetBeansProjects/CProjects/Potencias» make: *** [.build-impl] Error 2 BUILD FAILED (exit value 2, total time: 166ms)

El error se produce en la función main, en la línea 20, cuando utilizo la operación potencia (elevar un número a algo). ¿Qué está pasando? ¿Por qué no es capaz de encontrar esa instrucción si he importado la librería math?

Como ya he explicado, las librerías que se incluyen por defecto son stdio y stdlib. No solo se incluyen, sino que además están automáticamente enlazadas con el ejecutable. Esto significa que no hace falta indicarle al compilador que debe contar con ellas en nuestro proyecto.

Para poder entenderlo, debemos saber que cuando escribimos #include<libreria.h> simplemente estamos indicándolo para nuestra propia información (y para que el compilador sepa qué símbolos debe aceptar si enlazamos la librería). Es un paso opcional, pero debemos realizar otro, vital para que se complete ese proceso de inclusión, que es enlazar esa librería con nuestro proyecto. Podemos hacerlo indicándoselo al compilador o a través de las preferencias del proyecto. Esto es así porque en los archivos de cabecera (libreria.h) no es el lugar donde "viven" las funciones. Para poder implementarlas, debemos hacer referencia al archivo de librería (library file).

Con el botón derecho hacemos click en el nombre de nuestro proyecto, y elegimos la opción propiedades. Luego solo tenemos que añadir ese link a la librería math. Se lo indicamos al compilador escribiendo -lm en el apartado Linker.

Después de compilar y ejecutar, obtenemos el siguiente output:

Por favor, introduce el numero 2 Por favor, introduce el exponente 3 El resultado es 8 RUN FINISHED; exit value 0; real time: 3s; user: 0ms; system: 0ms

Seguramente os estéis preguntando por qué se incluyen esas librerías y no otras, o por qué debemos enlazar manualmente las librerías que no lo están por defecto. No he podido encontrar una respuesta oficial, pero la razón que más se valora tiene que ver con el tamaño del programa. Solo se incluyen stdio y stdlib porque en algún lugar se debe trazar la línea, no sería factible incluir todas las librerías en nuestros proyectos. Estaríamos ocupando espacio de forma innecesaria, ya que es posible que no estemos aprovechando ninguna de sus utilidades. Como sabéis, C es un lenguaje de programación relativamente viejo y esta decisión se tomó en su momento en un contexto en el que el tamaño se valoraba mucho más que hoy en día. Solo hay que ver cómo han evolucionado las memorias desde los diskettes hasta los discos duros de Terabytes que utilizamos ahora mismo.

¿Os gusta saber cómo ocurren las cosas a un nivel tan profundo o preferís conformaros con que todo funcione siguiendo un par de pasos de algún tutorial? Yo lo tengo claro: me encantaría saber cómo funciona todo, pero no tengo tiempo para averiguarlo. La mayoría de veces me conformo con poner un parche y que todo funcione como debe.