C++ - introduzione, compilazione e linking
In questa pagina 5
Questa nota apre la parte di C++ del corso: cosa si ottiene scrivendo un programma C++, il programma più piccolo possibile e il percorso che porta dal file di testo all'eseguibile. Il passaggio a più file sorgente e le librerie è in C++ - scope, namespace e librerielo scope è la regione di testo in cui un nome è valido, dal punto di dichiarazione alla fine dello scope; i tipi di scope sono globale (di file), di classe, locale (blocco o lista argomenti di una funzione), di statement (for) e di namespace; funzioni dentro funzioni sono illegali, le classi locali da evitare; i namespace raggruppano nomi senza creare un tipo e si accedono col nome qualificato namespace::membro oppure con using (la using directive va evitata negli header); un header file è un insieme di dichiarazioni che chi lo include può usare (<> per i file di sistema, "" per quelli del progetto), dev'essere protetto dalle include guard (#ifndef/#define/#endif) per non includere due volte le stesse definizioni; il linker unisce i file oggetto con le librerie, statiche (copiate in ogni eseguibile, self-contained) o dinamiche (una sola copia condivisa, .so su Linux, .dll su Windows, ricompilabili senza toccare gli eseguibili).C++ - scope, namespace e librerie →; per progetti grandi vedi C++ - buona interfaccia e progetti su più fileUna classe C++ ben progettata decide esplicitamente le sette operazioni essenziali (costruttore con argomenti, di default, di copia, assegnamento di copia, costruttore e assegnamento di spostamento, distruttore): se serve il distruttore perché si possiede una risorsa, servono quasi sempre anche copia e spostamento; un costruttore a un argomento definisce una conversione implicita, che si blocca con explicit; per capire quando i costruttori e il distruttore vengono chiamati si traccia una struct che stampa ogni chiamata. Un progetto su più file separa interfaccia (header .h con definizioni di classe e dichiarazioni, protetto da include guard) e implementazione (.cpp, compilati uno per uno, mai inclusi); al compilatore si passano solo i .cpp, il main sta in un file a sé; gli errori di compilazione (sintassi, tipi, dichiarazioni mancanti) vengono dal compilatore, quelli di linking (definizione mancante, due main, nessun main) dal linker.C++ - buona interfaccia e progetti su più file → e CMake - build di progetti C++Un progetto C++ non è fatto solo di sorgenti: bisogna dire quali file formano ogni eseguibile e ogni libreria (target), quali librerie linkare, dove stanno gli header e con quale standard compilare. CMake è un meta-sistema di build indipendente da compilatore e piattaforma: si descrive il progetto in un file di testo CMakeLists.txt (cmake_minimum_required, project, add_library, add_executable, target_link_libraries, include directories) e CMake genera i file del sistema di build nativo (Makefile, progetto di IDE). Lato utente si fa una configurazione (mkdir build, cd build, cmake ..) e poi la compilazione (make o cmake --build .) in una cartella di build separata dai sorgenti; le librerie di soli template (header-only) si dichiarano INTERFACE perché non producono codice precompilato.CMake - build di progetti C++ →. Per chi viene da Python, la differenza di fondo (compilato contro interpretato) è in Eseguire un programma PythonInterprete interattivo e script .py, struttura di un file Python, indentazione, commenti, istruzioni ed espressioni, errori di sintassi.Eseguire un programma Python → e Dal Python al CStruttura di un programma C, compilazione con gcc (preprocessore, compilatore, linker), main e valore di ritorno, dichiarazioni con tipo statico, printf di base; corrispondenze con Python.Dal Python al C →; la compilazione di un programma C fino al codice macchina è in Dal programma C all'eseguibile ARMCatena di traduzione compilatore, assemblatore, linker e loader; file oggetto, tabella dei simboli e rilocazione; collegamento statico e dinamico; organizzazione della memoria di un processo; chiamare funzioni assembly da C e viceversa, con esempio.Dal programma C all'eseguibile ARM →.
Perché C++
Definizione. C++ è un linguaggio compilato, staticamente tipizzato, standard (ISO) e multipiattaforma: lo stesso sorgente si compila su Windows, Linux e macOS.
Due proprietà lo rendono particolare:
Il corso non insegna "tutto il C++" ma la programmazione con C++ come strumento: i concetti (tipi, memoria, interfacce, generalità) valgono anche in altri linguaggi. Un concetto che in C++ si vede a nudo, come la gestione della memoria, in Java o Python è nascosto dal linguaggio: per questo capirlo qui serve anche altrove.
Il programma più piccolo: Hello, world
#include <iostream>
int main()
{
std::cout << "Hello, World!\n"; // stampa "Hello, World!"
return 0;
}Uscita:
Hello, World!Riga per riga:
Gli << si possono concatenare perché ogni << restituisce lo stream stesso: std::cout << "a" << 1 << '\n' stampa tre cose di fila. Il significato di << dipende dai tipi degli operandi (inserimento di una stringa, di un intero...): è un esempio di operatore sensibile al tipo (C++ - tipi, variabili, espressioni e dichiarazioniun tipo definisce contenuto, intervallo di valori e operazioni; un oggetto è una regione di memoria con un tipo, il valore è la configurazione di bit letta in base al tipo, una variabile è un oggetto con un nome; inizializzazione (dare il primo valore) e assegnamento (cambiare il valore) sono diversi; le conversioni verso un tipo più capiente sono sicure, quelle verso un tipo meno capiente (narrowing, double->int, int->char) non lo sono e il compilatore le segnala di rado, ma la notazione con graffe {} le vieta; un'espressione calcola un valore da operandi e operatori, un lvalue è ciò che può stare a sinistra di =, constexpr vuole un valore noto in compilazione mentre const solo un valore che non cambia; gli operatori con side effect (++, --, +=) non vanno mischiati con altri usi della stessa variabile nella stessa espressione perché l'ordine di valutazione non è definito; una dichiarazione introduce un nome e un tipo, una definizione fornisce anche l'entità (memoria per una variabile, corpo per una funzione).C++ - tipi, variabili, espressioni e dichiarazioni →).
Da sorgente a eseguibile
C++ è un linguaggio compilato: il codice sorgente, leggibile da noi, viene tradotto una volta per tutte in codice macchina, che il processore esegue direttamente. Il traduttore non corregge gli errori, li segnala: spesso in modo ovvio, a volte oscuro. Un errore a tempo di compilazione è molto più semplice da correggere di uno a tempo di esecuzione, perché il compilatore indica la riga.
Il processo ha tre fasi:
hello.cpp --[preprocessore]--> hello.i --[compilatore]--> hello.o --[linker]--> hello
sorgente espande #include codice file oggetto + librerie eseguibile
e macro macchina| fase | input | output | cosa fa |
|---|---|---|---|
| preprocessore | file sorgente .cpp |
codice modificato (.i) |
esegue le direttive che iniziano con #: copia gli header al posto di #include, espande le macro, taglia i pezzi esclusi da #ifdef |
| compilatore | codice modificato | file oggetto .o |
controlla sintassi e tipi, traduce in codice macchina; il file oggetto non è ancora eseguibile perché i nomi definiti altrove sono lasciati in sospeso |
| linker | uno o più file oggetto (e librerie) | eseguibile | collega ogni nome usato al punto in cui è definito: unisce i file oggetto tra loro e con le librerie necessarie |
Il file sorgente è il formato leggibile, il file oggetto è codice macchina, l'eseguibile è codice macchina più le librerie necessarie all'esecuzione.
Da riga di comando
Il compilatore si invoca da riga di comando o da un IDE (con GCC su Linux è g++, su Windows MinGW o Visual Studio):
g++ -o my_exec my_source.cpp -lmy_lib| parte | significato |
|---|---|
-o my_exec |
nome dell'eseguibile (output) |
my_source.cpp |
nome del sorgente (input) |
-lmy_lib |
libreria da linkare (input): cerca libmy_lib.so o libmy_lib.a |
Le opzioni più usate in laboratorio:
| opzione | effetto |
|---|---|
-c |
si ferma al file oggetto (compila ma non linka) |
-E |
si ferma al preprocessore: stampa il codice espanso |
-Wall -Wextra |
attiva gli avvisi utili (variabili inutilizzate, conversioni sospette, ...) |
-std=c++17 |
sceglie la versione dello standard (11, 14, 17, 20, ...) |
-g |
aggiunge le informazioni per il debugger |
-O2 |
ottimizza |
Poi si esegue con ./my_exec su Linux. Compilare tante volte, a ogni pezzo di codice scritto, è la regola: gli errori da correggere sono pochi e vicini.
Chi segnala quale errore
Gli errori si dividono per fase, e riconoscere la fase dice dove cercare:
| fase | errore tipico | esempio |
|---|---|---|
| preprocessore | header non trovato | #include "mio.h" ma il file non esiste |
| compilatore | sintassi (; mancante), tipo (int dato a un vector), nome non dichiarato, conversione non valida |
std::string s = 5; |
| linker | simbolo dichiarato, usato ma non definito | undefined reference to f(int) |
Un esempio di errore di linking: la funzione f è dichiarata (quindi il compilatore accetta la chiamata) ma non è definita in nessun file.
int f(int i); // dichiarazione: basta al compilatore
int main() { return f(3); } // chiamata: nessun errore di compilazioneCompilando fino al file oggetto tutto va bene (g++ -c); il linker invece si ferma perché non trova il corpo di f e segnala undefined reference to 'f(int)' (con il linker di LLVM: undefined symbol: f(int)). La stessa cosa succede se la definizione sta in un file .cpp che non viene passato al compilatore, o in una libreria che non è stata indicata con -l.
Più file, un solo eseguibile
Appena un programma cresce, si divide in più file: l'interfaccia (le dichiarazioni) nell'header .h, l'implementazione (le definizioni) nel .cpp. Il preprocessore, il compilatore e il linker agiscono ora su più file:
main.cpp --(preprocessore+compilatore)--> main.o --\
my_func.h (copiato in entrambi) +--(linker)--> eseguibile
my_func.cpp --(preprocessore+compilatore)--> my_func.o --/Ogni .cpp è una unità di traduzione (translation unit): viene compilata da sola, e vede degli altri file solo gli header che include. Esempio con tre file:
#ifndef MY_FUNC_H
#define MY_FUNC_H
// Interfaccia: cosa si può usare
int f(int i);
#endif // MY_FUNC_H#include "my_func.h"
// Implementazione: come funziona
int f(int i)
{
return i + 2;
}#include <iostream>
#include "my_func.h"
int main()
{
int i = 0;
i = f(i);
std::cout << "f(0) = " << i << '\n';
return 0;
}Si compila e si linka con g++ -o prog main.cpp my_func.cpp (oppure, in due passi, g++ -c main.cpp, g++ -c my_func.cpp, g++ -o prog main.o my_func.o). Uscita:
f(0) = 2Le righe #ifndef MY_FUNC_H ... #endif si chiamano include guard e servono perché un header incluso due volte non produca definizioni doppie: i dettagli, con gli errori che compaiono senza, sono in C++ - scope, namespace e librerielo scope è la regione di testo in cui un nome è valido, dal punto di dichiarazione alla fine dello scope; i tipi di scope sono globale (di file), di classe, locale (blocco o lista argomenti di una funzione), di statement (for) e di namespace; funzioni dentro funzioni sono illegali, le classi locali da evitare; i namespace raggruppano nomi senza creare un tipo e si accedono col nome qualificato namespace::membro oppure con using (la using directive va evitata negli header); un header file è un insieme di dichiarazioni che chi lo include può usare (<> per i file di sistema, "" per quelli del progetto), dev'essere protetto dalle include guard (#ifndef/#define/#endif) per non includere due volte le stesse definizioni; il linker unisce i file oggetto con le librerie, statiche (copiate in ogni eseguibile, self-contained) o dinamiche (una sola copia condivisa, .so su Linux, .dll su Windows, ricompilabili senza toccare gli eseguibili).C++ - scope, namespace e librerie →, insieme a librerie statiche e dinamiche.
Domande d'esame
1. Quali sono le fasi che trasformano un file sorgente in un eseguibile? Chi segnala un errore di sintassi e chi uno di undefined reference?
Traccia. Preprocessore (espande #include e macro), compilatore (controlla sintassi e tipi, produce il file oggetto), linker (unisce file oggetto e librerie risolvendo i nomi). La sintassi è del compilatore; un simbolo dichiarato e usato ma mai definito (o con la libreria mancante) è del linker.
2. Un programma compila senza errori ma il linker si lamenta di undefined reference: da che cosa può dipendere?
Traccia. Funzione dichiarata ma non definita; definizione in un .cpp non passato al compilatore; libreria non indicata con -l; firma diversa tra dichiarazione e definizione (due funzioni diverse in C++, perché i parametri fanno parte del nome).
3. Che cos'è main e che cosa significa return 0?
Traccia. Punto di partenza di ogni eseguibile, unico; il valore restituito arriva al sistema operativo come codice di uscita, 0 = successo.
Versione ripasso
- C++ = compilato, tipizzato staticamente, standard, multipiattaforma; basso livello (memoria, puntatori) + alto livello (classi, template,
vector). Il corso insegna programmazione, il C++ è lo strumento. Confronti: Dal Python al CStruttura di un programma C, compilazione con gcc (preprocessore, compilatore, linker), main e valore di ritorno, dichiarazioni con tipo statico, printf di base; corrispondenze con Python.Dal Python al C →, Eseguire un programma PythonInterprete interattivo e script .py, struttura di un file Python, indentazione, commenti, istruzioni ed espressioni, errori di sintassi.Eseguire un programma Python →, Dal programma C all'eseguibile ARMCatena di traduzione compilatore, assemblatore, linker e loader; file oggetto, tabella dei simboli e rilocazione; collegamento statico e dinamico; organizzazione della memoria di un processo; chiamare funzioni assembly da C e viceversa, con esempio.Dal programma C all'eseguibile ARM →. - Hello world:
#include <iostream>(direttiva del preprocessore: copia l'header),int main()(punto di partenza, uno solo),std::cout << "..." << '\n'(<<inserisce in uno stream e restituisce lo stream),return 0;(codice di uscita; inmainè implicito),//commento. - Tre fasi: sorgente
.cpppreprocessore (#include, macro,.i) compilatore (sintassi e tipi, file oggetto.oin codice macchina) linker (unisce.oe librerie, eseguibile). - Comando:
g++ -o eseguibile sorgente.cpp -lLibreria;-csi ferma all'oggetto,-Eal preprocessore,-Wall -Wextraavvisi,-std=c++17,-gdebug,-O2ottimizza. Si esegue con./eseguibile. - Errori per fase: preprocessore = header non trovato; compilatore = sintassi, tipi, nomi non dichiarati; linker =
undefined reference(dichiarato, usato, mai definito:.cppmancante, libreria non linkata, firma diversa). - Più file: header
.h= interfaccia (dichiarazioni),.cpp= implementazione (definizioni); ogni.cppè una unità di traduzione compilata da sola; il linker li unisce:g++ -o prog main.cpp my_func.cpp. Include guard obbligatorie: C++ - scope, namespace e librerielo scope è la regione di testo in cui un nome è valido, dal punto di dichiarazione alla fine dello scope; i tipi di scope sono globale (di file), di classe, locale (blocco o lista argomenti di una funzione), di statement (for) e di namespace; funzioni dentro funzioni sono illegali, le classi locali da evitare; i namespace raggruppano nomi senza creare un tipo e si accedono col nome qualificato namespace::membro oppure con using (la using directive va evitata negli header); un header file è un insieme di dichiarazioni che chi lo include può usare (<> per i file di sistema, "" per quelli del progetto), dev'essere protetto dalle include guard (#ifndef/#define/#endif) per non includere due volte le stesse definizioni; il linker unisce i file oggetto con le librerie, statiche (copiate in ogni eseguibile, self-contained) o dinamiche (una sola copia condivisa, .so su Linux, .dll su Windows, ricompilabili senza toccare gli eseguibili).C++ - scope, namespace e librerie →, C++ - buona interfaccia e progetti su più fileUna classe C++ ben progettata decide esplicitamente le sette operazioni essenziali (costruttore con argomenti, di default, di copia, assegnamento di copia, costruttore e assegnamento di spostamento, distruttore): se serve il distruttore perché si possiede una risorsa, servono quasi sempre anche copia e spostamento; un costruttore a un argomento definisce una conversione implicita, che si blocca con explicit; per capire quando i costruttori e il distruttore vengono chiamati si traccia una struct che stampa ogni chiamata. Un progetto su più file separa interfaccia (header .h con definizioni di classe e dichiarazioni, protetto da include guard) e implementazione (.cpp, compilati uno per uno, mai inclusi); al compilatore si passano solo i .cpp, il main sta in un file a sé; gli errori di compilazione (sintassi, tipi, dichiarazioni mancanti) vengono dal compilatore, quelli di linking (definizione mancante, due main, nessun main) dal linker.C++ - buona interfaccia e progetti su più file →, CMake - build di progetti C++Un progetto C++ non è fatto solo di sorgenti: bisogna dire quali file formano ogni eseguibile e ogni libreria (target), quali librerie linkare, dove stanno gli header e con quale standard compilare. CMake è un meta-sistema di build indipendente da compilatore e piattaforma: si descrive il progetto in un file di testo CMakeLists.txt (cmake_minimum_required, project, add_library, add_executable, target_link_libraries, include directories) e CMake genera i file del sistema di build nativo (Makefile, progetto di IDE). Lato utente si fa una configurazione (mkdir build, cd build, cmake ..) e poi la compilazione (make o cmake --build .) in una cartella di build separata dai sorgenti; le librerie di soli template (header-only) si dichiarano INTERFACE perché non producono codice precompilato.CMake - build di progetti C++ →. - Hello world completo:
#include <iostream>
int main()
{
std::cout << "Hello, World!
"; // uscita: Hello, World!
return 0;
}- Esempio di
undefined reference:int f(int i); int main(){ return f(3); }compila con-cma il linker si ferma perchéfnon ha corpo; la stessa cosa se il.cppche la definisce non è nella riga dig++o se manca-l. - Traccia delle risposte d'esame: fasi = preprocessore (espande
#includee macro), compilatore (sintassi e tipi.o), linker (unisce.oe librerie); sintassi = compilatore; simbolo non definito = linker;main= punto di partenza unico, il valore restituito è il codice di uscita (0= successo). - Sorgente / oggetto / eseguibile: sorgente = leggibile; oggetto = codice macchina con nomi ancora in sospeso; eseguibile = codice macchina + librerie.
- Errori tipici: credere che il compilatore corregga gli errori; confondere errore di compilazione e di linking; dimenticare un
.cppnella riga dig++; non compilare spesso.
Esercizi su questo argomento
- Esercizio - Hello world, variabili e funzioni (laboratorio 1)
- Esercizio - Quiz su contenitori STL, override, sizeof, void e virtual table (secondo appello 2021-22, quiz)
- Esercizio - Quiz su operatore =, dangling pointer, enumerazioni e at() (quarto appello 2021-22, quiz)
- Esercizio - Quiz su record di attivazione, new[], dangling pointer e struct (terzo appello 2022-23, quiz)