La compilación y los módulos en C y C++

Julio 2017

El objetivo de este articulo es el de presentar los fundamentos de la compilación en C, C++ y de la programación modular.

Esto nos permitirá comprender mejor los mensajes de error del compilador. Las nociones abordadas aquí son independientes del sistema operativo utilizado (Windows o Linux). No obstante, supondremos que estamos bajo Linux para ver con más detalle lo que ocurre durante la compilación de un programa en C o C++.



Introducción


De manera general, el objetivo de un compilador es el de convertir un archivo texto (conteniendo código fuente) en un archivo binario (por ejemplo un ejecutable). Una vez creado el ejecutable, éste es ejecutado como cualquier programa. Este programa puede ejecutarse sin que se disponga del código fuente.

Un lenguaje compilado (como C o C++) es diferente a un lenguaje interpretado (por ejemplo un script de shell) o a un seudo-interpretado (por ejemplo python).

En C, la compilación transformará el código C de un programa en código nativo, es decir una serie de instrucciones binarias que pueden ser directamente comprendidas por el procesador.

Instalación de un compilador C y C++


Bajo Linux


En general se utiliza gcc y g++. Para instalarlo se utiliza su gestor de paquetes habitual. Por ejemplo bajo Debian (o cualquier distribución basada en debian) basta con escribir como root o con un sudo:

aptitude update
aptitude safe-upgrade
aptitude install gcc g++


Del mismo modo podemos instalar un entrono de desarrollo como por ejemplo kdevelop (bajo KDE) o anjuta (bajo gnome).

Bajo Windows


Podemos utilizar dev-cpp o code::blocks: dos entornos de desarrollo libres que se basan en gcc y g++:
http://www.bloodshed.net/devcpp.html
http://www.codeblocks.org/

Articulo relacionado: http://es.ccm.net/faq/sujet 2817 compilar un programa en c bajo linux

Las fases de compilación


La primera fase consiste en escribir el código fuente en lenguaje C o C++ (archivos con extensión .c y .h en C y .cpp y .hpp en C++). Luego se efectúa la compilación, por ejemplo con gcc (en C) o g++ (en C++). La compilación se desarrolla en tres grandes fases.

1) El preprocesado


El compilador comienza por aplicar cada instrucción pasada al preprocesador (todas las líneas que comienzan con #, entre estas las #define). Estas instrucciones son en realidad muy simples ya que únicamente copian o eliminan secciones de código sin compilarlas.

Es en esta fase que las #define que se encuentran en un archivo fuente (.c o .cpp) o en un header (.h o .hpp) son reemplazadas por código C/C+. Al final de esta etapa, no habrán instrucciones comenzando por #.

2) La compilación


Luego, el compilador compila cada archivo fuente (.c y .cpp), es decir crea un archivo binario (.o) para cada archivo fuente, excepto para el archivo conteniendo la función main. Esta fase constituye la compilación propiamente dicha.

Estas dos primeras etapas son realizadas por cc cuando se utiliza gcc/g++.

3) El enlazado


Finalmente, el compilador une cada archivo .o con los archivos binarios de las librerías que son utilizadas (archivos .a y .so bajo Linux, archivos .dll bajo Windows).

Especialmente, verifica que cada función llamada en el programa no esté solamente declarada (esto es hecho durante la compilación) sino también implementada. También verifica que una función no esté implementada en varios archivos .o.

Esta fase constituye la fase final para obtener un ejecutable (.exe bajo Windows, generalmente sin extensión bajo Linux).

Esta fase es realizada por ld cuando se utiliza gcc/g++.

Warning y errores


Evidentemente, en un entorno de desarrollo, basta con hacer clic sobre “build” y estas tres fases se llevan a cabo automáticamente. No obstante, es importante conocerlas para interpretar correctamente los mensajes de error o warning de un compilador.

Un warning significa que el código es ambiguo y que puede ser interpretado de manera diferente de un compilador a otro, pero el ejecutable puede ser creado.

En cambio, un error significa que el código no ha podido ser compilado completamente y que el ejecutable no ha podido ser creado.

Las grandes etapas para escribir un programa en C o C++


Escribir el código fuente


Un simple bloc de notas puede ser suficiente, por ejemplo podemos escribir en el fichero plop.c:

#include <stdio.h>

int main(){
    printf("plop !\n");
    return 0;
}

Compilar


Bajo Linux llamamos directamente a gcc (-W y –Wall permiten mostrar más mensajes para verificar si el código es “limpio”, -o plop.exe indica que el ejecutable que será creado debe llamarse plop.exe):

gcc -W -Wall -o plop.exe plop.c


Implícitamente el compilador hace las tres etapas descritas anteriormente.

1) El preprocesado

/* Todo lo que es definido por <stdio.h>, incluyendo printf() */

int main(){
    printf("plop !\n");
    return 0;
}


2) Compilación (encuentra sin problemas printf ya que éste es declarado en <stdio.h>)
3) Enlazado (encuentra sin problemas printf en el binario de la lib c). También lo podemos verificar bajo Linux con ldd:

ldd plop.exe


Lo que da:
 linux-gate.so.1 =>  (0xb7f2b000)
        libc.so.6 => /lib/i686/cmov/libc.so.6 (0xb7dbb000)
        /lib/ld-linux.so.2 (0xb7f2c000)



En la segunda línea podemos ver que utiliza la lib c. luego crea plop.exe. Por otra parte comprobamos que no hay error ni warning.

Ejecución


Tan solo queda ejecutarlo:

./plop.exe


Lo que da como esperado:

plop!


Si algún error se produce aquí (error de segmentación, falta de memoria, etc.), por lo general habrá que recurrir a un depurador (por ejemplo gdb o ddd), revisar el código fuente, etc. En todos los casos, no será un problema de compilación.

Articulo relacionado: error de segmentación

Atención: Bajo Windows existen dos métodos para ejecutar un programa:

Método 1: podemos ejecutar un programa a través de los comando ms-dos (haciendo clic en Inicio / Ejecutar y escribir “cmd”). Con el comando cd nos colocamos en el directorio y ejecutamos el programa. En este caso todo ira bien.

Método 2: Si ejecutamos el programa desde el explorador, no podremos ver el programa a menos que pongamos una pausa justo antes del final del programa.

#include <stdio.h>

int main(){
    printf("plop !\n");

    getchar(); /* el programa se detiene a menos que presionemos una tecla */
    return 0;
}

Los errores clásicos


Error de compilación


Supongamos que olvidamos incluir en nuestro código fuente el archivo <stdio.h> (en el que está declarada la función printf), o un ";", tendremos entonces un mensaje de error de compilación. Es el error más típico.

Error de enlazado


Estos errores son más sutiles, ya que no tienen que ver con la sintaxis, sino con la manera en que esta estructurado y compilado el programa. Son fáciles de reconocer cuando se utiliza gcc o g++ ya que los mensajes de error correspondientes mencionan a ld (el linker).

Primer ejemplo: multidefinición

Un error de enlazado puede ocurrir cuando se escribe el código de un programa utilizando varios ficheros. A continuación veremos este tipo de error:

Supongamos que nuestro programa es escrito utilizando 3 archivos: a.h, a.c, y main.c. El encabezado a.h esta incluido en los dos archivos fuente main.c y a.c. el archivo main.c contiene la función main().

1) Si compilamos únicamente a.c, el archivo que no contiene la función main, es necesario indicarle al compilador (opción –c en gcc) sino no sabrá cómo crear un ejecutable, ya que no hay punto de partida. Es por ello que el archivo conteniendo la función main (sin opción –c) y los otros archivos fuente se compilan de manera diferente. En este caso:

gcc -W -Wall -c a.c
gcc -W -Wall -o plop.exe main.c a.o


Las opciones –W y –Wall permiten mostrar más mensajes de warning.
-El primer comando construye a.o a partir de a.c.
-El segundo genera el binario asociado a main.c, lo une con a.o, y produce así un ejecutable (plop.exe)

Podemos observar que si el programa contiene un error en a.c, el compilador producirá un error al momento de compilar a.c. esto provocará errores en cascada en los otros archivos. Por ello cuando un programa no compila, se comienza por lo primeros mensajes de error, los solucionamos, lo volvemos a compilar, etc…hasta que todos los errores sean solucionados.

2) Recordemos que normalmente, declaramos la función en el encabezado (por ejemplo a.h):

void plop();


…y que lo implementamos en el archivo fuente (por ejemplo a.c):

#include "a.h"
#include <stdio.h>

void plop(){
  printf("plop !\n");
}


Ahora supongamos que implementamos la función plop() en a.h (es decir que la función no está declarada en a.h). En otras palabras, el archivo a.h contiene

#include <stdio.h>

void plop(){
  printf("plop !\n");
}


…y el fichero a.c contiene por ejemplo:

#include "a.h"

void f(){
  plop();
}


El fichero a.h es incluido por main.c y a.c. De este modo el contenido de a.h es copiado en a.c y en main.c. Así, cada uno de estos dos archivos fuentes dispondrán de una implementación de la función plop(), pero el compilador no sabrá cual tomar y generará un error de multi definición al momento del enlazado:


(mando@aldur) (~) $ gcc -W -Wall main.c a.o
a.o: In function `plop':
a.c:(.text+0x0): multiple definition of `plop'
/tmp/ccmRKAvQ.o:main.c:(.text+0x0): first defined here
collect2: ld returned 1 exit status


Esto muestra por que en general la implementación de una función se debe hacer en un archivo fuente (.c o .cpp) y no en un encabezado (.h y .hpp). Solo hay dos excepciones a esta regla en C+: las funciones inline y las funciones template (o los métodos de una clase template). Para mayor información ver estos dos artículos:

Link1
http://es.ccm.net/faq/sujet 2823 la funcion inline en c

Programación modular: multi-definición, bucles de inclusión…


Supongamos que tenemos 3 archivos: main.c y los archivos a.h y b.h. supongamos que a.h y b.h se incluyen mutuamente. Estos dos archivos se incluyen mutuamente indefinidamente! Es aquí que el preprocesador viene a nuestra ayuda, ya que vamos a poder evitar que los #include no se hagan indefinidamente poniendo en a.h:

#ifndef A_H
#define A_H
#include "b.h"
/* El contenido del encabezado a.h */

#endif


Y en b.h:

#ifndef B_H
#define B_H
#include "a.h"
/* El contenido del header b.h */

#endif


¿Concretamente, qué es lo que pasará? El compilador comenzará con un archivo (por ejemplo a.h). Como en ese momento A_H no está definido, éste avanza, luego define a.h y continua leyendo el encabezado a.h, incluyendo b.h. en ese momento B_H no está definido, por lo tanto de la misma manera entramos en el encabezado b.h y activamos A_H.

Ahora leemos el contenido de b.h que quiere incluir a.h. Entramos nuevamente en a.h pero esta vez, A_H está definido por lo que ignoramos el contenido del encabezado. Terminamos de leer el encabezado b.h, lo que resuelve el #include "b.h" que estábamos tratando en a.h. Luego terminamos el encabezado b.h.

Este caso puede parecer extraño, pero hay que tener en cuenta que cuando un encabezado está incluido varias veces, pueden aparecer multidefiniciones (en particular si son declaradas estructuras en el encabezado). Por ello es que ponemos sistemáticamente este mecanismo de cerrojo en todos los encabezado.

Funcion declarada…pero no encontrada


Si una función es declarada, utilizada, pero no implementada, también se producirá un error de enlazado. Esto ocurre típicamente en dos casos:
-se ha declarado una función pero aun no se la ha implementado
-se desea utilizar una función de una librería, se han incluido los encabezados correspondientes, pero se ha olvidado pasar como parámetro del compilador dichas librerías.

Ir más lejos con la compilación: makefile


Cuando no se dispone de entorno de desarrollo, a fin de evitar teclear manualmente un “gcc” por fichero fuente, hacemos un fichero “makefile” que se ocupa de describir cómo construir el ejecutable.

En la practica, es buena idea escribir un archivo además de nuestros ficheros C/C+ para que el código sea fácil de compilar. Si este fichero es correctamente escrito, tan solo hay que ejecutar makefile para construir íntegramente el programa. Bajo Linux escribimos simplemente el comando:

make


PD: El artículo original fue escrito por Jeff, contribuidor de CommentCaMarche

Consulta también

Publicado por Carlos-vialfa. Última actualización: 3 de junio de 2009 a las 00:57 por Carlos-vialfa.
El documento «La compilación y los módulos en C y C++» se encuentra disponible bajo una licencia Creative Commons. Puedes copiarlo o modificarlo libremente. No olvides citar a CCM (es.ccm.net) como tu fuente de información.