Le stringhe in C
- Dichiarare una stringa
- Mostrare in output una stringa
- Leggere in input una stringa
- Leggere in input più stringhe
- Domande & risposte
- Accedere ai caratteri di una stringa
- Argomenti da riga di comando
- La libreria
<ctype.h> - La libreria
<string.h> - Stringhe e funzioni
Nei linguaggi di programmazione, i testi, le sequenze di caratteri, sono comunemente chiamati stringhe.
Dichiarare una stringa
Vediamo un primo esempio di programma, in cui semplicemente, dichiariamo una variabile stringa:
#include <stdio.h>
#define STR_LEN 20
int main()
{
char nome[STR_LEN + 1] = "Angelo";
return 0;
}
Quello qui sopra è una soluzione corretta.
Alcune osservazioni:
- nel linguaggio C, una stringa è rappresentata come un vettore di
char. - la stringa deve sempre terminare con il carattere speciale
\0. Per questo motivo, inizializziamo la variabilenomecon lunghezzaSTR_LEN + 1: la sequenza di caratteri che salveremo innomepotrà al massimo essere lungaSTR_LENcaratteri, nell’ultima posizione [+ 1] salveremo\0. - quando si assegna direttamente una stringa a una variabile, il compilatore,
automaticamente, pone il simbolo
\0al termine della stringa. In altri termini, l’istruzione:
char nome[STR_LEN + 1] = "Angelo";
equivale a:
char nome[STR_LEN + 1] = {'A', 'n', 'g', 'e', 'l', 'o', '\0'};
Solo che, per noi esseri umani, la prima istruzione è un po’ più semplice da scrivere.
- Una stringa deve essere chiusa tra doppi apici. I singoli apici si usano solo per singoli caratteri char.
char nome[STR_LEN + 1] = 'Angelo'; // SBAGLIATO!
- se a
nomesi assegna una stringa lunga più diSTR_LENcaratteri, solo i primiSTR_LENcaratteri sono effettivamente salvati nella stringa!
// Di queste 50 's', solo le prime STR_LEN saranno salvate in `nome'
char nome[STR_LEN + 1] = "ssssssssssssssssssssssssssssssssssssssssssssssssss";
- il carattere
' '(lo spazio vuoto) è considerato un carattere, al pari di tutti gli altri!
Mostrare in output una stringa
Per mostrare in output una stringa si usa la solita funzione printf(), con lo specificatore di conversione %s:
#include <stdio.h>
#define STR_LEN 20
int main()
{
char nome[STR_LEN + 1] = "Angelo";
printf("Ciao, %s!\n", nome);
return 0;
}
Leggere in input una stringa
Per leggere in input una stringa, la tentazione è di usare la funzione scanf():
#include <stdio.h>
#define STR_LEN 20
int main()
{
char nome[STR_LEN + 1];
printf("Come ti chiami? ");
scanf("%s", &nome);
printf("Ciao, %s!\n", nome);
return 0;
}
Il codice qui sopra è corretto, ma ha due importanti limitazioni, per le quali l’utilizzo della funzione scanf() per la lettura di stringhe è fortemente sconsigliato!
- Se l’input contiene degli spazi, la scanf considera solo la prima parola inserita, tralasciando tutte le successive! Perciò, se ad esempio l’utente ha due nomi, solo il primo sarà salvato nella variabile ‘nome’. 🙁 Il programma qui sopra avrebbe il segunte input/output:
Come ti chiami? Angelo Sala
Ciao, Angelo!
- FATAL ERROR: Se l’input è più lungo di
STR_LENcaratteri, la scanf se ne frega e salva tutti i caratteri in memoria. Prova a mettere come inputAngelo Alessandro Sala, che è lungo 22 caratteri (quindi più diSTR_LEN== 20). Il compilatore e l’esecutore non segnalano nessun errore, ma stiamo sovrascrivendo parti di memoria che in teoria non dovremmo toccare: è la base di tantissimi attacchi informatici!
In sintesi: la funzione scanf non è adatta a leggere stringhe. Dobbiamo ricorrere a un’altra soluzione. Eccola qui:
#include <stdio.h>
#define STR_LEN 20
int main()
{
char nome[STR_LEN + 1];
printf("Come ti chiami? ");
int len = 0;
char c = getchar();
while(len < STR_LEN && c != '\n')
{
nome[len] = c;
len += 1;
c = getchar();
}
nome[len] = '\0';
printf("Ciao, %s!\n", nome);
return 0;
}
Focalizziamoci sul codice che, effettivamente, si occupa di leggere una stringa in input e di salvarla nella variabile nome:
int len = 0;
char c = getchar();
while(len < STR_LEN && c != '\n')
{
nome[len] = c;
len += 1;
c = getchar();
}
nome[len] = '\0';
Per leggere correttamente, in maniera sicura, senza sorprese, l’input, si utilizza la funzione getchar().
getchar() legge un singolo carattere dall’input: per questo è spesso usata in combinazione con un ciclo while. Di solito, il ciclo è interrotto se:
- l’ultimo carattere letto in input da
getchar()è stato\n - è stato raggiunto il massimo numero di caratteri che possono essere immessi nella stringa,
STR_LEN
Nota bene: quando si usa
getchar()il carattere speciale di terminazione di una stringa\0non è scritto automaticamente nella stringa: esso deve essere aggiunto manualmente.
Confrontiamo i risultati di getchar() e scanf(), a seconda dell’input fornito. Si assume una lunghezza massima di 20 caratteri per la stringa:
| Stringa di input | Stringa salvata usando getchar() |
Stringa salvata usando scanf() |
|---|---|---|
Angelo |
✅ Angelo |
✅ Angelo |
Angelo Sala |
✅ Angelo Sala |
❌ Angelo |
Angelo Alessandro Sala |
✅ Angelo Alessandro Sa |
❌ Angelo Alessandro Sala |
Leggere in input più stringhe
Sembra tutto chiaro, ma ci sono degli accorgimenti da considerare. Soprattutto quando il nostro programma deve leggere più stringhe! Consideriamo il seguente programma:
#include <stdio.h>
#define STR_LEN 10
int main()
{
char nome[STR_LEN + 1], città[STR_LEN + 1];
printf("Ciao! Come ti chiami? ");
int len_nome = 0;
int c = getchar();
while (len_nome < STR_LEN && c != '\n')
{
nome[len_nome] = c;
len_nome += 1;
c = getchar();
}
nome[len_nome] = '\0';
printf("Dove vivi? ");
int len_città = 0;
c = getchar();
while (len_città < STR_LEN && c != '\n')
{
città[len_città] = c;
len_città += 1;
c = getchar();
}
città[len_città] = '\0';
printf("\nCiao %s!\n", nome);
printf("Vivi a %s\n", città);
return 0;
}
Il programma fa questo:
- chiede all’utente di inserire il suo nome
- chiede all’utente di inserire il nome del paese in cui vive
- mostra in output il nome dell’utente e la città in cui vive.
Insomma, ci piacerebbe avere un input/output di questo tipo:
Ciao! Come ti chiami? Angelo
Dove vivi? Osnago
Ciao Angelo!
Vivi a Osnago
Ed effettivamente, inserendo in input le stringhe Angelo e Osnago, entrambe più corte della massima lunghezza accettata dal programma, 10. Tutto funzionerebbe a meraviglia.
E se… inserissi un nome più lungo di 10 caratteri? Ops!:
Ciao! Come ti chiami? Angelo Alessandro
Dove vivi?
Ciao Angelo Ale!
Vivi a ssandro
Angelo Alessandro è lungo 17 caratteri, e questo rovina tutto: il programma neanche lascia il tempo di rispondere alla domanda: Dove vivi?, e mostra subito in output Vivi a ssandro.
Che cosa è successo? Cosa abbiamo sbagliato? 🙁
Quando abbiamo riempito la stringa nome, abbiamo lasciato i rimanenti caratteri inseriti dall’utente prima di \n. Perciò, la funzione getchar(), quando utilizzata per leggere il nome del paese in cui vivo, trovando già dei caratteri in input (quelli rimasti d’avanzo prima!) ha letto quelli, e finito, dal suo punto di vista, il suo compito.
Okay, tutto chiaro: come risolvere però?
Ogni volta che si finisce di leggere una stringa in input, perché è stata raggiunta la lungheza massima, bisogna consumare tutti gli eventuali caratteri rimasti in input.
In sintesi, basta aggiungere queste due righe di codice dopo ogni lettura dell’input:
if (c != '\n')
while (getchar() != '\n');
Il nostro programma, corretto, diventerebbe questo:
#include <stdio.h>
#define STR_LEN 10
int main()
{
char nome[STR_LEN + 1], città[STR_LEN + 1];
printf("Ciao! Come ti chiami? ");
int len_nome = 0;
int c = getchar();
while (len_nome < STR_LEN && c != '\n')
{
nome[len_nome] = c;
len_nome += 1;
c = getchar();
}
nome[len_nome] = '\0';
if (c != '\n')
while (getchar() != '\n');
printf("Dove vivi? ");
int len_città = 0;
c = getchar();
while (len_città < STR_LEN && c != '\n')
{
città[len_città] = c;
len_città += 1;
c = getchar();
}
città[len_città] = '\0';
if (c != '\n')
while (getchar() != '\n');
printf("\nCiao %s!\n", nome);
printf("Vivi a %s\n", città);
return 0;
}
Domande & risposte
Prof., cosa succede se dichiaro una stringa lunga, ad esempio, 20 caratteri, ma poi in essa ne salvo solo 6? Cosa c’è scritto nelle rimanenti 14 posizioni?
Ottima domanda. Se dichiaro una variabile stringa lunga 20 caratteri (STR_LEN + 1 == 20), e poi in essa salvo la parola Angelo, lunga soli 6 caratteri, la stringa sarà così formata:
- i primi 6 caratteri saranno le lettere di
Angelo - il settimo carattere sarà il carattere speciale
\0 - i rimanenti 13 caratteri devono essere considerati valori casuali, da trascurare. Nei fatti, le più recenti versioni di GCC mettono tutti quei caratteri uguali a
\0, ma non è un comportamento standardizzato, perciò: mai farci affidamento.
Prof., coi suoi metodi può capitare che, ad esempio, come nome di una persona si salvi Angelo Ass, che non è bellissimo come nome da salvare. Posso fare in modo che, se l’utente prova a inserire un nome, ma questo è troppo lungo, gli si mostra un avviso, o altro?
Certamente. Un ottimo esercizio! Qui sotto ti mostro un programma che chiede all’utente di insierire il suo nome. Se il nome è troppo lungo per stare nella variabile stringa, si mostra all’utente un avviso, e si ri-chiede di inserire il nome.
#include <stdio.h>
#include <stdbool.h>
#define STR_LEN 10
int main()
{
char nome[STR_LEN + 1];
bool nome_letto_correttamente = false;
while (nome_letto_correttamente == false)
{
printf("Ciao! Come ti chiami? ");
int len = 0;
char c = getchar();
while (len < STR_LEN && c != '\n')
{
nome[len] = c;
len += 1;
c = getchar();
}
nome[len] = '\0';
if (c == '\n') // il nome è stato letto per intero: abbiamo raggiunto la fine della riga!
nome_letto_correttamente = true;
else // rimangono dei caratteri in input, quindi il nome non è stato letto per intero!
{
// 1. consumiamo i caratteri rimasti
while (getchar() != '\n')
;
// 2. avvisiamo l'utente che qualcosa è andato storto
printf("ATTENZIONE! Posso leggere nomi lunghi al massimo %d caratteri. Riprova.\n", STR_LEN);
// 3. facciamo in modo che tutto il codice sia ri-eseguito!
nome_letto_correttamente = false;
}
}
printf("\nCiao, %s!\n", nome);
return 0;
}
Un esempio dell’input/outut di questo programma:
Ciao! Come ti chiami? Angelo Alessandro
ATTENZIONE! Posso leggere nomi lunghi al massimo 10 caratteri. Riprova.
Ciao! Come ti chiami? Angelo
Ciao, Angelo!
Accedere ai caratteri di una stringa
Una stringa è un vettore di caratteri. Perciò, come per qualsiasi altro vettore, possiamo accedere ai singoli caratteri di una stringa usando l’indicizzazione.
Supponiamo di voler contare quanti spazi vuoti (caratteri ' ') sono presenti in una stringa s, di lunghezza len. Il nostro algoritmo apparirà così:
#include <stdio.h>
#define MAX_LEN 100
int main()
{
char s[MAX_LEN + 1];
int len = 0;
/*
Qui va il codice che legge dei caratteri in input e li salva in 's',
aumentando opportunamente il valore di 'len'
*/
int conta = 0;
for(int i = 0; i < len; i++)
if(s[i] == ' ') // <-- QUI accediamo al signolo carattere di 's' in posizone 'i'
conta += 1;
printf("Numero di spazi salvati nella stringa: %d\n", conta);
return 0;
}
Argomenti da riga di comando
Ho deciso di rinviare questo argomento a data da destinarsi!
La libreria <ctype.h>
La libreria <ctype.h> fornisce due tipi di funzioni:
- le funzioni per la classificazione dei caratteri
- le funzioni di case mapping
Funzioni per la classificazione dei caratteri
Alcune funzioni della libreria <ctype.h> ricevono come parametro un singolo carattere e restituiscono un valore di verità (true oppure false) a seconda che quel carattere abbia la caratteristica richiesta dalla funzione, oppure no. Tra tali funzioni, è bene conoscere queste:
| Funzione | Utilizzo |
|---|---|
isalnum(c) |
Restituisce true se c è un carattere alfanumerico, altrimenti false |
isalpha(c) |
Restituisce true se c è un carattere alfabetico, altrimenti false |
isdigit(c) |
Restituisce true se c è un carattere che rappresenta una cifra decimale, altrimenti false |
ispunct(c) |
Restituisce true se c è un segno di punteggiatura, altrimenti false |
isupper(c) |
Restituisce true se c è una lettera maiuscola, altrimenti false |
islower(c) |
Restituisce true se c è una lettera minuscola, altrimenti false |
Le funzioni
isupper(c)eislower(c)danno sicuramente come risultatofalsese il carattere non è una lettera.
Le funzioni appena riportate sono ottime da usare come condizioni, ad esempio nei blocchi if/else: queste funzioni, infatti, restituiscono solo true oppure false: esattamente ciò che i blocchi if/else si aspettano!
Ad esempio, se vogliamo sapere quante sono le lettere maiuscole e quante sono le lettere minuscole presenti in una stringa, possiamo procedere così:
#include <stdio.h>
#include <ctype.h> // RICORDATI DI INCLUDERE QUESTO!
#define MAX_LEN 100
int main()
{
char s[MAX_LEN + 1];
int len = 0;
/*
Qui va il codice che legge dei caratteri in input e li salva in 's',
aumentando opportunamente il valore di 'len'
*/
int maiuscole = 0, minuscole = 0;
for(int i = 0; i < len; i++)
if(isupper(s[i])) // 's[i]', se ci pensi, è un singolo carattere ✅
maiuscole += 1;
else if (islower(s[i]))
minuscole += 1;
// Se 's[i]' non è una lettera, non incrementiamo niente
printf("Numero di maiuscole: %d\n", maiuscole);
printf("Numero di minuscole: %d\n", minuscole);
return 0;
}
Le funzioni di case mapping
La funzione toupper(c) restituisce la versione maiuscola della lettera c ricevuta come parametro; la funzione tolower(c) restituisce la versione minuscola della lettera c ricevuta come parametro. Se c non è una lettera, queste due funzioni restituiscono c così com’è, senza modificare nulla.
La libreria <string.h>
La libreria <string.h> contiene molte funzioni utili per lavorare con le stringhe. Di tutte queste, per ora, a noi ne interessa una soltanto: strlen(s).
La funzione strlen(s) riceve come parametro una stringa s e ne restituisce la lunghezza, ovvero il numero di caratteri presenti in s fino al primo carattere \0.
La funzione strlen(s) è utile, ad esempio, quando si riceve una stringa da riga di comando: per ottenere la lunghezza di tale stringa, occorre strlen.
Stringhe e funzioni
Usare una stringa come parametro di una funzione
La seguente funzione riceve una stringa s come parametro:
int conta_vocali(const char s[]);
Quando la funzione è chiamata, bisogna passarle il nome della stringa, e basta. Ad esempio:
#include <stdio.h>
int conta_vocali(const char s[]);
int main() {
char s[] = "aiuola";
int n = conta_vocali(s);
printf("%s contiene %d vocali.\n", s, n);
}
Modifiche di una funzione su una stringa ricevuta come parametro
Considera questo esempio:
#include <stdio.h>
void modifica(int a, char s, char c[]);
int main() {
int anno = 4;
char sez = 'E';
char corso[] = "LSA";
printf("%d%c%s\n", anno, sez, corso);
modifica(anno, sez, corso);
printf("%d%c%s\n", anno, sez, corso);
}
void modifica(int a, char s, char c[]) {
a = 2;
s = 'A';
c[0] = 'A';
c[1] = 'F';
c[2] = 'M';
}
Il main() qui sopra contiene due printf().
La prima printf mostra in output: 4ELSA.
La seconda printf mostra in output: 4EAFM.
Quindi, le modifiche fatte nella funzione modifica:
- sono visibili al di fuori della funzione quando applicate agli elementi della stringa
- sono invisibili al di fuori della funzione quando applicate a variabili
intochar
Perché? In C, quando si chiama una funzione, ogni parametro riceve una copia del valore passato. Per le variabili int e char la copia è il valore stesso: la funzione modifica la propria copia, e la variabile originale resta intatta. Per un vettore (e quindi per una stringa), invece, la copia è l’indirizzo del primo elemento: la funzione non riceve una copia del contenuto, ma sa dove si trovano gli elementi originali, e quindi può modificarli.
La regola generale è la seguente: le modifiche fatte all’interno di una funzione agli elementi di un vettore sono visibili al di fuori della funzione. Le modifiche fatte all’interno di una funzione a una variabile non vettoriale sono invisibili fuori dalla funzione.
La parola chiave const
Per impedire a una funzione di modificare gli elementi di un vettore ricevuto come parametro, si usa la parola chiave const. Ad esempio, il codice seguente non compila:
#include <stdio.h>
void modifica(int a, char s, const char c[]);
int main() {
int anno = 4;
char sez = 'E';
char corso[] = "LSA";
printf("%d%c%s\n", anno, sez, corso);
modifica(anno, sez, corso);
printf("%d%c%s\n", anno, sez, corso);
}
void modifica(int a, char s, const char c[]) {
a = 2;
s = 'A';
c[0] = 'A'; // errore di compilazione: 'c' è dichiarato 'const', quindi non si può modificare il suo contenuto
}
La parola chiave const serve anche come documentazione: leggendo il prototipo di una funzione, si capisce subito che la funzione non altera la stringa ricevuta (come in conta_vocali).
Restituire una stringa come risultato di una funzione
Considera la seguente funzione:
char * no_vocali(const char s[]);
Il tipo restituito char * indica che la funzione restituisce l’indirizzo del primo carattere di una stringa. (Non si può scrivere char[] come tipo restituito: si usa sempre char *.)
Non si può restituire un vettore dichiarato dentro la funzione, perché sparirebbe al termine della funzione. Per creare una nuova stringa che sopravviva alla funzione, bisogna riservare la memoria con malloc(), così:
#include <string.h> // per usare strlen()
#include <stdlib.h> // per usare malloc()
char * no_vocali(const char s[]) {
size_t len = strlen(s);
char * new_s = malloc(len + 1); // +1 per il terminatore '\0'
/*
`new_s` ha spazio per tanti caratteri quanti ne ha `s`,
ma il suo contenuto è casuale: non è ancora una stringa valida.
Scrivi qui del codice che riempie `new_s` e che
ricordi di terminarla con '\0'.
*/
return new_s;
}
La funzione si usa così:
#include <stdio.h>
int main() {
char s[] = "aiuola";
char * t = no_vocali(s);
printf("%s -> %s\n", s, t);
}
Per semplicità, in questi esempi non liberiamo la memoria con
free(): la memoria riservata conmalloc()resta occupata fino alla fine del programma. Questo va bene negli esempi brevi, ma non è una buona pratica: la gestione corretta della memoria sarà trattata… più avanti.