Convenzioni per la realizzazione di programmi c
Convenzioni di Leggibilità
Un programmatore produce centinaia di programmi, spesso in collaborazione con altri professionisti; per un lavoro proficuo occorre, quindi, porsi alcuni obiettivi fondamentali:
- leggibilità del sorgente per una facile verifica e ricerca degli errori;
- riutilizzo del codice in problemi differenti;
- cooperazione e scambio di routine tra programmatori;
- manutenzione rapida ed efficiente del codice anche a distanza di tempo.
Occorre perciò stabilire alcune norme di Leggibilità nella stesura dei programmi.
Molte di queste regole sono dettate dal buon senso; altre da qualche docente/datore di lavoro esigente; tutte dovrebbero concorre a migliorare la qualità del lavoro di un analista/programmatore professionale.
Commenti
La quantità di commenti deve superare abbondantemente il codice C.
In pratica un altro programmatore deve capire come funziona una routine senza chiedervi nulla. Inseriremo perciò alcuni righi di spiegazione all’inizio di una funzione o di un segmento di programma, una descrizione accanto ad ogni definizione di variabili, qualche chiarimento accanto ad istruzioni significative. Ovviamente
a = a + 2; // aggiungo 2 alla variabile a
non è un commento ma una scemenza: meglio chiarire il motivo di questo incremento.
I commenti devono essere scritti come PRIMA COSA, in modo da formare l’ossatura su cui verrà scritto il programma, non DOPO, per far contento il principale o il professore!
Definizione di costanti
Utilizzare il #define per i valori costanti, migliora la leggibilità del programma; inoltre future modifiche sono più rapide. Le costanti vanno scritte tutto in MAIUSCOLO. Ad esempio:
#define PIGRECO 3.1415927
#define ESC 27
Nomi di variabili e funzioni
Utilizzare sempre nomi che richiamino l’uso della variabile e della funzione.
Ricordando che il C è case-sensitive, usare l’iniziale minuscola e poi il cosiddetto "camel Case": i nomi composti hanno l'iniziale delle parle successive alla prima maiuscole. Ad esempio:
numLati, areaCerchio, raggio, maxValore
(per le variabili spicciole, quali gli indici, usiamo pure i, j, k ecc… ).
Dichiarare TUTTE le variabili necessarie all’inizio della funzione, ciascuna con una breve spiegazione sull’utilizzo se si potrebbe prestare ad ambiguità ed, eventualmente, sul campo dei valori ammessi; separare, inoltre, la fase di allocazione delle variabili dalle istruzioni di inizializzazione: sarà più facile in futuro riciclare fette di codice. Ad esempio:
int a, b=5, c=9;
deve essere codificato
int a, // ...
b, // ... 2 <= b <= 8
c; // ... 1 <= b <= 9
b = 5; // valore centrale ...
c = 9; // estremo superiore ...
Allineamento e indentazione
Come in qualsiasi altro linguaggio strutturato, occorre allineare le istruzioni, disponendo opportunamente quelle che sono subordinate ad una condizione o ad un ciclo, qualche colonna più a destra.
I simboli di inzio e fine di un blocco, ovvero { e } devono essere sempre allineati con l’istruzione cui fa riferimento il blocco ( if, while, do, switch ).
Tutte le istruzioni del blocco devono rientrare: 3 spazi dovrebbe essere una misura corretta, purché venga usata sempre la stessa quantità (per evitare le il codice assomigli ad un albero di Natale).
Per contro, un'indentazione esagerata lascia poco spazio agli indispensabili commenti.
Spaziatura
Inserire qualche rigo vuoto tra le diverse funzioni e tra le varie sezioni del programma, nonché inserire gli spazi tra i nomi e gli operatori, migliora la leggibilità del codice (anche in Italiano, si mettono gli spazi tra le parole e dopo la punteggiatura). Esempio:
for (i = 0, j = N; i < j && j > 10; i ++, j --)
if ( a[i][j] )
a[i][j] *= i;
è sicuramente più chiaro di
for(i=0,j=N;i<j&&j>10;i++,j--)if(a[i][j])a[i][j]*=i;
Variabili globali
L’uso delle variabili globali è fortemente SCONSIGLIATO per vari motivi:
- consente a tutte le procedure di accedere ai dati, con conseguente difficoltà di debug di eventuali errori;
- rende difficoltoso il riutilizzo del codice;
- quasi sempre, impedisce l’uso della ricorsione: ogni istanza di una funzione ricorsiva agisce su un’immagine diversa dei dati.
La comunicazione tra programma chiamante e funzione deve avvenire attraverso il passaggio di parametri (per valore o indirizzo) e con il valore di ritorno.
Memoria Dinamica
L’allocazione e il rilascio di memoria dinamica HEAP devono essere eseguite, tramite le funzioni malloc() e free() da funzioni posizionate allo stesso livello logico di nidificazione: sarà più facile evitare di occupare memoria che non verrà rilasciata. Questo tipo di errore viene fuori, spesso, solo dopo un lungo utilizzo del software.
Struttura dei sottoprogrammi
Cercate di scrivere molte funzioni, ognuna delle quali svolge, se possibile, UN SOLO COMPITO.
Ogni funzione (livello inferiore) fornisce un servizio al programma chiamante (livello superiore) che non deve sapere come questo compito viene svolto.
Ad esempio un programma che deve scambiare dati tra due PC, utilizzerà un gruppo di funzioni del tipo connetti/scrivi/leggi/disconnetti, senza sapere se queste si appoggiano alla porta seriale, alla porta parallela o alla NIC Ethernet.
Se necessario, verrà preparato un pacchetto di funzioni per ciascun canale di comunicazione, da unire al modulo principale (totalmente indipendente) secondo le esigenze: nel caso ideale basta solo sostituire i file nel project e ricompilare.
Uscita da una funzione
Nella programmazione strutturata un blocco di istruzioni deve avere UN punto di inizio e UN punto di uscita, eliminando le conseguenze nefaste del famigerato GOTO.
Questo vale anche per i sottoprogrammi e il programma principale. Dunque:
- Non usare break per interrompere un ciclo (salvo al termine di un case).
- L’unico return di una funzione deve essere l’ultima istruzione.
Io sono per l'efficienza, costringere ad un unica uscita spesso comporta if e cicli molto nidificati che rendono difficile la lettura e il debug del codice. Sono ammesse uscite anticipate in casi ben chiari e definiti (errori, casi particolari etc.).
Disposizione sottoprogrammi
In C, diversamente dal Pascal, è usuale inserire le funzioni DOPO il main, utilizzando il prototype per la visibilità. Nell'evoluzione del C, il C++, l'uso dei prototipi è obbligatoria.
Se l’ambiente di sviluppo lo consente, è meglio costruire un progetto suddividendo il codice in più file sorgenti (.c), uno per il main e uno per ogni gruppo di funzioni o, addirittura, uno per funzione: in questo modo il prima o dopo perde di significato.
I prototipi verranno inseriti in altrettanti file di instestazione (.h) da richiamare, ove occorra, all’inizio dei file.c; nei file.h saranno inserite anche le definizioni di costanti e i typedef.
Denominazione file
Nella suddivisione in file del proprio lavoro, è bene evitare nomi come:
- Media ponderata con il secondo metodo matematico di Eulero.c
- lino&mariù.c
- prova %4.c
- dque.c
Usare invece nomi che ricordino il lavoro svolto in modo sintetico:
- Media_Aritmetica.c
- Primitive_Grafo.c
- Gestione_RS232.c
- Dynamic_Queue.c
- MediaAritmetica.c
- PrimitiveGrafo.c
- GestioneRS232.c
- DynamicQueue.c
L’uso di nomi corti, senza spazi e senza caratteri speciali o accentati, oltre a facilitare la manipolazione dei file, garantisce la compatibilità con altri sistemi operativi e protocolli di trasferimento (ad esempio il protocollo Joliet dei CD-ROM consente al massimo 31 caratteri per i nomi dei file); usare quindi le iniziali maiuscole e il simbolo di underscore _
Stessa attenzione per il nome del Progetto e delle Cartelle.
Se è necessario tener traccia delle successive revisioni del software, in modo da poter ritornare alla versione precedente, è buona norma usare una codifica del tipo nomefile_xx.c dove nomefile è il nome da assegnare al lavoro in svolgimento mentre le due cifre xx corrispondono alla versione del lavoro svolto:
- MediaAritmetica_v02.c
- PrimitiveGrafo_v13.c
- GestioneRS232_v22.c
- DynamicQueue_v01.c
Esempio:
/* = = = = = = = = = = = = = = = = = = = = = = =
specifiche del Software, autori, data relase
- - - - - - - - - - - - - - - - - - - - - - - - */
#include <stdio.h>
#define PIGRECO 3.14159
int sub1 ( float );
void sub2 ( int, int *);
int main ( void ) {
float area; // ... uso della variabile
float raggio; // ...
int i, j;
istruzione;
istruzione;
do {
istruzione;
. . . .
//----------------- motivo della selezione
if ( condizione ) { // prima alternativa
istruzione;
for (i = 0, j = 100; i < j ; i ++, j -= 13){
istruzione;
istruzione;
}
} else if ( condizione ) { // altra alternativa
istruzione;
for (i = 10, j = 1; i != j ; i ++, j -= 2){
istruzione;
}
} else { // altra alternativa
istruzione;
istruzione;
area = raggio>0 ? raggio*raggio*PIGRECO : 0;
}
//----------------- termine selezione
}while (condizione);
}
/* = = = = = = = = = = = = = = = = = = = = = = =
specifiche della funzione,
data ultima modifica ...
INPUT: parametri di ingresso e campo dei valori
x: dimensione... 3.2 <= x <= 123.56
OUTPUT: valore di uscita
- - - - - - - - - - - - - - - - - - - - - - - - */
int sub1 (float x){
int k; // .....
istruzione;
switch ( k ) {
case 1:
istruzione;
istruzione;
break;
case 23:
istruzione;
istruzione;
break;
case 4:
istruzione;
istruzione;
break;
default:
istruzione;
}
return valore;
}
Documentazione del software
È una descrizione, scritta, di come il programma dovrebbe lavorare e come dovrebbe essere usato.
Documentazione interna
Ad uso del programmatore, conterrà una descrizione accurata del modo in cui operano i vari moduli del programma al fine di effettuare delle prove o modificarlo successivamente. In pratica un altro programmatore, anche a distanza di tempo, deve poter rielaborare il software.
- Intestazione:
- Titolo e Finalità del programma;
- Autore/i;
- Data inizio progetto e Data ultima modifica.
- Comprensione ed analisi del problema.
- Struttura dei dati necessaria:
- Costanti utilizzate
- Variabili di Input
- Variabili di lavoro e di Output. Per ogni variabile:
- utilizzo della variabile.
- struttura (elemento singolo, array, record)
- tipo (int, float, ecc...)
- campo dei valori ammessi e valore iniziale;
- Dimensione dell’eventuale memoria dinamica allocata in proporzione ai dati di Input.
- Formulazione dell’algoritmo:
- descrizione discorsiva;
- eventuale flowchart;
- complessità computazionale;
- Test di verifica dell’algoritmo, con dati che mettano in difficoltà il normale svolgimento del programma.
- Linguaggio e ambiente di sviluppo utilizzato (Dev-C)
- File del progetto:
- file.c (ciascuno con l’elenco delle funzioni contenute)
- file.h (con elenco delle costanti e dei tipi di dati definiti)
- altri file (project, documenti, grafici, Help, ecc...).
- schema di collegamento tra i file
- Codifica nel linguaggio di programmazione, con abbondanti commenti, anche se duplicano informazioni già esposte nella documentazione.
- Descrizione delle prove di esecuzione con:
- dati di cui si conosce l’elaborazione;
- dati per casi limite (evidenziati al punto 4);
- dati di input fuori del campo dei valori ammessi.
Documentazione esterna
È destinata all’utente finale (manuale d’uso) e descrive, accuratamente, il modo in cui si deve usare il software; deve essere scritta in maniera non tecnica e non deve contenere nessun particolare sul modo in cui il programma opera.
- Intestazione:
- Titolo e Finalità del programma;
- Autore/i;
- Data ultima modifica.
- Istruzioni d’uso:
- Modalità di installazione;
- Elenco file associati all’eseguibile;
- Risorse aggiuntive necessarie per l’esecuzione;
- Procedura per eseguire il programma (descrizione discorsiva);
- Limitazioni dati d’ingresso (es. tipo di dati non accettati);
- Interpretazione dell’output (in che modo vanno interpretati i dati visualizzati).
- Situazioni di errore riconosciute.
Prova di accettazione
Viene eseguita dal cliente (ovvero il docente) ed è finalizzata alla verifica del corretto funzionamento del programma; in particolare si cercherà di mettere in crisi i vari moduli con casi complicati o con dati inesatti.
Tutto il codice fornito deve essere ricompilabile con l’ambiente di sviluppo DEV-C 4.9.
(Tratto da questo sito e rielaborato)