Dal C al C++/Introduzione

Indice del libro

I destinatari del libro

modifica

Il linguaggio C++ deriva dal linguaggio C di cui è sostanzialmente un soprainsieme. Pertanto, alcuni autori consigliano di imparare prima il linguaggio C anche a chi ha il solo scopo di saper programmare in C++, in quanto molto codice C++ esistente nell'industria è in realtà un incapsulamento di vecchio codice C; se non si conosce il linguaggio C, si fatica a comprendere tale codice. Inoltre esistono molte librerie scritte in C; tali librerie sono utilizzabili anche da programmi scritti in C++ a condizione di conoscere le differenze tra i due linguaggi. Pertanto per tali autori un buon programmatore C++ dovrebbe conoscere anche il linguaggio C.

Altri autori non sono d'accordo, cioè ritengono preferibile iniziare imparando direttamente il linguaggio C++ senza perdere tempo e rischiare di confondersi le idee con l'apprendimento di un altro linguaggio: quando si rendesse necessario, si potrà passare a studiare anche il C.

Comunque, esistono già molte persone che sanno programmare in linguaggio C e che vogliono imparare il C++. Per tali persone sarebbe una perdita di tempo rivedere tutti i concetti del linguaggio C presenti anche nel C++.

Inoltre, per spiegare un concetto, si deve sempre fare riferimento ad altri concetti già noti. Sapendo che il lettore conosce il linguaggio C, si possono spiegare i costrutti del C++ in termini di costrutti del C. Senza poter utilizzare un linguaggio di programmazione, si sarebbe dovuto ricorrere a un formalismo matematico o a esempi della vita quotidiana. Ma i formalismi matematici sono difficili e non sono alla portata di tutti, e gli esempi della vita quotidiana sono così lontani dai concetti di programmazione che le spiegazioni sarebbero state troppo imprecise o troppo lunghe.

Pertanto, questo testo si rivolge a persone che abbiano già una discreta conoscenza del linguaggio C, in quanto non spiega approfonditamente i concetti del C++ già presenti nel linguaggio C, e in quanto utilizza alcuni concetti del linguaggio C per spiegare i nuovi concetti del C++.

Chi volesse imparare il linguaggio C, può leggere il wikibook sul linguaggio C.

Infine, si noti che questo è un tutorial, non un manuale di riferimento, quindi non ha la pretesa di essere completo e dettagliato. Tuttavia, dopo aver appreso i concetti spiegati in questo libro, il lettore dovrebbe essere in grado di programmare in C++, purché tenga a fianco (oppure online) un manuale di riferimento sul linguaggio e sulle librerie che intende utilizzare (standard e non standard).

La struttura del libro

modifica

Il libro ha una difficoltà crescente. Chi fatica a comprendere i primi capitoli dovrebbe sospendere la lettura e studiare meglio il linguaggio C.

La prima parte è l'introduzione.

La seconda parte descrive sommariamente la programmazione all'interno di una funzione, che è poi quello che i programmatori fanno per la maggior parte del tempo. In questa parte si vede come il programmatore applicativo può utilizzare le funzioni, le classi e altri costrutti forniti da librerie già esistenti. In particolare si vede come utilizzare le parti più utili e importanti della libreria standard.

Nella terza parte si approfondiscono le tecniche di sviluppo di piccoli programmi che utilizzino librerie già esistenti scritte in C++, senza ancora entrare nel merito di come si sviluppa una libreria.

Nella quarta parte si affronta lo sviluppo di grandi applicazioni e di librerie, cioè di codice destinato a essere richiamato in più punti.

Nella quinta parte si affronta una tecnica molto avanzata, chiamata metaprogrammazione, con la quale si effettuano preelaborazioni in fase di compilazione.

Nella sesta e ultima parte si descrive come la progettazione del software si modifica passando dal C al C++. Ovviamente, per apprezzarla bisognerebbe aver progettato qualche programma in C di dimensione non banale.

Gli esempi usano esclusivamente il linguaggio C++ e la sua libreria standard, secondo lo standard del 1998.

Questo significa che l'interfaccia utente è esclusivamente a riga di comando, ossia è basata su console (chiamata anche "shell", o "prompt dei comandi").

Impieghi del linguaggio

modifica

Il linguaggio C++ è la più diffusa estensione del linguaggio C, con l'aggiunta di numerose caratteristiche per consentire la programmazione orientata agli oggetti, la programmazione generica e la metaprogrammazione. Questi tre paradigmi di programmazione si uniscono alla programmazione procedurale propria del C, per formare un linguaggio multi-paradigma.

Il linguaggio C è nato con lo scopo di scrivere il sistema operativo Unix e le utility di corredo. Si tratta quindi prevalentemente di software di sistema o di programmi con interfaccia utente a riga di comando (in inglese, "command-line interface"). Con le eccezioni del sistema operativo e del compilatore C stesso, i programmi sviluppati in C inizialmente erano tipicamente piuttosto piccoli. In realtà, lo stesso sistema operativo Unix degli anni 1970 e lo stesso compilatore C non erano molto complessi, se paragonati, rispettivamente, ad un sistema operativo Unix di oggi, e a un compilatore C++.

Man mano che gli utenti del linguaggio C hanno incominciato a sviluppare applicazioni di maggiori dimensioni, hanno trovato difficoltà sempre maggiori a gestire la complessità inerente in tali applicazioni. In effetti, il linguaggio C ha molti limitazioni nella capacità di astrazione e nello sviluppo di librerie di codice che siano allo stesso tempo riutilizzabili, efficienti, e tipizzate.

Il linguaggio C++ è stato inventato e viene tuttora proposto come sostituto del linguaggio C per lo sviluppo di qualunque tipo di applicazione. Infatti, con il C++ si può fare tutto ciò che si può fare in C, ma quando risultasse opportuno, con esso si possono adottare paradigmi di programmazione di livello più alto, come la programmazione orientata agli oggetti, la programmazione generica, e la metaprogrammazione.

Tali paradigmi tuttavia non sono forzati. Sta alla disciplina dello sviluppatore adottare il paradigma che meglio si attaglia alle esigenze dell'applicazione che si vuole creare. È anche ragionevole adottare diversi paradigmi per diverse porzioni di una grande applicazione.

Lo svantaggio principale dell'uso del C++, rispetto al C, è la difficoltà di uso e di apprendimento, che risulta grandemente accresciuta dal moltiplicarsi dei paradigmi, dei costrutti linguistici, dalle parole-chiave, e soprattutto dal notevole estendersi della libreria standard.

Un altro svantaggio sta nel fatto che il compilatore è più complesso e quindi richiede più risorse per il lavoro di compilazione.

Non è vero, come alcuni credono, che il C++ sia necessariamente meno efficiente del C. Si può scrivere un programma in C che sia valido anche in C++; e per tale programma un buon compilatore C++ genera lo stesso codice macchina di un buon compilatore C. In taluni casi, la libreria standard del C++ è più efficiente di quella del C.

Quello che è invece vero è che, adottando il paradigma di programmazione orientata agli oggetti, si tende ad effettuare astrazioni così elevate da non rendersi conto di usare strutture dati o algoritmi inefficienti. Quindi spesso i programmi che usano uno stile orientato agli oggetti sono inefficienti, ma solo perché il progettista o il programmatore non si è reso conto delle implicazioni prestazionali delle scelte progettuali.

Un altro potenziale problema di inefficienza si ha con un uso massiccio di template, in particolare nella metaprogrammazione. Siccome i template permettono di generare codice macchina arbitrariamente grande, se non si fa attenzione a tale aspetto, si corre il rischio di un'esplosione di codice macchina (in inglese, "code bloat"), che appesantisce il programma.

Alcuni ulteriori problemi di efficienza si hanno nell'uso della libreria standard. Infatti, le implementazioni di alcune funzioni di tale libreria, per favorire la comodità d'utilizzo o perché non ottimizzate, risultano meno efficienti delle funzioni equivalenti della libreria standard del C. Sta al programmatore, in fase di ottimizzazione del sistema, sostituire l'utilizzo delle funzioni che rallentano significativamente l'esecuzione con funzioni appositamente sviluppate.

A causa della difficoltà del linguaggio, il C++ non è destinato a persone con una formazione commerciale, come i ragionieri programmatori, ma piuttosto a persone con una formazione tecnico/industriale, come i periti informatici, i laureati in informatica, o i laureati in ingegneria elettronica.

I settori applicativi di questo linguaggio sono disparati, ma sono essenzialmente quelli per i quali negli anni '80 si usavano il C e il Pascal, cioè lo sviluppo di software di sistema, e lo sviluppo di software applicativo ad alte prestazioni.

Finora, per la programmazione di piccoli sistemi dedicati (i cosiddetti sistemi embedded), il C++ è ancora poco usato, in quanto gli stringenti requisiti di memoria non consentono l'utilizzo della libreria standard e dell'allocazione dinamica della memoria. Tuttavia, è possibile usare il linguaggio C++ astenendosi dall'utilizzo della libreria standard e della memoria dinamica. Inoltre, man mano che la disponibilità di memoria dei sistemi embedded si espande, sarà sempre più appropriato usare tutte le funzionalità del C++ anche per tali sistemi.

La libreria standard del C++, come già quella del C, non comprende funzioni per la gestione dell'interfaccia utente, quindi non sono gestiti né tastiera, né mouse, né altri dispositivi di ingresso, e non sono gestiti né schermi in modalità testuale né schermi in modalità grafica. Pertanto, gli unici programmi portabili e che utilizzano solo la libreria standard sono quelli privi di interfaccia utente oppure con interfaccia utente a riga di comando.

Ovviamente è possibile realizzare programmi interattivi e programmi grafici; anzi, moltissimi programmi interattivi e grafici sono stati scritti in C++, ma questo viene fatto appoggiandosi a librerie esterne non standard o direttamente al sistema operativo.

Per sviluppare un'applicazione interattiva è pertanto necessario prima scegliere una libreria appropriata. In questo libro non si parla di tali librerie né di altre librerie non standard.

Panoramica

modifica

Il C++, come il C, è non completamente specificato, nel senso che numerose espressioni sintatticamente corrette, e quindi ammesse dal compilatore, non hanno una semantica definita, e quindi in fase di esecuzione possono produrre risultati dipendenti dall'implementazione del compilatore, se non addirittura risultati variabili da un'esecuzione all'altra. Per esempio, si consideri questo programma, valido sia in C che in C++:

void f(int a, int b) { }
int g() { return 1; }
int h() { return 2; }
int main() {
    f(g(), h());
    *(char *)0 = 3;
}

Nella prima istruzione della funzione "main", sia il C che il C++ non definiscono se verrà chiamata prima la funzione "g" o la funzione "h".

La seconda istruzione della funzione "main" scrive all'indirizzo zero dello spazio di indirizzamento del processo. Questa operazione è notoriamente illegale e produce un errore in fase di esecuzione, ma viene accettata sia dai compilatori C che dai compilatori C++.

Le caratteristiche più importanti che il C++ aggiunge al C sono le seguenti:

  • Le classi, con funzioni virtuali ed ereditarietà multipla
  • Le eccezioni
  • I namespace
  • I template di classe
  • I template di funzione
  • La libreria di ingresso/uscita
  • La libreria per gestire stringhe
  • La libreria per gestire contenitori

Ecco un programma C++ che esemplifica l'utilizzo di tutte le suddette caratteristiche:

#include <iostream>
#include <vector>
#include <complex>
using namespace std;
int main() {
    complex<double> c1(2, 3); // c1 è un numero complesso
    complex<double> *pc2; // pc2 è un puntatore a un numero complesso
    const string s = "Risultato: "; // s è un oggetto stringa
    try {
        pc2 = new complex<double>(5, min(17, 12));
    } catch (...) {
        cerr << "Memoria insufficiente\n";
        return 1;
    }
    vector<complex<double> > vcd;
    vcd.push_back(c1 + *pc2);
    cout << s << vcd.front();
    delete pc2;
}

Diciamo subito che cosa fa questo programma. Se non riuscisse ad allocare una manciata di byte di memoria, il che è praticamente impossibile, emetterebbe sulla console il messaggio d'errore "Memoria insufficiente", e terminerebbe. Altrimenti, cioè quasi sicuramente, emette sulla console la scritta "Risultato: (7,15)". Tale valore rappresenta il numero complesso 7 + 15i, che si ottiene sommando 2 + 3i a 5 + 12i.

Esaminiamo il significato di ogni riga del programma. Data la concentrazione di molti concetti disparati, il lettore non si spaventi se non tutto risultasse chiaro.

#include <iostream>

Questa riga include le dichiarazioni della libreria di ingresso/uscita su canali (stream input/output), nonché gli oggetti su cui si basa l'ingresso/uscita su console. Ha la stessa finalità del file "stdio.h" della libreria standard del linguaggio C. Si noti che manca l'estensione ".h" dopo "iostream".

#include <vector>

Questa riga include le dichiarazioni della libreria che gestisce gli array a dimensione dinamica, chiamati appunto "vector".

#include <complex>

Questa riga include le dichiarazioni della libreria di gestione dei numeri complessi. Ricordiamo che i numeri complessi sono entità matematiche rappresentabili come coppie di numeri reali: la parte reale e la parte immaginaria. Il numero complesso "(7,15)" ha "7" come parte reale e "15" come parte immaginaria. La somma di due numeri complessi ha come parte reale la somma delle parti reali e come parte immaginaria la somma delle parti immaginarie.

using namespace std;

Questa riga dichiara che d'ora in poi in questa unità di compilazione, ogni identificatore verrà cercato non solo nello spazio dei nomi globale (cioè quello del linguaggio C), ma anche nello spazio dei nomi "std", cioè quello della libreria standard del C++.

int main() {

Questa riga definisce il punto di ingresso del programma, analogamente ai programmi in linguaggio C. La differenza sta nel fatto che il valore di ritorno è obbligatoriamente "int", ma, a differenza di tutte le altre funzioni che rendono un "int" è consentito omettere l'istruzione di return in fondo alla funzione, sottintendendo "return 0;".

    complex<double> c1(2, 3);

Questa riga definisce sullo stack una variabile identificata dal nome "c1", il cui tipo è "complex<double>", e che è inizializzata con la coppia di valori "2, 3". Il tipo "complex<double>" rappresenta un numero complesso in cui sia la parte reale che la parte immaginaria sono memorizzate in numeri a virgola mobile di tipo "double".

L'identificatore "complex" rappresenta il tipo di un oggetto che rappresenta un numero complesso, ma tale definizione è parametrizzata dal tipo dei campi contenenti la parte reale e la parte immaginaria. Un tale tipo parametrizzato, o tipo generico, in C++ viene chiamato template di classe, cioè modello di classe.

Specificando un tipo tra parentesi angolari, si istanzia il template, cioè si genera un tipo concreto definito dall'utente, che può essere usato in modo analogo ai tipi predefiniti.

I tipi definiti dall'utente sono detti classi.

L'inizializzazione di un numero complesso richiede due valori, uno per la parte reale e uno per la parte immaginaria. Pertanto, passando i valori di inizializzazione all'oggetto in corso di creazione, questo riceve tali valori e li usa per inizializzare i propri campi. La sintassi è analoga a quella di una chiamata di funzione.

    complex<double> *pc2;

Questa riga definisce sullo stack un puntatore di nome "pc2", che può contenere l'indirizzo di un oggetto del tipo "complex<double>", appena visto. Tale variabile (puntatore) non è inizializzata, pertanto il suo contenuto è indefinito, e sarebbe un errore logico usarne il valore prima di assegnargli un valore valido.

    const string s = "Risultato: ";

Questa riga definisce sullo stack una variabile di nome "s", di tipo "const string", inizializzata dalla costante di stringa "Risultato: ".

Il tipo "string" non è un tipo predefinito ma una classe standard, cioè un tipo definito dall'utente, facente parte della libreria standard del C++.

Il modificatore "const" significa non modificabile, ossia a sola lettura. Le variabili il cui tipo non ha il modificatore "const" possono essere sia lette che scritte, mentre per quelle il cui tipo ha il modificatore "const" (quasi) ogni tentativo, diretto o indiretto, di scriverle produce un errore di compilazione.

L'inizializzazione segue lo stile delle inizializzazioni in linguaggio C. Una sintassi alternativa ma equivalente sarebbe stata la seguente:

    const string s("Risultato: ");

Se l'inizializzatore è uno solo, è possibile scegliere tra le due notazioni; mentre se se ci sono più inizializzatori, come nel caso della variabile "c1" sopra, è ammessa solo la notazione funzionale.

    try {

Questa riga inizia un blocco "try", utilizzato per la gestione delle eccezioni. Per eccezione si intende una situazione anomala rilevata all'interno di una funzione di libreria, e a causa della quale la funzione stessa non è in grado di completare il suo compito. Per segnalare al chiamante il proprio fallimento, la funzione termina sollevando un'eccezione.

Quando il codice contenuto all'interno di un blocco "try" solleva un'eccezione, il controllo passa immediatamente alla fine del blocco "try" stesso, dove c'è il blocco "catch".

        pc2 = new complex<double>(5, min(17, 12));

Questa riga contiene una chiamata all'operatore "new", che è l'operatore predefinito per allocare dinamicamente oggetti nella memoria libera.

È simile alla funzione "malloc", ma mentre quest'ultima alloca una sequenza di byte, l'operatore new alloca lo spazio necessario per contenere un oggetto del tipo specificato, ed eventualmente inizializza tale oggetto.

Inoltre, il puntatore reso da "new" non è di tipo puntatore a void, bensì di tipo puntatore a un oggetto della classe specificata.

La riga sopra alloca lo spazio per un oggetto di tipo "complex<double>", inizializza tale oggetto con i valori passati tra parentesi, e assegna alla variabile "pc2" un puntatore a tale oggetto.

L'inizializzatore ha due argomenti; il primo è la costante "5", che diventerà la parte reale del numero complesso; il secondo è ottenuto dalla valutazione dell'espressione "min(17, 12)".

La funzione della libreria standard "min" prende due argomenti dello stesso tipo e rende quello dei due di valore inferiore. Non si tratta di una semplice funzione, ma di un template di funzione, in quanto, al variare del tipo degli argomenti, rappresenta una funzione diversa. In questo caso, è una funzione che prende due espressioni di tipo "int" e rende un valore di tipo "int".

    } catch (...) {

Questa riga inizia il blocco "catch" dell'istruzione "try/catch". Se all'interno del blocco "try" precedente viene sollevata un'eccezione, il controllo passa all'interno di questo blocco, che quindi costituisce il gestore dell'eccezione.

        cerr << "Memoria insufficiente\n";
        return 1;

Queste due righe costituiscono il corpo del blocco "catch" e vengono eseguite solo se nell'esecuzione del blocco "try" viene sollevata un'eccezione.

Siccome l'unica eccezione che poteva essere sollevata dal codice contenuto nel blocco "try" era l'eccezione di fallimento di allocazione della memoria, qui si avvisa l'utente che non è stato possibile allocare la memoria necessaria alla prosecuzione del programma, e si termina il programma rendendo il valore "1".

L'oggetto "cerr" rappresenta il canale di uscita dei messaggi d'errore su console ("CERR" sta per Console ERRor stream). Corrisponde al canale "stderr" della libreria standard del linguaggio C, dichiarato nel file di intestazione "stdio.h".

L'operatore "<<", oltre ad essere l'operatore di scorrimento a sinistra dei bit se applicato a una variabile intera, se applicato a un canale di uscita è l'operatore di inserimento nel canale. Si tratta quindi di un'istruzione di uscita (output).

Usando tale operatore tra un canale di uscita e un'espressione di tipo stringa, tale stringa viene inserita nel canale. Siccome "cerr" è un canale collegato alla console, l'effetto è di stampare la stringa sulla console.

    vector<complex<double> > vcd;

Questa riga definisce sullo stack una variabile di nome "vcd" e di tipo "vector<complex<double> >", senza inizializzarla esplicitamente.

Il template di classe "vector" definisce un array dinamico, cioè di lunghezza variabile nel corso della sua esistenza.

Se non specificato diversamente, come in questo caso, la sua lunghezza iniziale è zero.

Il tipo degli elementi contenuti in un vector è specificato tra parentesi angolari. In questo caso, gli elementi contenuti sono di tipo "complex<double>".

Sebbene non ci sia un inizializzatore esplicito, gli oggetti di un tipo istanziato da vector vengono inizializzati implicitamente. Pertanto, appena dopo questa dichiarazione, l'oggetto rappresentato dalla variabile "vcd" contiene un array dinamico di numeri complessi a precisione double, che non contiene ancora nessun elemento.

    vcd.push_back(c1 + *pc2);

Questa riga applica all'oggetto "vcd" la funzione membro (chiamata anche metodo) "push_back", passandole un parametro.

La funzione "push_back", serve ad aggiungere un elemento in fondo a un contenitore sequenziale. L'elemento che verrà aggiunto al vector è il parametro della funzione. In questo caso è un'espressione, che dovrà essere prima valutata.

Sia "c1" che "*pc2" sono oggetti di tipo "complex<double>". L'operatore "+", oltre ad essere l'operatore di somma tra numerosi tipi predefiniti (come "int" e "double"), è stato ridefinito come operatore di somma tra due oggetti di un tipo istanziato da "complex". Pertanto l'espressione "c1 + *pc2" effettua la somma tra due numeri complessi, e rende un altro numero complesso temporaneo. Il numero complesso generato dalla somma viene subito dopo aggiunto in fondo al vector "vcd".

    cout << s << vcd.front();

Questa riga usa un altro oggetto di tipo canale su console, "cout", che rappresenta il canale di uscita su console ("COUT" sta per Console OUTput stream). Corrisponde al canale "stdout" della libreria standard del linuaggio C, dichiarato nel file di intestazione "stdio.h".

L'espressione "cout << s" ha l'effetto di stampare sulla console la stringa contenuta nella variabile "s".

Il valore di tale espressione non è "void", ma è l'oggetto "cout" stesso. Pertanto la successiva espressione "<< vcd.front()" esegue un'altra operazione di uscita sulla console.

Questa volta l'espressione inviata in uscita non è una semplice stringa, bensì il valore reso dall'applicazione della funzione membro "front" all'oggetto "vcd". La funzione membro "front" non richiede parametri e rende il primo valore di una collezione. Nel nostro caso, rende il numero complesso che è stato inserito nell'istruzione precedente.

Come si vede, l'operatore di inserimento in canale ("<<") non accetta solo stringhe, ma anche altri tipi; in questo caso un'espressione di tipo "complex<double>".

    delete pc2;

Questa riga distrugge dalla memoria dinamica l'oggetto il cui indirizzo si trova nel puntatore "pc2".

Corrisponde alla funzione "free" della libreria standard del linguaggio C, ma è applicabile solo a indirizzi resi da chiamate a "new".