Vai al contenuto
Topos Uranos

Riepilogo

Questa lezione costituisce l’unità iniziale del corso ed espone il suo piano di studi completo. Dichiara lo scopo del corso, che è la programmazione stessa e non un linguaggio; spiega perché i suoi fondamenti si insegnano in C++, un linguaggio che non nasconde la macchina, e non in uno dalla curva più amichevole; indica gli studenti a cui si rivolge, il poco che presuppone noto e i corsi a cui conduce; enuncia i sette obiettivi di apprendimento globali e le unità che contribuiscono a ciascuno; descrive le sue otto unità di contenuto e le lezioni che le compongono; stima l’impegno totale che il corso richiede, la sua distribuzione per unità, un ritmo di studio suggerito e i suoi crediti; spiega come sono fatte le sue lezioni e, infine, consiglia sul modo di studiarlo, sull’uso degli assistenti di intelligenza artificiale e sul controllo di versione.

Obiettivi di apprendimento

  1. Descrivere la sequenza delle otto unità del corso e spiegare che cosa ciascuna apporta alle successive.
  2. Enunciare i sette obiettivi di apprendimento globali del corso e riconoscere quali unità contribuiscono a ciascuno.
  3. Spiegare con un esempio perché il corso insegna i fondamenti della programmazione in C++ e non in un linguaggio che nasconda i tipi e la memoria.
  4. Calcolare, a partire dalle ore stimate per unità, l’impegno che richiede il corso o una sua parte, e pianificare con esso un ritmo di studio personale.

Scopo

Il corso insegna a scrivere programmi corretti partendo da zero: a convertire un problema enunciato a parole in una sequenza precisa di istruzioni, a scegliere come rappresentare i dati, a governare l’ordine in cui si esegue ciò che è scritto e a organizzare il codice in funzioni con un’interfaccia chiara. Se è così, il suo oggetto non è un linguaggio, ma la programmazione stessa: ideare un algoritmo, rappresentare i dati, scomporre un problema e ragionare sulla correttezza e sul costo di ciò che si scrive. Per questo si chiama «Fondamenti di programmazione», e per questo ogni costrutto di C++ si presenta insieme al problema che risolve e al modo di ragionarci sopra, e mai come un catalogo di sintassi.

C++ è, quindi, lo strumento del corso e non il suo fine. Ora, uno strumento non è indifferente a ciò che con esso si impara, e la scelta di questo, che a prima vista sembra la più difficile, è deliberata; la sezione successiva la giustifica. Conviene anticipare, tuttavia, la sua conseguenza più visibile: ogni sintesi di unità contiene una sezione fissa, «Ciò che vale in qualsiasi linguaggio», che separa ciò che si è appreso dalla sua forma in C++ e dice come si esprime, o che cosa cambia, in altri linguaggi. Ne segue che lo studente che termina il corso non saprà soltanto scrivere in C++, ma riconoscere in qualsiasi linguaggio le stesse idee sotto altra veste.

Perché C++ e non un linguaggio più amichevole

Ci sono linguaggi dalla curva più dolce, come Python, nei quali un primo programma utile si scrive in poche righe, senza dichiarare tipi, senza compilare e senza chiedersi dove vive un dato. Se lo scopo fosse scrivere presto programmi che funzionano, quella sarebbe la strada. Lo scopo di questo corso, invece, è capire che cosa fa un programma, e per questo conviene un linguaggio che non nasconda la macchina. C++ obbliga a vedere sei cose che i linguaggi più comodi risolvono in silenzio:

  • I tipi. Ogni dato si dichiara con un tipo, che fissa quali valori può assumere, quanto spazio occupa e quali operazioni ammette; il compilatore respinge ciò che non concorda prima di eseguire alcunché.
  • La compilazione. Scrivere, compilare, collegare ed eseguire sono momenti distinti, e ogni errore appartiene a uno di essi; sapere a quale è metà della sua correzione.
  • La memoria. Una variabile è una cella con un nome, con un tempo di vita e un ambito di visibilità; un riferimento è un altro nome per la stessa cella, e un riferimento a una cella che non esiste più è un errore reale.
  • Il costo della copia. Passare un vettore a una funzione può copiarlo per intero; scegliere tra valore e riferimento è una decisione del programmatore, e non della macchina.
  • I limiti dei numeri. Un intero a 32 bit non va oltre 2 147 483 6472\,147\,483\,647, e un numero in virgola mobile non conserva 0,10{,}1 in modo esatto.
  • Il comportamento indefinito. Ciò che lo standard non definisce (uscire da un array, far traboccare un intero con segno, leggere una variabile non inizializzata) non produce un errore garantito, ma qualsiasi cosa; per questo il corso compila sempre con gli avvisi e con i sanitizer.

L’esempio seguente lo mostra in piccolo. Il programma calcola i fattoriali da 11 a 1515 in una variabile di tipo int. In Python, i cui interi crescono senza limite, lo stesso calcolo stamperebbe senza protestare 13!=6 227 020 80013! = 6\,227\,020\,800; in C++, il prodotto di 12!12! per 1313 non entra in un int, e il sanitizer del comportamento indefinito, con cui il corso compila tutti i suoi esempi, arresta il programma alla riga esatta:

Programma in C++
#include <print>​int main(){    int factorial{1};    for (int n{1}; n <= 15; ++n) {        factorial = factorial * n;   // il prodotto entra in un int?        std::println("{}! = {}", n, factorial);    }}
Output
1! = 12! = 23! = 64! = 245! = 1206! = 7207! = 50408! = 403209! = 36288010! = 362880011! = 3991680012! = 479001600main.cpp:7:31: runtime error: signed integer overflow: 479001600 * 13 cannot be represented in type 'int'

Il programma è terminato con il codice 1.

Se è così, il linguaggio ha obbligato a porsi una domanda che l’altro avrebbe nascosto: quanto entra nel tipo scelto e che cosa fare quando non entra. Nessuna delle due risposte è migliore in astratto; ciò che importa è sapere che la domanda esiste. La lezione su gli interi e la loro rappresentazione vi risponde, e la sezione sul comportamento indefinito di ogni sintesi vi ritorna.

Ne segue un’asimmetria, che è la ragione di fondo della scelta. Chi impara i fondamenti in C++ capisce poi che cosa fanno per lui i linguaggi più comodi: il garbage collector, gli interi senza limite, il controllo degli indici, la conversione silenziosa dei tipi. Chi impara prima in uno di essi, invece, lascia quegli strati senza vederli, e vi inciampa più tardi, quando un programma reale fallisce per una delle cause che il linguaggio gli aveva nascosto. Pertanto, la strada che il corso sceglie è più esigente all’inizio, ma più trasferibile: ciò che si impara qui vale in qualsiasi linguaggio, e ciò che si impara in un linguaggio che lo nasconde non sempre vale qui.

Ora, C++ è un linguaggio grande, e un corso di fondamenti deve resistere alla tentazione di insegnarlo per intero. Questo primo corso si limita alla programmazione strutturata (dati, espressioni, controllo del flusso, sequenze, testo e funzioni) nello standard C++23, con le sue forme moderne: l’output con std::println, le sequenze con std::array e std::vector, il testo con std::string, gli algoritmi della libreria nella loro forma con i range. Lascia a Programmazione in C++ II ciò che richiede questa base: i puntatori e la memoria dinamica, le classi con metodi, i template propri, le funzioni anonime e le eccezioni.

Destinatari e conoscenze preliminari

Il corso si rivolge a chi non ha mai programmato, di qualsiasi percorso di studio e fin dal primo anno: studenti di informatica che hanno bisogno di un fondamento solido prima dei corsi di strutture dati e di sistemi, studenti di ingegneria e di scienze che hanno bisogno di calcolare con un computer senza fidarsene ciecamente, e studenti di matematica che vogliono vedere i loro algoritmi convertiti in programmi che si eseguono.

È un corso di livello 1, senza prerequisiti e autosufficiente. Presuppone solo due cose: l’aritmetica scolastica, con la divisione intera e il suo resto, e l’uso elementare di un computer (creare una cartella, salvare un file, aprire un programma). Ciò che usa della rappresentazione binaria, del ragionamento per casi, della logica delle condizioni o dell’induzione lo insegna nel punto in cui ne ha bisogno: il sistema binario, nella lezione sugli interi; le leggi di De Morgan, nella lezione sulle relazioni e la logica; l’induzione, nella lezione sulla ricorsione. I rimandi ad altri corsi del sito che compaiono nelle lezioni sono, pertanto, approfondimenti, e non requisiti.

Dove conduce

Da quanto precede segue il posto del corso nel sito. Non ha prerequisiti ed è meta di due percorsi di studio, Informatica e calcolo, e Ingegneria, perché in entrambi la programmazione è insieme strumento e oggetto. Conviene, perciò, che sia uno dei primi corsi di quei percorsi, insieme a LaTeX per la Scrittura Accademica e a Logica Matematica I.

Ha un corso complementare, che si può seguire prima, in parallelo o dopo. Logica Matematica I studia come teoria ciò che qui si usa come pratica: le condizioni composte, le tavole di verità e le leggi di De Morgan dell’unità 3, e l’idea di dimostrare che un procedimento è corretto, che in questo corso prende la forma dell’invariante di un ciclo e dell’induzione su una ricorsione. Algebra e Geometria I, da parte sua, dimostra i risultati sui numeri che il corso programma: la divisione euclidea, l’algoritmo di Euclide, i sistemi di numerazione e lo schema di Horner. E LaTeX per la Scrittura Accademica insegna, nella sua lezione Scrivere in gruppo, il controllo di versione con Git, che questo corso raccomanda di usare fin dal primo programma.

Ora, il corso lascia deliberatamente fuori ciò che richiede la sua base. La sua continuazione prevista è Programmazione in C++ II, che tratterà i puntatori e la memoria dinamica, le classi e gli oggetti, i template, le funzioni anonime, le eccezioni e la libreria standard in profondità. Donde il corso non è un primo tratto di un manuale di C++, ma un corso completo di fondamenti, che può chiudersi qui; chi prosegue, invece, troverà nel corso II la stessa disciplina applicata a programmi la cui dimensione non permette più di farne a meno.

Obiettivi di apprendimento globali

Gli obiettivi seguenti descrivono ciò che uno studente che completa il corso deve essere in grado di fare. Ciascuno è formulato come una prestazione osservabile, in modo che possa essere verificato con un compito concreto; ogni lezione di contenuto dichiara a sua volta i propri obiettivi, che servono a uno o più di quelli globali, e la lezione di chiusura mostra in quali unità e lezioni si lavora ciascuno. I sette obiettivi sono numerati perché si possano citare.

  1. Obiettivo 1. Risolvere problemi con programmi: specificare che cosa entra e che cosa esce, progettare un algoritmo, seguirlo a mano, e scriverlo, compilarlo, eseguirlo e farne il debug, interpretando i messaggi e gli avvisi del compilatore.
  2. Obiettivo 2. Scegliere il tipo adeguato per ogni dato e ragionare sui limiti della sua rappresentazione: l’intervallo degli interi, l’overflow, l’arrotondamento della virgola mobile e la codifica del testo.
  3. Obiettivo 3. Costruire espressioni corrette, conoscendo la precedenza, le conversioni di tipo, gli effetti collaterali e ciò che lo standard lascia indefinito.
  4. Obiettivo 4. Progettare il controllo del flusso con decisioni e cicli corretti, che terminano, e giustificarne la correttezza con un invariante.
  5. Obiettivo 5. Scomporre un problema in funzioni con interfacce chiare, scegliere come passare i parametri, e risolvere problemi per ricorsione.
  6. Obiettivo 6. Rappresentare ed elaborare dati composti (sequenze, tabelle, record, testo e file) con gli algoritmi di base e con la libreria standard, stimandone il costo.
  7. Obiettivo 7. Organizzare programmi più grandi in più file, specificarli con una precondizione e una postcondizione, testarli in modo sistematico e argomentarne la correttezza.

Cinque obiettivi hanno la loro unità: il 2, quella dei dati e dei tipi; il 3, quella delle espressioni; il 4, quella del controllo del flusso; il 5, quella delle funzioni; e il 6, quella dei dati composti, che si prolunga in quella del testo e dei file, dove l’obiettivo 2 ricompare con la codifica UTF-8. Gli altri due, invece, percorrono il corso intero. L’obiettivo 1 ha come unità principale la prima, che insegna il ciclo di specificare, scrivere, compilare ed eseguire, e la lezione sul debugging dell’ultima; ma si esercita in tutte, perché ogni programma del corso si compila, si esegue e si corregge. L’obiettivo 7 ha come unità principale l’ultima, quella dei programmi più grandi; ma comincia nella guida di stile della prima, prosegue nelle costanti della seconda, prende forma nei contratti della quinta, che da allora accompagnano ogni funzione, e negli invarianti della quarta e della sesta. Ne segue che il loro raggiungimento non può essere attribuito a una sola unità, e che conviene coltivarli lungo l’intero corso.

Contenuti

Il corso si organizza in dieci unità: questa presentazione, otto unità di contenuto e una chiusura. Ogni unità di contenuto si apre con una lezione di introduzione, che presenta le sue lezioni, ciò che dà per scontato e il problema che risolve, e comincia con una valutazione d’ingresso; si chiude con una lezione di sintesi, che ricostruisce la catena dei concetti dell’unità, separa ciò che vale in qualsiasi linguaggio dalla sua forma in C++, raccoglie il comportamento indefinito dell’unità, ripete in una valutazione d’uscita le abilità di quella d’ingresso e propone sfide di difficoltà superiore a quella dei problemi delle lezioni.

Unità 0. Presentazione del corso

Consta di questa lezione, che fissa lo scopo, la scelta del linguaggio, gli obiettivi globali, i contenuti, l’impegno e il disegno del corso.

Unità 1. Primi passi

Il suo obiettivo è portare lo studente da un problema enunciato a parole fino a un programma che compila, si esegue, legge dati e scrive risultati, sapendo che cosa ha fatto ogni strumento lungo il cammino. Comincia senza codice, perché l’algoritmo precede il programma; alla fine, lo studente sa ancora poco di C++, ma sa come ci si lavora.

  1. Introduzione: primi passi.
  2. Dal problema all’algoritmo: la specifica di input e output, i passi numerati, la traccia a mano, la sequenza, la decisione e la ripetizione.
  3. Programmi, compilatori e C++: il preprocessing, la compilazione e il collegamento; il compilato di fronte all’interpretato; gli standard del linguaggio.
  4. Installare l’ambiente su Windows, macOS e Linux: le tre guide complete, il compilatore, Visual Studio Code e la sua estensione per C++, la verifica e i malfunzionamenti frequenti.
  5. Compilare, eseguire e configurare: le opzioni del corso, gli errori e gli avvisi, i sanitizer, il task di compilazione dell’editor e la codifica della console.
  6. Anatomia di un programma: l’inclusione, main, le istruzioni, i blocchi, i commenti e la guida di stile del corso.
  7. Input, output ed errori: std::cin, std::println e le specifiche di formato, l’input malformato e la classificazione degli errori.
  8. Sintesi e sfide: primi passi.

Unità 2. Dati e tipi

Il suo obiettivo è insegnare che ogni dato vive in memoria con un tipo che fissa quali valori può assumere, quanto spazio occupa e quali operazioni ammette, e che un numero del programma non è un numero della matematica, ma una rappresentazione con dei limiti.

  1. Introduzione: dati e tipi.
  2. Variabili e memoria: la dichiarazione e l’inizializzazione con le graffe, l’assegnamento, la traccia dello stato, i nomi e la variabile non inizializzata.
  3. Gli interi e la loro rappresentazione: le basi 2 e 16, l’intervallo di ogni tipo, il complemento a due e l’overflow con segno e senza segno.
  4. I numeri in virgola mobile: perché 0,10{,}1 non è esatto, l’arrotondamento accumulato, il confronto con tolleranza, l’infinito e il valore non numerico.
  5. Caratteri, booleani, conversioni e costanti: il codice di un carattere, le conversioni implicite ed esplicite, il rifiuto delle conversioni che perdono informazione, const, constexpr e auto.
  6. Sintesi e sfide: dati e tipi.

Unità 3. Espressioni e operatori

Il suo obiettivo è insegnare a costruire espressioni il cui valore si possa prevedere con certezza: quale operazione si fa per prima, di che tipo è il risultato, quali effetti collaterali ha e quali espressioni sono prive di significato. L’unità non ha ancora decisioni né cicli: le condizioni si valutano e si stampano.

  1. Introduzione: espressioni e operatori.
  2. Aritmetica: il quoziente e il resto con i negativi, l’albero di un’espressione, la precedenza, l’associatività e il tipo del risultato.
  3. Relazioni e logica: le condizioni composte, la loro negazione secondo De Morgan, la valutazione a cortocircuito e l’errore di 1 < x < 5.
  4. Assegnamento, effetti collaterali e comportamento indefinito: il valore e l’effetto di un’espressione, gli incrementi, l’ordine di valutazione e il catalogo di ciò che lo standard non definisce.
  5. Bit e libreria matematica: gli operatori bit a bit, gli scorrimenti, le maschere, <cmath> e le costanti di <numbers>.
  6. Sintesi e sfide: espressioni e operatori.

Unità 4. Controllo del flusso

Il suo obiettivo è insegnare a governare l’ordine di esecuzione con decisioni e cicli, e a ragionare sulla loro correttezza: che ogni caso sia trattato, che ogni ciclo faccia ciò che deve e che termini. Realizza ciò che la prima lezione del corso aveva annunciato: la decisione e la ripetizione, ora scritte e verificate.

  1. Introduzione: controllo del flusso.
  2. Decisioni: if ed else concatenati e annidati, i casi disgiunti ed esaustivi, switch con enum class e le sue trappole.
  3. Cicli: while, do e for, la traccia di un ciclo, la conversione tra le sue forme e i cicli annidati.
  4. Progettare cicli corretti: gli schemi contatore, accumulatore, massimo e sentinella, l’invariante, la terminazione e un primo contatto con il debugger.
  5. Algoritmi numerici: la primalità, l’algoritmo di Euclide, il cambio di base, lo schema di Horner e la bisezione, ciascuno con la sua giustificazione.
  6. Sintesi e sfide: controllo del flusso.

Unità 5. Funzioni

Il suo obiettivo è insegnare a scomporre un problema in parti con un nome, ciascuna con un’interfaccia e un contratto, e a risolvere problemi per ricorsione. Converte i programmi lunghi dell’unità 4 in programmi composti di parti che si verificano una per una.

  1. Introduzione: funzioni.
  2. Funzioni: la dichiarazione, la definizione e la chiamata, lo stack delle chiamate, la precondizione e la postcondizione, e la funzione che non restituisce valore.
  3. Parametri e ambito di visibilità: il passaggio per valore, per riferimento e per riferimento costante, i risultati multipli con i binding strutturati, l’ambito di visibilità e la durata delle variabili.
  4. Progettare con le funzioni: il raffinamento successivo, l’interfaccia prima del corpo, la regola «una funzione, un compito» e l’overloading.
  5. Ricorsione: il caso base e quello ricorsivo, la correttezza e la terminazione per induzione, lo stack e il costo della ricorsione.
  6. Sintesi e sfide: funzioni.

Unità 6. Dati composti

Il suo obiettivo è insegnare a rappresentare collezioni di dati e a elaborarle con gli algoritmi di base, scritti a mano e presi dalla libreria, stimando quanto costano. Porta dal dato isolato alla collezione, e dalla domanda «è corretto?» alla domanda «quanto costa?».

  1. Introduzione: dati composti.
  2. Array: std::array, la scansione con indici e per intervallo, l’indice entro i limiti e gli array C come contrasto.
  3. Vettori: std::vector, la lettura di una serie di lunghezza sconosciuta, l’accumulo, il filtraggio e la trasformazione, la dimensione e la capacità.
  4. Il costo di un algoritmo: il conteggio delle operazioni dominanti, gli ordini di crescita e la misura del tempo.
  5. Cercare e ordinare: la ricerca lineare e quella binaria, l’ordinamento per selezione e per inserimento con i loro invarianti, e gli algoritmi della libreria.
  6. Tabelle: le tabelle bidimensionali, le loro scansioni per righe e per colonne, la trasposta, il prodotto e la tabella in un vettore piatto.
  7. Record: struct, gli inizializzatori designati, i record come parametri e l’ordinamento per un campo.
  8. Sintesi e sfide: dati composti.

Unità 7. Testo e file

Il suo obiettivo è insegnare a trattare il testo come dato e a far sì che un programma legga e scriva file senza rompersi davanti a un input malformato. Alla fine, lo studente può scrivere programmi che elaborano dati reali, accenti ed errori compresi.

  1. Introduzione: testo e file.
  2. Stringhe: std::string e le sue operazioni, npos, le viste std::string_view e le stringhe C come contrasto.
  3. Il testo non è ASCII: la codifica UTF-8, i byte e i caratteri, e le operazioni che rompono il testo.
  4. Leggere bene: std::getline, lo stato del flusso, il ripristino dopo un dato non valido e la validazione con std::from_chars.
  5. File di testo: aprire, leggere riga per riga, scrivere, aggiungere, ed elaborare un file di valori separati da virgole.
  6. Sintesi e sfide: testo e file.

Unità 8. Programmi più grandi

Il suo obiettivo è insegnare a organizzare, verificare e fare il debug di programmi che non stanno più in un file né nella testa. Chiude il cerchio che la prima lezione aveva aperto: specificare, costruire, verificare; la sua sintesi termina con un progetto finale che riunisce l’intero corso.

  1. Introduzione: programmi più grandi.
  2. Più file e librerie proprie: l’header e l’implementazione, le guardie di inclusione, la regola della definizione unica e la compilazione separata.
  3. Testare: i casi di test a partire dalla specifica, i test automatici, assert e static_assert.
  4. Debugging: il debugger di Visual Studio Code, i punti di interruzione, lo stack delle chiamate e una strategia sistematica per localizzare un errore.
  5. Sintesi e sfide: programmi più grandi.

Unità 9. Chiusura del corso

Consta della lezione Bilancio finale di Programmazione in C++ I, che ricorda i sette obiettivi globali, mostra in una figura in quali unità e lezioni si lavora ciascuno, riunisce per obiettivo il primo tentativo delle valutazioni d’uscita e indica dove prosegue lo studio.

Impegno e crediti

L’impegno di un corso non si misura con le ore di lezione frontale, che in un corso asincrono non esistono, ma con il lavoro totale dello studente: leggere, scrivere e compilare gli esempi, risolvere problemi e ripassare. La stima segue lo stesso metodo in tutti i corsi del sito: conta il tempo di lettura del testo di ciascuna lezione, compreso il codice dei suoi esempi, a cento parole al minuto, venti minuti per problema risolto, trentacinque per problema proposto, quarantacinque per sfida e dodici per valutazione d’ingresso o d’uscita, e aggiunge un margine di un decimo per il ripasso.

Risultati

Le lezioni del corso contengono nel complesso 105105 problemi risolti, 105105 problemi proposti e 5252 sfide, oltre a sedici valutazioni di unità e settanta esercizi brevi che il sito corregge all’istante. L’impegno stimato per unità, arrotondato all’ora, è il seguente:

  • Unità 0, presentazione del corso: 11 ora.
  • Unità 1, primi passi: 2828 ore.
  • Unità 2, dati e tipi: 2222 ore.
  • Unità 3, espressioni e operatori: 2222 ore.
  • Unità 4, controllo del flusso: 2222 ore.
  • Unità 5, funzioni: 2222 ore.
  • Unità 6, dati composti: 3030 ore.
  • Unità 7, testo e file: 2121 ore.
  • Unità 8, programmi più grandi: 2020 ore.
  • Unità 9, chiusura del corso: meno di 11 ora.

Il totale è di circa 188188 ore di lavoro dello studente. Conviene leggere questa cifra per ciò che è: una stima per uno studente che scrive e compila tutti gli esempi e risolve tutti i problemi e tutte le sfide. Chi ha già programmato in un altro linguaggio impiegherà parecchio meno nelle prime unità; chi non ha mai installato un programma né letto un messaggio di errore può averne bisogno di più, soprattutto nell’unità 1, che è anche, con quella dei dati composti, la più estesa.

Ora, la cifra è nominale: è quella che fissa i crediti del corso, e presuppone una lettura a ritmo costante e tre quarti d’ora per sfida. Finché non sarà misurata con studenti reali, conviene accompagnarla con una fascia realistica. Se la lettura procedesse alla metà del ritmo supposto, come accade quando ogni esempio si scrive a mano, si compila e si altera per vedere che cosa cambia, e ogni sfida richiedesse settantacinque minuti invece di quarantacinque, lo stesso metodo darebbe circa 250250 ore. Pertanto, l’impegno prevedibile si colloca tra circa 188188 e circa 250250 ore. L’estremo superiore è una stima prudente, non una misura; la fascia sarà corretta quando si conoscerà il tempo che impiega chi studia il corso, e fino ad allora i crediti si calcolano con la cifra nominale.

Ritmo suggerito

Per denominare i ritmi, questo piano usa lo stesso criterio degli altri piani del sito: l’impegno a tempo pieno è quello di un anno accademico di 6060 crediti, circa 16501650 ore distribuite in una quarantina di settimane, cioè circa 4040 ore settimanali, e il tempo parziale ne è la metà, circa 2020 ore settimanali. Ne segue un ritmo raccomandato di un semestre di sedici settimane, con circa 1212 ore settimanali di studio, poco meno della terza parte del tempo pieno; con la fascia realistica, da 1212 a 1616 ore settimanali. Quel semestre può essere condiviso con Logica Matematica I, le cui tavole di verità e la cui idea di dimostrazione si riconoscono nelle condizioni e negli invarianti di questo corso.

Un ritmo intensivo, di circa 2020 ore settimanali, cioè a tempo parziale, completa il corso in nove o dieci settimane. Un ritmo più disteso, di circa 66 ore settimanali, lo porta a due semestri, che se lo ripartiscono quasi a metà: le unità da 0 a 4, dall’algoritmo al controllo del flusso, occupano circa 9595 ore, e le unità da 5 a 9, dalle funzioni al progetto finale, circa 9393. Il taglio è naturale: al termine dell’unità 4, lo studente scrive già programmi completi in un solo main, e la seconda metà gli insegna a organizzarli.

Crediti

I crediti di questo sito misurano l’impegno totale che un corso richiede a chi lo studia (la lettura, i problemi, le sfide e il ripasso), e non le ore di lezione. Un credito equivale a circa 27,527{,}5 ore di lavoro, e un anno a tempo pieno a 6060 crediti, cioè a circa 16501650 ore; è lo stesso criterio con cui la sottosezione precedente denomina i ritmi di studio.

Ora, dividendo le 188188 ore stimate per 27,527{,}5 si ottiene 6,8, che si arrotonda a 77. Se si ammette per il credito una fascia da 2525 a 3030 ore di lavoro, il numero CC di crediti del corso risulta compreso tra 6,3 e 7,5:

(18830)≤C≤(18825)\left( \frac{188}{30} \right) \leq C \leq \left( \frac{188}{25} \right)

Pertanto, il corso ha 77 crediti, poco più della nona parte di un anno a tempo pieno. La cifra non è una svista: il corso dura quanto deve durare per insegnare i fondamenti, e non è stato accorciato per adattarsi a un numero tondo di crediti.

La loro utilità è pratica: permettono di confrontare tra loro il carico dei corsi del sito e di decidere quanti convenga studiarne contemporaneamente senza superare il tempo disponibile. Chi possa dedicare, per esempio, un tempo parziale a un semestre di sedici settimane, cioè circa 320320 ore, può affrontare corsi che sommino circa 1212 crediti; dedicando a tempo pieno lo stesso semestre, circa 2323.

Questi crediti sono una misura propria del sito per pianificare lo studio: non corrispondono ad alcun sistema ufficiale di crediti né comportano il riconoscimento accademico di alcuna istituzione.

Progettazione del corso

Modalità

Il corso è asincrono, online e a ritmo autonomo: non ha date, sessioni dal vivo né scadenze, e ogni studente avanza alla velocità che gli consentono la sua preparazione e il suo tempo. Sarà offerto nelle quattordici lingue del sito. Gli identificatori dei programmi sono in inglese e sono uguali in tutte le versioni; i commenti e i messaggi che i programmi mostrano, invece, sono nella lingua della pagina, in modo che ogni versione si compila e si esegue con il proprio output. Le lezioni dichiarano quali altre lezioni conviene aver studiato prima, in modo che l’ordine raccomandato sia esplicito.

La struttura di una lezione

Tutte le lezioni di contenuto seguono la stessa struttura. Cominciano con un riassunto e con un elenco di obiettivi di apprendimento propri, formulati come prestazioni osservabili. Segue l’esposizione, il cui nucleo sono i programmi. Ogni programma completo del corso è stato compilato, prima di costruire il sito, con lo standard C++23, con tutti gli avvisi attivati e con i sanitizer degli errori di memoria e del comportamento indefinito, ed è stato eseguito; ciò che si pubblica sotto il codice è il suo output reale, oppure i messaggi reali del compilatore quando ciò che si insegna è un errore o un avviso, oppure il rapporto reale del sanitizer quando il programma incorre in un comportamento indefinito. Se è così, nessun output del corso è scritto a mano: il sito verifica, quando viene costruito, che ogni programma abbia salvato l’output della sua versione attuale, e non sarebbe pubblicato se a qualcuno mancasse. Altre figure animano passo per passo ciò che il testo ragiona: le celle della memoria con il loro nome e il loro valore, lo stack delle chiamate, la scansione di un ciclo, i bit di un numero.

Chiudono ogni lezione tre problemi risolti, con la loro soluzione passo per passo, e tre problemi proposti, ciascuno con un suggerimento ripiegato e, dopo, la risposta, talvolta accompagnata da una soluzione sviluppata che conviene aprire solo dopo aver scritto la propria. I problemi richiedono un’idea, mai la ripetizione meccanica di un esempio, e usano solo ciò che è stato insegnato fino a quella lezione. Ogni lezione include, inoltre, due esercizi brevi che il sito corregge all’istante: in uno si prevede l’output esatto di un programma, e nell’altro si completa o si corregge un frammento. Conviene sapere che cosa fa e che cosa non fa questa correzione: il sito non compila il codice dello studente, ma verifica con dei pattern che l’essenziale sia presente e che l’errore cercato sia scomparso. Pertanto, un esercizio superato nel sito deve ancora essere compilato ed eseguito sul proprio computer, che è l’unico giudice definitivo.

Le lezioni di introduzione e di sintesi hanno una struttura propria. L’introduzione presenta l’unità, le sue lezioni e il filo che le unisce, ciò che dà per scontato e il problema che risolve, e contiene la valutazione d’ingresso. La sintesi ricostruisce la catena dei concetti dell’unità, con la lezione in cui si stabilisce ogni anello e ciò che ciascuno usa; contiene due sezioni fisse, «Ciò che vale in qualsiasi linguaggio» e «Il comportamento indefinito dell’unità», la valutazione d’uscita e sei sfide (dieci nell’ultima unità, il cui progetto finale si scompone in fasi), ciascuna con suggerimento, risposta e, quando il ragionamento lo merita, una soluzione sviluppata ripiegata.

Il lettore vocale

Ogni lezione si può ascoltare ad alta voce con il lettore del sito, che usa le voci del dispositivo stesso: il testo si legge così come è scritto, il codice citato in esso si pronuncia con senso e i blocchi di codice si annunciano, senza essere letti carattere per carattere. Ciò permette di ripassare l’esposizione lontano dallo schermo; i programmi, invece, richiedono sempre l’editor.

Valutazione

Il corso non assegna voti: la sua valutazione è formativa e si appoggia sull’autovalutazione. I problemi proposti permettono di verificare il raggiungimento degli obiettivi di ogni lezione, e le sfide, quello degli obiettivi di ogni unità; in tutti, il giudice ultimo è il compilatore, con i suoi avvisi e i suoi sanitizer, perché un programma che compila senza avvisi, supera i suoi casi di test e non sveglia alcun sanitizer è una prova che non ammette discussione, benché, come insegna la lezione sui test, non una dimostrazione.

A ciò si aggiungono le valutazioni di unità. Quella d’ingresso, nell’introduzione, è un’autovalutazione breve che misura il punto di partenza; quella d’uscita, nella sintesi e prima delle sfide, misura ciò che si è ottenuto e si confronta con quella d’ingresso. Entrambe si correggono nel browser stesso: ogni risposta riceve una spiegazione, ogni errore tipico un’osservazione che lo identifica, e ogni domanda rimanda alla lezione che conviene ripassare. Nessuna di esse assegna voti né certifica, e i loro risultati si salvano solo nel browser dello studente; la convalida delle abilità acquisite si farà più avanti su un’altra piattaforma. Il bilancio finale riunisce il primo tentativo di ogni valutazione d’uscita in un profilo per obiettivo globale.

Gli assistenti di intelligenza artificiale

Esistono oggi assistenti di intelligenza artificiale capaci di scrivere in pochi secondi la soluzione di quasi qualsiasi problema di questo corso. Conviene dire con chiarezza quale posto spetti loro, e la ragione è la stessa che giustifica tutto il resto: il corso non allena la produzione di programmi, ma una capacità, quella di pensare un problema fino a convertirlo in un programma corretto. Se è così, delegare l’esercizio a un assistente annulla l’esercizio, così come guardare la risposta di un problema prima di tentarlo lo converte in lettura. Il programma risultante può essere corretto; chi lo consegna, invece, non ha imparato a scriverlo, e non saprà farlo quando l’assistente sbaglierà, che è proprio quando ce n’è più bisogno.

Ne segue una regola d’uso, e non un divieto. Un assistente serve a spiegare un messaggio di errore che non si capisce, a proporre casi di test a cui non si era pensato, a confrontare una propria soluzione già terminata con un’altra, o a chiarire un’idea dell’esposizione da un’altra angolazione. Non serve a scrivere gli esercizi, i problemi né le sfide. E in tutti i casi vale un principio che si applica a qualsiasi codice altrui, che venga da un assistente, da un forum o da un compagno: si legge, si testa e si capisce prima di accettarlo. Un assistente può produrre codice che compila e sembra corretto, ma che incorre in un comportamento indefinito o usa una forma antiquata del linguaggio; lo studente che ha fatto il corso saprà riconoscerlo, e quello che ha delegato il corso, no.

Come studiare questo corso

Il corso è pensato per essere studiato in ordine. Ogni unità presuppone le precedenti: le espressioni si scrivono con i tipi della seconda unità, i cicli con le espressioni della terza, le funzioni incapsulano i cicli della quarta, e i dati composti si elaborano con funzioni; saltare una lezione obbliga, prima o poi, a tornarci sopra.

La regola principale è una sola: scrivere e compilare ogni esempio. La programmazione non si impara leggendo, ma scrivendo, compilando e leggendo ciò che il compilatore risponde. Per questo, lo studente installa il proprio ambiente seguendo la lezione Installare l’ambiente su Windows, macOS e Linux, e riproduce ogni programma nel suo Visual Studio Code, con le opzioni che la lezione Compilare, eseguire e configurare insegna a fissare; chi usa un computer su cui non può installare programmi dispone, in via provvisoria, di un compilatore online, che la lezione di installazione descrive. Conviene scrivere gli esempi a mano invece di copiarli, perché la mano trattiene ciò che l’occhio lascia sfuggire; conviene prevedere l’output di ogni programma prima di eseguirlo, perché la differenza tra ciò che si è previsto e ciò che si è ottenuto è esattamente ciò che resta da imparare; e conviene leggere ogni avviso del compilatore come una domanda che merita risposta, e non come un rumore che si tollera. Dopo che un programma funziona, conviene inoltre alterarlo di proposito (cancellare un punto e virgola, cambiare un tipo, uscire da un vettore) e leggere ciò che dicono il compilatore o il sanitizer, perché ogni errore provocato è un errore che poi si riconoscerà.

Ogni unità ha due punti di controllo. La valutazione d’ingresso, nell’introduzione, è il punto di partenza: conviene rispondervi prima di studiare l’unità, senza consultare nulla, e annotare le lezioni a cui rimandano gli errori. La valutazione d’uscita, nella sintesi e prima delle sfide, è il confronto: vi si risponde al termine delle lezioni, anche qui senza consultare nulla, e la si confronta con quella d’ingresso. In entrambe conta il primo tentativo, che è quello che si confronta e quello che il profilo del bilancio finale raccoglie. Nei problemi proposti e nelle sfide, conviene aprire il suggerimento solo quando il tentativo si è esaurito, e la risposta solo alla fine, per verificare. Un problema la cui risposta si è consultata prima di compilare la propria non è stato risolto, ma letto.

Controllo di versione

Infine, conviene conservare il lavoro del corso con un sistema di controllo di versione fin dal primo programma. Git registra ogni stato di una cartella di lavoro, permette di tornare a uno qualsiasi di essi e mostra che cosa è cambiato tra due; per chi programma, ciò significa poter sperimentare senza paura di perdere ciò che funzionava, e localizzare, confrontando versioni, la modifica che ha introdotto un errore, il che porta nella storia del programma la bisezione che insegna la lezione sul debugging. Il corso non insegna Git, perché un altro corso del sito lo fa già: la lezione Scrivere in gruppo, del corso LaTeX per la Scrittura Accademica, spiega il flusso di lavoro minimo, quali file si conservano e quali si escludono, e come si risolve un conflitto. Ciò che lì vale per i sorgenti di un documento vale ugualmente per quelli di un programma; basta escludere, invece dei file ausiliari di LaTeX, gli eseguibili e i file oggetto che la compilazione rigenera.