Esercizi di lettura di codice C

From Sistemi Operativi
Jump to navigation Jump to search

Questi programmi non sono necessariamente stilisticamente belli (non sono "esempi da copiare") ma presentano dei passaggi di non semplice lettura.

Il consiglio e' di compilarli, provarli e capirne passo-passo il funzionamento. (o spiegare perche' non funzionino).

Ovviamente potete proporre ulteriori esempi. Renzo (talk) 15:42, 10 October 2015 (CEST)

programma 0

#include <stdio.h>

int main(int argc, char *argv[]) {
        int c;
        while ((c=getchar())!=EOF)
                putchar(c);
        return 0;
}

In questo esempio tutto ciò che viene preso in input viene messo in output, fino a quando non viene digitato il carattere di End of File (EOF). Solitamente nei sistemi DOS il carattere EOF viene definito come la combinazione di tasti CTRL+Z, mentre nei sistemi basati su Unix si utilizzano i tasti CTRL+D. (S.G)

Nel caso l'input provenga da file e non da tastiera (ad esempio eseguendo da terminale la riga di comando "./programma0 < testo"), il carattere speciale EOF viene inserito automaticamente alla fine del file "testo". In questo caso il programma prende in input il contenuto di "testo" e stampa a video (carattere per carattere), il contenuto del file, finchè non riconosce il carattere speciale EOF, che fa terminare il ciclo while.



questo è l'esercizio visto in aula. Il punto da notare è l'uso dell'assegnazione all'interno del controllo del ciclo while. E' una struttura tipica del linguaggio C che semplifica sintatticamente la scrittura del programma. Renzo (talk) 10:02, 17 October 2015 (CEST)

programma 1

#include <stdio.h>

char s1[]="hello world";
char *s2="hello world";

void foo(char *s) {
        printf("%s\n",s);
        s[4]=',';
        printf("%s\n",s);
}

int main(int argc, char *argv[]) {
        foo(s1);
        foo(s2);
        return 0;
}

In questo esempio viene mostrata la differenza tra stringa variabile (s1) e stringa costante (s2). Nel primo caso a tempo di compilazione la stringa costante "hello world" viene copiata passo nella stringa variabile s1 consentendo le normali operazioni di modifica della stringa stessa. Mentre nel secondo caso a tempo di compilazione viene creata la stringa costante "hello world" e memorizzato il suo indirizzo nella variabile s2 NON consentendo le normali operazioni di modifica delle stringa stessa in quanto appunto un costante. (S.G)

programma 2

#include <stdio.h>

char t[]="test";
struct st {
        char s[5];
        char *t;
} st0={"test",t};

void foo (struct st i) {
        i.s[2]='n';
        i.t[2]='n';
}

void bar (struct st *i) {
        i->s[2]='x';
        i->t[2]='x';
}

int main(int argc, char *argv[]) {
        printf("%s %s\n",st0.s,st0.t);
        foo(st0); /* Vengono copiati i valori della struttura st0 in i, le uniche modifiche saranno sul campo i.t in quanto puntatore. */
        printf("%s %s\n",st0.s,st0.t); 
        bar(&st0); /* Viene copiato l'indirizzo della struttura st0 in i, le modifiche vengono apportate su tutti i campi delle struttura. */
        printf("%s %s\n",st0.s,st0.t);
        return 0;
}

Output del programma:

test test

test tent

text text

(S.G)


Sì, ma perché? Come è fatta la struttura st? Renzo (talk) 10:06, 17 October 2015 (CEST)


La struttura st è fatta da un array di 5 caratteri e da un puntatore a caratteri (che viene usato anch'esso per puntare ad un'array di caratteri).

Con la prima chiamata a printf vengono stampati i valori dei campi della struttura non ancora modificati, dal momento che non è stata ancora chiamata nè la funzione foo nè la funzione bar (test test)

In seguito viene passata la struct st0 alla funzione foo. In particolare viene passata alla funzione una COPIA del primo campo e una COPIA del secondo campo; tuttavia essendo il secondo campo della struttura un puntatore, viene passato il valore del puntatore, che è un indirizzo, cioè l'indirizzo a cui si trova l'array t (char t[]="test"). Per questo motivo viene modificato solo il valore del secondo campo nella struttura st0, infatti quando la funzione termina, la copia del primo campo viene persa, senza aver modificato il valore originale, mentre il valore del secondo campo viene modificato perchè l'abbiamo modificato tramite l'indirizzo. L'output finale sarà quindi: test tent.

Alla chiamata alla funzione bar, viene invece passato l'INDIRIZZO della struttura st0, e per questo quando andiamo a modificare i campi della struttura all'interno della funzione bar, non stiamo modificando una copia di st0, ma stiamo modificando direttamente st0, accedendo tramite puntatore ai suoi singoli campi. Quindi quando bar termina, i valori originali sono stati modificati e l'output risulterà: text text.

(Alessio)

programma 3

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
        while (argc > 1) {
                int i,j;
                char *s=argv[1];
                for (i=0, j=strlen(s)-1; i < j; i++, j--)
                        s[i] ^= s[j], s[j] ^= s[i], s[i] ^= s[j];
                printf("%s ",s);
                argc--;
                argv++;
        }
        printf("\n");
        return 0;
}

In questo esempio il programma effettua un reverse di tutte le parole che gli vengo date in input ad esclusione di argv[0] che sarebbe il nome del programma, esempio:

INPUT: ./programma3 ciao sistemi operativi

OUTPUT: oaic imetsis ivitarepo

Il punto importante di questo codice è che lo scambio delle singole lettere viene effettuato con l'operatore logico XOR senza cosi avere bisogno di una "variabile di scambio". Qui viene spiegato nel dettaglio il funzionamento dell'operatore logico

(S.G)

programma 4

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct elem {
        char *s;
        struct elem *next;
};
struct elem *root=NULL;

void insert(char *s, struct elem **elp) { /* Inserimento in ordine alfabetico */
        if (*elp && strcmp((*elp)->s, s) < 0) /* se la lista elp non è vuota e se (*elp)->s è alfabeticamente minore di s, inserisce in coda s */
                insert(s,&((*elp)->next));
        else { /* nel caso in cui la lista è vuota o se (*elp)->s è alfabeticamente maggiore di s, inserisce in testa s */
                struct elem tmp={s,*elp};
                struct elem *new=malloc(sizeof(struct elem));
                *new=tmp;
                *elp=new;
        }
}

struct elem *print(struct elem *root) { /* stampa l'intera lista, alla fine dealloca root e restituisce NULL */
        if (root) {
                printf("%s ",root->s);
                root->next=print(root->next);
                free(root);
                return NULL;
        }
}

int main(int argc, char *argv[]) {
        for (;argc>1;argc--,argv++) /* Scorre tutti gli elementi in input del programma (tranne il nome stesso del programma), aggiungendoli in una lista */
                insert(argv[1],&root);
        root=print(root);
        printf("\n");
        return 0;
}

Il programma ordina alfabeticamente gli elementi presi input, esempio:

INPUT: ./programma4 ciao sistemi operativi e macchine virtuali

OUTPUT: ciao e macchine operativi sistemi virtuali

(S.G)


Però ci sono due tecniche diverse per la scrittura di funzioni ricorsive che modificano liste... Chi le spiega? Renzo (talk) 10:08, 17 October 2015 (CEST)

---

In quanto la lista va modificata, in questo esempio, è stato passato l'indirizzo di memoria del puntatore di testa della lista. Ne verrà quindi usata una copia all'interno della funzione ma, essendo una copia di un indirizzo di memoria, si andrà ugualmente ad agire fisicamente sulla struttura dati. La seconda opzione è quella di passare la testa della lista come parametro, così facendo però ne verrà creata una copia. Per rendere quindi effettive le modifiche che avvengono nella funzione, bisogna quindi che la funzione non abbia risultato Void bensì deve ritornare come valore la testa della lista modificata. Nella chiamata della funzione è quindi necessario un assegnamento del tipo: head = func(head);

Alessandro Bandini

Programma 5

Propongo un esempio relativo all'uso dello statement static:

#include <stdio.h>

int var;

int foo(){
	static int var=0;
	var++;
	return var;
}

int bar(){
	int var=0;
	var ++;
	return var;
}

int foobar(){
	var++;
	return var;
}

int main(int argc, char *argv[]){
	int sum1, sum2, sum3;
	int i;
	for(i=0;i<100;i++){
		sum1=foo();
	}
	printf("%d\n", sum1);
	for(i=0;i<100;i++){
		sum2=bar();
	}
	printf("%d\n", sum2);
	for(i=0;i<100;i++){
		sum3=foobar();
	}
	printf("%d\n", sum3);

	return 0;
}

Output del codice:

100

1

100


Le tre funzioni foo(), bar() e foobar(), sebbene si possa pensare agiscano sulla stessa variabile globale intera var, hanno comportamenti diversi proprio perche' non agiscono sulla stessa variabile. Nella funzione foo(), viene ridefinita una nuova variabile intera var che ha uno scope limitato al solo corpo della funzione (e' come se avesse una "precedenza" maggiore rispetto alla variabile globale); inoltre essa e' definita come static: il valore della varibile non viene quindi salvato nello STACK della funzione bensi' nel segmento DATA, permettendo di avere il valore salvato anche dopo piu' chiamate della funzione.

Il discorso e' leggermente diverso per la funzione bar(): anche qui viene ridefinita la variabile var ma non in modo static. Il valore di var e' quindi salvato nello STACK della funzione, che viene ricreato ad ogni chiamata. Sebbene bar() venga chiamata cento volte, a sum2 viene assegnato solo un incremento, a causa dell'inizializzazione a 0 di var che avviene per ogni chiamata.

Infine, all'interno della funzione foobar() non viene definita una nuova variabile, quindi la var usata nel corpo e' la variabile globale definita in precedenza. Similmente ad una variabile static, anche il valore di una variabile globale viene memorizzato nel segmento DATA del programma, ed il cui valore rimane in memoria nonostante le successive chiamate della funzione.

Matteo Del Vecchio

Programma 6

Challenge: Cosa fa questo programma (progettato per processori atmega8/88/168 etc)?

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/twi.h>
#include <inttypes.h>
#ifdef ATMEGA88
#define F_CPU 8000000UL  // 8 MHz
#else
#define F_CPU 1000000UL  // 1 MHz
#endif
#include <util/delay.h>

void delay_ms(unsigned int ms)
{
        while(ms){
                _delay_ms(0.96);
                ms--;
        }
}

#define DMASK 0x07
#define TWSR_STATUS_MASK 0xF8
#define TWCR_CMD_MASK 0x0F

unsigned char newB;
unsigned char oldB;
unsigned char diffB;
unsigned char status;
unsigned char unstable;
unsigned char step;
unsigned char value;
unsigned char d0,d1,d2,d3,d4,d5,d6,d7,d8;

static unsigned char i2cs_addr;

void i2cs_init(void)
{
        TWCR = 1<<TWINT | 1<<TWEA | 1<<TWEN | 1<<TWIE;
        TWAR = i2cs_addr;
}

void i2cs_command(unsigned char comm)
{
        switch (comm & 0xF8) {
                case 0x80:
                        if (step)
                                status &= ~(1<<(comm & 0x07));
                        else
                                PORTD &= ~(1<<(comm & 0x07));
                        break;
                case 0x88:
                        if (step)
                                status |= (1<<(comm & 0x07));
                        else
                                PORTD |= (1<<(comm & 0x07));
                        break;
        }
}


ISR(TWI_vect)
{
        unsigned char i2c_status= TWSR & TWSR_STATUS_MASK;
        switch (i2c_status) {
                case TW_SR_DATA_ACK:
                        i2cs_command(TWDR);
                        TWCR = (TWCR & TWCR_CMD_MASK) |  1<<TWINT | 1<<TWEA;
                        break;
                case TW_ST_SLA_ACK:
                case TW_ST_DATA_ACK:
                        TWDR = status;
                        TWCR = (TWCR & TWCR_CMD_MASK) |  1<<TWINT | 1<<TWEA;
                        break;
                default:
                        TWCR = (TWCR & TWCR_CMD_MASK) |  1<<TWINT | 1<<TWEA;
                        break;
        }
}

int main(void)
{
        DDRD = 0xFF;
        PORTB = 0xFF;
        PORTC = 0x0F;
        delay_ms(10);
        i2cs_addr= (0x20 | ((~PINC & 0x0F) << 1));
        step = (i2cs_addr & 0x10) == 0;
        i2cs_init();
        sei();
        while (1) {
                delay_ms(1);
                oldB=newB;
                newB=~PINB;
                diffB=oldB ^ newB;
                unstable=0;
                if (diffB & 0x01) d0 = 1; else d0++; 
                if (diffB & 0x02) d1 = 1; else d1++;
                if (diffB & 0x04) d2 = 1; else d2++;
                if (diffB & 0x08) d3 = 1; else d3++;
                if (diffB & 0x10) d4 = 1; else d4++;
                if (diffB & 0x20) d5 = 1; else d5++;
                if (diffB & 0x40) d6 = 1; else d6++;
                if (diffB & 0x80) d7 = 1; else d7++;
                if(d0 & DMASK) unstable |= 0x01;
                if(d1 & DMASK) unstable |= 0x02;
                if(d2 & DMASK) unstable |= 0x04;
                if(d3 & DMASK) unstable |= 0x08;
                if(d4 & DMASK) unstable |= 0x10;
                if(d5 & DMASK) unstable |= 0x20;
                if(d6 & DMASK) unstable |= 0x40;
                if(d7 & DMASK) unstable |= 0x80;
                if (step) {
                        status ^= (newB & ~value & ~unstable);
                        value = (value & unstable) | (newB & ~unstable);
                } else
                        status = (status & unstable) | (newB & ~unstable);
                if (step)
                        PORTD=status;
        }
        return(0);
}

Renzo (talk) 08:06, 21 October 2015 (CEST)

Programma 7

Guardate queste costanti predefinite. Come le utilizzereste?

#include <stdio.h>

int main(int argc, char *argv[]) {
  printf("%s: line %d\n",__FILE__,__LINE__);
  printf("built %s %s\n",__DATE__,__TIME__);
  return 0;
}

Programma 8

Un "effetto speciale" del preprocessore:

#include <stdio.h>
#define ik(x) int k##x = x
#define ck(x) char *k##x = #x

ik(0);
ik(1);
ik(2);
ck(tricky);

int main(int argc, char *argv[]) {
  printf("%d %d %d %s\n", k0, k1, k2, ktricky);
}

Il programma stampa: 0 1 2 tricky

Questo e' dovuto al fatto che il preprocessore sostituisce ogni occorrenza di ik(x) con int kx = x; Lo stesso avviene per la stringa tricky. Infatti se andiamo a guardare il programma facendo solo la fase di preproccessing dando il comando gcc -E [...] notiamo che diventa cosi':

int k0 = 0;
int k1 = 1;
int k2 = 2;
char *ktricky = "tricky";

int main(int argc, char *argv[]) {
      printf("%d %d %d %s\n", k0, k1, k2, ktricky);
}

L'operatore ## concatena due argomenti senza lasciare spazi bianchi tra di loro, in questo caso concatena k ad x.
Non importa che ci sia uno spazio tra k e x. Vedi [1]

Aletri (talk)

Programma 9

Ecco un esempio di uso della libreria stdarg.

#include <stdio.h>
#include <stdarg.h>

void intprint(int n, ...)
{
  va_list ap;

  va_start(ap, n);
  while (n>0) {
    int i;
    printf("%d ",va_arg(ap,int));
    n--;
  }
  printf("\n");
  va_end(ap);
}

int main(int argc, char *argv) {
  intprint(3,1,2,3);
  intprint(5,1,2,3,4,5);
}

L'uso della libreria stdarg e' particolarmente utile quando la funzione chiamata non conosce il numero di argomenti che le verranno passati. Infatti possiamo notare che intprint viene chiamata la prima volta con 4 parametri e la seconda volta con 6. Questo viene grestito dalla libreria mettendo gli argomenti nella lista va_list. Questa libreria utilizza delle macro per getire questa operazione. In particolare potete notare facendo solo il preprocessing che va_start e va_end vengono sostituiti con delle macro.

Il programma stampa 123 12345.

Aletri (talk)

Programma 10

Un po' di trucchi con le macro e le funzioni a numero variabile di parametri con passaggio di liste di parametri.

#define _GNU_SOURCE
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>

#define debugprintf(fmt, ...) private_debugprintf((fmt),__FILE__,__LINE__, ##__VA_ARGS__)

int private_debugprintf(const char *fmt, ...)
{
  va_list ap;
  char *newfmt;
  int rv;

  va_start(ap, fmt);
  asprintf(&newfmt,"File %%s Line %%d %s",fmt);
  rv=vfprintf(stderr, newfmt, ap);
  free(newfmt);
  return rv;
}

char *name="douglas adams";
int value=42;

int main(int argc, char *argv[]) {
  debugprintf("hello world, bug!\n");
  debugprintf("string: %s int: %d\n",name,value);
}

Programma 11

getopt e getopt_long sono molto utili.... ecco un esempio. Cosa fa questo programma?

#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <libgen.h>

void usage(char *progname) {
  fprintf(stderr,
      "Usage:\n"
      "\t%s [-m num] [-r] args...\n"
      "\t%s [--mul num] [--rev] args...\n",progname,progname);
  exit(1);
}

int main(int argc, char *argv[]) {
  int c, oindex;
  int rev=0;
  int mul=1;
  static struct option long_options[] = {
    {"mul", required_argument, 0, 'm'},
    {"rev", no_argument, 0, 'r'}};
  while ((c = getopt_long(argc, argv, "m:r",
          long_options, &oindex)) > 0) {
    switch (c) {
      case 'm': mul = atoi(optarg);
                break;
      case 'r': rev = 1;
                break;
      default:
                usage(basename(argv[0]));
    }
  }
  for (; mul>0; mul--) {
    if (rev) {
      for(oindex=argc-1; oindex>=optind; oindex--)
        printf("%s ",argv[oindex]);
    } else {
      for(oindex=optind; oindex<argc; oindex++)
        printf("%s ",argv[oindex]);
    }
  }
  printf("\n");
  return 0;
}

Il programma prende in input TOT argomenti e li manda tutti sullo standard output. Se aggiungiamo l'opzione -m (o --mul) seguita da un valore numerico N, ogni argomento viene mandato sullo standard output N volte. Se aggiungiamo l'opzione -r (o --rev), gli argomenti vengono stampati in ordine inverso.

Esempio

$ ./programma11 ciao mondo -m 2 -r

output: mondo ciao mondo ciao

$ ./programma11 ciao mondo hello world -m 3

output: ciao mondo hello world ciao mondo hello world ciao mondo hello world

Le opzioni vengono gestite dalla libreria getopt. Le opzioni sono rappresentate da un array di strutture (dove ogni elemento dell'array è un'opzione) denominato long_options. La definizione della struttura option è la seguente:

struct option {
     const char *name; //this field is the name of the option. It is a string
     int has_arg;      //this field says whether the option takes an argument. It is an integer, and there are three legitimate values: no_argument, 
                       //required_argument and optional_argument
     int *flag
     int val

}

Alessio

In questo esempio il programma ha diversi comportamenti a seconda delle opzioni specificate nella struttura long_options[]:

long_options[]={"nome completo dell'opzione", "numero di argomenti necessari", "indirizzo per settare una variabile", "nome abbreviato dell'opzione"}

- "m" o "mul": ripete n volte gli argomenti passati come input;

- "r" o "rev": effettua il reverse degli argomenti (eventualmente ripetuti n volte).

Si deve fare particolare attenzione alla funzione getopt_long, i cui argomenti (che non conosciamo) specificano:

- "m:r" : indica che l'opzione m deve essere seguita da un argomento e questo viene specificato tramite i due punti;

- long_options : indica l'indirizzo della struttura delle opzioni da analizzare;

- &oindex : qui viene salvata la posizione corrente dell'opzione a cui si sta accedendo (es. per l'opzione m oindex avrà valore 0, mentre per r valore 1).

Successivamente nel ciclo for vengono effettuate le semplici operazioni di "mul" e/o "rev"

(S.G)