Capitolo 5 - Impiego degli indici e degli array

 

 

 

 

 

            5.1 - La natura degli indici

 

 

                La notazione delle variabili nei linguaggi di programmazione é molto simile a quella utilizzata nell’algebra elementare, in cui la regola fondamentale é:

 

  (5.1.1)                   un nome simbolico rappresenta un valore in un certo insieme.

 

                Per molte esigenze essa é del tutto soddisfacente, ma per altre costituisce un limite tale da rendere addirittura impossibile la trattazione di un problema; in particolare, quando esso é formulato in modo tale da interessare una quantità variabile o generica di dati.

 

                Ad esempio, si supponga di avere una lista di numeri interi da ordinare in senso crescente; dalla formulazione é evidente che non si tratta di ordinare 2, 5, o 17 numeri dati, ma una quantità arbitraria di essi, che non può essere espressa da una costante fissata, ma solo da un valore simbolico, come in: data una lista di n numeri interi ...

 

                E’ evidente che in queste condizioni i dati non possono essere indicati con:

 

  (5.1.2)                   a, b, c, d, ....

 

o altri simili nomi fissi, che obbediscano cioè alla (5.1.1), perché non si potrebbe stabilire un criterio per l’ultimo nome,  e d’altra parte sarebbe molto difficile, se non impossibile, scrivere le istruzioni necessarie per elaborarli.

 

                La soluzione sta nell’adottare la notazione degli indici, scrivendo la sequenza come:

 

  (5.1.3)                   x1, x2, ..., xn-1, xn

 

o se si preferisce:

 

  (5.1.4)                   x0, x1, ..., xn-2, xn-1

 

a seconda che si voglia iniziare il conto delle posizioni, o coordinate nella lista, da zero o da uno.

 

                La differenza essenziale delle notazione degli indici rispetto ai nomi ‘normali’ é che in questo caso non é più vera la proprietà (5.1.1), ma la sua opposta:

 

  (5.1.5)                   un nome simbolico rappresenta un insieme di valori

 

ossia, al nome x delle (5.1.3) e (5.1.4) non corrisponde più un valore ben determinato; il solo x senza altra specificazione può solo essere inteso come nome collettivo dell’insieme degli n valori xk, per k = 1, 2, ..., n nel primo caso, oppure k = 0, 1, ..., n-1 nel secondo.

 

                Insiemi come quello in (5.1.2) si dicono vettori, (dalla terminologia matematica); la loro caratteristica é di essere composti da una sequenza ordinata in un numero finito di posizioni, di cui ogni elemento é detto componente del vettore, o coordinata nel vettore ed é individuato da un indice, che é un numero intero. Un’elencazione, o ordinamento di questo tipo si dice anche lineare, perché procede uniformemente in una ‘dimensione’.

 

                L’impiego degli indici non é limitato a questo caso: sono infatti molto comuni tabelle divise in righe e colonne; si tratta ancora di insiemi, in cui però l’individuazione di una particolare posizione richiede l’impiego di due numeri interi indipendenti, il numero di riga e quello di colonna. La notazione più opportuna, supponendo che il conto di entrambi gli indici inizi da zero e che la tabella sia composta da m+1 righe ed n+1 colonne é:


 

  (5.1.6)                   a0,0          a0,1          ...             a0,n

                               a1,0          a1,1          ...             a1,n

                               ..................

                               am,0          am,1          ...             am,n

 

                Un insieme di questo tipo si dice matrice, ancora dalla terminologia matematica; esso é ordinato bilinearmente, poiché ogni indice procede secondo un ordinamento lineare indipendente.

 

                Un problema che richiede tipicamente matrici é la soluzione si sistemi di equazioni lineari (di primo grado), che non solo non può essere risolto, ma nemmeno formulato nella sua generalità senza ricorrere ad una notazione come la (5.1.6). L’impiego di matrici é comune anche in altri campi, ad esempio per la raccolta di dati statistici; esse sono pure lo strumento alla base dei fogli elettronici (in inglese, spreadsheet) di noti programmi applicativi, quali ad esempio Lotus 1-2-3 ed Excel.

 

                L’estensione della notazione delle matrici a tre o più indici, con ordinamento trilineare o di ordine superiore, é abbastanza ovvia e non richiede ulteriori commenti.

 

                E’ invece opportuno menzionare il nome con cui nel gergo informatico vengono indicati collettivamente gli insiemi di dati strutturati come in (5.1.4), (5.1.6) e nella loro estensione a più di due indici: tale termine è array, che é più restrittivo di insieme, perché implica per le posizioni degli elementi un ordinamento che sia un ‘allineamento’; esso viene infatti impiegato per indicare una organizzazione in righe, come ad esempio di soldati in una parata. Più esattamente, si parla di array ad una dimensione, a due dimensioni, ecc.

 

                Per terminare, osserviamo come:

 

  (5.1.7)                   l’individuazione di un elemento in un array richiede due informazioni logicamente

                               indipendenti: il nome dell’insieme ed il valore dell’indice (o degli indici, se ne esiste

                               più di uno); senza una della due parti l’informazione é incompleta e l’elemento non

                               é determinabile.

 

                Si potrebbe dire che gli indici ‘aggiungono una dimensione’ (e ciò in senso abbastanza letterale) alla capacità di rappresentazione delle variabili, aprendo così orizzonti applicativi altrimenti preclusi.

 

 

 

 

 

            5.2 - Notazione degli indici in C

 

 

                Analizzato il problema posto dalle variabili dotate di indici, o array, per averne la disponibilità in un linguaggio di programmazione é sufficiente definire una notazione adeguata, che non può essere, ad esempio:

 

  (5.2.1)                   x0, x1, x2, ....

 

perché, stando alle regole di formazione dei nomi, identificatori di questo tipo sono già impegnati, e riconosciuti dal compilatore, come variabili ordinarie, per le quali vale la regola (5.1.1).

 

                La scrittura ordinaria degli indici, ossia il loro ‘abbassamento’, utilizza implicitamente lo spazio bidimensionale del foglio di scrittura; questo non é però possibile nella scrittura di istruzioni di un linguaggio di programmazione, che richiede espressioni unidimensionali.

 

                La soluzione sta nell’introduzione di un simbolo speciale (non alfanumerico); il C utilizza per questo scopo le parentesi quadre ‘[‘ e ‘]’, che sono denominate operatori di array, in una notazione la cui forma generale é:

 

  (5.2.2)                   identificatore [ espressione ]

 

in cui, a differenza delle normali convenzioni per descrizioni sintattiche, le parentesi quadre non indicano stavolta un’opzione, ma sono parte integrante della definizione.

 

 

                Nella (5.2.2) identificatore é un nome creato dall’utente con le regole consuete (solo alfanumerici e ‘_’), mentre espressione é una qualunque espressione (aritmetica) legittima per il C, che nella maggior parte dei casi si riduce ad una costante, una variabile, o una combinazione limitata di entrambe. Poiché gli indici debbono essere numeri interi, se l’espressione non é già intera, il suo valore viene troncato alla parte intera..

 

                Se gli indici sono due, la (5.2.2) si modifica in:

 

  (5.2.3)                   identificatore [ espressione_1 ] [ identificatore_2 ]

 

che é una notazione un po' meno ‘naturale’ di quella del Fortran o del Basic, in cui gli indici sono racchiusi in parentesi tonde e separati da virgole quando ne esiste più d’uno, come in m(i,j). La ragione della scelta del C è nel modo di associare puntatori agli array, come mostrato più avanti.

 

                Anche in questo caso l’estensione a più di due indici è immediata.

 

                Abbiamo già incontrato la notazione degli indici nella tabella degli operatori (4.4.5) ed in quella delle loro precedenze (4.4.6) e nella seconda essi figurano al primo posto della lista, che implica che in una espressione in cui compaia una variabile dotata di indici, la valutazione degli indici é sempre la prima operazione da compiere; l’esigenza di questa convenzione é ovvia, poiché senza il valore esatto dell’indice non é determinato l’elemento dell’array, vale a dire che non sarebbero possibili altre elaborazioni di dati nell’espressione in cui la variabile dotata di indice compare.

 

                La scrittura di una lista come la (5.1.4)  é quindi in C:

 

  (5.2.2)                   x[0], x[1], ..., x[n-1], x[n]

 

ed ogni elemento che in essa compare individua una ben precisa posizione di memoria, esattamente come accade per una semplice variabile ordinaria. Ne deriva che essi possono comparire in qualunque contesto in cui é autorizzato l’impiego di variabili, cioè come:

 

  (5.2.3)                   (a) - oggetto di assegnazioni

                               (b) - componente di espressioni

                               (c) - argomento di funzione, in particolare, elemento di liste I/O

 

                Consideriamone alcuni esempi, in cui si suppongono sempre ben definite (cioè dichiarate ed inizializzate o calcolate) tutte le espressioni che figurano come indici:

 

  (5.2.4)                   (a):          x[k] = 5 ;                                              a[3][n-2] = 3 * ( n - 2 ) ;

                               (b):          y = x[k/2] ;                                           x[k+1] = 2 * x[k] ;

                               (c):          z = sqrt ( abs ( x[p] ) + 1 ) ;                printf ( “%d”, x[i+j] ) ;

 

da cui si rileva che l’impiego delle variabili dotate di indici non sembra porre problemi particolari.

 

                Accenniamo ancora ad un argomento che dovrà essere ripreso trattando le funzioni e le strutture complesse di dati; nell’esame condotto fin qui si sono considerati gli indici solo in riferimento alle variabili, ma nel linguaggio C la loro disponibilità é molto più ampia; ad esempio, l’identificatore in (5.2.2) o (5.2.3) può essere quello di un puntatore o di una funzione; possono cioè esistere array di puntatori, di funzioni ed altri ancora.

 

 

 

 

 

            5.3 - Dimensionamento degli array

 

 

                Si é appena detto che l’impiego degli indici non sembra porre problemi, ma in effetti almeno uno esiste e riguarda la collocazione in memoria dei nuovi oggetti logici, gli array, o insiemi di variabili dotate di indici. Il problema riguarda il compilatore, che deve decidere come predisporre l’impiego della memoria per la rappresentazione dei dati, sia per il numero di byte necessari, che per il modo di trattarli (tipo di variabile).


 

                Tale compito non presenta difficoltà per le variabili ordinarie, perché la loro dichiarazione le associa ad un tipo e quindi ad una precisa occupazione di memoria: due byte per un intero, otto per un double, ecc.; é però diverso il caso degli array, perché dalla sola indicazione che esistono indici non é possibile dedurre qual’è il loro valore massimo, cioè quanto spazio di memoria serva effettivamente per memorizzare l’array.

 

                Ciò viene risolto in sede di dichiarazione del dato, adottando la medesima notazione degli indici, ma con un diverso significato; la descrizione formale della dichiarazione, in cui si noti però che le parentesi quadre non indicano una opzione, ma sono parte integrante della sintassi, é:

 

  (5.3.1)                   tipo identificatore[dimensione] ;

 

in cui tipo é uno qualunque dei tipi semplici, completati da modificatori, o generati da una typedef, mentre identificatore segue le consuete regole di formazione dei nomi, e dimensione deve essere una costante intera, eventualmente scritta in forma simbolica tramite una precedente #define.

 

                E’ pure possibile l’inizializzazione con una lista di costanti coerenti con il tipo di dato, nella forma:

 

  (5.3.2)                   tipo identificatore[dimensione] = { valore_1, valore_2, ... , valore n } ;

 

con la restrizione che il numero di elementi della lista non superi la dimensione. In questo caso si ha però una ulteriore possibilità: poiché in presenza di una lista di inizializzazione si può delegare al compilatore il conteggio del numero di componenti, é ammessa l’omissione dell’indicazione esplicita di dimensione, nella forma:

 

  (5.3.3)                   tipo identificatore[ ] = { valore_1, valore_2, ... , valore n } ;

 

che può però subire qualche restrizione in presenza del modificatore extern (si veda la parte sulle classi di memoria).

 

                Per le matrici si ha invece:

 

  (5.3.4)                   tipo identificatore[dimensione_1][dimensione_2] ;

 

oppure:

 

  (5.3.5)                   tipo identificatore[dimensione_1][dimensione_2] = { riga_1, riga_2, ... , riga n } ;

 

ove con ‘riga’ si é indicata una lista di costanti simile a quella che compare a secondo membro nella (5.3.2), vale a dire che le parentesi graffe in (5.3.5) racchiudono liste di parentesi graffe. Al solito, é ovvia l’estensione ad array con tre o più dimensioni.

 

                Ad esempio:

 

  (5.3.6)                   char lista[30] ;                    int vettore[50] ;                   double numeri[6] ;

 

che equivalgono rispettivamente ad una occupazione di memoria di 30, 50*2 = 100 e 6*8 = 48 byte; come per le variabili ordinarie, non si deve assumere che la memoria venga automaticamente inizializzata a valori particolari, ad esempio tutti nulli: è probabile che un particolare compilatore faccia proprio questo, ma tutti i dati debbono ritenersi indefiniti fino alla loro determinazione esplicita, ad esempio tramite una inizializzazione, oppure con una operazione di input da un dispositivo esterno.

 

                Sono invece esempi di array inizializzati:

 

(5.3.7)                     int vett[6] = { 2, -4, 7 }  ;

                               double mat[2][2] = { { 1, 0 }{ 0, 1 } }  ;

                               long primi[ ] = { 2, 3, 5, 7, 11, 13, 17, 19 } ;

 

ove nel primo caso il numero di costanti é minore della dimensione di vett, le cui prime tre componenti assumono i valori dati, mentre le rimanenti sono indefinite; nel secondo la matrice viene inizializzata per righe in modo completo e con conversione dei dati dalla forma intera a quella double; nel terzo, infine, l’array primi ha dimensione 8, che il compilatore determina automaticamente.


 

                Nella dichiarazione é possibile associare semplici variabili, puntatori ed array come in:

 

  (5.3.8)                   int numero, *ipunt, dati[3] = { 2, -4, 7 }, altri_dati[40] ;

 

                Le seguenti due proprietà sono molto importanti per l’impiego degli array:

 

  (5.3.9)                   Il primo indice in un array ha sempre valore zero; vale a dire che se la dimensione è N,

                               sono indici legittimi i valori 0, 1, ... N-1.

 

  (5.3.10) Nelle configurazioni standard i compilatori non predispongono il controllo automatico di

                               validità degli indici in esecuzione, ossia é responsabilità dell’utente assicurarsi che l’effettivo

                               impiego degli indici nel programma sia coerente con le dimensioni degli array.

 

                La validità degli indici é molto importante, poiché errori in questo campo hanno sempre come conseguenza il deterioramento del programma eseguibile, sulla parte della memoria in cui sono contenuti i dati, oppure le istruzioni, o entrambe, ed in qualche caso anche per il sistema operativo.

 

                Se l’espressione utilizzata per un indice contiene degli errori logici, il valore che ne deriva può essere negativo, oppure superare il massimo dato dalla dimensione; quando in un programma sono presenti errori di questo tipo le conseguenze sono imprevedibili, ed essi sono la causa più frequente dei comportamenti anomali durante l’esecuzione, come l’improvviso arresto delle operazioni, il blocco del sistema, ecc.

 

                Quasi tutti i compilatori sono in grado di predisporre il controllo automatico degli indici, cioè di aggiungere  al programma eseguibile istruzioni che prima di utilizzare un indice ne controllano la compatibilità con la dimensione dell’array cui esso si riferisce. Questo richiede in genere di modificare il comando di compilazione in modo diverso da quello predefinito (default) ed ha l’effetto di aumentare l’occupazione di memoria della versione eseguibile del programma e di rallentarne notevolmente le operazioni ‘utili’; tale ‘costo’ del controllo consiglia di farne uso solo nelle fasi intermedie di sviluppo di un programma, ma non più nella costruzione della versione eseguibile finale.

 

 

 

 

 

 

 

            5.4 - Array e puntatori

 

 

                Se si utilizzano variabili dotate di indici con dichiarazioni come la (5.3.1) e le sue generalizzazioni, si vincola l’impiego dell’identificatore alla presenza dell’operatore di array; per fissare le idee, se in un programma si dichiara:

 

  (5.4.1)                   int xx[10] ;

 

ogni impiego successivo del simbolo xx comporta l’impiego delle parentesi quadre, all’interno delle quali deve comparire una espressione, per quanto osservato in (5.1.7).

 

                Poiché l’impiego del solo identificatore non avrebbe significato operativo, vi é l’opportunità per utilizzarlo in qualche altro scopo, e questo é appunto ciò che é stato fatto nel linguaggio C, in cui:

 

  (5.4.2)                   l’identificatore di un array é un oggetto logico indipendente, la cui natura é quella di un

                               puntatore, che contiene l’indirizzo della prima componente dell’array.

 

                Vale a dire, con la dichiarazione (5.4.1) il simbolo xx risulta automaticamente definito come:

 

  (5.4.3)                   xx = &xx[0] ;

 

esattamente come se esistesse la dichiarazione e che é scorretto scrivere congiuntamente alla (5.4.1)

 

  (5.4.4)                   int *xx ;


 

                La disponibilità automatica dei nomi di array come puntatori, associata all’aritmetica sugli indirizzi definita in (4.5.13), permette di utilizzare una notazione sostitutiva a quella tradizionale degli indici. L’ipotesi implicita, che trova riscontro in tutte le organizzazioni logiche di macchina, essendo il contrario troppo irrazionale, é che per un vettore (array unidimensionale) l’allocazione delle componenti sia fatta in posizioni consecutive della memoria, ordinate secondo gli indici crescenti.

 

                Noto l’indirizzo di memoria del vettore, cioè della sua componente di posto zero, quello di ogni altra é immediatamente calcolabile come somma di tale indirizzo e di un offset (scostamento) pari al valore dell’indice per il numero di byte che costituiscono l’unità di misura per il tipo di dato interessato.

 

                Ad esempio, se xx é definito come in (5.4.1), le due notazioni che seguono sono del tutto equivalenti e determinano il medesimo indirizzo:

 

  (5.4.5)                   &xx[k]                                  xx + k

 

e sono quindi equivalenti anche le due istruzioni C:

 

  (5.4.6)                   xx[k] = 15 ;                          * ( xx + k ) = 15 ;

 

di cui la seconda é potenzialmente più efficiente in fase di esecuzione; ciò dipende però dalle caratteristiche della macchina, del sistema operativo e del compilatore.

 

                Se si sostituisce la notazione ‘classica’ degli indici con l’aritmetica dei puntatori si deve però fare attenzione ad utilizzare espressioni come quella che figura nella seconda parte della (5.4.6), che utilizza una espressione il cui valore (identificabile con le parentesi tonde) é un indirizzo, ottenuto però senza modificare il puntatore xx. Ossia, non si debbono utilizzare operazioni come:

 

  (5.4.7)                   xx++ ;                    xx += k ;

 

perché esse alterano l’informazione sulla collocazione in memoria dell’intero array.  Se si ritiene necessaria qualche operazione di questo tipo, si definisca invece un nuovo puntatore indipendente con un altro nome, si copi in esso il valore (fisso) dell’indirizzo dell’array e si proceda poi a modificarlo.

 

 

                La relazione tra indici e puntatori diviene più complessa in presenza di più indici; consideriamo in particolare il caso delle matrici.

 

  (5.4.8)                   Per il C una matrice, o array bidimensionale, deve essere considerata come un vettore

                               di righe, ossia come un array lineare di elementi che sono a loro volta array lineari.

 

                Ne segue che, data la dichiarazione:

 

  (5.4.9)                   int mat[4][5] ;

 

il simbolo mat[k], k = 0, 1, .., 4 é l’identificatore di un array di cinque elementi:

 

  (5.4.10) mat[k][0],  mat[k][1],  mat[k][2],  mat[k][3],  mat[k][4]

 

e per quanto abbiamo visto sopra mat[k] é un puntatore; più esattamente sono simboli equivalenti, nel senso che essi rappresentano il medesimo indirizzo di memoria:

 

  (5.4.11) mat[k]                   &mat[k][0]

 

  (5.4.12) mat                         &mat[0]                                &mat[0][0]

 

                Al solito, la generalizzazione a più di due dimensioni è immediata. Possiamo ancora osservare che per la (5.4.8) le righe della matrice sono tra loro indipendenti, ossia che non é obbligatorio che esse siano memorizzate consecutivamente in memoria, anche se questa é la norma per motivi di semplicità ed efficienza.


 

                Con le matrici abbiamo incontrato un primo esempio di array di puntatori, in questo caso di un vettore le cui componenti rappresentano indirizzi: è l’identificatore mat in (5.4.12).

 

                L’operatore di array può essere applicato ad ogni genere di identificatore, non soltanto a quelli delle variabili; é quindi possibile dichiarare un array di puntatori, ossia una sequenza di indirizzi di memoria individuati da un indice, indipendentemente dall’utilizzazione degli indirizzi;  é però sempre necessario fare riferimento ad un tipo di dato, oppure utilizzare la parola chiave void. Ad esempio:

 

   (5.4.13)                long        *lpunt[20] ;

                               void         *vpunt[10] ;

 

con cui si predispongono in totale 30 posizioni di memoria adatte a contenere indirizzi (si ricordi che la struttura in byte di ognuno di essi non interessa), dei quali i primi 20 associati al tipo long, quindi collegati alla lunghezza implicita del dato di 4 byte ed a ben definite operazioni, mentre i secondi 30 sono indirizzi ‘puri’.

 

                Non si deve leggere la (5.4.13) avendo in mente la tabella delle precedenze (4.4.6), principalmente perché essa non vale per le dichiarazioni, ma per le espressioni. In questo contesto il simbolo ‘*’ deve essere inteso come un modificatore della natura del dato da dichiarare, cioè come se fosse scritto:

 

  (5.4.14) long*     lpunt[20] ;

                               void*      vpunt[10] ;

 

che é pure legittimo per la sintassi.

 

                Il caso più frequente di impiego di dichiarazioni come la (5.4.13) é probabilmente in riferimento ad un tipo particolare di array ed oggetto logico ad un tempo; si tratta delle stringhe.

 

 

 

 

 

 

            5.5 - Le stringhe

 

 

                E’ già accaduto di menzionare in modo informale le stringhe, ad esempio come primo argomento di una funzione printf; si é poi anche menzionata la notazione per le stringhe costanti, con la notazione dei doppi apici.

 

  (5.5.1)                   Per stringa si intende una arbitraria sequenza di caratteri ASCII, ognuno dei quali

                               richiede quindi un byte per la sua memorizzazione, considerata come unico oggetto

                               logico. Se costante, essa deve essere definita con una coppia di doppi apici in ruolo

                               di inizio-fine e se variabile deve essere memorizzata in un array di tipo char.

 

                Vale a dire, una stringa é fondamentalmente un vettore di caratteri. Nei linguaggi di programmazione esistono diversi criteri per rendere le stringhe oggetti definibili ai fini delle elaborazioni; quella adottata dal C é la convenzione detta ASCIZ (da Z per zero):

 

  (5.5.2)                   Una stringa si considera terminata quando si incontra un byte nullo, i cui otto bit sono

                               cioè tutti impostati a zero.

                               Per lunghezza della stringa si intende il numero di caratteri dal primo disponibile fino al

                               byte  nullo, escludendo quest’ultimo dal conto; la lunghezza può anche essere zero.

 

                Poiché le stringhe sono casi particolari di array di caratteri, se essa viene inizializzata secondo la (5.3.2), scrivendo cioè ad esempio:

 

  (5.5.3)                   char       forse_stringa[10] = { ‘A’, ‘B’, ‘C’, ‘D’ } ;

 

non si ottiene l’effetto voluto, ma semplicemente la definizione di un array di caratteri, dei quali i primi quattro determinati con la corretta notazione dei caratteri costanti, ed i rimanenti indefiniti.


 

                Ne segue che le funzioni per l’elaborazione delle stringhe disponibili nella libreria del C non possono operare correttamente su questo oggetto, che é magari legittimo da altri punti di vista, ma é scorretto dal punto di vista delle stringhe; esso potrebbe divenirlo modificandone la definizione in:

 

  (5.5.4)                   char       vera_stringa[10] = { ‘A’, ‘B’, ‘C’, ‘D’, ‘\0’ } ;

 

con cui diviene riconoscibile come stringa di lunghezza 4. La disponibilità della notazione per le stringhe costanti permette di semplificarne la dichiarazione, rendendo più evidente l’intenzione di creare un unico oggetto logico, come prova l’assenza delle parentesi graffe:

 

  (5.5.5)                   char       vera_stringa[10] = “ABCD” ;

 

                L’effetto delle (5.5.4) e (5.5.5) é del tutto identico, ma é ovvia la convenienza della seconda notazione; in entrambi i casi vengono allocati dieci byte di memoria, utilizzandone effettivamente solo cinque: ciò potrebbe essere giustificato dalla necessità di estendere la lunghezza della stringa con modifiche successive (mai però oltre nove caratteri ‘utili’). Se non esiste questa esigenza, meglio scrivere:

 

  (5.5.6)                   char       stringa[ ] = “ABCD” ;

 

che utilizza solo il numero di byte necessari. In questo caso si deve però fare attenzione a non cercare di estenderli in un secondo tempo, poiché verrebbero deteriorati dati e/o programma con conseguenze imprevedibili.

 

                Poiché la sintassi della dichiarazione non obbliga alla inizializzazione, é corretto scrivere:

 

  (5.5.7)                   char       ignoto [ ] ;

 

e ci si deve chiedere che genere di dato si genera con questa dichiarazione: poiché si é utilizzata la notazione di array, ma senza specificarne il numero di elementi, é comunque definito un puntatore a carattere di nome ignoto; vale a dire, la (5.5.7) é equivalente a:

 

  (5.5.8)                   char * ignoto ;

 

                Questa doppia possibilità può indurre facilmente in errore, se nel programma si cerca di modificare i caratteri di ignoto, inteso come stringa, non importa se con la notazione degli indici o con l’aritmetica dei puntatori: in ogni caso si commette lo stesso errore già evidenziato dopo (5.5.6), con la differenza che qui lo si commette in ogni caso.

 

                Mentre l’oggetto logico detto stringa é ben definito nella sintassi del C, non vi sono però operatori specifici per le stringhe; la loro intera elaborazione é rinviata ad opportune funzioni di libreria, che richiedono in genere il file di testata <string.h>; alcune delle più comuni sono:

 

  (5.5.9)                   funzione                                               descrizione

                               strlen ( stringa ) ;                              /* calcola la lunghezza in byte di stringa */

                               strset ( stringa, carattere ) ;            /* poni tutti i byte di stringa al carattere dato */

                               strcpy ( destinazione, origine ) ;     /* copia la stringa origine nella stringa destinazione */

                               strcat ( destinazione, origine ) ;      /* concatena la stringa origine alla stringa destinazione */

 

per le quali si suppone sempre che la stringa sia correttamente terminata da un byte nullo. Considerando ad esempio la terza, questo significa che tale byte costituisce il criterio di arresto per le operazioni di copia a partire dagli indirizzi del primo carattere delle due stringhe; quindi, se non esiste il byte nullo, l’operazione prosegue indefinitamente, con presumibili effetti distruttivi sul programma e spesso anche per il sistema operativo.

 

                Il seguente segmento di programma contiene un errore di questo tipo:

 

  (5.5.10) char       str [ ] ;

                               .........

                               strcpy ( str, “qualunque cosa, ma sempre distruttiva” ) ;

 

errore che il compilatore C non può riconoscere ed ‘intrappolare’, nemmeno se é attiva  l’opzione di controllo di validità degli indici, di cui si é detto in precedenza.


 

                Per maggiore facilità di impiego, alcune funzioni di stringa hanno una alternativa che utilizza un parametro in più; quest’ultimo é un intero che specifica il numero massimo di caratteri per cui l’operazione richiesta deve essere compiuta; ad esempio:

 

  (5.5.11) strnset ( stringa, carattere, numero ) ;

                               strncpy ( destinazione, origine, numero ) ;

 

spesso utilizzate nella forma:

 

  (5.5.12) strnset ( stringa, carattere, strlen ( stringa )  ) ;

 

in cui il calcolo delle posizioni é automatico e dipende della dichiarazione di stringa. Un semplice esempio di applicazione di queste funzioni può essere il segmento di programma:

 

  (5.5.13) char s1[ ] = “frase completa” , s2[ ] = “Questa é una “, s3[40] ;

                               .........

                               strcpy ( s3, s2 ) ;  strcat ( s3, s2 ) ;

                               printf ( “\n risultato : %s \n“, s3 ) ;

 

che forma e scrive la stringa unione delle due date; si osservi che dopo le operazioni sono presenti tre stringhe, di cui due invariate. Per quanto riguarda la funzione di scrittura a video printf, ci limitiamo per ora ad anticipare che ‘%s” é l’opzione di formato da utilizzare per descrivere la scrittura di una stringa; essa, come le funzioni di stringa, opera sull’ipotesi che esista un byte nullo per l’arresto della sequenza di caratteri da ‘scaricare’ in scrittura.

 

                Alla fine della parte su ‘array e puntatori’ si é citato il caso degli array di puntatori; essi trovano un utile caso di applicazione quando si voglia inizializzare un vettore di stringhe, con cui ci si attenderebbe forse una struttura equivalente a quella di una matrice.

 

                Questo non é però esatto; consideriamo come esempio la seguente dichiarazione:

 

  (5.5.14) char * lista_num [ ] = { “zero”, “uno”, “due”, “tre”, “quattro”, “cinque” } ;

 

che definisce un vettore di stringhe, i cui elementi sono lista_num[0] ( = “zero” ), lista_num[1] ( = “uno” ), ecc.

 

                E’ interessante notare che non tutte le stringhe hanno la stessa lunghezza; la normale allocazione in memoria delle sei stringhe dovrebbe essere composta dalla sequenza di byte di contenuto:

 

  (5.5.15) ‘z’, ‘e’, ‘r’, ‘o’, ‘\0’, ‘u’, ‘n’, ‘o’, ‘\0’, ‘d’, ‘u’, ‘e’, ‘\0’, ....

 

e come si vede più che ad una matrice, in cui tutte le righe sono strutturalmente identiche, si deve pensare alla strutturazione dello spazio lineare di un vettore con una legge di divisione interna data dagli zeri binari.

 

                Le singole stringhe di questo esempio sono naturalmente disponibili per elaborazioni; ponendo ad esempio:

 

  (5.5.16) lista_num[2][1] = ‘i’ ;

 

si ottiene che il valore della stringa lista_num[2] diviene “die”; si deve però porre cura particolare nella gestione degli indici: ad esempio se al posto di (5.5.16) si pone:

 

  (5.5.17)                 lista_num[2][3] = ‘i’ ;

 

ne deriva che lista_num[2] ha valore “dueitre”, mentre lista_num[3] rimane “tre”.

 

                Gli stessi avvertimenti valgono a maggior ragione se una stringa di un array di stringhe é utilizzato come argomento in una funzione del tipo di quelle in (5.5.9), specie come ‘destinazione’ dell’operazione.