Este documento está protegido por la Ley de Propiedad
Intelectual (Real Decreto Ley 1/1996 de 12 de abril).
Queda expresamente prohibido su uso o distribución
sin autorización del autor.
Guión de prácticas
Metodología de la Programación (grupo A)
Compilación separada
y toolchain básico
1. Objetivos y planificación........................................................................................................................ 2
Sesión 1............................................................................................................................................................... 3
2. Herramientas básicas de desarrollo para C/C++........................................................................4
3. Compilación desde la línea de órdenes.........................................................................................6
4. Creación de un segundo programa y división en ficheros..................................................7
5. Creación de bibliotecas....................................................................................................................... 21
Sesión 2............................................................................................................................................................ 25
6. Automatización del proceso de compilación con GNU Make.........................................26
Sesión 3............................................................................................................................................................ 34
7. Una herramienta de alto nivel: CMake........................................................................................ 35
8. El IDE Visual Studio Code................................................................................................................... 46
© Prof. Javier Martínez Baena
Dpto. Ciencias de la Computación e I. A.
Universidad de Granada
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
1. Objetivos y planificación
Esta primera práctica se plantea como una introducción a la construcción de proyectos software
en los que el código se divide en múltiples ficheros. Para ello el estudiante debe entender el
modelo de compilación de C++ y conocer herramientas para, a partir de los ficheros con código
fuente, construir los ejecutables finales. Aunque se explican todos los conceptos necesarios para
llevar a cabo la práctica, se recomienda revisar aquellas partes de la asignatura Fundamentos del
Software en las que se trabaja sobre esta temática (compilación, enlazado, make, …).
Los objetivos de esta práctica son los siguientes:
• Instalar un toolchain con las herramientas básicas para el desarrollo de software en C++.
• Comprender el modelo de compilación separada en C++. Comprender las distintas etapas
que se producen durante el proceso de traducción de C++ a código máquina
(preprocesamiento, compilación y enlazado).
• Aprender a modularizar y organizar el software en diversos ficheros y carpetas.
◦ División de los ficheros de código fuente en ficheros de cabecera y ficheros de
implementación.
◦ Comprender la separación entre parte pública y privada en la construcción de tipos de
datos abstractos (TDA).
◦ Construcción de bibliotecas.
• Usar herramientas de línea de órdenes para construir el proyecto de forma manual.
• Usar la herramienta GNU Make para automatizar el proceso de construcción del proyecto.
En una segunda etapa se plantea como objetivo el uso de herramientas de más alto nivel:
• Usar de forma elemental la herramienta CMake para alcanzar un mejor grado de
automatización.
• Usar Visual Studio Code como IDE de trabajo habitual de forma conjunta con CMake.
Todas las prácticas del curso, las posibles pruebas o exámenes que se hagan y las correcciones se
realizarán en un S.O. GNU/Linux. Aún así, se dan algunas indicaciones sobre cómo instalar y usar
las herramientas en Windows y MacOS.
Temporización
Este guión será desarrollado durante 3 sesiones de prácticas:
• Sesión 1: Instalación de herramientas básicas y compilación desde la línea de órdenes de
un programa con GNU g++.
• Sesión 2: Automatización del proceso de compilación con GNU Make y creación de
bibliotecas.
• Sesión 3: Automatización con CMake.
Puede comprobar que el documento contiene explicaciones de todo y que puede resultar algo
extenso por lo que es conveniente que sea leída la parte correspondiente antes de asistir a clase.
Podrá ver que hay algunos ejercicios propuestos como extensión a las explicaciones aunque lo
ideal es que vaya realizando todo lo expuesto en las explicaciones.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 2/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
Sesión 1
→ Instalación y uso de GNU g++
→ Modularización en múltiples ficheros
→ Creación de bibliotecas
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 3/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
2. Herramientas básicas de desarrollo para C/C++
El desarrollo de software es un proceso en el que se utilizan diversas herramientas para, a partir
del código fuente de un programa, llegar hasta los ejecutables finales. Ese conjunto de
herramientas es conocido como toolchain (cadena de herramientas) y para el caso de desarrollo
con C/C++ incluye programas como:
• Compilador.
• Enlazador.
• Depurador.
• Herramienta de automatización del proceso de compilación.
En esta asignatura usaremos como punto de partida la GNU Toolchain, que es open source y está
disponible en cualquier plataforma (GNU/Linux, Ms Windows, Mac OS, …). Concretamente
utilizaremos:
• GNU Compiler Collection (GCC)1. Este paquete incluye compiladores para C,
C++, Fortran, Objective-C, Go, etc.
• GNU binutils2. Desde nuestra perspectiva, las herramientas de este paquete
más relevantes son el enlazador o linker (ld) y una utilidad para trabajar con
fichero de código objeto que permite crear bibliotecas (ar).
• GNU debugger (GDB)3. Es un depurador.
• GNU make4. Es una herramienta para automatizar todo el proceso de creación del software
final.
Además, es habitual disponer de algunas herramientas para facilitar la creación de programas
como, por ejemplo, editores de texto o algún IDE (Integrated Development Environment). Para
comenzar haremos uso de algún editor sencillo de los que ya suelen venir instalados con nuestro
sistema. En el caso de GNU/Linux podemos usar gedit, geany, kwriter, kate, sublime, atom, vim, vi,
joe, etc. Tenga cuidado de no confundir “editor de texto” con “procesador de textos” (Ms Office
Word, Libreoffice Writer, etc.) puesto que la finalidad de ambos es completamente distinta.
Actualmente existe otro toolchain de código abierto que está ganando mucha
popularidad y que se plantea como un rival importante de GNU toolchain. Se trata de
LLVM5, que incluye su propio compilador (clang) así como del resto de herramientas
necesarias para todas las fases del proceso de compilación. Clang admite las
mismas opciones en línea de órdenes que g++ por lo que podríamos trabajar con cualquiera de
los dos indistintamente.
2.1. Instalación del toolchain básico
Para nuestro caso (Ubuntu GNU/Linux) la instalación de este toolchain básico consistirá en ejecutar
como administrador los siguientes comandos en una terminal:
prompt> sudo apt install build-essentials
prompt> sudo apt install gdb
Puede comprobar que la instalación se ha realizado con éxito ejecutando lo siguiente:
prompt> g++ --version
prompt> ld --version
prompt> ar --version
prompt> make --version
Aunque en la asignatura vamos a trabajar con GNU/Linux, se incluyen a continuación algunas
1 https://gcc.gnu.org/
2 https://www.gnu.org/software/binutils/
3 https://www.sourceware.org/gdb/
4 https://www.gnu.org/software/make/
5 https://llvm.org/
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 4/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
indicaciones sobre cómo podría desarrollar las prácticas en otros Sistemas Operativos. En primera
instancia se recomienda hacer una instalación de GNU/Linux en una partición independiente de
Windows y usar dual boot. Como segunda opción, si ya tiene instalado un sistema Windows, sería
usar Windows con WSL. Como última opción se recomienda usar Windows con MSYS. MacOS
también permite trabajar de forma muy similar a GNU/Linux. Pero, en cualquier caso, debe estar
familiarizado con el uso de un entorno nativo GNU/Linux para la realización de pruebas en la
asignatura.
Sistema operativo Microsoft Windows
En este sistema tiene varias posibilidades para desarrollar software con un toolchain como el de
GNU. Dos de las más populares son estas (cada una con un ámbito de aplicación distinto):
1) Instalar MSYS26. Se trata de una colección de herramientas base para disponer de un
entorno como el que se utiliza en GNU/Linux pero nativo de Ms Windows. Es decir, con
MSYS dispondrá en Windows de una shell como Bash, herramientas como g++, etc. Una
vez instalado deben añadirse los paquetes que incluyen las utilidades de GNU toolchain.
a) Descargue e instale el paquete MSYS2 desde https://www.msys2.org
b) MSYS2 dispone de diversos entornos7 (UCRT64, MSYS, MINGW64, …) que determinan el
toolchain que se utiliza por defecto y definen algunas variables de entorno. Se
recomienda utilizar UCRT64.
c) Ejecute pacman para actualizar paquetes y repositorios:
pacman -Syu
d) Instale GNU g++, GNU gdb y GNU make:
pacman -S mingw-w64-ucrt-x86_64-gcc
pacman -S mingw-w64-ucrt-x86_64-gdb
pacman -S mingw-w64-ucrt-x86_64-make
e) Alternativamente puede instalarlo todo con:
pacman -S base-devel mingw-w64-ucrt-x86_64-toolchain
2) Instalar WSL28 (Windows Subsystem for
Linux) junto con una distribución
GNU/Linux. En este caso se opta por
activar una característica de los sistemas
Ms Windows modernos que permite la
integración de un sistema GNU/Linux con
uno Ms Windows ya instalado. Esta opción
ejecuta una máquina virtual ligera sobre
Windows que permite ejecutar un núcleo
GNU/Linux para ejecutar aplicaciones
nativas de este último sistema que se integran perfectamente con el sistema de archivos y
el escritorio de Windows. Por tanto, debe tener en mente que con este modelo está usando,
conceptualmente, dos máquinas: una con Windows y otra con GNU/Linux.
Una de las principales diferencias entre este modelo y el que usa MSYS es que con WSL las
aplicaciones desarrolladas son nativas para GNU/Linux, es decir, que no se pueden ejecutar
en una máquina Windows. Solo pueden ejecutarse en máquinas GNU/Linux (bien nativas o
bien sobre WSL).
Si lo que quiere es diponer de un entorno nativo GNU/Linux pero sin instalarlo en una nueva
partición o en una máquina virtual como VirtualBox o VMWare esta es una buena opción. Si
lo que quiere es poder desarrollar software que se pueda ejecutar en máquinas Windows
debería optar por MSYS.
En el siguiente enlace tiene un sencillo tutorial de instalación:
6 https://www.msys2.org/
7 https://www.msys2.org/docs/environments/
8 https://learn.microsoft.com/es-es/windows/wsl/about
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 5/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
https://www.softzone.es/windows/como-se-hace/subsistema-windows-linux/
Sistema operativo Mac OS
Debe instalar las Xcode Command Line Tools. Tiene más instrucciones en estos enlaces:
• https://mac.install.guide/commandlinetools/
• https://mac.install.guide/commandlinetools/4
En este caso, el compilador que se instala es clang en lugar de GNU g++ aunque no va a notar la
diferencia ya que ambos se utilizan prácticamente igual. Además, tras la instalación se crea un
alias o enlace llamado g++ que ejecuta clang para que sea totalmente transparente al usuario.
3. Compilación desde la línea de órdenes
En esta primera parte de la práctica debe crear una aplicación usando el toolchain instalado y
haciendo uso de llamadas a las diferentes herramientas desde la línea de órdenes. La aplicación
consiste en un pequeño programa que es capaz de trabajar con formas geométricas sencillas
(puntos y rectángulos en 2D) y el objetivo es calcular si un punto se encuentra o no dentro de un
rectángulo.
En la siguiente figura puede ver
un ejemplo en el que se han
dibujado un rectángulo y 2
puntos, uno de ellos dentro y otro
fuera.
Para ello vamos a crear dos TDA
(tipo de dato abstracto): uno para
trabajar con puntos y otro para
trabajar con rectángulos. Cada
uno de ellos lo implementaremos
con una clase distinta. Dispone del código completo en un fichero llamado main_todo1.cpp en la web
de la asignatura y al final de esta sección.
Puede utilizar un editor de texto cualquiera (no confundir con un procesador de texto -Libreoffice
Writer, Microsoft Word, ...-) para editar dicho fichero. En GNU/Linux tiene muchos: vim, joe, geany,
emacs, kate, kwrite, gedit, sublime, ...
ᐒ Ejercicio 1 : Compilación y generación del ejecutable
Realice la compilación desde la línea de órdenes. Para ello debe llamar al compilador de la
siguiente forma:
prompt> g++ main_todo1.cpp -o miprograma1
en donde:
• g++ es el nombre del ejecutable del programa compilador GNU g++.
• main_todo1.cpp es el nombre del fichero que contiene el código C++ que vamos a compilar.
• -o miprograma1 es una opción para indicarle a g++ en qué fichero ha de almacenar el código
máquina resultante (el ejecutable, que en GNU/Linux no tiene extensión).
Si el código C++ es correcto puede comprobar que se ha generado correctamente con el comando
ls y ejecutarlo de la siguiente forma:
prompt> ./miprograma1
Aunque esto ha funcionado, no es la forma habitual de llevar a cabo todo este proceso. Para
empezar, hemos aprovechado la herramienta g++ para hacer todas las etapas de traducción con
un solo comando. Sin embargo, es frecuente separar el proceso en varias etapas … más adelante
veremos el motivo.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 6/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
Fichero main_todo1.cpp
#include <iostream> bool contain(const Point &p) const;
#include <string> };
#include <sstream>
#include <iomanip> Rectangle::Rectangle(const Point &pp1, const Point &pp2) {
// Nos aseguramos de que p1 es la esquina superior izquierda
using namespace std; // y de que p2 es la esquina inferior derecha
psupizq = Point(min(pp1.getX(),pp2.getX()),
// ************************************ max(pp1.getY(),pp2.getY()));
pinfder = Point(max(pp1.getX(),pp2.getX()),
double max(double v1, double v2) { min(pp1.getY(),pp2.getY()));
return (v1>v2) ? v1 : v2; }
}
string to_string(const Rectangle &r) {
double min(double v1, double v2) { ostringstream tmp;
return (v1<v2) ? v1 : v2; tmp.setf(std::ios_base::fixed);
} tmp << setprecision(1) << "[" << to_string(r.getTL()) << "-"
<< to_string(r.getBR()) << "]";
// ************************************ return tmp.str();
class Point { }
private:
double x, y; bool Rectangle::contain(const Point &p) const {
public: return p.getX()>=psupizq.getX() && p.getX()<=pinfder.getX() &&
Point(int px=0, int py=0) : x(px), y(py) { } p.getY()>=pinfder.getY() && p.getY()<=psupizq.getY();
double getX() const { return x; } }
double getY() const { return y; }
}; // ************************************
int main() {
string to_string(const Point &p) { Rectangle r1(Point(10,40),Point(40,20));
ostringstream tmp; Rectangle r2(Point(30,50),Point(50,30));
tmp.setf(std::ios_base::fixed); Point p1(20,30);
tmp << setprecision(1) << "(" << p.getX() << "," Point p2(60,60);
<< p.getY() << ")"; Point p3(30,50);
return tmp.str(); Point p4(30,30);
}
cout << "Puntos: " << endl;
// ************************************ cout << to_string(p1) << endl;
class Rectangle { cout << to_string(p2) << endl;
private: cout << to_string(p3) << endl;
Point psupizq, pinfder; cout << to_string(p4) << endl;
public: cout << "Rectángulos: " << endl;
Rectangle(const Point &pp1=Point(), const Point &pp2=Point()); cout << to_string(r1) << endl;
Point getTL() const { return psupizq; } cout << to_string(r2) << endl;
Point getBR() const { return pinfder; }
Point getTR() const { cout << "p1 dentro de r1: " << r1.contain(p1) << endl; // Sí (1)
return Point(pinfder.getX(),psupizq.getY()); } cout << "p2 dentro de r1: " << r1.contain(p2) << endl; // No (0)
Point getBL() const { cout << "p1 dentro de r2: " << r2.contain(p1) << endl; // No
return Point(psupizq.getX(),pinfder.getY()); } cout << "p3 dentro de r1: " << r1.contain(p3) << endl; // No
double width() const { return pinfder.getX() - psupizq.getX();} cout << "p4 dentro de r1: " << r1.contain(p4) << endl; // Sí
double height() const {return psupizq.getY() - pinfder.getY();} }
4. Creación de un segundo programa y división en ficheros
En este apartado deberá crear un nuevo
programa que, aprovechando el código que
ya tiene para trabajar con formas 2D, haga
una nueva tarea: calcular si dos rectángulos
se solapan o no.
En la figura puede ver un ejemplo con tres
rectángulos. Dos de ellos se solapan entre
sí y el tercero no solapa con ninguno.
Para ello añadiremos una nueva función y
modificaremos main() de esta forma:
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 7/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
bool overlaps(const Rectangle &r1, const Rectangle &r2) { int main() {
if (r1.getTL().getX()>r2.getBR().getX() || Rectangle r1(Point(15,40),Point(70,10));
r1.getBR().getX()<r2.getTL().getX() || Rectangle r2(Point(30,50),Point(45,20));
r1.getTL().getY()<r2.getBR().getY() || Rectangle r3(Point(75,35),Point(90,30));
r1.getBR().getY()>r2.getTL().getY()) cout << "Rectángulos: " << endl;
return false; cout << to_string(r1) << endl;
else cout << to_string(r2) << endl;
return true; cout << to_string(r3) << endl;
} cout << "Solapan r1 y r2: "<<overlaps(r1,r2)<<endl; // Sí
cout << "Solapan r1 y r3: "<<overlaps(r1,r3)<<endl; // No
cout << "Solapan r2 y r3: "<<overlaps(r2,r3)<<endl; // No
}
La cuestión es que si modificamos el programa main() que ya teníamos perderemos el programa
previo. Como en este ejercicio queremos tener los dos programas podríamos pensar en crear un
nuevo fichero main_todo2.cpp en el que copiemos todo el código de main_todo1.cpp, añadamos la
nueva función y sustituyamos la función main() por la nueva. De esta forma tenemos los dos
programas y podremos ejecutar uno u otro a nuestro antojo. Con este esquema de trabajo surgen
algunos inconvenientes:
• Si modificamos alguna clase (quizá porque hayamos detectado algún error o queramos
introducir alguna mejora) hemos de modificarla en todos y cada uno de los ficheros en
donde la hemos copiado y pegado.
• Si necesitamos hacer nuevos programas que hagan uso de nuestras clases hemos de
repetir el proceso de copiar y pegar. Si tenemos muchos programas esta técnica es
propensa a errores.
• Si deseamos vender o distribuir nuestras clases a terceros para que hagan nuevos
programas a partir de ellas hemos de proporcionar su código fuente para que puedan
repetir el procedimiento del copia y pega en sus programas. Esto no es necesariamente un
inconveniente pero es algo a tener en cuenta.
Además, surgen otros inconvenientes un poco más técnicos:
• Si trabajamos en equipo es más complejo gestionar el desarrollo en paralelo.
• Conforme aumenta el número de líneas de código de nuestras clases se hace más difícil
mantenerlo todo en un mismo fichero.
• Cada vez que creamos un nuevo programa hemos de llamar a g++ y este debe realizar el
proceso de traducción completo.
Por tanto, debemos buscar una solución que permita:
• Evitar la técnica de copiar y pegar cuando hacemos nuevas aplicaciones que utilizan las
clases.
• Dividir en ficheros más pequeños nuestro código para hacerlo más mantenible y facilitar el
trabajo en equipo.
• Hacer más eficiente el proceso de compilación evitando compilar múltiples veces un mismo
código.
• Ocultar, si lo vemos oportuno, nuestro código fuente a terceros.
ᐒ Ejercicio 2 : División en múltiples ficheros
Tome el código fuente original y divídalo en los siguientes módulos y ficheros:
• Módulo TDA Punto. Contiene todo lo relativo a la clase Point y constará de dos ficheros:
◦ punto.h Este fichero contendrá solo las declaraciones relativas a la clase Point. Es decir,
este fichero contiene la interfaz o parte pública del módulo, todo aquello que necesitará
conocer cualquier otro módulo que necesite compilar haciendo uso de este.
◦ punto.cpp Este otro fichero contendrá las implementaciones. Es decir, este fichero
contiene la parte privada del módulo, todo aquello que no se necesita en tiempo de
compilación por otros módulos que usen este.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 8/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
• Módulo TDA Rectángulo. Contiene todo lo relativo a la clase Rectangle y constará de dos
ficheros:
◦ rectangulo.h Contendrá la interfaz pública del módulo.
◦ rectangulo.cpp Contendrá la parte privada del módulo.
• Módulo auxiliar. Contiene elementos auxiliares que no son de ninguno de los módulos
anteriores. Concretamente las funciones min() y max(). De nuevo, dispondremos de dos
ficheros:
◦ auxiliar.h Contiene la interfaz pública del módulo (las declaraciones de las funciones).
◦ auxiliar.cpp Contiene su interfaz privada (las implementaciones de las funciones).
• Aplicación 1. Contiene la función main() del primer programa.
◦ main1.cpp
• Aplicación 2. Contiene la segunda función main().
◦ main2.cpp
En la siguiente figura puede ver cómo quedaría la división:
main1.cpp cout << to_string(p3) << endl;
cout << to_string(p4) << endl;
int main() { cout << "Rectángulos: " << endl;
Rectangle r1(Point(10,40),Point(40,20)); cout << to_string(r1) << endl;
Rectangle r2(Point(30,50),Point(50,30)); cout << to_string(r2) << endl;
Point p1(20,30); cout << "p1 dentro de r1: "<<r1.contain(p1)<< endl;
Point p2(60,60); cout << "p2 dentro de r1: "<<r1.contain(p2)<< endl;
Point p3(30,50); cout << "p1 dentro de r2: "<<r2.contain(p1)<< endl;
Point p4(30,30); cout << "p3 dentro de r1: "<<r1.contain(p3)<< endl;
cout << "Puntos: " << endl; cout << "p4 dentro de r1: "<<r1.contain(p4)<< endl;
cout << to_string(p1) << endl; }
cout << to_string(p2) << endl;
main2.cpp cout << to_string(r1) << endl;
cout << to_string(r2) << endl;
int main() { cout << to_string(r3) << endl;
Rectangle r1(Point(10,40),Point(40,20)); cout << "Solapan r1 y r2: " << overlaps(r1,r2) << endl;
Rectangle r2(Point(30,50),Point(50,30)); cout << "Solapan r1 y r3: " << overlaps(r1,r3) << endl;
Rectangle r3(Point(0,30),Point(20,10)); cout << "Solapan r2 y r3: " << overlaps(r2,r3) << endl;
cout << "Rectángulos: " << endl; }
punto.h punto.cpp
class Point { Point::Point(int px, int py) : x(px), y(py) { }
private: double Point::getX() const { return x; }
double x, y; double Point::getY() const { return y; }
public: std::string to_string(const Point &p) {
Point(int px=0, int py=0); std::ostringstream tmp;
double getX() const; tmp.setf(std::ios_base::fixed);
double getY() const; tmp << std::setprecision(1) << "(" << p.getX() << "," << p.getY() << ")";
}; return tmp.str();
std::string to_string(const Point &p); }
auxiliar.h auxiliar.cpp
double max(double v1, double v2); double max(double v1, double v2) { return (v1>v2) ? v1 : v2; }
double min(double v1, double v2); double min(double v1, double v2) { return (v1<v2) ? v1 : v2; }
rectangulo.h Point getTR() const;
Point getBL() const;
class Rectangle { double width() const;
private: double height() const;
Point psupizq, pinfder; bool contain(const Point &p) const;
public: };
Rectangle(const Point &pp1=Point(), const Point &pp2=Point()); std::string to_string(const Rectangle &r);
Point getTL() const; bool overlaps(const Rectangle &r1, const Rectangle &r2);
Point getBR() const;
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 9/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
rectangulo.cpp std::string to_string(const Rectangle &r) {
std::ostringstream tmp;
Rectangle::Rectangle(const Point &pp1, const Point &pp2) { tmp.setf(std::ios_base::fixed);
psupizq = Point(min(pp1.getX(),pp2.getX()), tmp << std::setprecision(1) << "[" <<
max(pp1.getY(),pp2.getY())); to_string(r.getTL()) << "-" <<
pinfder = Point(max(pp1.getX(),pp2.getX()), to_string(r.getBR()) << "]";
min(pp1.getY(),pp2.getY())); return tmp.str();
} }
Point Rectangle::getTL() const { return psupizq; } bool Rectangle::contain(const Point &p) const {
Point Rectangle::getBR() const { return pinfder; } return p.getX()>=psupizq.getX() && p.getX()<=pinfder.getX()
Point Rectangle::getTR() const { && p.getY()>=pinfder.getY() && p.getY()<=psupizq.getY();
return Point(pinfder.getX(),psupizq.getY()); }
} bool overlaps(const Rectangle &r1, const Rectangle &r2) {
Point Rectangle::getBL() const { if (r1.getTL().getX()>r2.getBR().getX() ||
return Point(psupizq.getX(),pinfder.getY()); r1.getBR().getX()<r2.getTL().getX() ||
} r1.getTL().getY()<r2.getBR().getY() ||
double Rectangle::width() const { r1.getBR().getY()>r2.getTL().getY())
return pinfder.getX() - psupizq.getX(); return false;
} else
double Rectangle::height() const { return true;
return psupizq.getY() - pinfder.getY(); }
}
Hasta aquí, únicamente hemos partido en trozos el fichero original en varios ficheros .cpp y .h. De
esta forma, podremos compilar por separado cada uno de los documentos .cpp una única vez para
después ensamblarlos y crear los dos ejecutables … aunque aún faltan algunos detalles que
completar para que todo funcione.
Además, ahora la compilación de estos ficheros no siempre va a poder generar un ejecutable. Por
ejemplo, si compilamos solo el fichero punto.cpp no tenemos ninguna función main().
Nota: un error típico y, a veces, difícil de detectar (sobre todo cuando se está aprendiendo) es
olvidar poner el punto y coma tras la llave de cierre de la definición de las clases en los ficheros de
cabecera.
4.1. Compilación con múltiples ficheros
El modelo de compilación de C++ sigue el esquema de la siguiente figura. La etapa de
compilación se aplica a cada unidad de compilación por separado. Las unidades de compilación
son las unidades mínimas con las que puede trabajar el compilador y se suelen corresponder con
los ficheros .cpp de nuestro proyecto. Por ahora nos vamos a centrar en las etapas de compilación
y enlazado, dejando el apartado de construcción de bibliotecas para más adelante.
Por tanto, en nuestro ejemplo deberíamos ejecutar los siguientes comandos:
prompt> g++ punto.cpp -o punto.o
prompt> g++ rectangulo.cpp -o rectangulo.o
prompt> g++ auxiliar.cpp -o auxiliar.o
prompt> g++ main1.cpp -o main1.o
prompt> g++ main2.cpp -o main2.o
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 10/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
De esta forma dispondríamos de todos los códigos objeto almacenados en sendos ficheros .o.
Faltaría ensamblarlos para crear los dos ejecutables, sin embargo, aun debemos hacer algunos
ajustes antes de poder conseguir eso.
Si intenta compilar punto.cpp obtendrá múltiples errores de compilación:
punto.cpp:1:1: error: ‘Point’ does not name a type; did you mean ‘int’?
1 | Point::Point(int px, int py) : x(px), y(py) {
| ^~~~~
| int
punto.cpp:4:8: error: ‘Point’ has not been declared
4 | double Point::getX() const {
| ^~~~~
punto.cpp:4:22: error: non-member function ‘double getX()’ cannot have cv-qualifier
...
Se deben a que cuando el compilador comienza a analizar el código de punto.cpp no existe ninguna
declaración del class Point (recuerde que se ha movido esa parte a punto.h). Para resolver este
problema incluiremos las siguientes líneas al comienzo de punto.cpp:
#include <sstream>
#include <iomanip>
#include "punto.h"
Point::Point(int px, int py) : x(px), y(py) {
}
...
Las dos primeras inclusiones son necesarias para la implementación de la función to_string(). El
tercer #include hace que el compilador incluya el contenido de punto.h en ese lugar y que, por
tanto, cuando empieza a compilar la implementación de los métodos ya se haya definido el class
Point. Observe que en este #include se utilizan comillas dobles en lugar de ángulos.
Por el mismo razonamiento, verá que dentro de punto.h se utiliza el tipo std::string, definido en el
fichero de cabecera string, por lo que en punto.h debería hacer #include <string>
Si ahora prueba de nuevo a ejecutar:
prompt> g++ punto.cpp -o punto.o
verá que se produce un nuevo error:
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o: in function
`_start':
(.text+0x1b): undefined reference to `main'
collect2: error: ld returned 1 exit status
que se debe a que tras compilar y generar el código objeto ha intentado enlazar y generar un
ejecutable, pero no ha encontrado ninguna función main(). El problema en este caso se resuelve
indicándole al compilador que únicamente lleve a cabo la etapa de compilación (pero no la de
enlazado) generando únicamente el código objeto (y no el ejecutable) con la opción -c:
prompt> g++ -c punto.cpp -o punto.o
ᐒ Ejercicio 3 : Inclusión de ficheros de cabecera
Modifique los ficheros del proyecto añadiendo las instrucciones #include necesarias en cada uno de
ellos. Tenga en cuenta las siguientes reglas:
• Solo se puede hacer #include de ficheros de cabecera (.h). En ningún caso se debe hacer
#include de ficheros de implementación (.cpp).
• Los #include de ficheros de nuestro proyecto se escriben con comillas dobles (y no con
ángulos, que se reservan para incluir ficheros del sistema o de terceros).
• Los #include se pueden escribir tanto en ficheros .h como en .cpp. Lo que tenemos que
tener en cuenta para decidir si hacemos una inclusión de un fichero en otro es: en un
fichero A (.cpp o .h) hacemos un #include de otro fichero B (.h) si en A se hace uso de
alguna definición que está hecha en B.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 11/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
En el siguiente diagrama tiene el esquema con las inclusiones que debería hacer para resolver el
ejercicio (se omiten en él los ficheros de cabecera del sistema, pero también deberá incluirlos
donde corresponda). Cada flecha indica que un fichero (el de abajo) hace uso de algo que está
definido en otro (el de arriba). Por ejemplo:
• punto.cpp hace uso del class Point que se declara en punto.h y, por tanto, en punto.cpp se
hará un #include “punto.h”
• rectangulo.h hace uso del class
Point que se declara en punto.h
• rectangulo.cpp hace uso de las
funciones min() y max()
declaradas en auxiliar.h
• rectangulo.cpp hace uso del
class Rectangle declarado en
rectangulo.h
• rectangulo.cpp también hace
uso del class Point por lo que deberíamos hacer un #include de punto.h también. Sin
embargo, no es estrictamente necesario porque hemos incluido rectangulo.h que, a su vez,
incluye a punto.h. Esta relación se indica con la flecha roja punteada. Más adelante
volveremos sobre esta situación y veremos que la recomendación es hacer la inclusión
aunque no sea necesaria (si lo hiciésemos en este momento no funcionaría porque aun hay
que incluir modificaciones en los ficheros .h).
• main1.cpp hace uso del class Rectangle y del class Point por lo que basta hacer una inclusión
de rectangulo.h para tener ambas declaraciones disponibles.
Una vez hecho compile todos los ficheros .cpp y genere sus correspondientes códigos objeto
(ficheros .o) utilizando la opción -c del compilador.
4.2. La directiva #include
El efecto de hacer #include de un fichero, desde un punto de vista lógico, es equivalente a sustituir
dicha línea #include por el contenido completo del fichero que se está incluyendo. Esta operación
de “sustitución” se realiza en la etapa de preprocesamiento, previa a la compilación. A este tipo de
comandos, pensados para ser usados en la etapa de preprocesamiento, se los conoce como
directivas del preprocesador.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 12/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
En la figura anterior vemos el resultado de ejecutar el preprocesador sobre punto.cpp . El resultado
sigue siendo código C++ pero con todos los #include sustituidos por el contenido de los ficheros
correspondientes. Este código intermedio no se suele almacenar en ningún fichero sino que se
envía directamente hacia la etapa de compilación.
Esta etapa normalmente se hace de forma transparente al programador como etapa previa a la
compilación pero podemos indicarle a g++ que realice solo esta etapa con la siguiente sintaxis y
que almacene el resultado en un fichero:
prompt> g++ -E punto.cpp -o temporal.cpp
Podemos ver que en temporal.cpp:
• El código de sstream, iomanip y string (en el ejemplo lo omitimos por claridad).
• Se incluyen anotaciones precedidas de # que indican lo que va haciendo el preprocesador.
Si queremos evitar estas anotaciones podemos añadir la opción -P:
prompt> g++ -E -P punto.cpp -o temporal.cpp
Si no se dice lo contrario, cuando se hace un #include con comillas dobles, el compilador espera
encontrar el fichero .h en la misma carpeta que está el fichero que está haciendo la inclusión.
Cuando tenemos proyectos complejos podemos usar la opción -I para indicarle al compilador otros
directorios en donde debe buscar ficheros de cabecera.
En cuanto a si debemos usar ángulos o comillas dobles para indicar el fichero a incluir, las reglas
que aplica el compilador son referidas a dónde debe buscar dicho fichero. Son las siguientes:
a) Si hacemos una inclusión con ángulos (p. ej. #include <iostream>) el compilador buscará los
ficheros en los directorios del sistema, en donde están los ficheros de las bibliotecas
estándares. Si no los encuentra ahí intentará localizarlos en los directorios especificados en
la opción -I (si se ha puesto).
b) Si hacemos la inclusión con comillas dobles (p. ej. #include “punto.h”) el compilador buscará
los ficheros en la misma carpeta en donde está el fichero que tiene el #include. Si no lo
encuentra ahí aplicará las reglas del caso a).
Las reglas que se utilizan para escribir los distintos #include son estas:
• Son lo primero que se escribe en cualquier fichero, antes de declaraciones y definiciones.
• Primero se ponen los #include de archivos del sistema con ángulos.
• Después se ponen los #include de archivos propios con comillas dobles.
• Cuando se está construyendo un TDA, el fichero de implementación ( .cpp) siempre hará
una inclusión de su fichero de interfaz pública ( .h).
ᐒ Ejercicio 4 : Inclusión de ficheros con ángulos y con comillas
Indique el resultado de compilar punto.cpp según utilicemos cada una de las siguientes inclusiones:
#include <sstream> #include "sstream" #include <sstream> #include "sstream"
#include <iomanip> #include "iomanip" #include <iomanip> #include "iomanip"
#include "punto.h" #include "punto.h" #include <punto.h> #include <punto.h>
4.3. Enlazado de códigos objeto
Finalmente, una vez obtenidos los diferentes códigos objeto, debemos pasar a la etapa de
enlazado para crear los ejecutables finales. Para ello disponemos de una herramienta llamada ld
(el enlazador de GNU) que recibe como parámetros los ficheros objeto que queremos ensamblar y
el nombre del fichero en donde pondrá el resultado (el ejecutable). Esta nueva herramienta,
aunque puede utilizarse de forma directa desde la línea de órdenes, conviene ser ejecutada a
través del propio g++ debido a que es más sencillo (hay que usar menos parámetros porque son
calculados automáticamente por g++).
ᐒ Ejercicio 5 : Enlazado de código objeto y obtención de ejecutables
Ejecute los siguientes comandos:
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 13/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
prompt> g++ auxiliar.o punto.o rectangulo.o main1.o -o main1
prompt> g++ auxiliar.o punto.o rectangulo.o main2.o -o main2
Si todo ha ido bien habrá generado los dos ejecutables. Observe que si a g++ le damos como
entrada ficheros objeto (.o) este entiende que se usarán en la fase de enlazado mientras que si le
damos ficheros .cpp los utilizará en la fase de compilación (sería posible darle ambos tipos de
ficheros en una misma ejecución).
Reutilización de módulos
Recuerde que uno de los objetivos de esta separación en ficheros es conseguir que el proceso de
compilación sea mucho más eficiente. Hasta el momento, el diagrama de dependencias entre
todos los ficheros que componen nuestro proyecto es el siguiente:
Con este esquema podemos ver claramente qué partes del proceso de compilación hemos de
repetir ante los cambios que se puedan producir en nuestro proyecto.
Por ejemplo, si tuviese que modificar el fichero auxiliar.cpp tendría que volver a reconstruir todo
aquello que dependa de esa modificación, en concreto:
1. Compilar auxiliar.cpp y volver a generar auxiliar.o
prompt> g++ -c auxiliar.cpp -o auxiliar.o
2. Volver a enlazar los códigos objeto y generar el ejecutable main1
prompt> g++ auxiliar.o punto.o rectangulo.o main1.o -o main1
3. Volver a enlazar los códigos objeto para crear el ejecutable main2
prompt> g++ auxiliar.o punto.o rectangulo.o main2.o -o main2
ᐒ Ejercicio 6 : Reutilización de módulos
Escriba los pasos que habría que rehacer si hiciésemos los siguientes cambios en el proyecto:
1. Modificamos rectangulo.h
2. Modificamos punto.h
3. Creamos un nuevo programa (main3.cpp)
4.4. Dónde hacer la inclusión de ficheros y using namespace
En ocasiones podemos tener dudas sobre dónde es conveniente hacer la inclusión de un
determinado fichero e incluso, para los ficheros del sistema, dónde es mejor usar using namespace
std. En nuestro proyecto podemos fijarnos en el módulo punto y en la primera aplicación:
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 14/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
punto.h punto.cpp main1.cpp
#include <string> #include <sstream> #include <iostream>
... #include <iomanip> #include "rectangulo.h"
std::string to_string(const Point &p); #include "punto.h" using namespace std;
... ...
std::string to_string( cout << "Puntos: " << endl;
const Rectangle &r) {
std::ostringstream tmp;
...
Podemos concluir que:
• punto.h necesita incluir a string porque en dicho fichero se utilizan recursos de string
(resaltados).
• punto.cpp necesita incluir sstream e iomanip porque utiliza recursos de ellos (ostringstream y
setprecision).
• punto.cpp no necesita estrictamente incluir a string, aunque haga uso de sus recursos,
porque se hace de forma indirecta al incluir a punto.h. También se podría haber incluido de
forma directa sin mayor problema.
• main1.cpp necesita incluir a iostream porque hace uso de sus recursos (cout). No necesita
incluir punto.h porque lo hace indirectamente a través de rectangulo.h.
En este punto podríamos plantear si tiene sentido, por ejemplo, incluir sstream y/o iomanip en
punto.h en lugar de en punto.cpp. La respuesta es que, si lo hacemos, todo sigue funcionando igual
ya que punto.cpp dispone de las definiciones de ambos ficheros indirectamente a través de la
inclusión de punto.h. Sin embargo, sería una mala decisión porque:
• punto.h no los necesita.
• provocaríamos que cualquier fichero que haga una inclusión de punto.h también incluya
dichos ficheros aunque no los utilice. Cuando ese fichero se compile se compilarán también
los otros innecesariamente y se generará código objeto que no sirve para nada.
Recuerde que la compilación separada tiene como uno de sus objetivos hacer más eficiente el
proceso de compilación por lo que siempre debería pensar en esos términos cuando use recursos.
La regla a recordar es: un fichero será incluido en otro solo solo si este último utiliza directamente
alguno de sus recursos.
En la implementación actual se ha optado por usar using namespace std solo en main1.cpp con el
único objetivo de simplificar un poco la redacción de su código. Podríamos haber usado esa
instrucción también en punto.h y/o punto.cpp, simplificando también su escritura. Si lo hubiésemos
usado en punto.cpp podemos omitir el uso de std:: a lo largo de dicho fichero. Sería correcto.
Si lo hubiésemos usado en punto.h conseguiríamos también simplificar su escritura pero, en este
caso, sí podemos tener efectos colaterales indeseados. Al hacerlo provocamos que cualquiera que
incluya a punto.h ya aplique los efectos de dicha instrucción y podría haber casos en los que esto
no sea deseable.
El uso de espacios de nombres permite, entre otras cosas, evitar posibles colisiones entre diversos
módulos. Imagine que disponemos de:
• Un espacio de nombres esp1 que incluye una función f().
• Un espacio de nombres esp2 que incluye una función f(). Observe que la función se llama
igual pero no es problemático puesto que está en un espacio de nombres diferente.
Desde una aplicación podríamos llamar a cada una de las funciones con la sintaxis esp1::f() o
esp2::f() según a cual nos estemos refiriendo. Si en la aplicación escribimos using namespace esp1;
cuando escribamos f() (sin indicar explícitamente el espacio de nombres) el compilador entenderá
que nos referimos a la del espacio esp1. Si además ponemos también using namespace esp2; cuando
llamemos a f() el compilador no podrá saber a cual de las dos nos estamos refiriendo.
Por tanto, se considera una buena práctica (y se desaconseja lo contrario) no hacer uso de using
namespace en ficheros de cabecera (.h). Solo debemos usar esta instrucción en ficheros .cpp.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 15/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
4.5. El problema de las dobles inclusiones e inclusiones cruzadas
En una sección anterior indicamos que en un fichero A (.cpp o .h) hacemos un #include de otro
fichero B (.h) si en A se hace uso de alguna definición que está hecha en B.
En nuestro ejemplo hemos visto también que hay algunas dependencias que no hace falta reflejar
en los #include debido a que podemos pensar que las inclusiones son algo así como “transitivas”.
Concretamente nos referimos a las dependencias con flechas rojas punteadas de la siguiente
figura: main2.cpp hará una inclusión de rectangulo.h pero no le hará falta incluir a punto.h.
Es decir, aunque la regla general dice una cosa,
nosotros, que conocemos bien el código y sabemos
cómo funciona el proceso de preprocesado, omitimos
algunas inclusiones. Sin embargo, esto no es lo
aconsejable por diversos motivos:
• Cuando trabajamos en un proyecto que puede
tener cientos o incluso miles de ficheros y en el
que podrían estar trabajando también cientos de
personas, es poco probable que el programador sepa qué ficheros incluyen a qué ficheros.
Podría averiguarlo pero sería un esfuerzo considerable y no es algo práctico.
• Estamos insistiendo en que un TDA (es el caso de Point y Rectangle, aunque esta idea es
extensible a otros ficheros aunque no sean TDA) debe ofrecer una interfaz pública y ocultar
todos los detalles de implementación a quien lo utilice. Según esta máxima, si un fichero
del TDA hace una inclusión de algún otro fichero, esto debería ser transparente para quien
usa el TDA. O, dicho de otra forma, si yo quiero usar el TDA Rectangle, no tendría porqué
saber si este incluye o no al fichero de cabecera del TDA Point. Por ello, si en main2.cpp voy
a hacer uso de ambos TDA lo lógico sería hacer la inclusión de ambos ficheros de cabecera.
Por lo tanto, el fichero main2.cpp debería hacer estas inclusiones:
#include <iostream>
#include "rectangulo.h"
#include "punto.h"
Observe que también debería ser indistinto el orden en el que incluimos nuestros TDA. Si hacemos
esa modificación en main2.cpp y compilamos obtendremos lo siguiente:
prompt> g++ -c main2.cpp -o main2.o
In file included from main2.cpp:3:
punto.h:1:7: error: redefinition of ‘class Point’
1 | class Point {
| ^~~~~
In file included from rectangulo.h:1,
from main2.cpp:2:
punto.h:1:7: note: previous definition of ‘class Point’
1 | class Point {
| ^~~~~
Lo que está ocurriendo es que con la primera inclusión ( rectangulo.h) se está definiendo la clase
Rectangle pero como en ese fichero se hace una inclusión de punto.h también se está definiendo la
clase Point. Cuando a continuación hacemos #include “punto.h” se vuelve a definir la clase Point y
eso provoca un error de compilación (no se puede definir dos veces una misma cosa).
Esto se conoce como problema de la doble inclusión de ficheros. Para resolver la situación se
utiliza lo que se conoce como salvaguarda del fichero de cabecera. Esta no es más que una
definición que se hace al comienzo de cada fichero de cabecera para que el preprocesador evite
incluir un fichero si este fue incluido con anterioridad. Todo fichero de cabecera, sin excepción,
deberá tener el siguiente esquema:
#ifndef FICHERO__H
#define FICHERO__H
// ... contenido del fichero
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 16/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
#endif
FICHERO__H es la salvaguarda y es un identificador9 que usualmente tiene algo que ver con el
nombre del fichero de cabecera añadiendo algunos caracteres que eviten que pueda ser
confundido con cualquier otra cosa de nuestro programa (variables, tipos, funciones, etc.). En el
caso de punto.h el fichero quedaría así:
#ifndef PUNTO__H
#define PUNTO__H
class Point {
...
};
#endif
#ifndef, #define y #endif son directivas del preprocesador, es decir, no es código C++ sino
instrucciones para que el preprocesador haga ajustes antes de compilar. Lo que vienen a decir es
que si no está definido el identificador PUNTO__H se compile el código desde #ifndef hasta #endif. En
ese código hay una segunda directiva #define PUNTO__H que define el identificador. Si hacemos una
segunda inclusión de punto.h el preprocesador se dará cuenta de que ya se definió antes PUNTO__H
y, por tanto, se saltará el código que hay entre #ifndef y #endif.
Para el caso de rectangulo.cpp, que utiliza punto.h, sí que podríamos omitir su inclusión y nos
bastaría con la inclusión de rectangulo.h. Piense que rectangulo.h y rectangulo.cpp son parte de un
mismo TDA por lo que no hay ninguna contradicción en pensar que rectangulo.cpp debe conocer el
detalle de implementación de rectangulo.h (y, por tanto, sabrá que ya ha incluido a punto.h).
ᐒ Ejercicio 7 : Inclusión de salvaguardas
Modifique todos los ficheros de cabecera del proyecto para incluir sus salvaguardas. A
continuación modifique el contenido de los ficheros main1.cpp y main2.cpp para que incluyan a
punto.h. Pruebe a reconstruir el proyecto completo y asegúrese de que todo funciona bien.
En rectangulo.cpp no es necesario hacer la inclusión de punto.h.
El problema de las inclusiones cruzadas
Además de los casos de dobles inclusiones se f4.h incluye a f3.h
puede dar el caso de inclusiones cruzadas, esto es,
f3.h incluye a f2.h
un fichero A incluye a otro B y ese otro B también
incluye a A (bien de forma directa o bien de forma f2.h incluye a f1.h
indirecta). Observe que aquí el problema no se da
f1.h incluye a f3.h
porque haya múltiples definiciones de cosas sino
porque entraríamos en un proceso de inclusión f3.h incluye a f2.h
infinita. En la figura puede ver una ilustración de
…
esto.
La solución a esto pasa por el uso de salvaguardas y por el uso de declaraciones adelantadas.
La directiva #define
Esta es una directiva que permite definir macros. Una macro es algo así como una etiqueta que es
sustituida por un valor durante la etapa de preprocesamiento. A esa sustitución se la conoce como
expansión de la macro. Para definirlas disponemos de la directiva #define con la siguiente sintaxis:
#define ETIQUETA REEMPLAZO
En los siguientes ejemplos vemos a la izquierda un código de ejemplo y a la derecha el resultado
de expandir las macros realizado durante el preprocesamiento:
9 Hablando con propiedad, no deberíamos decir que PUNTO__H es un identificador como los que se usan en C++ para
nombres de variables, funciones, constantes, etc. Este elemento se denomina macro y es una herramienta que usa el
preprocesador para hacer cosas como la que estamos viendo y otras.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 17/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
Código original Código preprocesado
#define TAM 256 ...
... int v[256];
int v[TAM]; ...
... for (int i=0; i<256; i++) {
for (int i=0; i<TAM; i++) { ...
... }
}
#define TAM 10 int main() {
#define repetir for int n;
#define condicion(x) x<TAM for (int i=0; i<10; ++i) {
#define entrada cout << "Dime un número: " << endl; cin >> n; cout << "Dime un número: " << endl; cin >> n;
cout << "El número es: " << n << endl;
int main() { }
int n; }
repetir (int i=0; condicion(i); ++i) {
entrada
cout << "El número es: " << n << endl;
}
}
Como ve, el uso de las macros ofrece muchas posibilidades, incluso permiten disponer de
parámetros. Para lo que nos interesa en este documento, hemos de saber que existe la posibilidad
de definir macros sin que haya ningún reemplazo por hacer. Es decir, que podemos declarar
macros de esta forma:
#define ETIQUETA
Observe que no hemos indicado por lo que hay que reemplazar la etiqueta. En esta situación lo
que estamos haciendo es, simplemente, definiendo una macro (o etiqueta) para, más adelante,
poder comprobar si esa etiqueta está o no definida y actuar en consecuencia.
Una macro existe desde el punto en el que está definida, pero no antes, y hasta que finaliza el
preprocesamiento. Por ejemplo, en el segundo caso de la tabla previa, no podríamos hacer uso de
la etiqueta TAM antes de la línea en la que se ha hecho el correspondiente #define.
Podemos usar la directiva #undef para hacer que la macro deje de existir desde el punto en que se
usa esta. En la siguiente tabla puede ver un ejemplo:
Código original Código preprocesado
#define TAM 10 int main() {
int main() { cout << "TAM vale: " << 10 << endl;
cout << "TAM vale: " << endl; cout << "Y ahora vale " << TAM << endl;
#undef TAM }
cout << "Y ahora vale " << TAM << endl;
}
Puede ver que la macro se ha sustituido en la primera línea cout pero no en la segunda. Este
código no compilaría puesto que no existe ninguna variable llamada TAM.
Por cierto, aunque lo habitual es ver este tipo de directivas al comienzo de los ficheros, podemos
usarlas en cualquier parte.
Las directivas condicionales
Disponemos de varias directivas que permiten hacer la compilación de determinados trozos de
código condicionado a que ocurran ciertas condiciones. Vemos la sintaxis a continuación:
#if <expresión> #if <expresión 1>
<bloque si verdad> <bloque si verdad expresión 1>
#else #elif <expresión 2>
<bloque si falso> <bloque si verdad expresión 2>
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 18/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
#endif ...
#else
<bloque si no es verdad ninguna>
#endif
Un caso típico de uso se puede dar cuando estamos desarrollando código multiplataforma para
diversos sistemas operativos. Dependiendo del SO puede que tengamos que usar unos u otros
ficheros de cabecera por lo que las directivas #include pueden variar o incluso el código que
compilemos también puede variar en función del SO. A continuación tiene un ejemplo en el que
hacemos inclusión de un fichero de cabecera u otro en función del SO:
#define SISTEMA 'L'
#if SISTEMA=='L’
#define CABECERA "linux.h"
#elif SISTEMA=='W’
#define CABECERA "windows.h"
#elif SISTEMA=='M’
#define CABECERA "macos.h"
#endif
#include CABECERA
Las expresiones pueden incluir:
• Constantes enteras
• Constantes de tipo char (carácter, no cadena)
• Macros (definidas con #define)
Cualquier otro identificador se asume que es la constante cero.
Finalmente, podemos usar también las siguientes directivas para verificar si una macro está o no
definida:
#ifdef ETIQUETA #ifdef ETIQUETA #ifndef ETIQUETA #ifndef ETIQUETA
... ... ... ...
#endif #else #endif #else
... ...
#endif #endif
Se utilizará #ifdef para comprobar si una macro está definida y se usará #ifndef para comprobar si
no está definida.
ᐒ Ejercicio 8 : Compilación condicional
¿En cuáles de los siguientes casos queda definida la macro OK?
#define x 9 int y=9; int z=9; #define t 'a'
#if (x==9) #if (y==9) #if (z==0) #if (t=='a')
#define OK #define OK #define OK #define OK
#endif #endif #endif #endif
ᐒ Ejercicio 9 : El TDA círculo [✲ Opcional ✲]
Añada un nuevo TDA para representar círculos. La representación interna consistirá en el punto
que define el centro del círculo junto con su radio.
• Defina una interfaz pública similar a la de los TDA punto y rectángulo.
• Añada funciones o métodos para:
◦ Comprobar si un punto está o no dentro de un círculo. Habría que comprobar si la
distancia entre el punto y el centro del círculo es menor o no que el radio. Puede añadir
nueva funcionalidad en el TDA punto para calcular la distancia entre dos puntos.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 19/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
◦ Comprobar si un círculo se solapa con otro. Habría que comprobar si la distancia entre
los dos centros es menor o no que la suma de los radios.
◦ Comprobar si un círculo se solapa con un rectángulo 10.
• Añada una nueva aplicación main3 con código que permita probar el nuevo módulo.
Una vez implementado indique la secuencia de comandos (g++) a ejecutar para construir el
proyecto completo y añada los nuevos ficheros al diagrama de dependencias.
4.6. Opciones de g++
En este punto es interesante conocer algunas opciones 11 12 adicionales que se pueden pasar a g+
+ y que nos van a facilitar el proceso de desarrollo así como ajustarnos a los estándares de C++:
• -std Esta opción le dice al compilador qué estándar 13 del lenguaje debe utilizar. Puede
tomar valores como c++11, c++17, c++20, etc. Se recomienda usar c++17.
• -Wall Con esta opción g++ mostrará durante la compilación multitud de mensajes
informativos sobre warnings (avisos) alertando de cuestiones que podrían ser errores.
• -Werror Cuando g++ detecta un warning avisa pero sigue el proceso de compilación. Con
esta opción lo que hacemos es convertir los warnings en errores y se fuerza la detención de
la compilación cuando los encuentra.
• -Wextra Habilita algunos avisos adicionales como, por ejemplo, avisar cuando hay variables
sin inicializar.
• -pedantic / -Wpedantic Los compiladores suelen incluir algunas extensiones al lenguaje para
aportar alguna funcionalidad extra al programador. Con esta opción indicamos que
queremos compilar usando el estándar ISO C++ de forma estricta, deshabilitando así las
extensiones del compilador. Un caso típico es el uso de arrays de longitud variable14
(Variable length array o VLA) que son admitidos por g++ pero que no están en el estándar
de C++.
• -g Indica a g++ que incluya información extra para poder depurar el código. Si no se indica
no es posible depurar. Se utiliza cuando generamos la versión durante la etapa de
desarrollo (Debug) y se omite cuando vamos a generar la versión de explotación ( Release) de
nuestro proyecto.
Para la etapa de preprocesamiento también tenemos alguna opción de interés:
• -D Con esta opción podemos definir macros desde la línea de órdenes.
Otras opciones:
• -O Cuando hemos finalizado el desarrollo y vamos a distribuir la aplicación, se debe
compilar en el conocido como modo release (modo en producción). La aplicación que
vamos a distribuir nos interesa que sea lo más eficiente posible por lo que esta opción
suele utilizarse combinada con otras:
◦ No se usa la opción -g para quitar de los ejecutables la información para depurar, que
ya no es necesaria.
◦ Al quitar esa información de depuración el tamaño de los ejecutables es menor.
◦ Se define la macro NDEBUG (con la opción -DNDEBUG) para anular algunas cuestiones
relacionadas con la depuración y la etapa de desarrollo como, por ejemplo, las
aserciones.
10 Puede consultar esta URL con código de ejemplo: https://colisiones22.rssing.com/chan-59382643/article2.html
11 https://linux.die.net/man/1/g++
12 https://caiorss.github.io/C-Cpp-Notes/compiler-flags-options.html
13 https://gcc.gnu.org/projects/cxx-status.html
14 VLA: array cuyo tamaño se define en tiempo de ejecución (no es constante, no se conoce en tiempo de compilación):
int n;
cin >> n;
int v[n]; // Este es un VLA puesto que n es conocido en tiempo de ejecución (y no en tiempo de compilación)
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 20/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
◦ Con -O le podemos indicar al compilador que aplique técnicas de optimización (puede
hacerse en distintos grados) para acelerar la ejecución. Habitualmente se utiliza un
nivel de optimización 2 o 3 (-O2 o -O3).
Para esta asignatura, se recomienda usar siempre estas opciones:
-std=c++17 -Wall -Wextra -pedantic -g
De esta forma, la compilación de punto.cpp se haría así:
prompt> g++ -c punto.cpp -o punto.o -std=c++17 -Wall -Wextra -pedantic -g
5. Creación de bibliotecas
Hasta el momento tenemos tres TDA para trabajar con figuras geométricas. Cada vez que
hagamos una nueva aplicación (main) es muy probable que tengamos que hacer inclusión de sus
tres ficheros de cabecera para trabajar indistintamente con cualquiera de las formas. Además,
siempre hemos de enlazar con los tres códigos objeto. Conforme el número de ficheros aumenta,
se hace más laborioso trabajar con ellos en aplicaciones.
Considere también el caso de que una vez construidos todos los TDA queremos poner el código a
disposición de terceros. Una opción es dar todos los códigos fuente así como instrucciones de
compilación y enlazado de los mismos. Otra opción es dar solo los códigos objeto para ocultar
nuestros fuentes.
Cuando construimos software como este, que no implementa una aplicación, sino que consiste en
una colección de tipos y funciones (o clases y métodos) relacionados entre sí y que están
diseñados para ser utilizados por otras aplicaciones, aparece el concepto de biblioteca 15. Puesto
que es un concepto muy utilizado en el mundo del desarrollo de software ya existen mecanismos
estandarizados para trabajar de manera que se facilite a ese software de terceros la tarea de
compilación y enlazado. Dicha biblioteca, de nuevo, aplica el concepto de encapsulamiento y
ocultamiento de información. Por ello, la biblioteca dispondrá siempre de:
• Una interfaz pública. Las declaraciones de nuevos tipos de datos así como las
declaraciones de funciones (o métodos) para trabajar con ellos. Esta se incluirá en ficheros
de cabecera (.h).
• Una implementación privada. Consiste en la implementación de todas las funciones y otros
detalles internos. El desarrollador de la biblioteca dispondrá de esto en ficheros .cpp pero
cuando lo distribuya lo que hará será empaquetar todos los códigos objeto en un único
fichero (podría ser más de un fichero). Este fichero es el que se suele conocer como
biblioteca.
Por tanto, de forma sencilla, una biblioteca no es más que un fichero que contiene código objeto
preparado para ser enlazado con otro software16. Existen dos tipos de bibliotecas según la forma
en la que se enlazan con el ejecutable que las va a utilizar:
• Bibliotecas estáticas (static library). La biblioteca es un fichero con código objeto que se
enlaza por el desarrollador de la aplicación con otros códigos objeto (en particular con
alguno que contenga una función main()) para crear el ejecutable. Dicho ejecutable
contiene todo el código de la biblioteca (o, al menos, la parte de ella que va a usar). En
esta asignatura trabajaremos únicamente con este tipo de bibliotecas.
El nombre de los ficheros de biblioteca es de la forma libXXX.EXT en donde XXX tiene que ver
con el nombre de la biblioteca (por ejemplo “formasgeometricas”) y EXT es la extensión del
fichero, que dependerá del sistema operativo en el que estemos trabajando:
◦ GNU/Linux. La extensión es .a
◦ Windows. La extensión es .lib
◦ MacOS. La extensión es .a
15 Seguramente oirá hablar de “bibliotecas” o “librerías” de forma indistinta. En inglés, el concepto se conoce como
library, cuya traducción es biblioteca, por la analogía con las bibliotecas de libros. Cuando alguien habla de “librería”
está haciendo una mala traducción y, por tanto, debería evitar ese término. Recuerde que librería en inglés es
bookshop o bookstore (y no library).
16 Esta es una definición sencilla para entender el concepto de biblioteca.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 21/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
Además, es posible que el nombre incluya el número de versión.
• Bibliotecas dinámicas (shared library). Sigue siendo código objeto pero esta vez, en lugar
de ser enlazada por el desarrollador de la aplicación, es enlazada por el sistema operativo
en tiempo de ejecución. Es decir, la aplicación construida por el programador (el
ejecutable) no incluye el código objeto de las bibliotecas dinámicas. De esta manera,
cuando ejecutamos la aplicación y esta hace uso de elementos de la biblioteca dinámica, el
sistema operativo se encarga de hacer, en ese momento, el enlazado. El nombre de los
ficheros es similar al de las bibliotecas estáticas aunque con distinta extensión:
◦ GNU/Linux. La extensión es .so (viene de shared object).
◦ Windows. La extensión es .dll (viene de dynamic link library).
◦ MacOS. La extensión es .dylib o .so (viene de dynamic library).
La ventaja que aportan las bibliotecas dinámicas es que permiten reutilizar una misma biblioteca
por multitud de aplicaciones evitando que todas y cada una de las aplicaciones contengan dentro
el mismo código repetido multitud de veces. Lo normal será que un ejecutable que usa bibliotecas
dinámicas sea relativamente pequeño mientras que uno que usa versiones estáticas de esas
bibliotecas ocupe más espacio.
Esto tiene especial relevancia si pensamos que hay bibliotecas que son usadas por prácticamente
el 100% de los programas que ejecutamos en nuestro ordenador. Por ejemplo, existe una
biblioteca conocida como glibc que es usada por la práctica totalidad de los programas ya que
contiene lo que se conocen como llamadas al sistema (reserva y liberación de memoria dinámica,
manipulación de caracteres, trabajo con cadenas C, funciones de internacionalización, E/S, acceso
al sistema de ficheros, operaciones aritméticas con enteros o reales, etc.).
En la siguiente tabla tiene una comparativa de algunas características de ambos tipos de
bibliotecas:
Biblioteca estática Biblioteca dinámica
El código de la biblioteca está contenido en El código de la biblioteca no se incluye
el ejecutable final. Por esto, el tamaño del dentro del ejecutable.
fichero suele ser mayor.
La etapa de enlazado se hace después de la La etapa de enlazado se hace en tiempo de
compilación. ejecución. Por esto, el rendimiento puede ser
ligeramente inferior.
Una vez enlazada la biblioteca, el ejecutable El ejecutable no es autocontenido. El éxito de
es autocontenido. su ejecución dependerá de que la biblioteca
esté instalada en el sistema y de que no
haya incompatibilidades.
Una actualización en la biblioteca obliga a Una actualización en la biblioteca no requiere
un nuevo enlazado con el ejecutable. de un nuevo enlazado. Sin embargo, si esa
actualización tiene alguna incompatibilidad
con la versión anterior el ejecutable podría
dejar de funcionar.
Cada programa tiene su propio segmento de Los programas en ejecución comparten la
memoria para ejecutarse. biblioteca dinámica.
Para desarrollar nuevo software que use la Para desarrollar nuevo software que use la
biblioteca necesitaremos: biblioteca necesitaremos:
• Sus ficheros de cabecera (.h) • Sus ficheros de cabecera (.h)
• Su código objeto (libXXX.a) • Su código objeto (libXXX.so)
Para distribuir aplicaciones que usen la Para distribuir aplicaciones que usen la
biblioteca hemos de aportar: biblioteca hemos de aportar:
• El ejecutable • El ejecutable
• La biblioteca dinámica (libXXX.so)
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 22/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
Creación de bibliotecas en GNU/Linux
Como hemos visto, las bibliotecas se construyen a partir de varios ficheros de código objeto. En
GNU/Linux disponemos de la herramienta ar (viene de archiver) para realizar esta tarea. Su uso es
extremadamente sencillo, basta con ejecutar:
prompt> ar rsv libformas.a punto.o rectangulo.o circulo.o
En donde:
• rsv son opciones para:
◦ r: crea el archivo e inserta los códigos objeto. Si alguno de los códigos objeto que
vamos a añadir ya estaba en el archivo biblioteca entonces se reemplaza, evitando así
incluir varias veces un mismo código objeto si ejecutamnos ar en más de una ocasión.
◦ s: incluye un índice en el archivo de biblioteca.
◦ v: modo verbose para que informe de las operaciones que va realizando.
• el primer fichero es la biblioteca que se va a crear ( libformas.a).
• el resto de ficheros son los códigos objeto que se van a empaquetar en la biblioteca.
En este momento ya disponemos de nuestra biblioteca lista para enlazar:
prompt> g++ libformas.a main1.o -o main1
Aunque se puede hacer como se indica en el comando anterior, se recomienda usar las opciones
específicas de g++ para enlazar con bibliotecas:
• -lnombre para indicar el nombre de la biblioteca con la que enlazar. El compilador buscará
un fichero de la forma libnombre.a para el caso de bibliotecas estáticas. Observe que con
esta sintaxis usamos el nombre de la biblioteca y no el nombre del fichero. De esta forma el
comando es válido independientemente, por ejemplo, de la extensión del fichero ( .a o .lib
según el sistema operativo).
• -Ldirectorio para indicar en qué carpetas debe buscar los ficheros de biblioteca. Por defecto
el compilador buscará en las carpetas preestablecidas en el sistema (p. ej. /usr/lib). Si
usamos esta opción también podrá buscar en los directorios de nuestro proyecto.
Por tanto, para enlazar y crear el ejecutable de nuestro ejemplo deberá ejecutar:
prompt> g++ main1.o -lformas -L. -o main1
Con -L. estamos informando a g++ de que busque los ficheros de biblioteca en el directorio
actual.
Hay un último detalle, muy importante, a tener en cuenta. Cuando enlazamos ficheros objeto no
es relevante el orden en el que los ponemos en la llamada a g++, es decir, cualquiera de los
siguientes enlazados es correcto y produce el mismo resultado:
prompt> g++ main1.o punto.o rectangulo.o auxiliar.o -o main1
prompt> g++ auxiliar.o punto.o main1.o rectangulo.o -o main1
prompt> g++ rectangulo.o auxiliar.o punto.o main1.o -o main1
Sin embargo, el orden sí es importante cuando se trata de archivos de biblioteca 17. Supongamos
que tenemos dos bibliotecas (libA.a y libB.a) y una aplicación (main.o) que hace uso de ellas (de
una o de ambas). Supongamos además que libA.a hace uso de libB.a. En esta situación, al escribir
el comando de enlazar, hay que tener cuidado con el orden en el que se escriben las bibliotecas
respetando las siguientes reglas:
• Si una biblioteca hace uso de otra, esta debe escribirse antes en el comando de enlazado.
• Si un código objeto hace uso de una biblioteca este debe escribirse antes.
Es decir, la llamada correcta a g++ para enlazar es:
prompt> g++ main.o -lA -lB -o main
17 En realidad esto podría variar dependiendo del Sistema Operativo y del compilador o enlazador. Por ejemplo con GNU
g++ y GNU/Linux sí importa el orden pero con Clang y MacOS no.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 23/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
Si cambiamos el orden de -lA y/o -lB y/o main.o el proceso de enlazado fallará.
Observe que este problema no se da cuando lo que enlazamos son únicamente códigos objeto. En
ese caso el orden es indiferente.
ᐒ Ejercicio 10 : Orden de enlazado con bibliotecas
Pruebe a ejecutar los siguientes comandos y compruebe el resultado:
prompt> g++ main1.o -lformas -o main1 -L.
prompt> g++ -lformas main1.o -o main1 -L.
La interfaz pública de la biblioteca
Finalmente, quedaría facilitar la inclusión de los ficheros de cabecera. Habría que crear un nuevo
fichero formas.h que simplemente haga una inclusión de punto.h, rectangulo.h y circulo.h. A
continuación modificaremos main1.cpp, main2.cpp y main3.cpp y sustituiremos los #include por este
nuevo (y único) #include.
ᐒ Ejercicio 11 : Crear biblioteca y construir el proyecto
Cree la biblioteca formas tal y como se explica en el apartado anterior (libformas.a y formas.h) y
modifique los ficheros con las tres aplicaciones para actualizar sus inclusiones.
Dibuje el nuevo diagrama de dependencias de todos los ficheros del proyecto.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 24/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
Sesión 2
Uso de GNU make: automatización del proceso de
construcción de software
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 25/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
6. Automatización del proceso de compilación con GNU Make
El diagrama de dependencias del proyecto hasta el momento es este:
Observe como se simplifica el uso de los TDA por parte de aplicaciones interponiendo una
biblioteca como capa intermedia entre ambos. Cualquier aplicación únicamente ha de hacer un
#include y enlazar con un fichero biblioteca.
La secuencia de comandos para construir el proyecto completo es esta (se omiten las opciones
adicionales para la detección de errores de g++ para hacer más legible el documento pero se
recomienda utilizarlas siempre):
• Compilación de código fuente y obtención de códigos objeto:
prompt> g++ punto.cpp -o punto.o
prompt> g++ rectangulo.cpp -o rectangulo.o
prompt> g++ circulo.cpp -o circulo.o
prompt> g++ auxiliar.cpp -o auxiliar.o
prompt> g++ main1.cpp -o main1.o
prompt> g++ main2.cpp -o main2.o
prompt> g++ main3.cpp -o main3.o
• Creación de bibliotecas:
prompt> ar rsv libformas.a punto.o rectangulo.o circulo.o auxiliar.o
• Enlazado y creación de ejecutables:
prompt> g++ main1.o -lformas -L. -o main1
prompt> g++ main2.o -lformas -L. -o main2
prompt> g++ main3.o -lformas -L. -o main3
Cada vez que modifiquemos algún fichero fuente solo ejecutaremos aquellos comandos que
dependan de dicha modificación. La gestión manual de todo esto puede hacerse extremadamente
compleja (piense en proyectos con miles de ficheros). La alternativa es ejecutar todos los
comandos anteriores cada vez que se modifica algo pero eso sería extremadamente ineficiente
(piense, de nuevo, en grandes proyectos).
Por tanto, se hace imprescindible el uso de alguna herramienta que simplifique esta tarea a los
desarrolladores y que automatice el proceso de construcción en base a las dependencias del
código. Existen diversas alternativas para esto y, como casi cualquier cosa en el mundo del
desarrollo de software, con diferentes niveles de abstracción.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 26/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
• GNU Make18. Herramienta de la GNU para automatizar la construcción de proyectos muy
veterana y con un gran nivel de soporte.
• Ninja19. Diseñado para ser más rápido en el proceso de construcción. No está pensado para
ser utilizado directamente por los desarrolladores sino para ser utilizado por otras
herramientas de más alto nivel.
• CMake20. Diseñado para simplificar la configuración de proyectos grandes y facilitar la
construcción multiplataforma. Su objetivo es generar ficheros de configuración para otras
herramientas de más bajo nivel como GNU Make o Ninja o para entornos de desarrollo
como Visual Studio o Xcode.
• Scons21. Es una herramienta que utiliza scripts Python para la configuración del proyecto.
• Ant22. Orientado a la construcción de aplicaciones Java.
• … y otros muchos.
Los entornos de desarrollo modernos (IDE) suelen simplificarnos mucho las tareas de construcción
de software pero debemos ser conscientes de que lo que hacen es usar alguna(s) de estas
herramientas por debajo. En la asignatura Fundamentos de Programación se ha usado algún IDE
como Code::Blocks, DevC++ u otro similar. Cada vez que se pulsa el botón de “compilar” o
“compilar y ejecutar” el IDE está utilizando alguna de estas herramientas para generar el
ejecutable.
En este documento abordaremos el uso de GNU Make para tener unas nociones básicas de cómo
se puede automatizar todo ese proceso. Más adelante veremos también alguna noción básica de
CMake por ser posiblemente la herramienta de este tipo más extendida en la actualidad y que
suele usarse de forma conjunta con diversos IDE como, por ejemplo, Visual Studio Code.
6.1. GNU Make
Como se ha dicho, esta herramienta va a automatizar el proceso de construcción del proyecto.
Para ello lo que se hace es crear un fichero en el que se plasman todas las dependencias del
proyecto en base a reglas de la forma:
Objetivo : Lista de dependencias
Acciones
en donde:
• Objetivo. En inglés target. Se trata de lo que queremos construir. Puede ser el nombre de
un fichero (obtenido como resultado de procesar otros ficheros) o puede ser una etiqueta
arbitraria.
• Lista de dependencias. Es una lista de ítems de los que depende la aplicación de la regla.
Pueden ser nombres de ficheros o bien otros objetivos. La herramienta GNU Make debe
asegurar que todas esas dependencias están actualizadas antes de llevar a cabo las
acciones de esta regla.
• Acciones. Esta es la secuencia de comandos que se deben ejecutar para conseguir el
objetivo marcado. Normalmente son las llamadas a g++, ar u otras herramientas.
Importante: la indentación es obligatoria y consiste en un carácter tabulador.
Por ejemplo, la siguiente sería una regla válida:
main1 : libformas.a main1.o
g++ main1.o -lformas -L. -o main1
En ella decimos que vamos a construir el fichero main1 (el ejecutable) y que, para ello, se necesitan
los ficheros libformas.a y main1.o. Haciendo uso de otras reglas, GNU Make se encargará de
construir ambos ficheros (los construye si no existen o los reconstruye si están desactualizados).
18 https://www.gnu.org/software/make/
19 https://ninja-build.org/
20 https://cmake.org/
21 https://scons.org/
22 https://ant.apache.org/
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 27/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
Una vez los tenga pasará a ejecutar la acción indicada: ejecutar g++ para enlazar y generar el
ejecutable.
GNU Make recorrerá la lista de dependencias en orden y buscará alguna regla que tenga como
objetivo cada uno de los elementos de dicha lista para saber cómo se construye ese elemento. En
el ejemplo, GNU Make dará estos pasos:
1 Dependencia libformas.a. Se busca una regla con ese objetivo. Si hemos construido bien el
fichero de configuración deberíamos haber incluido una regla como la siguiente:
libformas.a : punto.o rectangulo.o auxiliar.o
ar rsv libformas.a punto.o rectangulo.o circulo.o auxiliar.o
con esa regla sabe cómo se construye la biblioteca libformas.a. Se aplicará para ella el
mismo procedimiento.
2 Dependencia main1.o. Deberíamos tener una regla para indicar cómo se construye este
fichero similar a esta:
main1.o : main1.cpp punto.h rectangulo.h
g++ -c main1.cpp -o main1.o
En este caso la dependencia es un fichero .cpp. Este es un fichero escrito por nosotros que
no se construye a partir de otros. En este caso lo que comprueba GNU Make es si está o no
actualizado, que se traduce en comparar la fecha de modificación de main1.cpp y del
objetivo main1.o de manera que:
◦ Si la fecha de main1.cpp es posterior a la de main1.o quiere decir que ha sido modificado
después de que, en algún momento previo, haya sido compilado. En ese caso debe
ejecutar las acciones.
◦ Si la fecha de main1.cpp es anterior a la de main1.o quiere decir que main1.o está
actualizado y, por tanto, no necesita ser generado de nuevo (no hace falta ejecutar las
acciones).
Como también hay dependencias de punto.h y rectangulo.h se procede de igual forma de
manera que, si alguno de los tres ficheros tiene fecha de modificación posterior a main1.o (o
main1.o no existe) se ejecutan las acciones. Si ya existe main1.o y todos son de fecha
anterior entonces no se ejecutan las acciones. En este ejemplo y sucesivos se omite el uso
de formas.h pero podría incluirse sin ninguna dificultad.
En caso de que haya implementado formas.h, el fichero main1.cpp haría #include “formas.h”
en lugar de hacer las inclusiones de punto.h y rectangulo.h por lo que la regla, en principio,
sería esta otra (más adelante matizaremos esto):
main1.o : main1.cpp formas.h
g++ -c main1.cpp -o main1.o
De esta forma GNU Make va buscando reglas una a una y decidiendo cuáles ha de aplicar y cuáles
no en función de si los ficheros de los que depende cada regla están o no actualizados. Todas las
reglas se almacenan en un fichero llamado Makefile y es irrelevante el orden en el que las
escribamos.
Esta herramienta permite hacer cosas bastante más complejas y construir reglas genéricas
parametrizables (o reglas basadas en patrones -pattern rules-) que evitan tener que escribir una
regla por cada fichero que se va a construir. También se puede automatizar la generación de
nuevas reglas a partir del contenido de ficheros fuente. Sin embargo, no es nuestro objetivo
conocer en profundidad la herramienta sino, simplemente, tener una noción básica de su uso.
ᐒ Ejercicio 12 : El fichero Makefile para el proyecto
Cree un fichero Makefile con todas las reglas necesarias para construir el proyecto. Utilice el
diagrama de dependencias de ejemplos anteriores para crear la lista de dependencias de cada
regla. Use las siguientes reglas como punto de partida:
# Construcción de ejecutables
main1 : libformas.a main1.o
g++ main1.o -lformas -L. -o main1
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 28/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
# Construcción de bibliotecas
libformas.a : punto.o rectangulo.o auxiliar.o
ar rsv libformas.a punto.o rectangulo.o circulo.o auxiliar.o
# Construcción de códigos objeto
main1.o : main1.cpp formas.h
g++ -c main1.cpp -o main1.o
Nota 1: observe que puede incluir comentarios con el símbolo hashtag.
Nota 2: la indentación se hace forzosamente con un carácter tabulador (no con espacios en
blanco).
6.2. Ejecución de GNU Make y otras reglas estándares
Una vez tengamos el fichero de reglas, podemos ejecutar GNU Make con el comando:
prompt> make
Esta es la forma más simple de uso en la que se asume que el fichero de reglas es Makefile.
Podríamos crear ficheros de reglas con otros nombres y usar la opción -f para invocar a GNU
Make.
Con esta llamada, se intentará construir el primer objetivo que hayamos escrito en el fichero. Y
construirá, en cadena, solo aquellos objetivos necesarios para construir ese. Podemos indicar que,
en lugar del primer objetivo, construya otro con la sintaxis:
prompt> make objetivo
En este caso buscará la regla que tenga el objetivo marcado y aplicará el proceso. Por ejemplo,
podemos construir los dos ejecutables de nuestro proyecto así:
prompt> make main1
prompt> make main2
Como es frecuente que deseemos construir múltiples objetivos, se suelen establecer algunas
reglas en las que el objetivo no es un nombre de fichero sino una etiqueta. Por ejemplo, es
habitual encontrar una regla23 con un objetivo all, escrita la primera, que indica todos los objetivos
que hay que construir cada vez que hagamos alguna modificación en el proyecto:
all: main1 main2
Esta regla tiene algunas características especiales:
• all no es un fichero. Cuando GNU Make la analiza siempre detectará que no está
“actualizada” (no existe un fichero llamado all) por lo que siempre comprobará sus
dependencias y, si procede, ejecutaría sus acciones.
• No tiene acciones. La misión de la regla no es ejecutar acciones sino desencadenar las
reglas que permiten actualizar las dependencias.
También es posible escribir reglas sin lista de dependencias. Un caso típico es la regla clean,
utilizada para borrar todos los ficheros intermedios generados en el proyecto (normalmente los
códigos objeto o los ficheros de backup de los fuentes):
clean :
rm *.o *.a
Al no tener dependencias, su misión es ejecutar una serie de acciones sin hacer ningún tipo de
comprobación previa. En este ejemplo hemos borrado también la biblioteca pero podríamos optar
por dejarla si pretendemos distribuirla a terceros.
Hay otra regla de limpieza estándar para borrar también los ejecutables finales:
distclean : clean
rm main1 main2
En esta regla se incluye el objetivo clean en la lista de dependencias para borrar los ficheros
23 https://www.gnu.org/software/make/manual/html_node/Standard-Targets.html
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 29/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
intermedios y se añaden en la lista de acciones los comandos para borrar los ejecutables.
ᐒ Ejercicio 13 : Reglas estándares del fichero Makefile
Complete el fichero Makefile añadiendo las reglas estándares all, clean y distclean. A continuación,
pruebe a modificar alguno de los ficheros fuente y ejecute make all para verificar que solo ejecuta
aquellos comandos que son dependientes de la modificación efectuada.
6.3. Obtención automática de dependencias [✲ Opcional ✲]
Aunque podemos disponer del mapa de dependencias como hemos hecho en este proyecto
(manualmente), cuando el proyecto crece en número de ficheros esta tarea puede ser compleja.
Por ello, el propio g++ nos puede facilitar esta tarea usando la opción -MM para ficheros fuente. Por
ejemplo, podemos conocer las dependencias de ficheros fuente de main1.cpp así:
prompt> g++ -MM main1.cpp
main1.o: main1.cpp formas.h rectangulo.h punto.h
El resultado de la ejecución es la regla que debemos usar para obtener el código objeto del fichero
fuente que pasamos como argumento de g++. Puede ver como en la lista de dependencias
aparece formas.h pero también punto.h y rectangulo.h. En la sección siguiente tratamos ese tema.
Observe que en las reglas de Makefile también se han de poner los ficheros .h en las listas de
dependencias allí donde proceda. Recuerde que estos ficheros no se compilan expresamente sino
que se van a compilar por estar en #include de otros ficheros.
6.4. Dependencias de ficheros de cabecera
En este punto, cabe preguntarse cómo han de tratarse las inclusiones de ficheros indirectas. Por
ejemplo, si en rectangulo.cpp hacemos un #include de rectangulo.h y otro de auxiliar.h la regla sería
esta:
rectangulo.o : rectangulo.cpp rectangulo.h auxiliar.h
g++ -c rectangulo.cpp -o rectangulo.o
Que viene a decir que si rectangulo.cpp, rectangulo.h o auxiliar.h son modificados entonces se
ejecute la acción de compilar.
Sin embargo, como rectangulo.h hace un #include de punto.h también deberíamos compilar de
nuevo rectangulo.cpp cuando se modifique punto.h. Es decir, que deberíamos incluir punto.h en la
lista de dependencias de la regla. En la sección anterior se ha visto otro caso similar: main1.cpp
incluye a formas.h y este, a su vez, incluye a punto.h y rectangulo.h.
Para resolver esto tenemos varias opciones:
• Analizar todos los ficheros manualmente y buscar todas las inclusiones directas o indirectas
para incluirlas en la lista de dependencias. Esta tarea puede ser compleja en proyectos
grandes y es fácil omitir, por error, alguna dependencia.
• Usar g++ con la opción -MM manualmente para averiguar las dependencias. Con esa
información escribir la regla en Makefile.
• Usar g++ con la opción -MM en el propio Makefile para que sea GNU Make el que determine
automáticamente las dependencias. Esto es más complejo y escapa al ámbito de este
documento.
• No incluir punto.h en la lista de dependencias (por no hacer #include explícito) y crear reglas
adicionales para forzar la actualización de objetivos como esta:
rectangulo.h : punto.h
touch rectangulo.h
La interpretación de esta regla es:
• El objetivo es rectangulo.h (cuando otra regla incluya a rectangulo.h se mirará esta
para saber cómo ha de construirse). En este caso no es algo que deba construirse
por parte de GNU Make puesto que se trata de un fichero fuente.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 30/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
• La lista de dependencias es punto.h. Eso quiere decir que si punto.h se actualiza,
deben ejecutarse las acciones de esta regla.
• La acción es ejecutar el comando touch sobre el fichero rectangulo.h. Este comando
lo único que hace es modificar la fecha de modificación de rectangulo.h al tiempo de
reloj actual. Es decir, estamos modificando la fecha del fichero para que aparente
haber sido modificado.
De esta forma, si modificamos punto.h obligaremos a modificar la fecha de modificación de
rectangulo.h y provocaremos que se vuelvan a ejecutar las acciones de aquellas reglas que
dependan de rectangulo.h.
ᐒ Ejercicio 14 : Completando el fichero Makefile
Complete el fichero Makefile para tener en consideración las posibles dependencias indirectas que
haya en el proyecto. Use la última de las opciones planteadas en la sección anterior.
6.5. Variables en Makefile
Para finalizar, hablaremos de la posibilidad de definir variables en los ficheros Makefile y de
algunas que están estandarizadas y que podrá ver en prácticamente cualquier Makefile que pueda
consultar. Para definir una variable usamos esta sintaxis:
VARIABLE = VALOR
y para usarla:
$(VARIABLE)
Por ejemplo, podríamos reescribir la regla para construir la biblioteca si agrupamos todos los
ficheros objeto en una variable de esta forma:
# Construcción de bibliotecas
OBJ = punto.o rectangulo.o auxiliar.o
libformas.a : $(OBJ)
ar rsv libformas.a $(OBJ)
Simplemente se hace una sustitución de la variable por su contenido allí donde lo indiquemos. El
uso de variables da una potencia extra a la construcción de Makefile complejos pero, de nuevo, no
es el objetivo de este documento.
Hay algunas variables que están estandarizadas y que verá casi en cualquier Makefile:
• CXX. Contiene el nombre del ejecutable de g++
• CPPFLAGS. Son las opciones que se le pasan al preprocesador. Las más frecuentes son -D
(para definir macros) y -I (para indicar dónde debe buscar los ficheros de cabecera).
• CXXFLAGS. Son las opciones que se le pasan al compilador.
• LD. Nombre del ejecutable del enlazador.
• LDFLAGS. Son las opciones que se le pasan al enlazador.
• AR. Nombre del ejecutable montador de bibliotecas.
• ARFLAGS. Son las opciones que se le pasan al montador.
En nuestro ejemplo, podría modificar Makefile de la siguiente forma:
CXX = g++ # Nombre del compilador
CPPFLAGS = -I. # Opciones para el preprocesador
CXXFLAGS = -c -g -std=c++17 -Wall -Wextra -Werror -Wpedantic # Opciones para el compilador
LD = g++ # Nombre del enlazador
LDFLAGS = -L. -lformas # Opciones para el enlazador
AR = ar # Nombre del montador de bibliotecas
ARFLAGS = rsv # Opciones para el montador de bibliotecas
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 31/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
# Construcción del proyecto completo
all: main1 main2
# Compilación de código fuente
main1.o : main1.cpp formas.h punto.h rectangulo.h
$(CXX) main1.cpp -o main1.o $(CPPFLAGS) $(CXXFLAGS)
# Montaje de bibliotecas
libformas.a : punto.o rectangulo.o auxiliar.o
$(AR) $(ARFLAGS) libformas.a punto.o rectangulo.o auxiliar.o
# Enlazado de ejecutables
main1 : libformas.a main1.o
$(LD) main1.o -o main1 $(LDFLAGS)
6.6. Uso avanzado de GNU Make [✲ Opcional ✲]
Reglas basadas en patrones (pattern rules)
Puede apreciar que la construcción del fichero Makefile es un poco repetitiva. Para compilar cada
fichero .cpp escribimos una regla que es casi un copia/pega de las otras salvo por el nombre del
fichero. Cada vez que añadimos un nuevo fichero fuente al proyecto hemos de crear una nueva
regla para él y actualizar dependencias de otros objetivos.
Para evitar esta tarea tan tediosa (y propensa a errores), podemos usar un tipo de regla un poco
especial con la siguiente sintaxis:
%.o : %.cpp
$(CXX) $< -o $@ $(CPPFLAGS) $(CXXFLAGS)
en donde:
• $< es una variable automática; se sustituye por la primera dependencia de la lista (en este
caso el nombre del fichero fuente).
• $@ es una variable automática; se sustituye por el nombre del objetivo
y lo que hace esta regla es definir cómo se construye cualquier fichero .o a partir de un .cpp con
igual nombre (salvo por la extensión). Esta regla valdría para compilar todos los ficheros .cpp. Si
añadimos nuevos ficheros fuente al proyecto no hay que tocar Makefile.
Faltaría por incluir las dependencias de los ficheros de cabecera. Podría hacerse utilizando la
opción -MM del compilador para averiguarlas automáticamente y añadiendo algunas reglas
adicionales. Como se ha indicado antes, este documento solo pretende aportar conocimientos
básicos sobre GNU Make y, por tanto, esto escapa al alcance del mismo. Sin embargo, es
interesante apuntar posibilidades como esta para saber hasta donde se puede llegar.
Uso de funciones
También disponemos de funciones24 para hacer algunas manipulaciones de, por ejemplo, los
nombres de los ficheros. Algunas de ellas son:
• $(dir lista_de_ficheros) Devuelve la lista con el directorio de cada uno de los ficheros de la
lista, es decir, devuelve la misma lista pero eliminando en cada fichero su nombre. Ejemplo:
$(dir src/f1.cpp /usr/local/src/sistema.h src/lib/mod/readme.txt main.cpp)
devolvería:
“src/ /usr/local/src/ src/lib ./”
• $(basename lista_de_ficheros) Devuelve la lista de ficheros sin la extensión. Ejemplo:
$(basename src/f1.cpp /usr/local/src/sistema.h src/lib/mod/readme.txt main.cpp)
24 https://www.gnu.org/software/make/manual/html_node/File-Name-Functions.html
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 32/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
devolvería:
“src/f1 /usr/local/src/sistema src/lib/mod/readme main”
• $(wildcard patrón) Devuelve la lista de ficheros que coinciden con el patrón. Ejemplo:
$(wildcard *.cpp)
devolvería (en nuestro proyecto):
“auxiliar.cpp main1.cpp main2.cpp punto.cpp rectangulo.cpp”
Hay también funciones para manipulación de cadenas 25:
• $(subst original,reemplazo,texto) En la cadena texto sustituye las ocurrencias de original
por reemplazo. Ejemplo:
$(subst to,XYZ,esto es un texto de prueba)
devolvería:
“esXYZ es un texXYZ de prueba”
• $(patsubst patrón,reemplazo,texto) Busca cadenas separadas por espacios en blanco
(palabras) en el texto que coincidan con el patrón y las cambia por reemplazo cada vez que
las encuentre. Ejemplo:
$(patsubst %.cpp,%.o,f1.cpp f2.cpp f3.h)
devolvería:
“f1.o f2.o f3.h”
• $(variable:patrón=reemplazo) es una sintaxis alternativa26 para patsubst cuando el texto está
almacenado en una variable. El ejemplo anterior se reescribiría así:
FICHEROS = f1.cpp f2.cpp f3.h
$(FICHEROS:%.cpp=%.o)
También podría reescribirse así:
$(FICHEROS:.cpp=.o)
Podemos usar combinaciones de estas funciones. Para nuestro proyecto podríamos almacenar en
sendas variables:
• Los nombres de los ficheros fuente del proyecto ( FICHEROSCPP).
• Los nombres de los ficheros fuente que se corresponden con aplicaciones ( FICHEROSMAIN).
• Los nombres de los ficheros fuente para la biblioteca, es decir, los que no son aplicaciones
(FICHEROSLIB). Con filter-out calculamos la diferencia entre las dos variables anteriores.
• Los nombres de los ficheros objeto para la biblioteca ( FICHEROSLIBOBJ).
FICHEROSCPP = $(wildcard *.cpp)
FICHEROSMAIN = $(wildcard main*.cpp)
FICHEROSLIB = $(filter-out $(FICHEROSMAIN),$(FICHEROSCPP))
FICHEROSLIBOBJ = $(FICHEROSLIB:=.o)
de esta forma podríamos escribir reglas en las que intervengan esos ficheros. Por ejemplo la que
permite crear la biblioteca:
libformas.a : $(FICHEROSLIBOBJ)
$(AR) $(ARFLAGS) libformas.a $(FICHEROSLIBOBJ)
Como puede ver, el nivel de automatización que podemos conseguir es muy elevado si tenemos el
suficiente dominio de GNU Make.
25 https://www.gnu.org/software/make/manual/html_node/Text-Functions.html
26 https://www.gnu.org/software/make/manual/html_node/Substitution-Refs.html
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 33/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
Sesión 3
→ Instalación y uso de CMake: una herramienta
de alto nivel para la automatización de la
construcción de software
→ Instalación de Visual Studio Code
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 34/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
7. Una herramienta de alto nivel: CMake
Como se ha visto, GNU Make es muy potente y permite un control muy fino sobre cómo realizar la
construcción de un proyecto. El inconveniente es que puede ser complejo cuando trabajamos con
proyectos grandes.
En general, los sistemas de construcción de software se pueden enmarcar dentro de dos grandes
categorías27:
• Build systems. Son sistemas como GNU Make para controlar el proceso de construcción.
Algunos son:
◦ GNU Make (desde 1976). El más veterano, con muy buen soporte y uno de los más
utilizados.
◦ Ninja (desde 2012). Es muy eficiente pero no está pensado para que una persona cree
sus ficheros de configuración sino para que otros sistemas más avanzados lo gestionen.
◦ Scons (desde 2001). Los ficheros de configuración se escriben en Python.
◦ Bazel (desde 2015). Escrito en Java es utilizado por Google para proyectos en diversos
lenguajes (también C++) y tiene un buen comportamiento en grandes proyectos.
◦ Apache Ant (desde 2000). Está diseñado para construir proyectos hechos en Java.
• Build systems generators. Son sistemas que, con una sintaxis simplificada, permiten definir
cómo construir un proyecto. Una de sus características más destacadas es que permiten
trabajar de forma transparente para el programador con diferentes compiladores (MinGW,
GNU g++, Clang, etc.), plataformas (GNU/Linux, MacOS, Windows, etc.) e incluso build
systems. Con un lenguaje descriptivo relativamente sencillo son capaces de generar
ficheros de configuración complejos para otros sistemas de construcción como GNU Make o
Ninja así como para algunos IDE como Visual Studio o Xcode y también manejar dichas
herramientas de una forma sencilla. Algunos son:
◦ CMake (desde 2000). Desarrollado por la empresa Kitware es código libre, como la
mayoría.
◦ Autotools (o GNU build system) (desde 1983). Fue muy usado hace algún tiempo pero
ha ido bajando en popularidad debido a que han ido surgiendo otros sistemas más
simples.
◦ Meson (desde 2013). Se basa en Ninja para sistemas GNU/Linux, Visual Studio para
sistemas Windows y Xcode para sistemas MacOS. Es sencillo y fácil de aprender aunque
no tiene tantas características como otros.
◦ Qmake (desde 1990-1995). Permite generar ficheros Makefile y se integra con las
herramientas Qt application framework. Es utilizado por el IDE Qt Creator.
Sin duda, cada uno tiene sus ventajas e inconvenientes y dependiendo de diversos
factores puede que unos sean más adecuados que otros para un determinado proyecto.
En la actualidad la tendencia es usar sistemas generadores (de forma conjunta con
algún IDE) y, entre ellos, posiblemente CMake sea el más utilizado en proyectos C++.
Será el que utilicemos en esta asignatura, aunque de una manera bastante elemental.
7.1. Instalación de CMake
Lo primero que debemos hacer es instalar CMake. En GNU/Linux lo normal es que esté en los
repositorios de nuestra distribución. Para el caso de Ubuntu ejecutaremos esto:
prompt> sudo apt install cmake
Para verificar que está instalado puede ejecutar:
prompt> cmake --version
27 https://en.wikipedia.org/wiki/List_of_build_automation_software
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 35/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
Instalación en Windows
En Windows, si se ha optado por la opción de instalar MSYS, debe ejecutar:
pacman -S mingw-w64-ucrt-x86_64-cmake
En caso de estar usando WSL la instalación se hará igual que en GNU/Linux.
Instalación en MacOS
En MacOS debe ejecutar en una shell:
brew install cmake
7.2. Uso de CMake
De forma análoga a como hicimos con GNU Make, hemos de crear un fichero de configuración del
proyecto. Este se va a llamar CMakeLists.txt y en él escribiremos las instrucciones de construcción
con una nueva sintaxis (tenga cuidado con mayúsculas y minúsculas en el nombre del fichero).
Hasta el momento, en nuestro proyecto teníamos una única carpeta en la que hemos almacenado:
• Ficheros de código fuente (.h y .cpp).
• Fichero Makefile.
• Ficheros generados: código objeto, bibliotecas y ejecutables.
En proyectos de cierta envergadura es conveniente tener subdirectorios para que esté todo mejor
organizado. Por ejemplo:
• Un subdirectorio para contener los ficheros fuente (src e include).
• Un subdirectorio para resultados intermedios de compilación ( obj).
• Un subdirectorio para bibliotecas (lib).
• Un subdirectorio para los ejecutables (bin).
• … y otros que puedan ser de interés para almacenar documentación, datos, etc.
La organización en subdirectorios puede variar dependiendo del sistema de construcción que
estemos usando y de la envergadura el proyecto.
En el caso de CMake se recomienda utilizar, para empezar, dos carpetas en un mismo nivel (o en
distintas ubicaciones):
• Source directory (directorio fuente) para los ficheros fuente y otros que nosotros creamos
expresamente como, por ejemplo, CMakeLists.txt. Es el directorio en el que trabajamos con
nuestro editor o IDE.
• Build directory (directorio de construcción o destino) para los ficheros generados en el
proyecto: ejecutables, códigos objeto, bibliotecas, etc. Aquí se almacena cualquier
elemento que se ha generado por CMake.
La idea es simple: separar los ficheros originales del proyecto de los que se obtienen
automáticamente a partir de ellos. Así, siempre tendremos nuestra carpeta original sin ficheros
intermedios preparada para llevarla a cualquier sistema y lanzar CMake.
Como primer ejemplo vamos a crear un ejecutable con el fichero main_todo1.cpp (el mismo con el
que comenzamos este guión de prácticas). Para ello nos ubicaremos en la carpeta en donde
vamos a guardar todas las prácticas y crearemos la siguiente estructura:
├── pr01-src
│ ├── CMakeLists.txt
│ └── main_todo1.cpp
└── pr01-build
Hemos creado las carpetas pr01-src (en ella pondremos los ficheros fuente del proyecto) y pr01-
build (para generar los resultados). A continuación hemos copiado el fichero main_todo1.cpp y
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 36/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
hemos creado un fichero CMakeLists.txt con el siguiente contenido:
# Mi primer CMakeLists.txt
cmake_minimum_required(VERSION 3.12)
project(MiPractica VERSION 0.1.0 LANGUAGES CXX)
# Algunas opciones para el compilador
set (CMAKE_CXX_STANDARD 17)
set (CMAKE_CXX_FLAGS "-Wall -Wextra -Wpedantic")
# A partir de aquí especificamos las dependencias
add_executable(main_todo1 main_todo1.cpp)
Los elementos que aparecen son estos:
• Se pueden escribir comentarios precedidos del símbolo hashtag.
• cmake_minimum_required() indica la versión mímina de CMake que se necesita. Aquí cabe decir
que mucha gente habla de “Modern CMake” refiriéndose a las versiones de CMake a partir
de las cuales se soportan una serie de características que le dan bastante versatilidad.
Aunque puede variar según la fuente consultada, un valor adecuado es 3.12.
• project() indica, en este orden:
◦ El nombre que le queremos dar al proyecto. No tiene porqué coincidir ni con el nombre
de los subdirectorios ni con el de ningún fichero en particular.
◦ La versión que le estamos dando a nuestro proyecto (opcional).
◦ Los lenguajes que vamos a utilizar. Es opcional y si se omite asume C y CXX por defecto.
◦ Se pueden añadir otros elementos como una descripción, una URL, etc.
• set(CMAKE_CXX_STANDARD 17) asigna un valor a la variable CMAKE_CXX_STANDARD que se utiliza para
compilar utilizando un determinado estándar de C++. Se pueden usar otros estándares
como C++98, C++11, C++23, etc.
• set(CMAKE_CXX_FLAGS …) asigna un valor a una variable CMAKE_CXX_FLAGS que contiene las
opciones que se le van a pasar al compilador (tanto para compilar como para enlazar).
• add_executable(main_todo1 main_todo1.cpp) le indica a CMake que debe generar reglas para
crear un ejecutable. El primer argumento es el nombre del ejecutable y después podemos
poner uno o varios ficheros fuente, que son los que hay que compilar para obtener el
ejecutable.
En relación con lo visto anteriormente puede observar que:
• No utilizamos la opción -g de g++. CMake ya sabe si hay que generar o no información
para el depurador dependiendo de si estamos generando la versión debug o release.
• En lugar de usar el flag -std=c++17 se utiliza la variable estándar de CMake con el mismo
efecto.
En este punto ya estamos en condiciones de compilar y, para ello, lo primero es ejecutar CMake y
decirle en qué carpeta están los fuentes que queremos compilar y en qué carpeta queremos poner
el resultado. Lo haremos con la sintaxis siguiente:
prompt> cmake -S source_dir -B build_dir
Se puede ejecutar desde cualquier carpeta teniendo cuidado de poner correctamente las rutas
(relativas o absolutas). Nosotros lo haremos desde la carpeta en la que tenemos los fuentes:
prompt%~/MP/pr01-src> cmake -S . -B ../pr01-build
-- The CXX compiler identification is GNU 13.2.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 37/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
-- Configuring done (0.4s)
-- Generating done (0.0s)
-- Build files have been written to: /home/jbaena/MP/pr01-build
Si miramos ahora el contenido de ~/MP/pr01-build veremos que hay varios ficheros y directorios:
prompt%~/MP/pr01-build> tree -L 1
├── CMakeCache.txt
├── CMakeFiles
├── cmake_install.cmake
└── Makefile
Entre ellos un Makefile, por lo que podríamos ejecutar GNU Make y se construiría nuestro proyecto:
prompt%~/MP/pr01-build> make
[ 50%] Building CXX object CMakeFiles/main_todo1.dir/main_todo1.cpp.o
[100%] Linking CXX executable main_todo1
[100%] Built target main_todo1
Sin embargo, en lugar de llamar a make llamaremos a cmake de esta forma:
prompt%~/MP/pr01-build> cmake --build .
[ 50%] Building CXX object CMakeFiles/main_todo1.dir/main_todo1.cpp.o
[100%] Linking CXX executable main_todo1
[100%] Built target main_todo1
El resultado es el mismo. La opción --build le dice a CMake lo que queremos hacer (hay otras
acciones disponibles) y . le dice en qué directorio se van a construir los binarios. Esta será la
forma con la que trabajaremos en este guión. Recuerde que CMake ha creado un fichero Makefile
en este caso pero podría haber creado ficheros para otros sistemas (Ninja, etc.) y, por tanto, la
orden make no funcionaría.
Puede comprobar que se han generado los ficheros objeto y ejecutables (resaltados), entre otros
muchos:
prompt%~/MP/pr01-build> tree
├── CMakeCache.txt
├── CMakeFiles
│ ├── 3.28.3
│ │ ├── CMakeCXXCompiler.cmake
│ │ ├── CMakeDetermineCompilerABI_CXX.bin
│ │ ├── CMakeSystem.cmake
│ │ └── CompilerIdCXX
│ │ ├── a.out
│ │ ├── CMakeCXXCompilerId.cpp
│ │ └── tmp
│ ├── cmake.check_cache
│ ├── CMakeConfigureLog.yaml
│ ├── CMakeDirectoryInformation.cmake
│ ├── CMakeScratch
│ ├── main_todo1.dir
│ │ ├── build.make
│ │ ├── cmake_clean.cmake
│ │ ├── compiler_depend.make
│ │ ├── compiler_depend.ts
│ │ ├── DependInfo.cmake
│ │ ├── depend.make
│ │ ├── flags.make
│ │ ├── link.txt
│ │ ├── main_todo1.cpp.o
│ │ ├── main_todo1.cpp.o.d
│ │ └── progress.make
│ ├── Makefile2
│ ├── Makefile.cmake
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 38/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
│ ├── pkgRedirects
│ ├── progress.marks
│ └── TargetDirectories.txt
├── cmake_install.cmake
├── main_todo1
└── Makefile
ᐒ Ejercicio 15 : Compilando el proyecto modularizado
Cree una nueva pareja de carpetas pr02-src y pr02-build en su directorio de trabajo. Copie en la
carpeta de fuentes los ficheros: punto.h, punto.cpp, rectangulo.h, formas.h, rectangulo.cpp, auxiliar.h,
auxiliar.cpp, main1.cpp y main2.cpp. A continuación cree un fichero CMakeLists.txt e incluya
instrucciones para crear los dos ejecutables. Note que bastaría con escribir dos instrucciones de
tipo add_executable con la lista de ficheros que componen cada ejecutable.
Ejecute CMake y cree los ejecutables. A continuación, modifique alguno de los ficheros fuente y
ejecute de nuevo CMake para verificar que solo se reconstruye aquello que depende de los
ficheros modificados.
Flujo de trabajo con CMake
Resumiendo, para trabajar con proyectos CMake hemos de:
• Cuando creamos el proyecto:
1. Crear un directorio para los ficheros fuente:
prompt%~MP> mkdir src
2. Crear un CMakeLists.txt dentro de src
3. Crear un directorio para el resultado de la compilación a partir de src:
promtp%~MP/src> cmake -S . -B ../build
• Cada vez que editemos los fuentes:
1. Construir de nuevo el proyecto:
prompt%~MP/build> cmake --build .
Reconstrucción del proyecto con CMake
En ocasiones podría necesitar recompilar el proyecto completo independientemente de que ya se
hubiesen generado previamente ficheros intermedios (códigos objeto) o resultados finales
(ejecutables y bibliotecas). Para ello tiene dos opciones:
• Limpiar ficheros intermedios (objeto) y resultantes (ejecutables o bibliotecas):
cmake --build . --target clean
• Limpiar ficheros y, a continuación, reconstruir:
cmake --build . --clean-first
Tenga en cuenta que estas opciones no eliminan los ficheros intermedios de CMake (por ejemplo
CMakeCache.txt) por lo que si necesitase reconstruirlos también lo más sencillo es borrar el build
directory y generarlo de nuevo o, alternativamente, ejecutar:
prompt> cmake -S source_dir -B build_dir --fresh
7.3. Creación de bibliotecas con CMake
En el ejercicio previo, al repetir los ficheros fuente en cada add_executable, CMake genera reglas
que hacen que estos se compilen dos veces (una para cada ejecutable):
prompt%~/MP/pr02-build> cmake --build .
[ 10%] Building CXX object CMakeFiles/main1.dir/main1.cpp.o
[ 20%] Building CXX object CMakeFiles/main1.dir/auxiliar.cpp.o
[ 30%] Building CXX object CMakeFiles/main1.dir/punto.cpp.o
[ 40%] Building CXX object CMakeFiles/main1.dir/rectangulo.cpp.o
[ 50%] Linking CXX executable main1
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 39/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
[ 50%] Built target main1
[ 60%] Building CXX object CMakeFiles/main2.dir/main2.cpp.o
[ 70%] Building CXX object CMakeFiles/main2.dir/auxiliar.cpp.o
[ 80%] Building CXX object CMakeFiles/main2.dir/punto.cpp.o
[ 90%] Building CXX object CMakeFiles/main2.dir/rectangulo.cpp.o
[100%] Linking CXX executable main2
[100%] Built target main2
Para evitar esta doble compilación (innecesaria) vamos a repetir el esquema de ejercicios
anteriores y crear una biblioteca que luego se enlazará con los códigos objeto de ambos
ejecutables. Usaremos dos nuevas instrucciones de CMake:
• add_library(nombrebib lista_ficheros_fuente) Esta crea una biblioteca llamada nombrebib a
partir de los ficheros de código fuente.
• target_link_libraries(objetivo nombrebib) Esta genera una dependencia entre el objetivo a
construir y la biblioteca.
De esta forma, el fichero CMakeLists.txt quedaría así:
cmake_minimum_required(VERSION 3.12)
project(MiPractica VERSION 0.1.0 LANGUAGES CXX)
set (CMAKE_CXX_STANDARD 17)
set (CMAKE_CXX_FLAGS "-Wall -Wextra -Wpedantic")
add_library(formas auxiliar.cpp punto.cpp rectangulo.cpp)
add_executable(main1 main1.cpp)
target_link_libraries(main1 formas)
add_executable(main2 main2.cpp)
target_link_libraries(main2 formas)
Observe que ahora:
• Los códigos fuente de nuestros TDA se van a compilar y van a crear la biblioteca formas.
• Para crear cada ejecutable solo hay que compilar main1.cpp o main2.cpp.
• Añadimos una dependencia para enlazar cada ejecutable con la biblioteca formas.
El resultado de ejecutar CMake sería este:
prompt%~/MP/pr02-build> cmake --build .
[ 12%] Building CXX object CMakeFiles/formas.dir/auxiliar.cpp.o
[ 25%] Building CXX object CMakeFiles/formas.dir/punto.cpp.o
[ 37%] Building CXX object CMakeFiles/formas.dir/rectangulo.cpp.o
[ 50%] Linking CXX static library libformas.a
[ 50%] Built target formas
[ 62%] Building CXX object CMakeFiles/main1.dir/main1.cpp.o
[ 75%] Linking CXX executable main1
[ 75%] Built target main1
[ 87%] Building CXX object CMakeFiles/main2.dir/main2.cpp.o
[100%] Linking CXX executable main2
[100%] Built target main2
7.4. Trabajando con múltiples directorios
Hasta el momento tenemos todos los ficheros de código fuente en una misma carpeta: los de la
biblioteca y los distintos ejecutables. Cuando los proyectos son grandes y, particularmente,
cuando tienen bibliotecas, lo habitual es usar distintos directorios para tener mejor organizado el
código. En nuestro ejemplo vamos a crear un nuevo proyecto ( pr03-src) con dos subdirectorios:
uno para los fuentes de la biblioteca y otro para los main que hemos creado a modo de ejemplo de
uso de la biblioteca.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 40/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
prompt%~/MP/pr03-src> tree
├── CMakeLists.txt
├── ejemplos
│ ├── main1.cpp
│ └── main2.cpp
└── lib
├── auxiliar.cpp
├── auxiliar.h
├── formas.h
├── punto.cpp
├── punto.h
├── rectangulo.cpp
└── rectangulo.h
El contenido de CMakeLists.txt sería este:
cmake_minimum_required(VERSION 3.12)
project(MiPractica VERSION 0.1.0 LANGUAGES CXX)
set (CMAKE_CXX_STANDARD 17)
set (CMAKE_CXX_FLAGS "-Wall -Wextra -Wpedantic")
set(FORMAS_DIR lib)
set(EJEMPLOS_DIR ejemplos)
add_library(formas ${FORMAS_DIR}/auxiliar.cpp ${FORMAS_DIR}/punto.cpp
${FORMAS_DIR}/rectangulo.cpp)
include_directories(${FORMAS_DIR})
add_executable(main1 ${EJEMPLOS_DIR}/main1.cpp)
target_link_libraries(main1 formas)
add_executable(main2 ${EJEMPLOS_DIR}/main2.cpp)
target_link_libraries(main2 formas)
Los cambios respecto a la versión previa son:
• Se ha creado una variable FORMAS_DIR para indicar el directorio en donde están los fuentes
de la biblioteca.
• Se ha creado una variable EJEMPLOS_DIR para indicar el directorio en donde estarán las
aplicaciones de prueba.
• Se han utilizado sendas variables allí donde se utilizaba cada fichero fuente.
• Cuando se compila main1.cpp (o main2.cpp) y se haga un #include de punto.h o rectangulo.h el
compilador buscará dichos ficheros en la misma carpeta en donde está main1.cpp. Con
include_directories() le decimos a CMake que, además de en la carpeta del fichero
compilado, busque ficheros de cabecera también en otros directorios. Lo que hace CMake
es incluir esos directorios en la opción -I del compilador g++.
Modularización de los ficheros CMakeLists.txt
Con esta estructura tenemos mejor organizados los ficheros pero podríamos incluir otras mejoras.
El fichero CMakeLists.txt contiene instrucciones para construir el proyecto completo pero
podríamos dividirlo y crear uno en cada subdirectorio de la siguiente forma:
prompt%~/MP/pr03-src> tree
├── CMakeLists.txt
├── ejemplos
│ ├── CMakeLists.txt
│ ├── main1.cpp
│ └── main2.cpp
└── lib
├── auxiliar.cpp
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 41/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
├── auxiliar.h
├── CMakeLists.txt
├── formas.h
├── libformas.a
├── punto.cpp
├── punto.h
├── rectangulo.cpp
└── rectangulo.h
CMakeLists.txt ejemplos/CMakeLists.txt lib/CMakeLists.txt
cmake_minimum_required(VERSION 3.12) add_executable(main1 main1.cpp) add_library(formas auxiliar.cpp
project(MiPractica VERSION 0.1.0 target_link_libraries(main1 formas) punto.cpp rectangulo.cpp)
LANGUAGES CXX)
set (CMAKE_CXX_STANDARD 17) add_executable(main2 main2.cpp) target_include_directories(
set (CMAKE_CXX_FLAGS target_link_libraries(main2 formas) formas PUBLIC .)
"-Wall -Wextra -Wpedantic")
add_subdirectory(lib)
add_subdirectory(ejemplos)
Aparecen dos elementos nuevos en estos ficheros:
• add_subdirectory() lo que hace es incluir el fichero CMakeLists.txt del subdirectorio indicado.
• target_include_directories(objetivo PUBLIC directorio) esta instrucción la hemos puesto en
sustitución de include_directories(). Es más específica y sirve también para indicar los
directorios en donde se han de buscar los ficheros de cabecera. La diferencia estriba en:
◦ Acota los objetivos sobre los que se aplica. Mientras que la anterior era global para
cualquier proceso de compilación, ahora acotamos el ámbito a un determinado objetivo
(formas en nuestro ejemplo).
◦ Usamos una palabra clave PUBLIC para decir que, cualquiera que haga uso de esta
biblioteca formas, también deberá asumir esta ruta de búsqueda para los ficheros de
cabecera. Si hubiésemos utilizado include_directories() esta propiedad no se propagaría
a proyectos externos y deberíamos indicar en ellos donde localizar los ficheros de
cabecera.
Se ha puesto como ruta de búsqueda el directorio actual (en el que está situado ese fichero
CMakeLists.txt).
Vemos como, en realidad, seguimos teniendo un único proyecto: solo hay un CMakeLists.txt
completo, los otros dos no son autocontenidos sino que están pensados para ser incluidos desde el
principal.
ᐒ Ejercicio 16 : Uso de múltiples ficheros CMakeLists.txt
Haga las modificaciones propuestas en esta sección en el proyecto y compruebe que todo sigue
funcionando bien.
7.5. Trabajando con múltiples proyectos
Recuerde que uno de los objetivos que se persiguen cuando creamos bibliotecas es poder crear
nuevas aplicaciones que hagan uso de ellas como proyectos independientes reutilizando su
código. En este punto debería tener una carpeta pr03-src con la biblioteca formas y sus aplicaciones
de ejemplo. Vamos a crear un proyecto nuevo (e independiente) en otro directorio. Dicho proyecto
contendrá un fichero main.cpp con una nueva función main() y el objetivo será enlazarlo con la
biblioteca del ya existente (formas en pr03-src):
prompt%~/MP> tree
├── app
│ ├── CMakeLists.txt
│ └── main.cpp
├── app-build
│ └── ...
├── pr03-build
│ └── ...
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 42/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
└── pr03-src
└── ...
Para indicarle a la nueva aplicación (app) que utilice la biblioteca formas, su CMakeLists.txt tendría el
siguiente contenido:
cmake_minimum_required(VERSION 3.12)
project(Aplicacion VERSION 0.1.0 LANGUAGES CXX)
set (CMAKE_CXX_STANDARD 17)
set (CMAKE_CXX_FLAGS "-Wall -Wextra -Wpedantic")
add_subdirectory(../pr03-src buildlib)
add_executable(main main.cpp)
target_link_libraries(main formas)
Los cambios son:
• Hemos modificado el nombre del proyecto (Aplicacion).
• Hemos incluido una nueva instrucción add_subdirectory(directorio nuevobuild) que sirve para
indicarle a nuestro proyecto que incluya otro (ambos autocontenidos e independientes).
Con directorio decimos dónde están ubicados los fuentes de ese otro proyecto y con
nuevobuild le decimos dónde debe crear el build directory para ese otro proyecto. Note que
estamos prescindiendo de pr03-build, es decir, que el proyecto app se va a construir a partir
de los fuentes de formas (independientemente de si ya habíamos construido formas o no).
Este nuevo proyecto construye de nuevo formas dentro de su build directory. Dentro de app-
build se creará una carpeta buildlib que será el build directory de formas.
Así, cuando hagamos cualquier modificación a la biblioteca formas y construyamos la aplicación
app, esta volverá a compilar formas para tener en cuenta las modificaciones. Y todo de forma
automática sin que tengamos que estar pendientes.
ᐒ Ejercicio 17 : Nueva aplicación
Cree una nueva aplicación tal y como se explica en la sección y haga que esta funcione usando el
proyecto pr03-src tal y como se ha explicado. Puede crear una función main() totalmente nueva o,
simplemente, puede copiar main1.cpp o main2.cpp para hacer la prueba.
7.6. Trabajando con proyectos binarios de terceros
Vamos a plantear un último escenario de trabajo. Suponga el caso de que alguien (un tercero) ha
desarrollado la biblioteca formas y que la ha distribuido en su formato binario (ya compilado, sin el
código fuente). Concretamente nos ha enviado únicamente los ficheros necesarios para compilar y
enlazar nuevas aplicaciones:
prompt%~/MP> tree formas
formas
├── CMakeLists.txt
├── ejemplos
│ ├── main1.cpp
│ └── main2.cpp
├── include
│ ├── formas.h
│ ├── punto.h
│ └── rectangulo.h
└── lib
└── libformas.a
Observe que disponemos de:
• Ficheros de cabecera para poder incluirlos en nuestras aplicaciones y compilarlas. No es
necesario aportar auxiliar.h puesto que este era utilizado internamente por rectangulo.cpp y
no es una dependencia para compilar aplicaciones externas.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 43/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
• Fichero binario libformas.a para enlazar con él.
• Ficheros con aplicaciones de ejemplo (main1.cpp y main2.cpp).
Además, quien haya desarrollado esa biblioteca también nos ha suministrado el siguiente
CMakeLists.txt (podríamos haberlo hecho nosotros):
add_library(formas STATIC IMPORTED)
set_target_properties(formas PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_LIST_DIR}/include"
IMPORTED_LOCATION "${CMAKE_CURRENT_LIST_DIR}/lib/libformas.a"
)
Este fichero lo que hace es:
• add_library(formas STATIC IMPORTED) definir un target llamado formas, que es una biblioteca
importada y de tipo estático. Esto quiere decir que no disponemos de los fuentes sino que
solo tenemos los binarios.
• set_target_properties() define algunas propiedades necesarias para usar la biblioteca:
dónde están los ficheros de cabecera y cuál es el fichero de la misma. Gracias a esto,
cuando en otros proyectos se defina una dependencia de esta biblioteca CMake ya sabrá
como trabajar con ella. La variable ${CMAKE_CURRENT_LIST_DIR} contiene la ruta del fichero
CMakeLists.txt actual.
Por otra parte tendremos nuestra nueva aplicación app2 con los siguientes ficheros:
prompt%~/MP> tree app2
app2
├── CMakeLists.txt
└── main.cpp
El fichero CMakeLists.txt tendrá este contenido:
cmake_minimum_required(VERSION 3.12)
project(Aplicacion2 VERSION 0.1.0 LANGUAGES CXX)
set (CMAKE_CXX_STANDARD 17)
set (CMAKE_CXX_FLAGS "-Wall -Wextra -Wpedantic")
include(../formas/CMakeLists.txt)
add_executable(main main.cpp)
target_link_libraries(main formas)
Con include() hemos definido la información necesaria para trabajar con formas y con
target_link_libraries() establecemos todas las dependencias necesarias entre app2 y formas.
ᐒ Ejercicio 18 : Uso de binarios de terceros
Cree el proyecto formas tal y como se explica en la sección. A continuación cree una nueva
aplicación tal y como se explica y haga que esta funcione. Puede crear una función main()
totalmente nueva o, simplemente, puede copiar main1.cpp o main2.cpp para hacer la prueba.
7.7. Otras funcionalidades de CMake
Para cambiar la compilación entre los modos Debug o Release debemos generar el build directory de
esta forma:
• cmake -S . -B build/ -D CMAKE_BUILD_TYPE=Debug --fresh
• cmake -S . -B build/ -D CMAKE_BUILD_TYPE=Release --fresh
Tenga en cuenta que cada vez que cambiamos de uno a otro modo estamos eliminando los
códigos generados con anterioridad (por usar la opción -- fresh).
CMake admite trabajar con diferentes versiones del software de forma simultánea aunque ouede
depender del build generator que esté utilizando. Por ejemplo, GNU Make no admite este tipo de
configuraciones mientras que Ninja sí.
Para indicar la versión que queremos compilar ejecutaremos:
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 44/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
• cmake --build . --config Debug
• cmake --build . --config Release
En esta asignatura siempre trabajaremos en modo Debug.
La herramienta gráfica cmake-gui, una vez creado el build directory, se puede utilizar para editar el
fichero de caché (CMakeCache.txt). No es posible utilizarla para crear un fichero CMakeLists.txt desde
cero.
7.8. Otras opciones de configuración de CMake
Otras variables de interés
A continuación tiene algunas variables que pueden resultar de utilidad en la configuración de
CMake:
• SET(CMAKE_EXE_LINKER_FLAGS "...")
Permite indicar opciones que se le pasan al enlazador.
• SET(CMAKE_VERBOSE_MAKEFILE ON)
Muestra los comandos que ejecuta GNU Make. Con ello podemos ver cómo se llama al
compilador, enlazador u otras herramientas. Puede ser útil para asegurarnos de que se
están pasando los argumentos correctamente.
Añadir elementos a una variable
En ocasiones es de utilidad poder añadir valores a una variable ya existente. Para ello usamos el
comando SET de la siguiente forma:
SET(VAR "${VAR} nuevos_elementos")
En el siguiente ejemplo definimos una variable y a continuación añadimos nuevos elementos a la
misma:
SET(CMAKE_CXX_FLAGS "-Wall")
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wextra -Wpedantic")
7.9. El módulo CPack
CMake incluye una herramienta para generar paquetes (packages) distribuibles del software
construido llamada CPack. Esta permite crear un fichero que incluye todo lo necesario para instalar
el software en ordenadores de terceros. Por ejemplo, podemos crear un fichero .deb para su
distribución en sistemas GNU/Linux o un fichero .dmg para distribuirlo en sistemas MacOS.
También permite crear un paquete con los fuentes del proyecto. Esto es de interés en la asignatura
para poder hacer las entregas de prácticas. Para ello debemos añadir las siguientes líneas a
nuestro CMakeLists.txt:
set(CPACK_SOURCE_GENERATOR ZIP)
include(CPack)
Se pueden indicar distintos formatos de compresión pero nosotros hemos elegido ZIP. Para crear
un fichero .zip con nuestro source directory simplemente hay que ejecutar:
cd builddirectory
cmake --build . --target package_source
Esto genera, entre otras cosas, un fichero llamado NombreProyecto-Version-Source.zip en su build
directory. Asegúrese de tener bien configurado su proyecto para generar el build directory fuera
del source directory. De no ser así estaría incluyendo también los binarios y todos los ficheros
intermedios del proceso de compilación en el fichero .zip que se acaba de crear.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 45/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
8. El IDE Visual Studio Code
Puede encontrar dos perfiles de desarrolladores de software en base al toolchain utilizado o, al
menos, en base a la herramienta de front-end que utilizan para el desarrollo. Por una parte hay
programadores que utilizan herramientas en línea de órdenes como las que hemos visto ( g++, make,
cmake, etc.) y que usan para escribir el software un simple editor de texto (emacs, vi, notepad++,
sublime, etc.). Por otra, los hay que prefieren utilizar un IDE (Integrated Development
Environment), que es un software que incluye un editor de texto junto con todas las herramientas
necesarias para invocar al resto de herramientas del toolchain. Para el caso de C/C++ hay gran
variedad de IDE y no siempre es fácil decantarse por uno u otro. Debería consultar características
de cada uno tales como:
• Compiladores que utiliza (GNU g++, Clang, MSVC, etc.).
• Integración con sistemas de construcción de software (GNU Make, Ninja, CMake, etc.).
• Detección de errores y análisis sintáctico en tiempo real. Esta característica hace que
conforme escribimos los programas nos avise de errores que tradicionalmente se han
detectado en la etapa de compilación o enlazado y, por tanto, permite acelerar el proceso
de desarrollo.
• Si tiene o no herramientas de refactoring. Estas permiten hacer modificaciones globales en
todo el software de forma simple (por ejemplo, cambiar el nombre de una clase o de una
función en cualquier sitio donde aparezca).
• Si incluye herramientas de depuración.
• Si incluye herramientas de profiling para medir el rendimiento.
• Si incluye herramientas para el testeo automático de programas (tests de unidad, etc.)
• Si admite uno o más lenguajes de programación y su uso conjunto.
• Si admite herramientas de control de versiones.
• Si se integra con alguna herramienta de IA como, por ejemplo, Github Copilot.
• ...
En Wikipedia28 tiene una tabla comparativa bastante completa. En la asignatura de Fundamentos
de Programación ya habrá utilizado seguramente alguno de estos:
• DevC++. Comenzó a desarrollarse en 1998 y es solo para sistemas Ms Windows. Es
software libre. Se suele utilizar en entornos docentes de primeros cursos o por gente que
está comenzando a programar. El proyecto no está demasiado activo.
• Code::Blocks. También es código abierto y comenzó su desarrollo en 2005. La ventaja de
este es que es multiplataforma (Windows, Linux, MacOS) aunque sigue siendo
relativamente sencillo y con un perfil también de inicio en el mundo del desarrollo de
software. Tampoco tiene actualizaciones frecuentes.
Para esta asignatura vamos a cambiar a un entorno más profesional que permita manejar el
toolchain que hemos visto con anterioridad y, en particular, la herramienta CMake. Podríamos
destacar estos:
• CLion. Desarrollado por la empresa JetBrains, es una de las muchas herramientas que
implementa para desarrolladores. Es multiplataforma y se integra perfectamente con
CMake. Posiblemente es uno de los mejores que puede encontrar en la actualidad. Sin
embargo, no es software libre, aunque ofrece licencias gratuitas para estudiantes.
• Qt Creator. Software libre creado por la empresa The Qt Company también multiplataforma.
Forma parte de un conjunto mayor de herramientas de desarrollo (no gratuitas) que
inicialmente estaba pensado para el desarrollo sobre la biblioteca gráfica Qt. Permite
trabajar con proyectos CMake y Qmake (otro build system generator). Admite otros
lenguajes como JavaScript o Python. Quizá, injustamente, sea el menos conocido de los
aquí citados.
28 https://en.wikipedia.org/wiki/Comparison_of_integrated_development_environments
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 46/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
• Microsoft Visual Studio y Microsoft Visual Studio Code. Son dos entornos distintos: el
primero es comercial y solo para plataformas Ms Windows (y con mejores prestaciones) y el
segundo es código libre y multiplataforma. Aunque VSCode no es tan completo, admite la
instalación de extensiones para mejorar la funcionalidad (en su marketplace hay más de
30.000 disponibles).
• Xcode. Es un IDE propietario específico para sistemas MacOS de Apple por lo que no es
multiplataforma. Queda fuera del alcance de esta asignatura pero sería una opción a
valorar si se trabaja en ese entorno.
Todos son profesionales pero teniendo en cuenta el tipo de licencia, que sea
multiplataforma y la popularidad, en esta asignatura nos decantaremos por VS Code.
Sin embargo, si prueba los demás, verá que la experiencia de usuario incluso podría
mejorar porque hay una mejor integración de todas las herramientas de desarrollo. En
The State of the C++ developer ecosystem29, la empresa JetBrains (desarrolladora de
CLion) muestra el resultado de una encuesta sobre el uso de IDE y otras cuestiones de interés.
Además, como VSCode permite desarrollar en multitud de lenguajes podría ser de utilidad en otras
asignaturas.
Para ser más exactos, podríamos decir que VSCode es un editor avanzado para programadores
más que un IDE ya que gran parte de las herramientas de desarrollo se instalan como extensiones
(plugins) dependiendo de las necesidades de cada cual. Por contra, los IDE ya traen integradas de
serie esas herramientas de uso habitual (depuración, integración con el compilador, etc.). En
cualquier caso, una vez instaladas las extensiones, en ambos casos tenemos un sistema de
desarrollo completo.
8.1. Instalación
Para la instalación se recomienda visitar la página web de VSCode 30 y seguir las instrucciones.
Basta con descargar el paquete según el sistema operativo que tenga y proceder a su ejecución.
En sistemas GNU/Linux como Ubuntu podemos hacer la instalación desde repositorios accesibles
por el gestor de paquetes del S.O.31.
Aunque la interfaz es muy configurable, por defecto tiene un aspecto similar al de la siguiente
figura:
29 https://www.infoworld.com/article/2335793/the-state-of-the-c-plus-plus-developer-ecosystem.html
30 https://code.visualstudio.com/
31 https://www.liberiangeek.net/2024/04/install-visual-studio-code-ubuntu-24-04/
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 47/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
Los elementos más significativos se han marcado con una etiqueta:
• Barra de actividad. Permite acceder a diversas herramientas tales como un explorador de
archivos, herramienta de búsqueda y sustitución, gestor de extensiones, configuración del
entorno, etc. Al instalar nuevas extensiones pueden aparecer nuevos ítems en esta barra.
• Barra lateral primaria. Contiene la información relativa a la actividad seleccionada en cada
momento. En la captura previa se puede ver el explorador de archivos (y puede ver
resaltada dicha actividad, se trata del primer icono de la barra de actividad).
• Editor. Es la parte principal y puede tener diversas configuraciones con múltiples editores.
En la parte superior tiene una barra de pestañas con los ficheros abiertos y también puede
tener algunos botones para acciones rápidas que pueden variar.
• Panel. En esta parte verá el resultado de la ejecución de sus programas, del proceso de
compilación o de depuración e incluso podrá abrir terminales para trabajar desde la línea
de órdenes.
• Barra de estado. Muestra información útil. Algunas extensiones incluyen desplegables y
botones para trabajar más cómodamente.
Una vez instalado se puede usar como un simple editor de texto (más o menos sofisticado) pero es
necesario instalar algunas extensiones para poder desarrollar programas en C++. Concretamente
deberá instalar las siguientes:
• C/C++ Extension Pack32 (Microsoft). Esto es un bundle
(paquete) que contiene varias extensiones a su vez
(C/C++33, C/C++ themes34, CMake35, CMake tools36). Con
esta extensión tendrá todo lo necesario para empezar a
trabajar ya que incluye:
◦ IntelliSense. Es una herramienta para el coloreo
sintáctico, detección de errores en tiempo real y
autocompletado.
◦ Soporte para el uso de CMake. Incluye coloreo de
sintaxis de ficheros de configuración de CMake,
autocompletado, snippets, etc. así como utilidades
que permiten manejar CMake desde la UI.
• Build Output Colorizer37 (Steve Bush Research). Esta
extensión sirve para que la salida de CMake (y de g++)
se coloree y sea más fácil identificar los errores o avisos.
No es imprescindible pero sí recomendable. Puede haber
otras similares en el marketplace como, por ejemplo IBM
Output Colorizer38.
Una alternativa a IntelliSense es clangd39. Aunque es parte del
toolchain de LLVM (que es una alternativa al GNU toolchain) se
puede utilizar de forma independiente como sustituto de
IntelliSense. En general parece funcionar más rápido y los
mensajes que aporta son más descriptivos. Si se instala, le
aparecerá una ventana pidiendo que desactive IntelliSense
(ambos a la vez no pueden funcionar). Si deseamos desinstalarlo y activar de nuevo IntelliSense
devemos hacerlo en Archivo → Configuración → “C_Cpp: Intelli Sense Engine”.
Como ve, VS Code dispone de multitud de extensiones para ampliar su funcionalidad y adaptarlo
al máximo a nuestras necesidades.
32 https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools-extension-pack
33 https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools
34 https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools-themes
35 https://marketplace.visualstudio.com/items?itemName=twxs.cmake
36 https://marketplace.visualstudio.com/items?itemName=ms-vscode.cmake-tools
37 https://marketplace.visualstudio.com/items?itemName=SteveBushResearch.BuildOutputColorizer
38 https://marketplace.visualstudio.com/items?itemName=IBM.output-colorizer
39 https://marketplace.visualstudio.com/items?itemName=llvm-vs-code-extensions.vscode-clangd
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 48/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
8.1.1. Instalación en Windows
Instalación y configuración con MSYS
Si está usando Windows con MSYS puede que con la configuración por defecto de CMake Tools no
encuentre el ejecutable de CMake. Para resolverlo tiene dos opciones:
1. Añadir la ruta completa de cmake.exe a la configuración. En “ File → Preferences → Settings”
busque la opción “Cmake: Cmake Path” y escriba la ruta completa al comando CMake que, si
ha hecho la instalación por defecto, sería esta: “c:\msys64\ucrt64\bin\cmake.exe”.
2. Añadir el path de todas las herramientas de MSYS al PATH del sistema. Para ello acceda al
“Panel de control” y busque “Variables de entorno”. Edite las variables de entorno del
sistema y localice la variable PATH. Añada la ruta “ c:\msys64\ucrt64\bin” (o aquella en la que
se hayan instalado los ejecutables).
Instalación y configuración con WSL
Si está usando Windows con WSL
el propio VSCode lo detecta y
pregunta si deseamos instalar
una extensión llamada WSL
(desarrollada por Microsoft):
dígale que sí. Con esa extensión
su aplicación VSCode ya instalada
en Windows se integra
perfectamente con el subsitema
GNU/Linux instalado en WSL40.
Como ya se dijo antes, con WSL
tiene dos máquinas: una Windows
y otra GNU/Linux. La nueva extensión lo que hace es instalar una aplicación VSCode-server en la
máquina GNU/Linux. De esta forma, cuando ejecute VSCode en Windows, este estará conectado
de forma permanente con el servidor de la máquina GNU/Linux y cualquier acción que realice
(compilación, depuración, ejecución, etc.) se realizará sobre la máquina GNU/Linux. Todo esto es
transparente al usuario una vez establecida la conexión entre ambas máquinas de forma que la
sensación es de que todo se está ejecutando en su máquina Windows (aunque no sea
exactamente así).
Una vez instalada la extensión debe prestar atención al icono de la izquierda de la barra de
estado. Si no se ha establecido conexión con el servidor de la máquina GNU/Linux verá que dicho
icono es . Si es el caso debe pulsarlo. Aparecerá un
desplegable como el de la figura. Elija “Connect to WSL” y
verá que el icono cambia a durante unos
segundos y, si todo funciona bien, acabará estableciéndose
como o similar. A partir de ese momento
está conectado a su máquina GNU/Linux y puede trabajar
con normalidad.
Observe que las extensiones instaladas en la máquina remota (GNU/Linux) pueden ser distintas a
las de la máquina local (Windows). De hecho, la primera vez que se conecte verá que en la
máquina remota no hay ninguna extensión: debe instalar en ella, de nuevo, C/C++ Extension Pack
y Build Output Colorizer.
8.2. ¡Hola, mundo! con VSCode
Para trabajar con esta y sucesivas prácticas vamos a crear un directorio de trabajo para escribir
nuestros códigos fuente. En adelante, en este guión llamaremos a ese directorio WORKDIR. Por
ejemplo, si vamos a guardar las prácticas de esta asignatura en ~/Documentos/MP/practicas, ese
sería nuestro WORKDIR.
40 https://code.visualstudio.com/docs/remote/wsl
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 49/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
Podemos verificar que todo está instalado correctamente creando el tradicional “¡Hola, mundo!”.
Para ello siga los siguientes pasos41:
1. Archivo → Nuevo archivo de texto (o pulse el botón marcado en la figura).
2. Archivo → Guardar como … y almacénelo en WORKDIR/hola/hola.cpp
3. Redacte el programa ¡Hola, mundo!
4. Ejecútelo pulsando el botón de la esquina superior derecha. Este botón será visible si
tiene instalado el C/C++ Extension Pack y si tiene abierto en el editor un fichero con
extensión .c o .cpp.
5. Si le aparece un desplegable con título “Seleccione una configuración de depuración” (o
parecido) con una lista de los compiladores detectados: elija g++ (o clang).
Si todo ha ido bien verá en el panel (ventana inferior) el resultado de compilar y ejecutar. Si hay
errores de sintaxis también podrá verlos en esa zona. Según haya resultado con errores o no podrá
tener varias vistas como las de la siguiente figura:
Este flujo de trabajo solo se puede hacer cuando tenemos un único fichero para compilar, que no
será nuestro caso. En caso de tener proyectos con más de un fichero hay que hacer algunos
ajustes en la configuración de VSCode.
8.3. Proyectos con VSCode
En esta sección vemos el flujo de trabajo que vamos a seguir cuando construyamos cada uno de
nuestros proyectos. Lo primero que cabe decir es que el concepto de proyecto como tal no existe
en VSCode. Aquí disponemos de los espacios de trabajo (workspaces) que no son más que una
colección de ficheros y/o carpetas que permanecen abiertas en una ventana de VSCode. Este
concepto permite tener configuraciones del entorno personalizadas para cada workspace.
En nuestro caso, un proyecto va a ser una colección de ficheros .h, .cpp, .a, etc. que van a servir
para construir uno o varios ejecutables y/o bibliotecas y que mantendremos dentro de una misma
carpeta. Como hemos visto anteriormente, necesitamos usar alguna herramienta que permita
construir los objetivos finales del proyecto. En nuestro caso será CMake por lo que dicha colección
41 https://code.visualstudio.com/docs/languages/cpp
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 50/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
incluirá un fichero CMakeLists.txt.
Por tanto, lo primero que hemos de hacer es abrir una carpeta
desde VSCode (Archivo → Abrir carpeta). Esto abrirá una nueva
ventana de VSCode en la que podemos empezar a trabajar
añadiendo ficheros, compilando, etc. En este ejemplo podemos
abrir la carpeta WORKDIR/hola que hemos creado en el ejemplo
anterior. Verá la barra primaria lateral similar a la de la figura de
la derecha. hola.DSYM y hola son una carpeta y un fichero que
fueron generados en la compilación de la sección anterior y no
nos interesan por lo que se pueden borrar. El único fichero que
es de utilidad es hola.cpp.
Lo siguiente es crear un fichero CMakeLists.txt y, una vez creado, el entorno lo detectará y
habilitará las herramientas para controlar CMake desde la barra de estado. Podemos optar por
añadir el fichero manualmente y escribirlo nosotros mismos o bien podemos hacer uso de la
extensión CMake tools que hemos instalado al comienzo de esta sección. Lo haremos con la
extensión.
VSCode dispone de una herramienta llamada paleta de comandos ( Ver → Paleta de comandos …) para
realizar multitud de tareas, muchas de ellas relacionadas con las extensiones instaladas. Desde
ahí buscaremos (o escribiremos) “CMake: inicio rápido” (CMake: quick start) y seguiremos los
siguientes pasos:
1. Asignar un nombre al proyecto. Es arbitrario y no tiene porqué coincidir ni con el nombre de
los ficheros ni con el de la carpeta. Se usará en la instrucción project() de CMake. Por
ejemplo: MiPrograma.
2. A continuación nos
pregunta si vamos a crear
un proyecto en C++ o en C.
Seleccionaremos C++.
3. Luego pregunta si vamos a
crear una biblioteca
(Library) o un ejecutable
(Executable).
Seleccionaremos
ejecutable.
4. La siguiente pregunta no es
de interés (compatibilidad
con CPack y CTest).
Dejamos sin marcar ambas
opciones.
5. La última pregunta nos pide
marcar aquellos destinos de
nuestro proyecto y
aparecerá una lista con los
ficheros de código fuente
que hay en la carpeta. En
nuestro caso solo está
hola.cpp: lo marcamos y
aceptamos.
6. Aun seguirá preguntando
alguna cosa más pero
podemos abandonar el proceso en este punto pulsando con el ratón sobre el editor.
Vemos como se ha creado un fichero CMakeLists.txt con el siguiente contenido:
cmake_minimum_required(VERSION 3.12)
project(MiPrograma VERSION 0.1.0 LANGUAGES C CXX)
add_executable(MiPrograma hola.cpp)
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 51/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
Además, verá en el panel varios mensajes generados por CMake. En el momento en que VSCode
detecta el fichero ejecuta CMake para crear el build directory a partir del source directory y eso es
lo que se muestra en dicho panel. Por defecto, la extensión CMake tools está configurada para que
cree dicho subdirectorio dentro del source directory (se llama build y aparece también en su
carpeta del proyecto). Como ya hemos visto no es la mejor opción, más adelante veremos como
cambiarlo.
Si observa la barra de estado apreciará que han aparecido varios ítems nuevos, que pueden variar
según la configuración de visibilidad de la extensión:
En todos los casos aparecen una serie de iconos con: ningún texto, un texto breve o un texto largo
según tengamos configurada la extensión. Se han marcado los tres que, por ahora, pueden ser de
mayor utilidad:
• Ejecuta el programa. Si no estaba compilado o se ha modificado el código fuente se
compila antes de ejecutarlo.
• Solo compila el programa, no lo ejecuta.
• Ejecuta el programa en modo depuración.
De ahora en adelante siempre ejecutaremos el programa desde la barra de estado y no
utilizaremos, en ningún caso, el icono de la esquina superior derecha. En la barra de pestañas del
panel haremos uso de las siguientes:
• Problemas. Es la que mejor información aporta cuando hay errores de compilación.
• Salida. Aquí veremos el resultado del proceso de compilación, enlazado, etc. (CMake, GNU
g++, etc.). Aunque se muestran los errores de compilación también, su lectura es algo más
complicada.
• Terminal. Es una terminal en la que VSCode ejecuta el programa y, por tanto, aquí es donde
vemos cómo se ejecuta (mensajes a cout y entradas desde cin).
Múltiples proyectos en una misma ventana de VSCode
Habrá observado que cada vez que abre una nueva carpeta se abre una nueva ventana de
VSCode. Cuando queremos trabajar, o simplemente tener accesibles, varios proyectos podemos
llegar a tener muchas ventanas abiertas. Podemos hacer que todos los proyectos se abran en una
misma ventana si en lugar de abrir nuevas carpetas lo que hacemos es “ Archivo → Agregar carpeta
al área de trabajo …”.
ᐒ Ejercicio 19 : Proyecto formas
Abra el proyecto de formas geométricas que vimos en la sección 7.4 (pr03-src) con VSCode y
pruébelo. Recuerde: “Archivo → Abrir carpeta ...” o bien “Archivo → Agregar carpeta al área de
trabajo …”.
En dicho proyecto se creaba una biblioteca y dos ejecutables por lo que tenemos varios objetivos
por construir. Por defecto se construyen todos (all) pero podríamos construir solo alguno de ellos.
Puede elegirlo si pulsa el botón correspondiente de la barra de estado:
En ese momento aparecerá un desplegable para que indique el objetivo que debe construir (ver
siguiente figura). Al haber dos ejecutables, cuando pulse el botón de ejecución también será
preguntado para saber cuál de los dos ha de ejecutar (ver figura siguiente).
En adelante no preguntará más pero podemos cambiarlo cuando queramos desde la barra de
estado, en la que aparecerá un desplegable para ello.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 52/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
8.4. La extensión CMake tools
Como ya hemos visto, esta extensión integra en la UI de VSCode los elementos necesarios para
invocar y configurar CMake. Además de lo visto, también aparece un icono en la barra de actividad
a través del cuál también podemos trabajar con CMake.
En el bloque “Estado del proyecto” podemos realizar tareas sobre los objetivos de CMake además
de otras acciones de configuración. Desde ahí podemos:
• Elegir el programa que queremos ejecutar.
• Ejecutar un programa.
• Compilar alguno de los objetivos (o todos).
• Etc.
En el bloque “Esquema del proyecto” se nos presentan los diferentes objetivos junto con los
ficheros de los que dependen y podemos hacer compilaciones o enlazados individualizados por si
tras hacer alguna modificación solo queremos reconstruir una parte del proyecto en vez de todo.
Configuración del build directory
Como vimos en secciones previas, la recomendación es que el build directory generado por CMake
esté fuera del source directory. Con la configuración por defecto esto no ocurre. Si entramos en la
configuración de VSCode y buscamos “Cmake: Build Directory” vemos que es una opción de
nuestra extensión y que, por defecto, vale “${workspaceFolder}/build”, es decir, que lo crea dentro
de la carpeta en la que estamos trabajando.
Debemos cambiarlo a otra ubicación. Si, por ejemplo, está realizando las prácticas en el directorio
~/Documentos/MP/practicas quizá sería buena idea tener otro directorio ~/Documentos/MP/build para
generar todos los ejecutables. De esta forma mantiene dos directorios completos independientes:
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 53/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
uno con todos los fuentes y otro con todos los ejecutables. En la nueva configuración deberá usar
esta ruta: ${userHome}/Documentos/MP/build/${workspaceFolderBasename}
Configuración del formato de la barra de estado
Por defecto, los ítems de la barra de estado tienen mucho texto y ocupan demasiado espacio. Para
modificar el aspecto de las herramientas de la barra de estado cambie, en la configuración, el ítem
“Cmake > Options: Status Bar Visibility”. Puede usar “Compact” o “Icon” para que ocupen menos.
Configuración con Windows y MSYS
Si está usando este sistema puede que haya alguna ruta que ajustar: por ejemplo la del ejecutable
de cmake. Busque la opción “Cmake: Cmake Path” y escriba la ruta completa al comando CMake. Si
ha hecho la instalación por defecto sería esta: “ c:\msys64\ucrt64\bin\cmake.exe”.
Más posibilidades de CMake Tools
Para ver el resto de operaciones que podemos realizar con CMake desde VS Code puede abrir el
menú “Ver → Paleta de comandos …” y comenzar a escribir “cmake …”. Verá todos aquellos comandos
que tienen que ver con esta extensión. A continuación tiene una lista con los que le pueden ser de
mayor utilidad y su equivalencia con la línea de órdenes de CMake:
CMake Tools Línea de órdenes Descripción
CMake: Inicio rápido Crear CMakeLists.txt Crear un nuevo proyecto cuando no existe un
(Quick start) Crear CMakePresets.json fichero CMakeLists.txt. Detecta los toolchain
cmake -S … -B ... disponibles (kits) y hace algunas preguntas
iniciales al usuario para crear el fichero de
configuración inicial de CMake.
CMake: Configurar cmake -S … -B ... Se utiliza para configurar el proyecto a partir de
(Configure) CMakeLists.txt. Crea el build directory a partir
del source directory y genera los ficheros
CMakeCache.txt, Makefile (o Ninja, etc.) necesarios
para construir la aplicación.
CMake: Seleccionar un kit Seleccionar kit Permite elegir un toolchain de trabajo
(Select a kit) cmake -S … -B ... (compilador, enlazador, etc.). Se presenta una
lista con los que se han detectado en el
sistema.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 54/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
CMake: Eliminar la Borra CMakeCache.txt Borra CMakeCache.txt y el directorio CMakeFiles del
memoria caché y volver a Borra CMakeFiles build directory para resetear todas las opciones
configurar cmake -S … -B ... por defecto de CMake. Es conveniente hacerlo
(Delete Cache and cuando se ha modificado el toolchain (kit de
Reconfigure) CMake) o cuando se cambia la configuración de
CMake sin hacer uso de CMake Tools (por
ejemplo desde editores externos o línea de
órdenes).
CMake: Versión de cmake --build Construye el target que esté establecido por
compilación defecto, que normalmente será --target all.
(Build)
CMake: Recompilación cmake --build --target Limpia el resultado de compilaciones previas y
limpia clean construye de nuevo el target que esté
(Clean Rebuild) cmake --build establecido por defecto, que normalmente será
--target all.
CMake: Limpiar cmake --build --target Limpia el resultado de compilaciones previas.
(Clean) clean
CMake: Depurar cmake –build Construye el proyecto y comienza la depuración
(Debug) Ejecuta depurador del target activo.
8.5. Depuración con VSCode
La depuración es otra herramienta que podemos usar con VSCode y CMake. Consiste en realizar la
ejecución paso a paso de un programa, es decir, instrucción a instrucción. De esta forma en cada
paso podemos, por ejemplo, ver o incluso modificar valores de algunas variables en el momento y
ver cómo funciona todo. Es muy útil para detectar errores en la lógica de los programas.
Para depurar un programa lo primero que hemos de establecer son los conocidos como puntos de
ruptura o de interrupción (breakpoints). Cuando comenzamos la ejecución en modo depuración
(use el botón ) el programa se ejecuta con aparente normalidad hasta llegar al primer punto de
ruptura. En ese momento detiene temporalmente la ejecución para que analicemos lo que está
ocurriendo. A partir de ese momento podemos avanzar la ejecución manualmente a través de la
barra de herramientas del depurador, que habrá aparecido en VSCode (posiblemente en la zona
superior derecha de su editor).
Por tanto, al menos hay que establecer un primer punto de ruptura. Si no lo hacemos la ejecución
comenzará y finalizará con normalidad.
Para establecer puntos de ruptura marcamos
con el ratón junto al número de la línea del
código fuente en la que queremos detener la
ejecución. Podemos poner tantos como
queramos.
Una vez comenzada la depuración aparece marcada en el código la siguiente instrucción que se va
a ejecutar cuando pulsemos el botón de avanzar y disponemos, en la barra lateral izquierda, de las
herramientas correspondientes:
• Listado con todas las variables disponibles en el punto actual. Permite ver y también
modificar el valor en tiempo real.
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 55/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
• Una ventana (inspección) para introducir expresiones válidas en C++ de manera que
podemos ver el resultado de su evaluación.
• Una ventana con la pila de llamadas a funciones.
• Un listado con los puntos de interrupción establecidos.
Configuración de la barra de herramientas del depurador
Por defecto la barra de herramientas del depurador se muestra en formato
“flotante” y habitualmente la verá en la zona de la esquina superior derecha.
Podemos desplazarla a otra ubicación y esta
será recordada en sucesivas depuraciones.
También podemos cambiar la configuración
para que en lugar de ser una barra flotante
aparezca fija en la parte superior de la barra
lateral primaria.
Para ello hay que buscar en la configuración
“Debug: Tool Bar Location” y cambiarlo a
“docked”.
8.6. VSCode y CPack
En nuestro caso se utilizará esta herramienta para crear un fichero comprimido con los fuentes
para hacer la entrega de prácticas (ver sección 7.9). Para utilizar CPack desde VSCode basta con
pulsar el correspondiente botón de la barra de herrramientas de la extensión CMake Tools.
Antes de hacerlo, configure dicha extensión para que genere el paquete con formato ZIP y para
que únicamente incluya el source directory. Acceda a la configuración de VSCode y localice la
opción “Cmake: Cpack Args”. A continuación añada estos 2 argumentos:
-G ZIP
--config CpackSourceConfig.cmake
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 56/57
Compilación separada y toolchain básico GNU g++, GNU Make, CMake y VSCode
Puede ver una captura de pantalla en la siguiente figura:
8.7. Un fichero CMakeLists.txt básico
Llegados a este punto, debería tener un fichero similar al siguiente como punto de partida para
sus proyectos.
cmake_minimum_required(VERSION 3.12)
project(main VERSION 0.1.0 LANGUAGES C CXX)
add_executable(main main.cpp)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "-Wall -Wextra -Wpedantic")
set(CPACK_SOURCE_GENERATOR ZIP)
include(CPack)
© Javier Martínez Baena. Departamento de Ciencias de la Computación e I. A. (Universidad de Granada) 57/57