Friday

Suggerimenti per la programmazione

Ci si aspetta che gli studenti delle classi CS junior e senior conoscano alcune informazioni di base su C, Unix e gli strumenti di sviluppo del software. Questa pagina descrive in dettaglio questo sfondo e suggerisce alcuni esercizi.

C


  1. C ti permette di dichiarare variabili al di fuori di qualsiasi procedura. Queste variabili sono chiamate variabili globali.
1. Una variabile globale viene allocata una volta all'avvio del programma e rimane in memoria fino al termine del programma.
2. Una variabile globale è visibile a tutte le procedure nello stesso file.
3. Puoi rendere una variabile globale dichiarata nel file A.c visibile a tutte le procedure in altri file B.c, C.c, D.c, ... dichiarandolo con il modificatore extern nei file B.c, C.c, D.c, ... come in questo esempio:

extern int theVariable

Se hai molti file che condividono la variabile, dovresti dichiararla extern in un file di intestazione foo.h (descritto di seguito) e usare #include foo.h nei file Bc, Cc, Dc, .... Devi dichiarare la variabile in esattamente un file senza il extern modificatore esterno o non viene mai assegnato affatto.

4. Non è una buona idea usare troppe variabili globali, perché non è possibile localizzare i luoghi in cui sono accessibili. Ma ci sono situazioni in cui le variabili globali ti permettono di evitare di passare molti parametri alle funzioni. 


         2.  Le stringhe sono puntatori agli array di caratteri con terminazione null.

a. Si dichiara una stringa come char * variableName.
b. Se la tua stringa è costante, puoi semplicemente assegnare una stringa letterale ad essa:

char * myString = "Questa è una stringa di esempio";

c.Una stringa vuota ha solo il terminatore null:

myString = ""; // stringa vuota, lunghezza 0, contenente null

d. Un puntatore nullo non è un valore stringa valido:

myString = NULL; // stringa non valida

È possibile utilizzare tale valore per indicare la fine di una matrice di stringhe:

argv [0] = "progName";
argv [1] = "firstParam";
argv [2] = "secondParam";
argv [3] = NULL; // terminatore

e. Se la stringa è calcolata in fase di esecuzione, è necessario riservare spazio sufficiente per tenerla. La stanza deve essere sufficiente per contenere il null alla fine:

char * myString;
myString = (char *) malloc (strlen (someString) + 1); // alloca lo spazio
strcpy (myString, someString); // copia someString su myString

f. Per evitare perdite di memoria, si dovrebbe infine restituire lo spazio che si alloca con malloc utilizzando gratuitamente. Il suo parametro deve essere l'inizio dello spazio restituito da malloc:

free ((void *) myString);

Per mantenere il tuo codice pulito e leggibile, devi chiamare free () nella stessa procedura in cui chiami malloc (); puoi chiamare altre procedure tra questi due punti per manipolare la tua stringa.

g. Se stai copiando stringhe, dovresti fare molta attenzione a non copiare più byte di quanto la struttura dei dati di destinazione possa contenere. Gli overflow del buffer sono la causa più comune di problemi di sicurezza nei programmi. In particolare, considera l'uso di strncpy () e strncat () invece di strcpy () e strcat ().

h. Se si utilizza C ++, è necessario convertire gli oggetti stringa in stringhe in stile C prima di passarli in una chiamata di sistema.

string myString // Questa dichiarazione funziona solo in C ++
...
someCall (myString.c_str ())

Sfortunatamente, c_str () restituisce una stringa immutabile. Se hai bisogno di una stringa mutabile, puoi copiare i dati usando strcpy () (come sopra) o puoi lanciare il tipo:

someCall (const_cast <char *> (myString.c_str ()))

Il cast non è sicuro come copiare, perché someCall () potrebbe effettivamente modificare la stringa, il che confonderebbe qualsiasi parte del programma che presuppone che myString sia costante, che è il solito comportamento delle stringhe C ++.

                    3. Un buffer è un'area di memoria che funge da contenitore per i dati. Anche se i dati potrebbero avere un'interpretazione (come una matrice di strutture con molti campi), i programmi che leggono e scrivono i buffer spesso li trattano come array di byte. Una serie di byte non è uguale a una stringa, anche se sono entrambi char char * o char [].

a. Potrebbero non contenere caratteri ASCII e potrebbero non essere terminati con null.
b. Non è possibile utilizzare strlen () per trovare la lunghezza dei dati in un buffer (poiché il buffer può contenere byte null). Invece, è necessario calcolare la lunghezza dei dati in base al valore restituito dalla chiamata di sistema (in genere letta) che ha generato i dati.
c. Non è possibile utilizzare strcpy (), strcat () o routine correlate su buffer di byte; invece, è necessario utilizzare memcpy () o bcopy ().
d. Scrivi un buffer di 123 byte in un file usando un codice come questo:

char * fileName = "/ tmp / foo"
#define BUFSIZE 4096
char buf [BUFSIZE]; // buffer contenente al massimo BUFSIZE byte
...
int outFile; // descrittore di file, un numero intero piccolo
int bytesToWrite; // numero di byte ancora da scrivere
char * outPtr = buf;
...
if ((outFile = creat (fileName, 0660)) <0) {// errore
// vedi i permessi dei file per capire 0660
perror (fileName); // causa stampa
exit (1); // e esci
}
bytesToWrite = 123; // inizializzazione; 123 è solo un esempio
while ((bytesWritten = write (outFile, outPtr, bytesToWrite)) <bytesToWrite) {
// non tutti i byte sono stati ancora scritti
if (bytesWritten <0) {// fallimento
perror ( "scrivere");
exit (1);
}
outPtr + = byte Scritto;
bytesToWrite - = bytesWritten;
}

e. Per far sì che il compilatore allochi spazio per i buffer, devi dichiarare il buffer con una dimensione che il compilatore può calcolare, come in

#define BUFSIZE 1024
char buf [BUFSIZE];

Se dichiari semplicemente il buffer senza dimensioni:

char buf [];

quindi ha dimensioni sconosciute e C non assegna nessuno spazio. Questo è accettabile se buf è un parametro formale (cioè, appare nell'intestazione di una procedura); il parametro attuale (fornito dal chiamante) ha una dimensione. Ma non è accettabile se buf è una variabile. Se non conosci la dimensione del buffer in fase di compilazione, dovresti usare un codice come questo:

char * buf = (char *) malloc (bufferSize);

dove bufferSize è il risultato di runtime di alcuni calcoli.

                 4. È possibile allocare e deallocare dinamicamente la memoria in modo dinamico.

a. Istanze individuali di qualsiasi tipo:

typedef ... myType;
myType * myVariable = (myType *) malloc (sizeof (myType));
// ora puoi accedere a * myVariable.
...
libero ((void *) myVariable);

Ancora una volta, è buona pratica di programmazione invocare free () nella stessa routine in cui si chiama malloc ().

b. Matrici monodimensionali di qualsiasi tipo:

myType * myArray = (myType *) malloc (arrayLength * sizeof (myType));
// myArray [0] .. myArray [arrayLength - 1] sono ora allocati.
...
gratuito ((void *) myArray);

c. Gli array bidimensionali sono rappresentati da una serie di puntatori, ognuno dei quali punta a un array:

myType ** myArray = (myType **) malloc (numRows * sizeof (myType *));
int rowIndex;
for (rowIndex = 0; rowIndex <numRows; rowIndex + = 1) {
myArray [rowIndex] = (myType *) malloc (numColumns * sizeof (myType));
}
// myArray [0] [0] .. myArray [0] [numColumns-1] .. myArray [numRows-1] [numColumns-1]
// sono ora assegnati. Potresti voler inizializzarli.
...
for (rowIndex = 0; rowIndex <numRows; rowIndex + = 1) {
gratuito ((void *) myArray [rowIndex]);
}
gratuito ((void *) myArray);

d. Se stai usando C ++, non mescolare new / delete con malloc / free per la stessa struttura dati. Il vantaggio di new / delete per le istanze di classe è che chiamano automaticamente i costruttori, che potrebbero inizializzare i dati, e i distruttori, che possono finalizzare i dati. Quando usi malloc / free, devi inizializzarlo e finalizzarlo esplicitamente.

                  5. Interi

a. C rappresenta solitamente numeri interi in 4 byte. Ad esempio, il numero 254235 viene rappresentato come numero binario 00000000,00000011,11100001,00011011.
b. D'altra parte, il testo ASCII rappresenta numeri come qualsiasi altro carattere, con un byte per cifra usando una codifica standard. In ASCII, il numero 254235 è rappresentato come 00110010, 00110101, 00110110, 00110010, 00110011, 00110101.
c. Se è necessario scrivere un file di numeri interi, è generalmente più efficiente, sia nello spazio che nel tempo, scrivere le versioni a 4 byte piuttosto che convertirle in stringhe ASCII e scriverle. Ecco come scrivere un singolo intero su un file aperto:

scrivi (outFile, & myInteger, sizeof (myInteger))

d. Puoi guardare i singoli byte di un intero gettandolo come una struttura di quattro byte:

int IPAddress; // memorizzato come numero intero, inteso come 4 byte
typedef struct {
char byte1, byte2, byte3, byte4;
} IPDetails_t;
IPDetails_t * details = (IPDetails_t *) (& IndirizzoIP);
printf ("il byte 1 è% o, il byte 2 è% o, il byte 3 è% o, il byte 4 è% o \ n",
dettagli-> byte1, dettagli-> byte2, dettagli-> byte3, dettagli-> byte4);

e. Gli interi multibyte possono essere rappresentati in modo diverso su macchine diverse. Alcuni (come Sun SparcStation) mettono prima il byte più significativo; altri (come Intel i80x86 e i suoi discendenti) mettono prima il byte meno significativo. Se si stanno scrivendo dati integer che potrebbero essere letti su altre macchine, convertire i dati in ordine di byte "di rete" da htons () o htonl (). Se stai leggendo dati interi che potrebbero essere stati scritti su altre macchine, converti i dati dall'ordine "rete" al tuo ordine di byte locale da ntohs () o ntohl ().

                   6. È possibile prevedere il layout di memoria delle strutture e il valore restituito da sizeof (). Per esempio,

struct foo {
char a; // usa 1 byte
// C inserisce qui un pad di 3 byte in modo che b possa iniziare su un limite di 4 byte
int b; // usa 4 byte
corto senza firma c; // usa 2 byte
char non firmati [2]; // usa 2 byte
};

Pertanto, sizeof (struct foo) restituisce 12. Questa prevedibilità (per una determinata architettura) è il motivo per cui alcuni chiamano C un "linguaggio assemblatore portatile". È necessario prevedere il layout della struttura quando si generano dati che devono seguire un formato specifico, ad esempio un'intestazione su un pacchetto di rete.

                7. È possibile dichiarare puntatori in C per qualsiasi tipo e assegnare loro valori che puntano a oggetti di quel tipo.

a. In particolare, C ti permette di costruire dei puntatori ai numeri interi:

int someInteger;
int * intPtr = & someInteger; // dichiara una variabile con valori puntatori e assegna un valore puntatore appropriato
someCall (intPtr); // passa un puntatore come parametro attuale
someCall (& someInteger); // ha lo stesso effetto di sopra

b. Una procedura di libreria C che prende un puntatore a un valore molto probabilmente modifica quel valore (diventa un parametro "out" o "out"). Nell'esempio sopra, è molto probabile che someCall modifichi il valore del numero intero someInteger.
c. È possibile creare un puntatore a un array di numeri interi e utilizzarlo per scorrere attraverso tale matrice.

#define ARRAY_LENGTH 100
int intArray [ARRAY_LENGTH];
int * intArrayPtr;
...
int sum = 0;
for (intArrayPtr = intArray; intArrayPtr <intArray + ARRAY_LENGTH; intArrayPtr + = 1) {
sum + = * intArrayPtr;
}

d. È possibile creare un puntatore a una matrice di strutture e utilizzarlo per scorrere attraverso tale matrice.

#define ARRAY_LENGTH 100
typedef struct {int foo, bar;} pair_t; // pair_t è un nuovo tipo
pair_t structArray [ARRAY_LENGTH]; // structArray è una matrice di elementi pair_t ARRAY_LENGTH
pair_t * structArrayPtr; // structArrayPtr punta ad un elemento pair_t
...
int sum = 0;
for (structArrayPtr = structArray; structArrayPtr <structArray + ARRAY_LENGTH; structArrayPtr + = 1) {
sum + = structArrayPtr-> foo + structArrayPtr-> bar;
}

e. Quando aggiungi un intero a un puntatore, il puntatore viene avanzato da quei molti elementi, indipendentemente da quanto siano grandi gli elementi. Il compilatore conosce le dimensioni e fa la cosa giusta.

                  8. Produzione

a. Si formatta l'output con printf o la sua variante, fprintf.
b. La stringa di formato utilizza % d,% s, % f per indicare che un intero, una stringa o un reale deve essere inserito nell'output.
c. La stringa di formato usa \ t e \ n per indicare tab e newline.
d. Esempio:

printf ("Penso che il numero% d sia% s \ n", 13, "fortunato");

e. Miscelare printf (), fprintf () e cout potrebbe non stampare elementi nell'ordine previsto. Usano aree di staging indipendenti ("buffer") che stampano quando sono piene.

               9. La routine main () accetta parametri di funzione che rappresentano i parametri della riga di comando.

a. Un modo comune per scrivere la routine principale è questo:

int main (int argc; char * argv []);

Qui, argc è il numero di parametri e argv è una matrice di stringhe, ovvero una matrice di puntatori agli array di caratteri con terminazione null.

b. Per convenzione, il primo elemento di argv è il nome del programma stesso.

int main (int argc; char * argv []);
printf ("Ho% d parametri, il mio nome è% s, e il mio primo parametro è% s \ n",
argc, argv [0], argv [1]);

        10. Funzioni del linguaggio a portata di mano

a. È possibile incrementare un numero intero o avere un puntatore puntato all'oggetto successivo usando l'operatore ++. Di solito è meglio posizionare questo operatore dopo la variabile: myInt++Se si inserisce il ++ prima della variabile, la variabile viene incrementata prima di essere valutata, il che raramente è ciò che si desidera.
b. Puoi creare un compito in cui la variabile del lato sinistro partecipa come prima parte dell'espressione sul lato destro:

myInt - = 3; // equivalente a myInt = myInt - 3
myInt * = 42; // equivalente a myInt = myInt * 42
myInt + = 1; // equivalente a e forse preferibile a myInt ++

c. È possibile esprimere numeri decimali, ottali (con il prefisso con la cifra 0, come in 0453) o esadecimale (con il prefisso con 0x, come in 0xffaa).

d. È possibile trattare un numero intero come un insieme di bit ed eseguire operazioni bit a bit:

myInt = myInt | 0444; // bitwise OR; 0444 è in ottale
myInt & = 0444; // bit a bit E con una stenografia di assegnazione
myInt = qualcosa ^ qualunque cosa; // XOR bit a bit

e. C e C ++ hanno espressioni condizionali. Invece di scrivere

se (a <7)
a = someValue
altro
a = someOtherValue;

tu puoi scrivere

a = a <7? someValue: someOtherValue;

f. Le assegnazioni restituiscono il valore del lato sinistro, in modo da poter includere un'assegnazione in espressioni più grandi come i condizionali. Ma dovresti seguire la convenzione che tali incarichi sono sempre racchiusi tra parentesi per indicare sia a qualcuno che legge il tuo codice che al compilatore che intendi davvero un compito, non un test di uguaglianza. Ad esempio, scrivi

if ((s = socket (...)) == -1)

non

if (s = socket (...) == -1)

La seconda versione è più difficile da leggere e, in questo caso, errata, perché l'operatore di uguaglianza == ha precedenza più alta rispetto all'operatore di assegnazione =.

            11. I programmi che non sono banalmente brevi dovrebbero di solito essere scomposti in più file sorgente, ognuno con un nome che termina in .c (per programmi C) o .cpp (per programmi C ++).

a. Prova a raggruppare le funzioni che manipolano le stesse strutture di dati o hanno scopi correlati nello stesso file.
b. Tutti i tipi, le funzioni, le variabili globali e le costanti manifest che sono necessarie per più di un file sorgente dovrebbero essere dichiarate in un file di intestazione, con un nome che termina in .h.
c. Ad eccezione delle funzioni inline, non dichiarare i corpi delle funzioni (o qualsiasi cosa che faccia compilare il codice o allocare lo spazio) nel file di intestazione.
d. Ogni file sorgente dovrebbe fare riferimento a quei file di intestazione di cui ha bisogno con una riga #include.
e. Mai # include un file .c.

             12. Quando si hanno più file sorgente, è necessario collegare insieme tutti i file oggetto compilati insieme alle librerie richieste dal programma.

1. Il metodo più semplice è usare il compilatore C, che conosce le librerie C:

gcc * .o -o myProgram

Questo comando chiede al compilatore di collegare tutti i file oggetto con la libreria C (che è inclusa implicitamente) e posizionare il risultato nel file myProgram, che diventa eseguibile.

2. Se il tuo programma ha bisogno di altre librerie, dovresti specificarle dopo i tuoi file oggetto, perché il linker raccoglie solo le routine dalle librerie che già sa di aver bisogno e collega i file nell'ordine specificato. Quindi se hai bisogno di una libreria come libxml2, il tuo comando di collegamento dovrebbe essere qualcosa del genere:

gcc * .o -lxml2 -o myProgram

Il compilatore sa come cercare varie directory standard per la versione corrente di libxml2.

               13. Debug dei programmi C

a. Se ottieni un errore di segmentazione, molto probabilmente hai un indice fuori intervallo, un puntatore non inizializzato o un puntatore nullo.
b. Puoi inserire istruzioni di stampa nel tuo programma per aiutarti a localizzare un errore.
c. È probabile che il debug abbia maggior successo se si utilizza gdb (descritto di seguito) per capire dove si trova il proprio errore.
d. I programmi eseguiti per un lungo periodo devono fare attenzione a liberare tutta la memoria allocata, o alla fine esauriscono la memoria. Per eseguire il debug di perdite di memoria, è possibile considerare questi articoli sul debug di perdite di memoria C e perdite di memoria C ++.

Unix


        1. Per convenzione, ogni processo inizia con tre file standard aperti: input standard, output standard e errore standard, associati ai descrittori di file 0, 1 e 2.

a. L'input standard è solitamente collegato alla tua tastiera. Tutto ciò che scrivi va al programma.
b. L'output standard è solitamente collegato allo schermo. Qualunque sia l'uscita del programma diventa visibile.
c. Anche l'errore standard è solitamente collegato allo schermo.
d. È possibile utilizzare la shell per richiamare i programmi in modo che l'output standard di un programma sia direttamente collegato ("piped") all'input standard di un altro programma:

ls | bagno

e. È possibile utilizzare la shell per richiamare i programmi in modo che l'input e / o l'output standard siano collegati a un file:

ls> lsOutFile
wc <lsOutFile
sort -u <largeFile> file ordinato

f. In generale, i programmi non sanno o si preoccupano se la shell ha riorganizzato il significato dei loro file standard.

          2. Comandi Unix

a. I comandi sono solo i nomi dei file eseguibili. La variabile d'ambiente PATH dice alla shell dove cercarli. In genere, questa variabile ha un valore simile
/ Bin: / usr / bin: / usr / local / bin: ..
b. Per vedere dove la shell trova un particolare programma, ad esempio, vim, dì dove vim.

          3. Le chiamate di sistema e le chiamate di libreria seguono alcune importanti convenzioni.

a. Il valore restituito dalla chiamata di solito indica se la chiamata è riuscita (in genere il valore è 0 o positivo) o non è riuscita (in genere il valore è -1).
b. Controllare sempre il valore restituito delle chiamate in biblioteca. Quando una chiamata di sistema fallisce, la funzione perror () può stampare l'errore (a errore standard):

int fd;
char * filename = "miofile";
if ((fd = open (nomefile, O_RDONLY)) <0) {
perror (nome file); // potrebbe stampare "myfile: nessun file o directory"
}

c. Una pagina di manuale per una chiamata di sistema o una routine di libreria potrebbe elencare un tipo di dati che non definisce, ad esempio size_t o time_t o O_RDONLY. Questi tipi sono in genere definiti nei file di intestazione menzionati nella pagina di manuale; è necessario includere tutti i file di intestazione nel programma C.

               4. I permessi dei file in Unix sono solitamente espressi con numeri ottali.

a. Nell'esempio di creat () sopra, 0660 è un numero ottale (che è lo 0 iniziale), che rappresenta 110.110.000 binari. Questo numero ottale concede le autorizzazioni di lettura e scrittura, ma non le autorizzazioni di esecuzione, al proprietario del file e al gruppo del file, ma nessuna autorizzazione ad altri utenti.
b. Si impostano le autorizzazioni quando si crea un file dal parametro alla chiamata creat ().
c. Il comando ls -l mostra le autorizzazioni dei file.
d. Puoi modificare i permessi di un file che possiedi usando il programma chmod.
e. Tutti i tuoi processi hanno una caratteristica chiamata umask, solitamente rappresentata come un numero ottale. Quando un processo crea un file, i bit nella umask vengono rimossi dalle autorizzazioni specificate nella chiamata creat (). Quindi se umask è 066, gli altri non possono leggere o scrivere file creati, perché 066 rappresenta permessi di lettura e scrittura per il tuo gruppo e per altre persone. Puoi ispezionare e modificare la tua umask usando il programma umask, che normalmente invochi nello script di avvio della shell (a seconda della shell, ~ / .login o ~ / .profile).


Strumenti di sviluppo software


          1. Usa un editor di testo per creare, modificare e ispezionare il tuo programma. Sono disponibili diversi editor di testo ragionevoli.

a. L'editor di vim e la sua interfaccia grafica, gvim, richiedono un certo sforzo per imparare, ma forniscono un set di strumenti di alta qualità per la modifica dei file di programma, tra cui evidenziazione della sintassi, corrispondenza di parentesi, completamento di parole, rientro automatico, ricerca per tag (che si muove rapidamente da un punto in cui il programma richiama una funzione nel punto in cui è definita la funzione) e nella ricerca integrata nella pagina manuale. Vim è progettato per l'uso con la tastiera; non hai mai bisogno di usare il mouse se non vuoi. È liberamente disponibile per i sistemi operativi Unix, Win32 e Microsoft. È la versione più sviluppata della serie di editor che include ed, ex, vi ed elvis. È possibile leggere la documentazione online per Vim e ottenere assistenza immediata tramite il comando help di vim.
b. L'editore di emacs è, semmai, più ricco di funzionalità di vim. Ci vuole anche uno sforzo significativo per imparare. È anche liberamente disponibile per sistemi operativi Unix e Microsoft. Puoi trovare la documentazione qui.
c. Sono disponibili molti altri editor di testo, ma in genere non forniscono le due funzionalità più utili necessarie per la creazione di programmi: l'indentazione automatica e l'evidenziazione della sintassi. Tuttavia, questi editor di testo hanno spesso il vantaggio di essere più facili da imparare, in linea con le loro capacità limitate. Tra questi editor di testo di bassa qualità ci sono (per Unix) pico, gedit e joe e (per Microsoft) notepad e word.
d. Potresti avere familiarità con un ambiente di sviluppo integrato (IDE) come Eclipse, Code Warrior o .NET. In questi ambienti sono generalmente presenti editor di testo integrati con debugger e compilatori. Se si utilizza un IDE di questo tipo, è opportuno utilizzare gli editor di testo associati.

          2. gdb è un debugger che comprende le tue variabili e la struttura del programma.

a. Puoi trovare la documentazione qui.
b. Per utilizzare efficacemente gdb, è necessario passare il flag -g al compilatore C o C ++.
c. Se il tuo programma myProgram ha fallito lasciando un file chiamato core, prova con gdb myProgram core.
d. Puoi anche eseguire il tuo programma dall'inizio sotto il controllo di gdb: gdb myProgram.
e. Tutti i comandi di gdb possono essere abbreviati in un prefisso univoco.
f. Il comando di aiuto è molto utile.
g. Il comando where mostra lo stack delle chiamate, compresi i numeri delle linee che mostrano dove si trova ogni routine. Questo è il primo comando da provare quando esegui il debug di un file core.
h. Per stampare il valore di alcune espressioni (puoi includere le tue variabili e i soliti operatori C), scrivi stampa espressione,pa, come in

print (myInt + 59) e 0444;

i. Per vedere il tuo programma, prova ad elencare myFunction o list myFile.c: 38.
j. Per impostare un record di attivazione diverso come corrente, utilizzare il comando su (per più recente) o verso il basso (per meno recente).
k. È possibile impostare un punto di interruzione su qualsiasi riga di qualsiasi file. Ad esempio, puoi dire break foo.p: 38 per impostare un breakpoint alla riga 38 nel file foo.p. Ogni volta che il tuo programma raggiunge quella linea mentre viene eseguito, si interromperà e gdb ti chiederà i comandi. Puoi guardare le variabili, ad esempio, o avanzare nel programma.
l. Il comando successivo fa avanzare una dichiarazione (chiamando e restituendo da eventuali procedure se necessario).
m. Il comando step inoltra un'istruzione, ma se l'istruzione implica una chiamata alla procedura, entra nella procedura e si ferma alla prima istruzione lì.
n. Se si immette il comando set follow-fork-mode child, quando il programma esegue la chiamata a fork (), gdb continuerà a eseguire il debug del child, non del genitore.
o. Lascia gdb inserendo il comando quit.
p. Potresti preferire usare il front-end grafico ddd su gdb.

            3. Fornisci sempre ai programmi del compilatore gcc o g ++ il flag -Wall per attivare un livello elevato di avvisi. Allo stesso modo dona a javac il comando -Xlint: all flag. Non attivare un programma che genera avvisi di compilazione.

            4. È possibile leggere il manuale per ottenere dettagli sui programmi, le routine di libreria C e le chiamate di sistema Unix utilizzando il programma man, come in man printf o man gcc.

a. A volte la funzione desiderata si trova in una sezione specifica del manuale Unix e devi richiederlo esplicitamente: man 2 open o man 3 printf. La sezione 1 riguarda i programmi, la sezione 2 riguarda le chiamate di sistema e la sezione 3 riguarda la libreria C e la sezione 8 riguarda l'amministrazione del sistema. Molto probabilmente non hai bisogno delle altre sezioni.
b. Puoi trovare se qualche programma, routine di libreria C o chiamata di sistema Unix è rilevante per qualche argomento usando il flag -k, come in man -k print.

        5. Usa il programma make per organizzare le ricette per ricompilare e ricollegare il tuo programma quando cambi un file sorgente.

a. Vedi questo tutorial o questo manuale per i dettagli.
b. Se il tuo programma è composto da diversi file, puoi compilarli separatamente e quindi collegarli insieme. Si compila con il flag -c, e si usa il flag -o per indicare il file di output. Un makefile ragionevole potrebbe assomigliare a questo:

SOURCES = driver.c input.c output.c
OBJECTS = driver.o input.o output.o
HEADERS = common.h
CFLAGS = -g -Wall

programma: $ (OGGETTI)
$ (CC) $ (CFLAGS) $ (OGGETTI) -o programma

$ (OGGETTI): $ (INTESTAZIONI)

testRun: programma
programma <testData

Questo makefile utilizza una definizione incorporata di CC e una regola incorporata per convertire i file di origine C come driver.c nel loro file oggetto. Se modifichi solo input.c, quindi make testRun farà in modo che il compilatore ricostruisca input.o, quindi farà in modo che il compilatore ricolleghi gli oggetti, crei un programma e poi esegua il programma con input standard reindirizzato dal file testData.

c. Se disponi di molti file sorgente e di molti file di intestazione, potresti voler utilizzare il programma makedepend per creare automaticamente le regole Makefile che specificano in che modo i file di origine dipendono dai file di intestazione. L'esempio precedente presuppone che tutti i file di origine dipendano da tutti i file di intestazione, che spesso non è il caso.

              6. Il programma grep può cercare rapidamente una definizione o una variabile, in particolare nei file include:

grep "struct timeval {" /usr/include/*/*.h

Esercizi

Fai questi esercizi in C.

  1. Scrivi un programma chiamato atoi che apre un file di dati chiamato sulla riga di comando e legge da esso una singola riga di input, che dovrebbe contenere un numero intero rappresentato in caratteri ASCII. Il programma converte quella stringa in un numero intero, moltiplica il numero intero per 3 e stampa il risultato sullo standard. Il programma non deve utilizzare la funzione atoi (). Dovresti usare il programma make. Il tuo Makefile dovrebbe avere tre regole: atoi, run (che esegue il tuo programma sui dati di test standard e reindirizza l'output in un nuovo file) e clean (che rimuove i file temporanei). Assicurati che il tuo programma funzioni correttamente su dati non corretti ed esce con un messaggio utile se il file di dati è mancante o illeggibile. Passa attraverso il tuo programma avviandolo con gdb, posizionando un breakpoint su main () e usando ripetutamente il comando step.
  2. Cerca la pagina del manuale per il programma gatto. Codifica la tua versione di gatto. La tua versione deve accettare parametri di nome file multipli (o non). Non è necessario accettare alcun parametro di opzione.
  3. Scrivi un programma removeSuffix che accetta un singolo parametro: il nome di un file suffisso. Il file suffisso ha una riga per voce. Una voce è una stringa non vuota, che chiamiamo suffisso, seguita dal segno>, seguita da un'altra (eventualmente vuota) stringa, che chiamiamo una sostituzione. Il tuo programma dovrebbe memorizzare tutti i suffissi e le loro sostituzioni in una tabella hash. Usa il concatenamento esterno. Il tuo programma dovrebbe quindi leggere gli input standard. Per ogni parola delimitata dallo spazio w nell'input, trova il suffisso più lungo s che appare in w e modifica w rimuovendo s e inserendo la sostituzione di s, creando w '. Emette una riga per parola modificata, nel formato w> w '. Non inviare alcuna parola che non sia stata modificata.






















No comments:

Post a Comment