giovedì 29 novembre 2018

C e C++: Appunti di una riscoperta 8

Abbiamo le variabili, abbiamo le costanti, abbiamo i letterali stringa... ma come facciamo a visualizzarli a video?

Per i letterali stringa abbiamo già risolto il problema con la printf

printf("Hello World");

Ma la printf fa di più che stampare letterali stringa. E' il caso di approfondire tale funzione che trova molteplici implementazioni, tutte fondamentalmente simili fra loro, nel linguaggio C.

Iniziamo subito con il vedere il prototipo di questa funzione ossia come è dichiarata nel file di intestazione stdio.h.

int printf(const char *format, ...);

Ovviamente printf è il nome della funzione o identificatore o simbolo o token definito nel linguaggio. Prima del nome della funzione c'è int, che sappiamo essere il tipo intero. ciò indica che la funzione, al suo termine, restituisce un valore intero che potremo utilizzare o ignorare come abbiamo fatto fin'ora.

Il valore restituito dalla funzione è negativo se c'è stato un errore, altrimenti è il numero di byte inviati, nel caso della printf, allo standard output che è la console.

La printf accetta dei parametri, ossia dei valori che saranno oggetto di elaborazione da parte della funzione. Il primo è const char* format. Intanto il nome del parametro formale è format, mentre const ci dice che ciò che passiamo come argomento, il parametro attuale, non subirà modifiche. Il secondo argomento indicato da ... è un modo speciale, implementato dalla libreria standard stdarg.h, per indicare che segue un numero variabile fra, 0 e quanto ci occorre, di argomenti.

Il char* ci dice che siamo difronte a un puntatore al tipo char, ma per ora accettiamolo come una stringa di caratteri.

La printf cosa fa di questi argomenti? Mostrerà a video il contenuto della stringa format sostituendo ogni segnaposto al suo interno con il primo argomento non utilizzato, preso dalla lista degli argomenti variabili, e utilizzando il formato specificato dal segnaposto stesso.

Ogni segnaposto inizia con il carattere %. Ciò implica che, se vogliamo visualizzare semplicemente il carattere %, occorrerà utilizzare la combinazione %%. Vediamo quali sono i possibili tipi abbinabili ai segnaposto. Il tipo è obbligatorio e serve ad indicare alla printf come interpretare e in alcuni casi formattare l'argomento in esame. Vediamo quali sono i tipi.
  • %d o  %i indica che il valore dell'argomento corrispondente è trattato come un intero in base dieci con segno
  • %u il valore dell'argomento corrispondente nella serie variabile è un intero in base dieci senza segno
  • %o numero intero senza segno mostrato in base otto
  • %x o %X numero intero senza segno mostrato in base sedici
  • %f o %F numero in virgola mobile double
  • %e o %E numero in virgola mobile double espresso come mantissa ed esponente (usando la e minuscolo dopo la mantissa - per avere la E utilizzare %E)
  • %g o %G numero in virgola mobile double utilizzando la forma che occupa il minor numero di caratteri fra %e e %f
  • %a o %A numero in virgola mobile double mostrato in formato esadecimale
  • %c un carattere
  • %s una stringa
  • %p un indirizzo di memoria
  • %n non visualizza nulla, ma richiede che il corrispondente argomento nella lista di quelli variabili sia un riferimento a una variabile di tipo intero. in tale variabile la printf memorizzerà per il chiamante il numero di byte inviati fino al momento del %n (in altri post parleremo di puntatori e riferimenti)
Le varianti minuscolo/maiuscolo influenzano l'uso di caratteri minuscoli o maiuscoli da parte di printf in fase di output. Nel caso di %x le cifre esadecimali da a a f saranno minuscole, mentre con %X saranno maiuscole. Nel caso dei numeri in virgola mobile influenza la e prima dell'esponente.

%n è disabilitato in Microsoft Visual Studio per motivi di sicurezza. Per abilitarlo occorre invocare la funzione presente in stdio.h chiamata _set_printf_count_output passandole un valore diverso da 0. Chiamandola nuovamente con un valore uguale a zero si disabilita il %n. Utilizzare il %n mentre è disabilitato genera un errore in fase di esecuzione.

Un semplice esempi d'uso del %n. Creiamo una nuova soluzione e nel file .c inseriamo il seguente codice, osservarlo ed  eseguirlo.

#include "stdio.h"

main() {
    int a = 0, b = 0, totale = 0;

    /*Abilito il funzionamento del %n*/
    _set_printf_count_output(1);

    /*una serie di visualizzazione sulla console*/
    totale = printf("Hello World%n o Ciao Mondo%n?\n\n", &a, &b);
    printf("Al primo %%n sono stati inviati alla console %d byte.\n", a);
    printf("Al secondo %%n erano stati inviati alla console %d byte.\n", b);
   printf("In totale la prima printf ha inviato %d byte alla console.\n\n", totale);
    /*Disattivo %n*/
    _set_printf_count_output(0);
}

L'output del programma precedente produce.


Il programma non fa altro che definire 3 variabili intere, quindi abilita il %n invocando la _set_printf_count_output e parte con la prima printf oggetto di osservazione da parte del programma. In particolare assegna alla variabile totale il numero di byte complessivo inviati alla console dalla istruzione. La & indica che le due variabili argomento passano il proprio riferimento e non il valore contenuto, quindi la funzione potrà modificarne il contenuto. Il nuovo contenuto delle due variabili sarà visualizzato nelle due istruzioni successive.

Si osservi come il primo segnaposto %n farà riferimento al primo argomento della lista variabile chiamato a e il secondo %n farà riferimento al secondo argomento della lista variabile chiamato b. Il %% non è un segnaposto, ma come per l'escaping serve solo a dire di visualizzare il simbolo %.

Le printf successive fanno uso del segnaposto %d, che indica di trattare il relativo argomento come un intero con segno in base 10. Infatti la seconda printf preleva il valore contenuto in a e lo visualizza a schermo come intero in base 10, ossia come sequenza di caratteri '1' e '0' e non il valore numerico 10 effettivamente memorizzato, e che sappiamo differire da quanto rappresentato a video. Quindi la printf sta effettuando una conversione in stinga dell'argomento, secondo quanto indicato dal segnaposto nella stringa di formato portando poi a video tale stringa.

I segna posto hanno però una definizione ben più complessa e articolata di quanto visto ora. vediamola per intero.

%[flags][ampiezza][.precisione][modificatoreDiTipo]specificatoreDiTipo

Quando si specifica il tipo, di default si parla di int, double, char o char* (stringhe). E' però possibile specificare un modificatore di tipo fra i seguenti:
  • hh - forza gli int in char
  • h - forza in short int
  • l - forza in long int
  • ll - forza in long long int
  • L - forza i double in long double
  • z - un tipo intero size_t
  • j - un tipo intero intmax_t
  • t - un tipo intero ptrdiff_t
Un esempio:

#include "stdio.h"

main() {
    int positivo = 1024;

    printf("%d    \n", positivo);
    printf("%hhd  \n", positivo);

}


Questo codice visualizzerà a video

1024
0

Questo perchè 1024 in binario è 00000100 00000000. Utilizzando hh, l'argomento è trattato come un char ed ha 1 byte. Ma i primi 8 bit (quelli meno significativi, contati a partire da destra) sono a 0 quindi sarà visualizzato 0.

La sezione precisione serve, nel caso dei numeri in virgola mobile a specificare il numero di cifre da visualizzare dopo la virgola, e nei numeri interi indica il numero minimo di cifre da visualizzare facendo precedere il numero da 0 per completare la precisione indicata. Nel caso delle stringhe invece, se la stringa è di lunghezza superiore allora è troncata al numero di caratteri indicati da precisione.

Nel caso di stringhe produce il troncamento della stringa alla precisione indicata.

Nel caso di numeri interi, se il numero occupa meno spazio di quando indicato da precisione, gli spazi in più sono riempiti con degli 0 antecedenti il numero.

Il valore di precisione deve essere preceduto da un punto. Vediamo un piccolo codice d'esempio. Creiamo un nuovo progetto e nel sorgente copiamo il codice.

#include    "stdio.h"

main() {
    const char *stringa = "Questa e' una stringa lunga";
    const int intero = 125867;
    const double reale = 1746573.473647563528;
    const char c = 'A';


    printf("%s            \n", stringa);
    printf("%i            \n", intero);
    printf("%f            \n", reale);
    printf("%c            \n", c);

    printf("\n\n");
    printf("Varianti con precisione piccola rispetto allo spazio necessario\n");
    printf("%.5s        \n", stringa);
    printf("%.5i        \n", intero);
    printf("%.5f        \n", reale);
    printf("%.0c        \n", c);

    printf("\n\n");
    printf("Varianti con precisione abbondante\n");
    printf("[%.60s]        \n", stringa);
    printf("[%.60i]        \n", intero);
    printf("[%.60f]        \n", reale);
    printf("[%.60c]        \n", c);
}


Il programma non fa altro che dichiarare 4 costanti e visualizzarle con la printf. Nel primo gruppo di printf abbiamo la visualizzazione semplice, con il solo tipo. Nel secondo gruppo utilizziamo la precisione dando un valore più piccolo rispetto a quanto necessario per visualizzare tutta la costante fornita. Nel terzo gruppo è fornito un valore molto più grande rispetto alla precisione della costante fornita.

L'output prodotto sarà


Si osservi come, se la precisione indicata è inferiore alla lunghezza dalla stringa, la stessa è troncata. Nel caso dei numeri interi se è inferiore al necessario, il numero è visualizzato comunque completo. Se superiore al necessario è preceduto da 0. Nel caso dei numeri in virgola mobile se superiore alla precisione dei numeri è riempita con degli zero, mentre se inferiore si ha l'arrotondamento alla cifra decimale indicata.

A sinistra di precisione troviamo ampiezza, che indica appunto l'ampiezza da mettere a disposizione dell'argomento per la sua visualizzazione al fine di poter organizzare i dati sullo schermo. Questo valore ha però efficacia se lo spazio offerto è superiore a quello necessario, nel senso che numeri e stringhe non subiranno troncamenti o arrotondamenti se richiedono più spazio.

Un esempio che estende il precedente chiarirà l'effetto di ampiezza. Per rendere più evidente l'effetto , e mostrare lo spazio messo a disposizione dalla printf per quel segnaposto, i valori sono tutti stati circondati in visualizzazione da parentesi quadre.

#include    "stdio.h"

main() {
    const char* stringa = "Questa e' una stringa lunga";
    const int intero = 125867;
    const double reale = 1746573.473647563528;
    const char c = 'A';


    printf("[%s]          \n", stringa);
    printf("[%i]          \n", intero);
    printf("[%f]          \n", reale);
    printf("[%c]          \n", c);

    printf("\n\n");
    printf("Varianti con ampiezza piccola rispetto allo spazio necessario\n");
    printf("[%5s]         \n", stringa);
    printf("[%5i]         \n", intero);
    printf("[%5f]         \n", reale);
    printf("[%0c]         \n", c);

    printf("\n\n");
    printf("Varianti con ampiezza abbondante\n");
    printf("[%40s]        \n", stringa);
    printf("[%40i]        \n", intero);
    printf("[%40f]        \n", reale);
    printf("[%40c]        \n", c);

    printf("\n\n");
    printf("Varianti con ampiezza e precisione\n");
    printf("[%25.15s]     \n", stringa);
    printf("[%25.15i]     \n", intero);
    printf("[%25.15f]     \n", reale);
    printf("[%25.15c]     \n", c);

    printf("\n\n");
    printf("Varianti con ampiezza e precisione ma i numeri fanno eccezione\n");
    printf("[%5.5s]        \n", stringa);
    printf("[%5.5i]        \n", intero);
    printf("[%5.1f]        \n", reale);
    printf("[%5.5c]        \n", c);
}


Questo programmino produce


Si osservi come una ampiezza insufficiente non modifichi in alcun modo la visualizzazione (primo e secondo gruppo). Se invece l'ampiezza è sufficiente i dati entrano nell'ampiezza specificata e l'eccesso è riempito con spazi (terzo gruppo). Una combinazione di ampiezza a precisione può forzare l'occupazione di spazio da dedicare a un segnaposto fintanto che i numeri riescono a rimanere in tali confini (quarto e quinto gruppo)

Quindi, se l'ampiezza è insufficiente comunque l'informazione è completa e occupa tutto lo spazio necessario. Nel caso di numeri reali si ha di default una precisione a 6 cifre dopo la virgola, ossia se non specificato è come se ci fosse .6 nel segnaposto. Se invece ampiezza è sovrabbondante abbiamo l'informazione completa, e di default un allineamento a destra con la parte sinistra riempita con spazi.
Facendo cooperare ampiezza e precisione riusciamo effettivamente a forzare la visualizzazione a una certa ampiezza come se fosse un campo di dimensione predefinita. Particolare attenzione però richiedono sempre i numeri che possono violare tale spazio se hanno molte cifre nella parete intera.
Infine i flags. Sono ammessi 4 valori
  • - questo flag modifica l'allineamento di default del testo, che è a destra, spostandolo a sinistra quando è specificata una 'ampiezza'
  • +  o uno spazio provoca la visualizzazione del segno + per i numeri positivi o in alternativa, con lo spazio vuoto, è posto uno spazio al posto del segno + del numero
  • 0 nell'allineamento a destra, riempi gli spazi vuoti con 0 se è specificata una ampiezza senza precisione
  • # Nelle visualizzazioni ottali ed esadecimali antepone uno 0 al numero o 0x o 0X a seconda che si usi %o, %x o %X.
Concludiamo quindi con un esempio che mostra l'utilizzo dei flag, che possono essere presenti anche in più d'uno in un segnaposto, in combinazione con le altre proprietà

#include    "stdio.h"

main() {
    const char* stringa = "Questa e' una stringa lunga";
    const int intero = 125867;
    const double reale = 1746573.473647563528;
    const char c = 'A';

    printf("Varianti con flag, ampiezza e precisione\n");

    printf("\n**** Stringhe ****\n");
    printf("%-12s -> [%30s]       \n", "%30s"     , stringa);
    printf("%-12s -> [%-30s]      \n", "%-30s"    , stringa);
    printf("%-12s -> [%30.15s]    \n", "%30.15s"  , stringa);
    printf("%-12s -> [%-30.15s]   \n", "%-30.15s" , stringa);

    printf("\n**** Interi ****\n");
    printf("%-12s -> [%25i]       \n", "%25i"     , intero);
    printf("%-12s -> [%025i]      \n", "%025i"    , intero);
    printf("%-12s -> [%25.15i]    \n", "%25.15i"  , intero);
    printf("%-12s -> [%+25.15i]   \n", "%+25.15i" , intero);
    printf("%-12s -> [% -25.15i]  \n", "% -25.15i", intero);
    printf("%-12s -> [%+-25.15i]  \n", "%+-25.15i", intero);   

    printf("\n**** Interi Esadecimali/Ottali****\n");
    printf("%-12s -> [%25x]       \n", "%25x"     , intero);
    printf("%-12s -> [%#25x]      \n", "%#25x"    , intero);
    printf("%-12s -> [%025x]      \n", "%025x"    , intero);
    printf("%-12s -> [%25.15x]    \n", "%25.15x"  , intero);
    printf("%-12s -> [%#25.15x]   \n", "%#25.15x" , intero);
    printf("%-12s -> [%#-25.15x]  \n", "%#-25.15x", intero);


    printf("\n**** Reali ****\n");
    printf("%-12s -> [%25f]       \n", "%25f"     , reale);
    printf("%-12s -> [%25.8f]     \n", "%25.8f"   , reale);
    printf("%-12s -> [%025.8f]    \n", "%025.8f"  , reale);   
    printf("%-12s -> [%+25.8f]    \n", "%+25.8f"  , reale);
    printf("%-12s -> [%-25.8f]    \n", "%-25.8f"  , reale);
    printf("%-12s -> [% -25.8f]   \n", "% -25.8f" , reale);
    printf("%-12s -> [%+-25.8f]   \n", "%+-25.8f" , reale);

    printf("\n**** Carattere ****\n");
    printf("%-12s -> [%25c]       \n", "%25c"     , c);
    printf("%-12s -> [%-25c]      \n", "%-25c"    , c);
}


L'output prodotto è


Ossia una buona tavola di riferimento quando occorre costruire dei segnaposto per la printf.

Una nota finale. Di solito per ampiezza e precisione sono stati utilizzati dei valori numerici. E' anche possibile sostituire il numero con un * e indicare il valore nella lista degli argomenti variabili, andando di fatto a parametrizzare ampiezza e precisione.

Ad esempio

printf("% -25.15i  \n", intero);

può essere scritta come

printf("% -*.*i  \n", 25, 15, intero);
ovviamente al posto di valori costanti numerici è possibile mettere delle variabili. Non è obbligatorio sostituire tutti i valori con *.

Abbiamo quindi appreso
  • l'uso della printf e della sua stringa di formato approfondendo l'utilizzo dei segnaposto