Introduzione al C

Discussioni su: C/C++, Visual Basic, Delphi, HTML, Java, VBScript, PHP, SQL ...
Rispondi
LoryOne
Senior Member
Senior Member
Messaggi: 31
Iscritto il: 19/07/2018, 9:56
Stato: Non connesso

Introduzione al C

Messaggio da LoryOne » 25/07/2018, 12:37

Keyboard Input
Questa guida in C, la prima nella sezione Sviluppo del grectech Forum, è rivolta ai principianti con una certa familiartità sui tipi di dato gestiti dal linguaggio C, a coloro che si accingono a compiere i primi passi nell'utilizzo di questo ostico ma potente linguaggio, fornendo al contempo importanti indicazioni preliminari per un corretto approccio alla programmazione "con metodo" e che vede impiegate poche ma semplici regole dogma, troppo spesso sottovalutate in fase di progettazione, che possono portare a comportamenti imprevedibili ed, in alcuni casi a dir poco disastrosi, se non seguite con attenzione.
Si prefigge tre scopi essenzialmente:
  • Il primo è quello di fornire una porzione di codice riutilizzabile;
  • Il secondo è quello di imporre la ricostruzione dell'intero script secondo logica, poichè le porzioni prese in esame volta per volta, saranno esaminate a casaccio;
  • Il terzo è quello di puntualizzare che una qualsiasi procedura, indipendentmente dalla sua complessità, deve essere prima scomposta in moduli e poi ricomposta, tenendo presente che:
  • Un programmatore non conosce un solo linguaggio;
  • Un programmatore conosce più di un linguaggio valutandone pregi e difetti in base a cio che deve sviluppare, scegliendo quello che maggiormente si addice alla tipologia dell'applicativo che sviluppa;
  • Un programmatore ama linguaggi che presentino interfacce di scambio tra tipi differenti con differenti potenzialità: Non è raro, infatti, che porzioni di codice che necessitino di velocità d'esecuzione maggiore vengano scritte in assembly o in C e successivamente integrati nel progetto principale.
Cominciamo dalla fine, ovviamente, e vediamo un esempio di cosa vogliamo ottenere:

Codice: Seleziona tutto

Immetti una lettera [A..D] e 2 cifre [0...9]:A12
Hai inserito: A12
Premere un tasto per continuare . . .
Ci viene richiesto di immettere una lettera e due cifre, ma le lettere possono essere solamente A,B,C,D e le due cifre possono avere un valore da 0 a 9.
Dopodichè, ci viene ripresentato cio che abbiamo digitato prima di attendere la pressione di un tasto per continuare.
Da quel che si nota a schermo, capiamo che:
  • Ci serve una funzione che acquisisca il valore che digitiamo;
  • Ci serve che detta funzione effettui un controllo sulla correttezza dei tasti che digitiamo in base a quali siano i caratteri accettati e quali no;
  • Ci serve che detta funzione accetti solo 3 caratteri, di cui uno letterale e due numerici.
Passiamo a quello che potrebbe essere l'aspetto della nostra funzione principale main() che viene sempre eseguita per prima.

Codice: Seleziona tutto

char *UserInput(int max){
.
..
...
main(void){
    printf("Immetti una lettera [A..D] e 2 cifre [0...9]:");
    printf("\nHai inserito: %s\n",UserInput(3));
    system("pause");
}
La nostra funzione main() non:
  • Ritorna alcun valore in uscita da essa;
  • Accetta alcun parametro dalla riga di comando.
La seconda istruzione printf è molto interessante, poiché va a capo (/n) in uscita dalla prima istruzione printf, scrive a video “Hai inserito :”, associa ad %s il valore della funzione UserInput che accetta solo 3 caratteri ed infine va nuovamente a capo, dopo averlo scritto.
E' davvero così ?

Codice: Seleziona tutto

Immetti una lettera [A..D] e 2 cifre [0...9]:_
L'esecuzione a video ci fornisce un risultato diverso: Il cursore si pone alla fine della prima stringa eseguita da printf e la nostra funzione resta in attesa di input; E' un po come se il computer desse precedenza alla nostra funzione rispetto alle istruzioni impartite sequenzialmente, cosa assurda se si considera che il computer è una macchina e che non può decidere da sola, a meno che siamo noi a pensare diversamente da come essa agisce, quindi dobbiamo tenerne conto in fase progettuale.
Esistono due punti fermi nella programmazione:
  • Il computer esegue sequenzialmente un' istruzione per volta;
  • Il computer non fa nulla se non gli s'impone di farlo.
Allora l'errore non può che essere nostro !
Niente di più vero: Se dopo innumerevoli tentativi vi ostinate a non capire il perchè la vostra testa vi dice ad un modo ed il computer fa in un altro, dovete mettervi a riscrivere il codice, ma l'ennesima volta in modo diverso.
Dovete infine accettare che l'istruzione che utilizzate è stata scomposta, testata, assemblata e ritestata a monte e che fa esattamente quello che deve, quando viene richiamata.

Codice: Seleziona tutto

main(void){
    printf("Immetti una lettera [A..D] e 2 cifre [0...9]:");
//    printf("\nHai inserito: %s\n",UserInput(3));
    char *c;
    c=UserInput(3);
    printf("\nHai inserito: %s\n",c);	
    system("pause");
}
Pertanto, avendo assunto che l'istruzione printf scrive valori a video, tali valori devono essere ricavati precedentemente e solo dopo visualizzati con apposita istruzione che, nella programmazione metodologica, va richiamata per ottenere esclusivamente la funzione per la quale è stata ideata.
Scrivere un codice pulito, ordinato e privo di fronzoli è fondamentale sia per facilitare l'operazione di debug effettuata anche da parte altrui, sia per facilitare l'operazione di manutenzione del codice, anche a distanza di tempo.
Dopo questa breve ma puntuale digressione, procediamo col codice:

Codice: Seleziona tutto

    char c,*Buff;
    register short int i=0,j=0,vmin,vmax;
        
    Buff=(char*)malloc(max + 1); /* Aggiunge spazio (max+1) per il terminatore */
    while(c!=13 || i<2){      /* Mai invio a vuoto. 
                                 Sempre almeno una lettera ed un numero */
E' importante spendere due parole sulla variabile Buff, definita come puntatore alla variabile Buff di tipo char; Facile intuire che detta variabile il cui nome è già esplicativo per se, predisponga il buffer che sarà riempito con i caratteri digitati e che costituirà il valore della funzione UserInput alla pressione del tasto invio.
Le operazioni che un computer svolge sono essenzialmente cinque:
  • Memorizzare un' informazione;
  • Spostarla da una locazione ad un'altra della memoria;
  • Agire su di essa in lettura/modifica/cancellazione;
  • Verificare sempre l'operazione effettuata;
  • Passare alla prossima istruzione.
Una volta definita una variabile o una sub procedura, sia essa un puntatore o meno, il suo peso in memoria (ossia quanto occupa) dipende dalla tipologia specificata. Cio significa che il computer deve allocare nella memoria cio che gli serve partendo da una locazione precisa per tutta la sua lunghezza. Una variabile puntatore ad un buffer di tipo char, non indica affatto quanto sia lungo, ma solo dove si trova.

Codice: Seleziona tutto

Buff=(char*)malloc(max + 1); /* Aggiunge spazio (max+1) per il terminatore */
L'allocazione diventa di tipo dinamico (malloc) a partire da posizione nota fino al termine specificato che in questo caso vale 4.
Perchè se io voglio solo 3 caratteri nel buffer (uno letterale + due numerici), devo tenerne presente uno in più ?
Siccome il computer non accetta mai valori vuoti nelle celle di memoria e la memoria è sempre piena di valori casuali in ogni sua locazione, bisogna specificare un valore che il computer identifichi come terminatore nullo, ossia il valore 0 (zero).
Infatti, se al quarto posto a cominciare dall'inizio della locazione si immetterà tale valore, il computer si fermerà li nell'elaborare il dato presente nel buffer.
E' buona norma, avendo definito un buffer dinamico, riempire a priori (memset) di valori pari a 0 l'intera lunghezza in modo che il valore di ogni singola cella sia considerata nulla fino a quando non siamo noi ad imporgli un valore differente da elaborare attraverso istruzioni successive.

Codice: Seleziona tutto

char c,*Buff;
    register short int i=0,j=0,vmin,vmax;
        
    Buff=(char*)malloc(max + 1); /* Aggiunge spazio (max+1) per il terminatore */
    memset(Buff,0,sizeof(Buff)); /* Riempio il buffer con valori nulli per sicurezza */
    while(c!=13 || i<2){      /* Mai invio a vuoto. 
                                 Sempre almeno una lettera ed un numero */
Cio che è stato detto per il buffer riguardo ai valori casuali in memoria, vale anche per le variabili.
Se è vero che prima del ciclo while la variabile "i" è stata posta pari a zero, lo stesso non si può dire per "c" che può avere un qualsiasi valore diverso da 13.

Codice: Seleziona tutto

    char c=0,*Buff;
    register short int i=0,j=0,vmin,vmax;
C'è quindi qualcos altro che si deve inizializzare, poiché il suo valore viene verificato in una condizione fissa ed immutabile: "c" deve essere diverso da 13.
In mancanza di inizializzazione della variabile "c", il primo ciclo while verrebbe comunque elaborato, poichè se anche "c" fosse casualmente pari a 13 dopo la sua inzializzazione, "i" minore di 2 come ultima condizione messa in OR con la prima, fornirebbe una condizione verificata positivamente, pertanto il computer valuterebbe il tutto come autorizzato a procedere.
A questo punto ci fermiamo un attimo e ricapitoliamo i punti fissi, costituenti dogma, che un programmatore deve considerare con grande attenzione quando sviluppa un applicativo stendendo il suo codice:
  • L'utilizzo delle variabili è subordinato alla scelta della loro tipologia, dichiarazione ed inzializzazione;
  • L'utilizzo del buffer, oltre a considerarlo come variabile soggetta alle regole precedenti, è subordinato anche al suo azzeramento preliminare;
  • L'esito di una condizione di verifica tra una o più variabili deve prevedere valori certi e sicuri delle stesse, sia alla base della verifica, sia nel prosieguo del flusso d'istruzioni.

Codice: Seleziona tutto

       switch(j){
            case 0:vmin='A';vmax='D'; break; /* Lettere vailde A..D */
            case 1:vmin='0';vmax='9';        /* Cifre valide 0..9 */
        }    
        c=toupper(getch());
Siamo infine giunti all'elaborazione dei tasti premuti per riempire il buffer di valori da riproporre all'utente, dopo la pressione del tasto invio.
Ogniqualvolta viene premuto un tasto, la tastiera traduce il carattere premuto nel suo codice numerico (ASCII) che successivamente viene rielaborato per fornire a schermo la relativa lettera.
In realtà, quando un utente preme un tasto, le operazioni che avvengono in trasparenza prima di quella che definiamo finale, sono poco note, prima fra tutte il così detto “rimbalzo”.
A causa dell'architettura strutturale della tastiera, alla pressione di un tasto la circuiteria interna mette in correlazione più contatti di una matrice, producendo una predefinita sequenza di 1 e 0 in base al tasto premuto, ma tale operazione non è priva di ridondanza; Per tale motivo, anche la tastiera è provvista di un buffer che terrà conto delle sequenze spurie da eliminare, mantenndo solo quella necessaria.
Attraverso l'utilizzo di getch(), il computer si frappone tra la tastiera ed il monitor, prelevando la sequenza di caratteri premuti presenti nel buffer della tastiera che per forza di cose può contenere codici ulteriori a quello letterale (ScanCode https://en.wikipedia.org/wiki/Scancode); La tastiera, è composta da numerosi tasti, alcuni dei quali rivestono funzionalità multiple se premuti da soli o in combinazione con altri.

Codice: Seleziona tutto

        if(c==0||c==-32) getch(); /* Elimina lo scan code, ossia 
                                     carico il succ. carattere dal buffer */
La riga di codice sopra riportata, elimina lo ScanCode eseguendo un'ulteriore richiesta di accesso al buffer della tastiera.

Codice: Seleziona tutto

       switch(c){
            case 8:               /* Backspace */
                if(i>0){
                    i--; if(i==0) j=0;
                    printf("\b%c\b",255);
                }break;
            default:
                    if((c>=vmin && c<=vmax) && i<max){
                        printf("%c",c);
                        Buff[i]=c;
                        i++; j=1;
                    }    
        }    
    }Buff[i]='\0';                /* Aggiungo il terminatore di stringa */
    return Buff;
}
L'ulteriore porzione di codice costituisce completamento della funzione UserInput.
Al di la della semplicità del codice commentato, ritengo di particolare rilievo l'istruzione printf(“\b%c\b”,255), nonchè Buff=”\0”.
Nel primo caso, quando si preme il tasto backspace, il cursore viene portato indietro di una colonna, al posto del carattere presente viene scritto il carattere il cui codice ASCII è 255 (carattere pieno riportante identico colore di background del testo) ed il cursore è nuovamente riportato indietro di una posizione, pronto per acquisire un nuovo valore.
Nel secondo caso, tale istruzione è posta per terminare la sequenza di caratteri presenti nel buffer, poiché sebbene esso possa essere lungo 3 caratteri+il terminatore, l'utente potrebbe premere invio prima di completare l'immissione di 3 caratteri come richiesto.
L'istruzione potrà sembrare superflua avendo preliminarmente riempito il buffer di valori nulli, ma bisogna considerare che nel codice così proposto, ai lettori più attenti non sfuggirà che è l'istruzione memset ad essere stata aggiunta in più, non Buff=”\0”.
Ad ogni modo, restano validi il primo e secondo dogma sopra riportati, entrambi posti in linea generale a baluardo contro un eventuale buffer overflow riscontrabile ancora oggi in applicativi di maggior complessità, rispetto a quello trattato da questa breve guida.
In ultimo, esorto i lettori a visionare le ottime guide su C (suddivise attualmente in livello base ed intermedio) presenti in playlist su youtube, prodotte dall amministratore Francesco Grecucci.
Practice feeds Skill,Skill limits Failure,Failure enhances Security,Security needs Practice

Rispondi