Salta al contingut
Llegat fiber

Compilar i executar

Del codi C++ a l'executable: compilar, enllaçar i executar. També aprendràs a provar-lo amb fitxers, redireccions i diff.

Conceptes clau

  • Preprocessar, compilar i enllaçar: .cc → .o → .exe
  • Opcions del Jutge: -Werror, -D_GLIBCXX_DEBUG…
  • Executar amb un camí: ./prog.exe
  • Canals estàndard i redireccions: <, >, >>, 2>, |
  • Encadenar comandes amb && i comparar amb diff
  • Empaquetar amb tar cf i tar xf

En què consisteix compilar

El processador només entén codi màquina: instruccions binàries molt simples (copia aquest valor a un registre, suma dos registres, salta a aquesta adreça) i diferents per a cada arquitectura (x86-64, ARM…).
En canvi, nosaltres escrivim el codi en C++, un llenguatge pensat perquè l’entenguin les persones.

Per poder executar el programa, primer cal traduir-lo a codi màquina. Aquesta traducció la fa un programa: el compilador.

C++ és un llenguatge compilat: la traducció es fa una vegada, abans d’executar, i en surt un fitxer executable: el programa en codi màquina.

El compilador que farem servir, g++, llegeix el teu codi i, per arribar a l’executable, fa dues feines diferents: compilar i enllaçar.
Sovint es diu «compilar» a tot el procés, però en sentit estricte és només la primera feina.

1. Compilar

prog.ccprog.oprog.exeresultatcompilarenllaçarexecutar#includecapçaleres:iostream, Punt.hh…biblioteca estàndardfitxers .o
Primer pas: compilar. De prog.cc en surt prog.o, i els #include hi copien les capçaleres.

Compilar consisteix en agafar un fitxer font .cc i traduir-lo a codi màquina.
Per fer-ho cal executar aquesta comanda, que crea un .o amb el mateix nom que el .cc:

g++ -c <fitxer.cc>

El compilador per dins ho fa en tres fases: preprocessar, comprovar i traduir.

1.1. Preprocessar

Les línies #include del principi del programa indiquen quines biblioteques i fitxers externs fas servir.
Per saber com són les funcions i els objectes que en fas servir (com es diuen, quins paràmetres reben, què retornen), el compilador consulta les seves capçaleres o headers: fitxers que descriuen les funcions, però que no en porten el codi.

  • cout, per exemple, el descriu la capçalera <iostream>, que forma part de la biblioteca estàndard, el conjunt de codi que ve amb el compilador.

En preprocessar, el compilador busca cada línia #include i la substitueix pel contingut sencer de la capçalera.
Així, en la fase següent, el compilador ja sabrà com és cada funció i objecte que fa servir el codi, i podrà comprovar que s’utilitzen correctament.

1.2. Comprovar

Amb les capçaleres ja copiades, el compilador llegeix el codi de dalt a baix i revisa que es compleixin les regles del llenguatge: que la sintaxi és correcta, que els tipus encaixen i que tot el que fas servir està ben declarat.

  • Com que ja sap com és cout, pot comprovar que l’estàs fent servir bé.

Aquí és quan salten els errors de compilació: si alguna cosa no compleix les regles, el compilador s’atura i t’avisa amb un error.

1.3. Traduir

Un cop comprovat que el codi està ben escrit, el compilador el tradueix a instruccions de codi màquina.

En surt un fitxer objecte, .o, amb les funcions del .cc ja traduïdes.

Alerta: aquest .o encara no es pot executar. El codi de les funcions i objectes que vénen de fora és en altres fitxers. Les capçaleres només deien com s’utilitzen, perquè el compilador pogués fer la comprovació.

  • El codi de cout no hi és. Al seu lloc, el compilador hi deixa un forat: una referència indefinida (undefined reference), que vol dir «aquí va cout; ja em diran on és el codi».

Els forats només es poden omplir quan se sap on són tots els trossos. D’això se n’encarrega el pas següent: enllaçar.

2. Enllaçar

prog.ccprog.oprog.exeresultatcompilarenllaçarexecutar#includecapçaleres:iostream, Punt.hh…biblioteca estàndardfitxers .o
Segon pas: enllaçar. prog.o s'ajunta amb la biblioteca estàndard i amb els altres fitxers .o del programa.

Enllaçar és ajuntar el teu .o amb la biblioteca estàndard (on hi ha el codi ja compilat de cout, string, vector…) i amb els altres fitxers .o que necessiti, per fer-ne l’executable: un sol fitxer amb tot el codi.

Ho fa el linker, que g++ crida automàticament quan li dones fitxers .o:

g++ -o <executable> <fitxers.o>

La seva feina és omplir les referències indefinides que han deixat els .o amb l’adreça del codi que els correspon.

Per saber com omplir-les, cada .o porta una taula de símbols amb dues llistes:

  • els noms que defineix: les seves funcions i variables globals;
  • els noms que necessita d’altres fitxers: les seves referències indefinides.

Per a cada nom que necessita un .o, el linker busca quin altre fitxer el defineix, i omple el forat amb l’adreça on es troba el codi.

Si un nom que es necessita no es defineix enlloc, el linker no pot omplir el forat i dona l’error undefined reference. El fitxer s’havia compilat bé, però en ajuntar-ho tot hi falta una peça.

prog.ccprog.oprog.exeresultatcompilarenllaçarexecutar#includecapçaleres:iostream, Punt.hh…biblioteca estàndardfitxers .o
Ja tenim l'executable, prog.exe. Compiles cada cop que canvies el codi; executes tantes vegades com vulguis.

Compilació a la pràctica amb g++

Ja hem vist una comanda per a cada pas:

g++ -c <fitxer.cc>                # compilar: crea fitxer.o
g++ -o <executable> <fitxers.o>   # enllaçar: crea l'executable

Però a la comanda d’enllaçar també li pots donar fitxers .cc directament.
Llavors g++ fa els dos passos seguits: primer compila cada .cc en un .o temporal (que no queda a la carpeta) i després ho enllaça tot.

Per això la forma general és aquesta:

g++ [opcions] -o <executable> <fitxers>
Part Què és
g++ La comanda de GCC per compilar i enllaçar programes C++.
[opcions] Com ha de compilar: quina versió del llenguatge, quins avisos, si ha d’afegir informació per depurar…
Cada opció comença amb - i l’ordre entre elles no importa.
-o <executable> El nom del fitxer executable que es crearà.
Compte: si ja existeix se sobreescriu.
<fitxers> Els fitxers font .cc que cal compilar i els .o que cal enllaçar. Es poden barrejar.
prog.ccprog.oprog.exeresultatcompilarg++ -c prog.ccenllaçarg++ -o prog.exe prog.oexecutar./prog.exe#includecapçaleres:iostream, Punt.hh…biblioteca estàndardfitxers .o
La comanda de cada pas. Amb una de sola, g++ -o prog.exe prog.cc, fas els dos primers de cop.

Amb molts fitxers, convé fer els dos passos per separat, perquè així només recompiles el que has canviat. Ho veuràs al capítol següent amb el Makefile.

  • Si g++ no diu res, ha anat bé. Si falla, surten errors i no tens un executable nou. Per tant si ja n’hi havia un de vell, encara hi serà.

Executar

Executar és activar el fitxer executable perquè l’ordinador en segueixi les instruccions i en surti el resultat.

prog.ccprog.oprog.exeresultatcompilarenllaçarexecutar#includecapçaleres:iostream, Punt.hh…biblioteca estàndardfitxers .o
Últim pas: executar prog.exe i obtenir-ne el resultat.

Per tal d’executar un fitxer cal escriure:

./<executable>

El ./ és el camí fins al fitxer: el . vol dir «la carpeta on ets» i la / separa la carpeta del nom.

  • Així, ./prog.exe vol dir «el fitxer prog.exe d’aquesta carpeta».
    Si en canvi escrius prog.exe sol, el terminal el busca només a les carpetes del sistema, no el trobarà i respondrà amb command not found.

Si l’executable és en una altra carpeta, el pots executar posant-hi el seu camí:

sessio1/prog.exe   # el prog.exe de la subcarpeta sessio1

Si escrius les dades a mà, pots acabar l’entrada amb Ctrl+D (a Windows, Ctrl+Z i Retorn) així el programa rep «final de fitxer» i el while (cin >> x) s’acaba.

Si el programa s’ha penjat, per exemple en un bucle infinit, el pots aturar amb Ctrl+C.

Redireccions

Fins ara, cada vegada que proves el programa has d’escriure les dades a mà. Amb les redireccions pots fer que les llegeixi d’un fitxer i que escrigui el resultat en un altre, i així no cal que hagis d’escriure les dades d’entrada cada vegada.

Els tres canals

Tot programa que engegues des del terminal té tres canals oberts des del primer moment:

Canal Número En C++ Per defecte va a…
Entrada estàndard (stdin) 0 cin el teclat
Sortida estàndard (stdout) 1 cout la pantalla
Sortida d’errors (stderr) 2 cerr la pantalla

El programa no sap què hi ha a l’altra banda de cada canal: cin >> x llegeix del canal 0, i prou. Qui decideix on va connectat cada canal és el terminal, abans d’engegar el programa.
Una redirecció és demanar-li que connecti un canal a un fitxer en lloc del teclat o la pantalla. El codi no canvia gens.

prog.in0 · cinprog.exe1 · coutprog.out2 · cerrpantalla
./prog.exe < prog.in > prog.out: el canal 0 llegeix de prog.in i l'1 escriu a prog.out. El 2 continua anant a la pantalla.

Llegir d’un fitxer: <

./prog.exe < prog.in

Ara cin llegeix de prog.in. Quan el fitxer s’acaba, cin arriba al final igual que quan prems Ctrl+D. La sortida continua apareixent a la pantalla.

Un exemple, amb un programa que suma tots els nombres de l’entrada:

#include <iostream>
using namespace std;

int main() {
    int x, suma = 0;
    while (cin >> x) suma += x;
    cout << "Suma: " << suma << endl;
}
$ cat prog.in
3 5
10 2
$ ./prog.exe < prog.in
Suma: 20

Escriure a un fitxer: > i >>

./prog.exe > prog.out

Tot el que el programa escriu amb cout va a prog.out, i a la pantalla no surt res. Si el fitxer no existeix, es crea. Si existeix, es buida primer.

Amb >> la sortida s’afegeix al final del fitxer, en lloc de buidar-lo:

./prog.exe < prova1.in >  totes.out   # totes.out només té la sortida de la prova 1
./prog.exe < prova2.in >> totes.out   # ara té la de la 1 i, a sota, la de la 2

Les dues coses alhora

./prog.exe < prog.in > prog.out

Llegeix de prog.in i escriu a prog.out. L’ordre de les redireccions no importa: ./prog.exe > prog.out < prog.in fa el mateix.

Els errors a part: 2>

cerr escriu al canal 2, i > no el toca. Per això els missatges que escrius amb cerr (i els de _GLIBCXX_DEBUG quan el programa peta) continuen sortint per pantalla encara que redirigeixis la sortida, i no embruten el fitxer que després compararàs. Si els vols en un fitxer, 2>:

cerr << "llegit: " << x << endl;   // una traça per depurar
./prog.exe < prog.in > prog.out              # la traça surt per pantalla; prog.out queda net
./prog.exe < prog.in > prog.out 2> traca.txt # la traça va a traca.txt

El Jutge només mira la sortida estàndard. Si depures amb cerr, una traça oblidada no et farà fallar la prova (però alenteix el programa: treu-la quan acabis).

Connectar dos programes: |

La canonada (pipe) connecta la sortida d’un programa amb l’entrada del següent, sense cap fitxer entremig:

echo "3 5 10 2" | ./prog.exe      # unes dades ràpides sense crear cap fitxer
./prog.exe < prog.in | less       # una sortida llarga, pàgina a pàgina

Comparar amb el resultat esperat

A tots els exercicis hi haurà jocs de proves amb un fitxer amb la sortida esperada. Pots comparar la teva sortida amb l’esperada fàcilment amb la comanda diff:

./prog.exe < prog.in > prog.out
diff prog.out esperat.out

Si diff no escriu res, les dues sortides són idèntiques.
Si escriu línies, són les que difereixen (< és la teva, > l’esperada).

Tot en una línia

Pots posar diverses comandes a la mateixa línia amb &&: la segona només s’executa si la primera ha anat bé. Així, si hi ha errors de compilació, no proves l’executable vell.

# compilar i enllaçar
g++ -c prog.cc && g++ -o prog.exe prog.o

# executar i comparar
./prog.exe < prog.in > prog.out && diff prog.out esperat.out

# tot
g++ -o prog.exe prog.cc && ./prog.exe < prog.in > prog.out && diff prog.out esperat.out

És la línia que repetiràs mil vegades: amb la fletxa amunt del terminal la recuperes.

Empaquetar fitxers

Per lliurar diversos fitxers junts es fa un .tar: un sol fitxer que en conté d’altres.
No els comprimeix, només els ajunta.

# crea el tar amb els fitxers que li diguis (c = create, f = file)
tar cf <paquet.tar> <fitxers>

# crea el tar amb tot el que hi ha a la carpeta
tar cf <paquet.tar> *

# extreu els fitxers del tar (x = extract, f = file)
tar xf <paquet.tar>

Quan hi ha molts fitxers

Escriure a mà totes aquestes comandes es fa pesat però amb dos o tres fitxers encara es pot fer.
Amb deu, recordar quins has canviat i quins .o cal refer és una liada.

Això ho podem simplificar fent un Makefile, un fitxer on apuntes com es fa cada cosa i de què depèn, i la comanda make només refà el que ha canviat.