Skip to the content.

Lezione 5 - Stringhe in C

Video di riferimento: Lezione 5 - Stringhe


1. Format Specifiers in printf

1.1 Come Funziona printf

Riprendiamo il nostro programma “Hello World”:

#include <stdio.h>

int main(void) {
    printf("Hello World\n");
    return 0;
}

printf prende una stringa di formato dove:

Esempio con placeholder:

#include <stdio.h>

int main(void) {
    printf("Hello World, %d\n", 42);
    return 0;
}

Output:

Hello World, 42

Il %d indica a printf:


2. Caratteri Letterali (char literals)

2.1 Apici Singoli vs Doppi

In C, a differenza di Python, apici singoli e doppi hanno significati diversi:

Sintassi Tipo Esempio Valore
'A' char (singolo carattere) char c = 'A'; 65 (valore ASCII)
"A" char[] (stringa) char s[] = "A"; {‘A’, ‘\0’}

Regola chiave: Gli apici singoli '' contengono esattamente un carattere e rappresentano il suo valore numerico ASCII.


2.2 Lo Standard ASCII

Per vedere la tabella ASCII completa su sistemi Unix-like:

$ man ascii

Esempi di valori ASCII:

Carattere Valore Decimale Valore Esadecimale
'A' 65 0x41
'B' 66 0x42
'a' 97 0x61
'0' 48 0x30
' ' (spazio) 32 0x20
'\n' 10 0x0A
'\0' (NULL) 0 0x00

2.3 char Come Numero Intero

Concetto fondamentale: In C, char è un tipo numerico.

char c = 'A';
printf("%d\n", c);  // stampa 65
printf("%c\n", c);  // stampa A

Viceversa:

char c = 65;
printf("%c\n", c);  // stampa A
printf("%d\n", c);  // stampa 65

Sono equivalenti!


2.4 Mixare Letterali e Numeri

Puoi mischiare valori ASCII e caratteri letterali:

printf("[%c%c%c%c]\n", 65, 66, 67, 68);
// Output: [ABCD]

printf("[%c%c%c%c]\n", 65, 66, 67, 'D');
// Output: [ABCD] (identico!)

printf("[%c%c%c%c]\n", 'A', 'B', 'C', 'D');
// Output: [ABCD] (identico!)

Tutte e tre le versioni producono lo stesso risultato perché 'D' e 68 sono la stessa cosa.


2.5 Caratteri Fuori ASCII

Se inserisci caratteri non-ASCII (es. caratteri Unicode, emoji), il comportamento dipende dall’encoding:

char c = 'è';  // Potrebbe essere mappato a più byte

Attenzione: char è solo 8 bit. Caratteri Unicode richiedono encoding multi-byte (UTF-8) o tipi più grandi (wide characters).


3. Array in C

3.1 Dichiarazione e Inizializzazione

Sintassi base:

int a[] = {10, 5, 50, 100, 7};

Le parentesi graffe {} servono per inizializzare gli elementi dell’array.

Accesso tramite indici (0-indexed):

printf("%d %d %d\n", a[0], a[1], a[2]);
// Output: 10 5 50

3.2 Dimensione dell’Array

Esplicita:

int a[5] = {10, 5, 50, 100, 7};  // Array di 5 elementi

Implicita (dedotta dal compilatore):

int a[] = {10, 5, 50, 100, 7};  // Dimensione dedotta: 5

Parziale:

int a[10] = {1, 2, 3};  // Primi 3 inizializzati, altri 7 a zero

4. Stringhe Come Array di char

4.1 Rappresentazione Base

In C, le stringhe sono array di char terminati da \0 (NULL terminator).

Metodo 1: Inizializzazione manuale con array

char str[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
printf("%s\n", str);
// Output: Hello

Memoria:

┌───┬───┬───┬───┬───┬───┐
│ H │ e │ l │ l │ o │ \0│
└───┴───┴───┴───┴───┴───┘
 [0] [1] [2] [3] [4] [5]

Lo \0 (o semplicemente 0) è il null terminator - segnala la fine della stringa.


4.2 Il Null Terminator È Fondamentale

Cosa succede senza \0?

char str[5] = {'H', 'e', 'l', 'l', 'o'};  // MANCA \0!
printf("%s\n", str);

Possibile output:

Hello???�����

printf legge i byte finché non incontra uno \0, quindi continua a leggere memoria non inizializzata (garbage) oltre la fine dell’array!

Visualizzazione:

┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ H │ e │ l │ l │ o │ ? │ ? │ ? │ ? │ ? │ \0│
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
                      ↑ garbage values

printf stamperà tutti i caratteri garbage finché non trova casualmente uno \0 in memoria.


4.3 Esempio Pratico del Problema

char a = 10;
char str[] = {'t', 'e', 's', 't'};  // MANCA \0!
printf("%s\n", str);
char b = 20;

Cosa potrebbe stampare:

Non è predicibile! È undefined behavior.


4.4 Metodo Semplificato: String Literals

Metodo 2: Inizializzazione con stringa letterale (raccomandato)

char str[6] = "Hello";
printf("%s\n", str);
// Output: Hello

Il compilatore:

  1. Converte automaticamente in {'H', 'e', 'l', 'l', 'o', '\0'}
  2. Aggiunge il null terminator automaticamente

Ancora più semplice (dimensione automatica):

char str[] = "Hello";
printf("%s World\n", str);
// Output: Hello World

sizeof(str) restituisce 6 (5 caratteri + \0).


4.5 Warning su Dimensioni

Array troppo piccolo:

char str[3] = "Hello";  // WARNING!
warning: initializer-string for char array is too long

Il compilatore tronca la stringa e non aggiunge il null terminator!

Array troppo grande:

char str[10] = "Hello";  // OK, nessun warning

Gli elementi rimanenti vengono inizializzati a \0:

┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ H │ e │ l │ l │ o │ \0│ \0│ \0│ \0│ \0│
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘

5. Manipolazione di Stringhe

5.1 Modificare Singoli Caratteri

Puoi modificare caratteri individuali tramite indici:

char str[] = "Hello";

str[3] = 'p';
printf("%s\n", str);  // Output: Helpo

Usando valori ASCII:

str[3] = 112;  // 'p' in ASCII è 112
printf("%s\n", str);  // Output: Helpo

Identico! Ricorda: char è un tipo numerico.


5.2 Incrementare Caratteri

Puoi fare aritmetica sui caratteri:

char str[] = "Hello";

str[3]++;  // 'l' (108) diventa 'm' (109)
printf("%s\n", str);  // Output: Helmo

⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⢰⣟⡶⣭⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⢵⣶⣭⣍⣛⠻⣿⣿⣿⣿⣿⣿⣿⣿⣶⠆⡇⡌⢷⡝⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡙⣷⠰⣍⢿⡈⢿⣿⣿⣿⣿⣿⠿⣣⡾⢁⣿⡆⢳⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⡿⢿⣿⣿⣿⣿⣿⡈⣇⢹⣆⠻⡌⢿⡿⣛⣭⠴⢛⣩⣴⣿⣿⢃⡼⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⡇⢾⣷⣮⡻⢿⣿⡿⣃⡾⢨⣿⡇⢻⠈⣶⢋⣴⣾⣿⣿⣿⡟⣡⠟⢡⣦⡹⣿⣿⣿⣿⡿⠛⡛⢿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣦⠹⡏⠻⡌⢃⡾⢋⣴⣿⣿⡇⢸⣤⠇⣾⣿⣿⣿⣿⣿⢰⠋⠀⣸⠏⢻⡌⢿⣿⡿⣁⡟⡿⢘⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⡿⠛⣛⠻⢿⣿⠟⣰⢇⡆⢱⣾⢁⣿⣿⣿⣿⣇⡓⣈⣂⣿⠟⣿⣿⣿⣿⣄⢿⢛⠏⣼⣆⡛⠶⢠⡾⢋⠀⣶⠲⣌⢿⣿⣿⣿⣿⣿ ⣿⣿⣷⡈⢿⡿⣇⣁⡾⢃⣾⣇⢸⡇⢸⣿⣿⣿⣿⣿⣿⣿⣿⣏⣴⣿⣿⣿⣿⣿⣷⣶⣾⣿⣿⡇⣼⡟⢰⣿⡀⠿⠀⡙⣧⢹⣿⣿⣿⣿ ⣿⣿⣿⣿⠌⣻⠈⡟⢱⣿⣿⣿⣦⡙⠈⣿⡇⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡘⠿⠈⣿⣿⣷⣾⡗⢸⠂⣿⣿⣿⣿ ⣿⣿⣿⡟⣸⠃⡆⠇⢸⣿⣿⣿⣿⣿⣿⡟⢸⣿⣿⣿⣿⣿⣿⠿⠟⠛⠿⣿⡿⠿⠿⠿⣿⣿⣿⣿⣿⣿⣇⣿⣿⠟⠉⠶⠋⠀⠈⠿⠿⠛ ⣿⡀⠈⠁⠉⠈⠀⠰⣿⣿⣿⣿⣿⣿⣿⣷⣿⣿⣿⣿⣿⡟⢁⣶⠈⠹⢓⡈⢴⠀⣘⣶⡌⢻⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠲ ⡉⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠛⠁⠸⣿⣷⠁⣿⣿⡆⢹⣿⣿⡿⠀⠛⢿⣿⣿⣿⣿⣿⠟⠀⠀⠀⠀⠀⠀⣤⣤ ⣿⠀⠀⠀⠀⠀⠀⠀⠙⢿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀⢿⡿⠃⠀⠈⠁⠀⠀⠀⠀⠈⠿⢿⡿⠃⠀⠀⠀⠀⠀⢰⣷⣍⣿ ⣷⣤⡄⢦⠀⠀⠀⠀⠀⠀⠻⣿⣿⣿⡋⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⠁⠀⠀⠀⠀⠀⢀⣼⣿⡿⣿ ⣿⣿⢃⣘⢷⣄⠀⠀⠀⠀⠀⠈⠻⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣭⣿⡿⢃⣼ ⡿⠟⢸⡟⠳⠞⣃⣄⠀⠀⠀⠀⠀⠘⢿⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⡖⣻⢰ ⣇⢻⡘⠻⢦⡙⢿⣿⣧⣄⠀⠀⠀⠀⠀⠈⠙⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⡿⠿⢛⣡⠏⣰ ⣿⣌⢷⣌⠳⣶⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⠃⣿⣉⢁⣾⣿ ⣿⣿⣷⣝⠳⠌⢻⣿⣿⡝⢿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣦⡍⢹⢸⣿⣿ ⣿⣿⣿⣿⣵⢆⣼⣿⣿⣿⣼⣿⣿⣿⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⠟⣛⣥⡶⢛⣼⣿⣿

Altro esempio:

char c = 'A';
c += 32;  // Conversione maiuscolo → minuscolo
printf("%c\n", c);  // Output: a

Questo funziona perché in ASCII:


5.3 Implementare printf(“%s”) Manualmente

Possiamo reimplementare la stampa di stringhe usando %c:

char str[] = "Hello";
int i = 0;

while (str[i] != '\0') {  // o semplicemente: while (str[i])
    printf("%c", str[i]);
    i++;
}
printf("\n");
// Output: Hello

Versione più compatta:

while (str[i] != 0) {  // 0 è equivalente a '\0'
    printf("%c", str[i++]);
}

Questo è essenzialmente ciò che fa printf con %s!


6. Sottostringa “Gratis”

6.1 Il Concetto

In C, ottenere una sottostringa è “gratis” (zero costo) perché le stringhe sono solo puntatori a byte in memoria.

Esempio:

char *s = "Hello";
s += 2;  // Sposta il puntatore di 2 posizioni
printf("%s\n", s);
// Output: llo

Cosa è successo:

Prima:

┌───┬───┬───┬───┬───┬───┐
│ H │ e │ l │ l │ o │ \0│
└───┴───┴───┴───┴───┴───┘
  ↑
  s punta qui

Dopo s += 2:

┌───┬───┬───┬───┬───┬───┐
│ H │ e │ l │ l │ o │ \0│
└───┴───┴───┴───┴───┴───┘
          ↑
          s punta qui

printf("%s") parte da dove punta s e legge fino a \0.


6.2 Perché È “Gratis”?

In linguaggi come Python o JavaScript:

s = "Hello"
sub = s[2:]  # Crea una NUOVA stringa in memoria

In C:

char *s = "Hello";
s += 2;  // Sposta solo il puntatore (nessuna copia!)

Non viene creata alcuna nuova stringa. Stai semplicemente leggendo la stessa memoria da un punto diverso.

Vantaggi:

Svantaggi:


7. Range dei char

7.1 char Come Tipo Signed

Le stringhe sono array di char, che è un intero a 8 bit.

Range di char (signed):

Range di unsigned char:


7.2 ASCII e il Valore 0

Lo standard ASCII è definito per valori da 0 a 127:

Perché \0 termina le stringhe:


8. Riepilogo Visivo

8.1 Rappresentazione in Memoria

char str[] = "Hello";

In memoria:

Indirizzo:  0x1000  0x1001  0x1002  0x1003  0x1004  0x1005
           ┌──────┬──────┬──────┬──────┬──────┬──────┐
Valore:    │  72  │ 101  │ 108  │ 108  │ 111  │  0   │
           │ 'H'  │ 'e'  │ 'l'  │ 'l'  │ 'o'  │ '\0' │
           └──────┴──────┴──────┴──────┴──────┴──────┘
Indice:      [0]    [1]    [2]    [3]    [4]    [5]

sizeof(str) = 6 byte


8.2 Confronto Tipi

char c = 'A';        // Singolo carattere, valore 65
char s[] = "A";      // Stringa, {'A', '\0'}, 2 byte
char *p = "Hello";   // Puntatore a stringa letterale

Differenze chiave:

Dichiarazione Tipo Dimensione Modificabile?
char c = 'A' Singolo char 1 byte
char s[] = "A" Array di char 2 byte
char *p = "A" Puntatore Dipende (punta a 2 byte) No (undefined behavior)

9. Esercizi con le Stringhe

Esercizio 1: Lunghezza di una Stringa

Scrivi una funzione che calcola la lunghezza di una stringa (senza usare strlen):

int string_length(char str[]) {
    int len = 0;
    while (str[len] != '\0') {
        len++;
    }
    return len;
}

int main(void) {
    char s[] = "Hello";
    printf("Lunghezza: %d\n", string_length(s));  // Output: 5
    return 0;
}

Esercizio 2: Convertire in Maiuscolo

Scrivi una funzione che converte tutti i caratteri minuscoli in maiuscoli:

void to_uppercase(char str[]) {
    int i = 0;
    while (str[i] != '\0') {
        if (str[i] >= 'a' && str[i] <= 'z') {
            str[i] -= 32;  // Differenza tra maiuscolo e minuscolo
        }
        i++;
    }
}

int main(void) {
    char s[] = "Hello World";
    to_uppercase(s);
    printf("%s\n", s);  // Output: HELLO WORLD
    return 0;
}

Esercizio 3: Invertire una Stringa

Inverti i caratteri di una stringa:

void reverse_string(char str[]) {
    int len = 0;
    // Trova la lunghezza
    while (str[len] != '\0') {
        len++;
    }
    
    // Inverti scambiando caratteri
    for (int i = 0; i < len / 2; i++) {
        char temp = str[i];
        str[i] = str[len - 1 - i];
        str[len - 1 - i] = temp;
    }
}

int main(void) {
    char s[] = "Hello";
    reverse_string(s);
    printf("%s\n", s);  // Output: olleH
    return 0;
}

Esercizio 4: Conta Vocali

Conta il numero di vocali in una stringa:

int count_vowels(char str[]) {
    int count = 0;
    int i = 0;
    
    while (str[i] != '\0') {
        char c = str[i];
        // Converti in minuscolo per semplificare
        if (c >= 'A' && c <= 'Z') {
            c += 32;
        }
        
        if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
            count++;
        }
        i++;
    }
    
    return count;
}

int main(void) {
    char s[] = "Hello World";
    printf("Vocali: %d\n", count_vowels(s));  // Output: 3
    return 0;
}

10. Approfondimenti

Spazio per ulteriori approfondimenti


Note Finali

Punti chiave da ricordare:

  1. char è un tipo numerico:
    • 'A' = 65 in ASCII
    • Puoi fare aritmetica: 'A' + 1 = 'B'
  2. Stringhe = Array di char + \0:
    • Il null terminator è obbligatorio
    • Senza \0 → undefined behavior, garbage output
  3. Inizializzazione:
    • char s[] = "Hello" → aggiunge \0 automaticamente
    • char s[] = {'H', 'e', 'l', 'l', 'o'} → MANCA \0, pericoloso!
  4. Flessibilità e Rischi:
    • Le stringhe sono “gratis” (puntatori)
    • Ma devi gestire tu la memoria e i limiti
    • Errori comuni: buffer overflow, mancanza di \0
  5. printf e format specifiers:
    • %s → stringa (array di char con \0)
    • %c → singolo carattere
    • %d → valore numerico del char

Le stringhe in C sono semplici ma potenti - sono solo array di byte. Questa semplicità dà velocità ma richiede attenzione ai dettagli!