Note su Caratteri e Stringhe

Per memorizzare singoli caratteri si usano variabili di tipo char. Le stringhe possono quindi essere memorizzate come array di caratteri. Per agevolare il loro trattamento, il C mette a disposizione la sintassi coi doppi apici (che abbiamo già utilizzato con printf e scanf).

Vediamo alcuni esempi di dichiarazione di stringhe:

// dichiara un vettore di 100 caratteri, senza inizializzarlo
char a[100]; 

// dichiara e inizializza un vettore di 5 caratteri
char vocali[ ] = { 'a' , 'e' , 'i' , 'o' , 'u' }; 

// dichiara e inizializza un vettore di 5 caratteri
// (tramite i corrispondenti codici ASCII)
char vocali_da_ASCII[ ] = { 97 , 101 , 105 , 111 , 117 }; 

// dichiara e inizializza una stringa
char saluto[ ] = "Ciao mondo!"; 

La sintassi dell'ultima istruzione definisce un array di lunghezza N+1, dove N è la lunghezza della stringa: l'ultima casella dell'array contiene il carattere speciale '\0'. Nell'esempio abbiamo un array di 12 posizioni.

C i a o   m o n d o ! \0 carattere
0 1 2 3 4 5 6 7 8 9 10 11 posizione

Notare la differenza tra l'uso di apici singoli e quello di apici doppi:


char x = 'a'; // ok

char y = "b"; // NO!!!

char z[ ] = "c"; // ok

char w[ ] = 'd'; // NO!!!

Sappiamo che per stampare un array è importante conoscerne la lunghezza, altrimenti potremmo accedere a zone di memoria che non erano state allocate per l'array. Lo stesso ragionamento vale per le stringhe, che sono array particolari. Come esempio è istruttivo provare a compilare ed eseguire il seguente programma:


/*
  Stampa i caratteri di una stringa ed i corrispondenti codici ASCII
*/

#include <stdio.h>
#include <stdlib.h>

#define LUNGHEZZA_MAX 20

int main ( ) {
                   
  int i;
  char stringa1[ ] = "Prova 1";
  char stringa2[LUNGHEZZA_MAX] = "Prova 2";
  char stringa3[ ] = "Prova 3";

  for (i = 0; i < LUNGHEZZA_MAX; i++)
    printf("%c (ASCII %d)\n", stringa1[i], stringa1[i]);

  printf("===========\n");

  for (i = 0; i < LUNGHEZZA_MAX; i++)
    printf("%c (ASCII %d)\n", stringa2[i], stringa2[i]);

  printf("===========\n");

  for (i = 0; i < LUNGHEZZA_MAX; i++)
    printf("%c (ASCII %d)\n", stringa3[i], stringa3[i]);

  return EXIT_SUCCESS;
}

Se mandate in esecuzione il programma dovreste notare qualcosa di particolarmente strano... che evidenzia il modo in cui sono allocate le variabili nella memoria.

Provate a rispondere alle seguenti domande:

  1. L'output è quello che vi aspettavate?
  2. Quali stranezze notate nell'output del programma?
  3. Come dovremmo correggere il programma?
  4. Come possiamo scorrere una stringa senza doverne conoscere / calcolare la lunghezza?

Per stampare una stringa possiamo utilizzare direttamente printf, senza doverci preoccupare di scorrerla carattere per carattere:


char nome[ ] = "Mario Rossi"; 

printf("ciao %s, come stai?\n", nome);
Analogamente, con scanf possiamo leggere da tastiera direttamente una stringa, senza doverci preoccupare di leggerla un carattere per volta:
char nome[51]; 

printf("Come ti chiami? (max 50 caratteri)\n");
scanf("%s", nome);

Notare che questa volta non serve il simbolo & davanti a nome.

Attenzione: Se si immette un numero maggiore di caratteri si rischia di sovrascrivere delle zone di memoria con altri dati.

Nota: In questo modo però la stringa viene letta solo fino al primo carattere di separazione (spazio, tabulazione, new line). Per evitarlo possiamo fare un ciclo di lettura che esamina un carattere alla volta e termina quando si digita un carattere speciale (es. il tasto ESC).

int i = 0;
char stringa[LUNGHEZZA_MAX+1];

scanf("%c", &stringa[i]); // leggo il primo carattere
while (i < LUNGHEZZA_MAX &&  // se c'è spazio e
       stringa[i] != 27  &&  // ... l'ultimo carattere letto non è ESC e
       stringa[i] != '\0') { // ... l'ultimo carattere letto non è '\0', allora...
  i++;
  scanf("%c", &stringa[i]); // leggo il prossimo carattere
}
stringa[i] = '\0'; // metto '\0' come ultimo carattere

Invece della primitiva scanf di lettura formattata, possiamo anche usare la funzione getchar( ):

int i = 0;
char stringa[LUNGHEZZA_MAX+1];

stringa[i] = getchar( ); // leggo il primo carattere
while (i < LUNGHEZZA_MAX &&  // se c'è spazio e
       stringa[i] != 27  &&  // ... l'ultimo carattere letto non è ESC e
       stringa[i] != EOF &&  // ... l'ultimo carattere letto non è EOF e
       stringa[i] != '\0') { // ... l'ultimo carattere letto non è '\0', allora...
  i++;
  stringa[i] = getchar( ); // ... leggo il prossimo carattere
}
stringa[i] = '\0'; // metto '\0' come ultimo carattere

Esercitazione extra

Scrivere un programma analizza.c che legga una stringa di lunghezza inferiore a 10000 e visualizzi la frequenza di ogni carattere alfabetico che compare nella stringa. Scaricare il file di testo "intro_C.txt" e usarlo per provare l'eseguibile sfruttando la redirezione dell'input.
Nota: Con il termine frequenza si intende il numero di volte che il carattere compare nel testo.

Una possibile soluzione:

/*
  Analizza la frequanza dei caratteri in una stringa letta
*/

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define LUNGHEZZA_MAX 10000

int main ( ) {
                   
  int i, codice, totale = 0;
  char stringa[LUNGHEZZA_MAX+1];
  int frequenza[256];

  for (i = 0; i < 256; i++)
    frequenza[i] = 0;

  i = 0;
  stringa[i] = getchar( );
  while ( i < LUNGHEZZA_MAX && 
          stringa[i] != 27 && 
          stringa[i] != EOF &&
          stringa[i] != '\0') {
    i++;
    stringa[i] = getchar( );
  }
  stringa[i] = '\0';
 
  for (i = 0; i < LUNGHEZZA_MAX && stringa[i] != '\0'; i++) {
    codice = stringa[i];
    frequenza[codice-CHAR_MIN]++;
    totale++;
  }
    
  printf("\n===========\n");

  for (i = 0; i < 256; i++)
    if (frequenza[i] != 0) 
      printf("%c (ASCII %d): %d\n", i + CHAR_MIN, i + CHAR_MIN, frequenza[i]);

  printf("===========\n");
  printf("Totale %d\n", totale);

  return EXIT_SUCCESS;
}