24 Jun 2023, 2:52 PM
24 Jun 2023, 2:52 PM

3. C

Vantaggi

Direttive e istruzioni fondamentali

Tipi e Casting

C è un linguaggio debolmente tipizzato che utilizza 8 tipi fondamentali. È possibile fare il casting tra tipi differenti:

float a = 3.5;
int b = (int)a;

La grandezza delle variabili è dipendente dall’architettura di riferimento, i valori massimi per ogni tipo cambiano a seconda se la variabile è signed o unsigned.

Attenzione

Non esiste il tipo boolean, ma viene spesso emulato con un char.

sizeof (operatore)

Si tratta di un operatore che elabora il tipo passato come argomento (tra parentesi) o quello dell’espressione e restituisce il numero di bytes occupati in memoria. A differenza di altri linguaggi, come per esempio Java, il numero di bytes occupati da ogni tipo dipende dal compilatore.

sizeof (type) / sizeof expression

Esempio:

#include <stdio.h>
int main() 
{
	int x = 10;
	printf("variable x : %lu\n", sizeof x); //Output: 4
	printf("expression 1/2 : %lu\n", sizeof 1/2); //Output: 2
	printf("int type : %lu\n", sizeof(int)); //Output: 4
	printf("char type : %lu\n", sizeof(char)); //Output: 1
	printf("float type : %lu\n", sizeof(float)); //Output: 4
	printf("double type : %lu\n", sizeof(double)); //Output: 8
	return 0;
}

Puntatori di variabili

C si evolve attorno all’uso di puntatori, ovvero degli alias per zone di memorie condivise tra diverse variabili/funzioni. L’uso di puntatori è abilitato da due operatori: '*' ed '&'.

* ha significati diversi a seconda se usato in una dichiarazione o in un’assegnazione:

int *punt; // → crea un puntatore ad intero
int valore = *(punt); // → ottiene valore puntato, a seconda del tipo del puntatore, gcc sa come interpretarlo

& ottiene l’indirizzo di memoria in cui è collocata una certa variabile.

long whereIsValore = &valore;

Esempio 1

float pie = 3.4;
float *pPie = &pie;
pie *= 2; //pie = 6.8, *pPie = 6.8*
float pie4 = *pPie * 2; //pie4 = 6.8 * 2 = 13.6
char *array = "str";
*array = 's';
array[1] = 't';
*(array + 2) = 'r';

Esempio 2

int i = 42;
int * punt = &i;
int b = *(punt);
Tipo Nome Valore Indirizzo
int i 42 0xaaaabbbb
int* punt 0xaaaabbbb 0xccccdddd
int b 42 0x11112222
i=20
Nome Valore
i 20
punt 0xaaaabbbb
b 42

Puntatori di funzioni

C consente anche di creare dei puntatori a delle funzioni: puntatori che possono contenere l’indirizzo di funzioni differenti. Sintassi simile ma diversa!

ret_type (* pntName)(argType,argType,...)
//Esempio
float (*punt)(float,float);

Esempio:

#include <stdio.h>
float xdiv(float a, float b) 
{
    return a/b;
}
float xmul(float a, float b) 
{
    return a*b;
}

int main() 
{
    float (*punt)(float,float);
    punt = xdiv;
    float res = punt(10,10); //res = 1
    punt = &xmul; //& opzionale
    res = (*punt)(10,10); //res = 100
    printf("%f\n", res);
    return 0;
}

int main()

Esempio di compilazione ed esecuzione

File main.c

#include <stdio.h>
int main(int argc, char **argv) 
{
    printf("%d\n", argc);
    printf("%s\n", argv[0]);
    return 0;
}

Compilazione:

gcc main.c -o main

Esecuzione:

./main arg1 arg2

In output si ha “3” (numero argomenti incluso il file eseguito) e “./main” (primo degli argomenti). In generale quindi argc è sempre maggiore di zero.

printf() / fprintf()

int printf(const char *format, ...)
int fprintf(FILE *stream, const char *format, ...)

Inviano dati sul canale stdout (printf) o su quello specificato (fprintf) secondo il formato indicato. Il formato è una stringa contenente contenuti stampabili (testo, a capo, …) ed eventuali segnaposto identificabili dal formato generale:

%[flags][width][.precision][length]specifier // Una specifica di conversione semplice contiene solo il segno di percentuale e un carattere tipo, ad esempio %s specifica una conversione di stringhe.

Ad esempio: %d (intero con segno), %c (carattere), %s (stringa), ecc.
Ad ogni segnaposto deve corrispondere un ulteriore argomento del tipo corretto.

Direttive

Il compilatore, nella fase di preprocessing, elabora tutte le direttive presenti nel sorgente. Ogni direttiva viene introdotta con ‘#’ e può essere di vari tipi:

#include <lib> copia il contenuto del file lib (cercando nelle cartelle delle librerie) nel file corrente
#include "lib" come sopra ma cerca prima nella cartella corrente
#define VAR VAL crea una costante VAR con il contenuto VAL, e sostituisce ogni occorrenza di VAR con VAL
#define MUL(A,B) A*B dichiara una funzione con parametri A e B. Queste funzioni hanno una sintassi limitata!
#ifdef, #ifndef, #if, #else, #endif rende l’inclusione di parte di codice dipendente da una condizione.

Le macro possono essere passate a gcc con -D NAME=VALUE. Questo significa che durante la compilazione, tutte le occorrenze della parola "NAME" nel codice sorgente verranno sostituite con il valore VALUE.

Esempio 1

#include <stdio.h>
#define ITER 5
#define POW(A) A*A
int main(int argc, char **argv) 
{
    #ifdef DEBUG
        printf("%d\n", argc);
        printf("%s\n", argv[0]);
    #endif
    int res = 1;
    for (int i = 0; i < ITER; i++)
    {
        res *= POW(argc);
    }
    return res;
}

In questo esempio viene utilizzata la direttiva #ifdef, che, nel caso in cui la macro DEBUG sia stata definita (indipendentemente dal suo valore), permetterà l'esecuzione delle due printf(). DEBUG può essere definita sia all'interno del codice mediante #define DEBUG 1, oppure al momento della compilazione utilizzando l'opzione -D NAME=VALUE.

Esempio:

gcc main.c -o main.out
./main.out 1 2 3 4 #Output: nothing
gcc main.c -o main.out -D DEBUG=0
./main.out 1 2 3 4 #Output: 5 .\main.out
gcc main.c -o main.out -D DEBUG=1
./main.out 1 2 3 4 #Output: 5 .\main.out

Se al posto di #ifdef avessimo messo per esempio #if, allora l'esito sarebbe stato il seguente:

gcc main.c -o main.out
./main.out 1 2 3 4 #Output: nothing
gcc main.c -o main.out -D DEBUG=0
./main.out 1 2 3 4 #Output: nothing
gcc main.c -o main.out -D DEBUG=69
./main.out 1 2 3 4 #Output: 5 .\main.out

Esempio 2

#include <stdlib.h>
#include <stdio.h>
#define DIVIDENDO 3
int division(int var1, int var2, int * result)
{
    *result = var1/var2;
    return 0;
}
int main(int argc, char * argv[])
{
    float var1 = atof(argv[1]);
    float result = 0;
    division((int)var1,DIVIDENDO,(int *)&result);
    printf("%d \n",(int)result);
}

Dato che la funzione division si aspetta come terzo argomento un puntatore a int, (int *)&result converte l'indirizzo di una variabile di tipo float in un puntatore a int. Questa conversione non cambia il fatto che la variabile result sia ancora di tipo float e quando la funzione division ci scrive il risultato della divisione, sta scrivendo un valore di tipo int nella memoria occupata da una variabile di tipo float. Poiché i tipi int e float hanno rappresentazioni diverse in memoria, possono capitare risultati imprevedibili.

Librerie standard

Le librerie possono essere usate attraverso la direttiva include.
Tra le più importanti vi sono:

Structs e Unions

Le struct permettono di aggregare diverse variabili, mentre le union permettono di creare dei tipi generici che possono ospitare solo uno di vari tipi specificati.

struct Books
{
    char author[50];
    char title[50];
    int bookID;
} book1, book2;
struct Books book3 = {“Rowling”,”Harry Potter”,2};
strcpy(book1.title,“Moby Dick”);
book2.bookID = 3;
union Result
{
    int intero;
    float decimale;
} result1, result2;
union Result result3;
result3.intero = 22;
result3.decimale = 11.5; //il valore di result3.intero viene perso!

In quest'ultimo esempio, visto che i membri dell'union condividono la stessa area di memoria, l'assegnazione di "result3.decimale" sovrascrive i dati precedentemente presenti in quella stessa area di memoria, inclusi i bit che rappresentano "result3.intero".

Typedef

Typedef consente la definizione di nuovi tipi di variabili o funzioni.

typedef unsigned int intero;
typedef struct Books
{
	//variabili
} bookType;
intero var = 22; //= unsigned int var = 22;
bookType book1; //= struct Books book1;

Enum

#include <stdio.h>
enum State {Undef = 9, Working = 1, Failed = 0};
int main() 
{
	enum State state=Undef;
	printf("%d\n", state); // output è “9”
    return 0;
}

exit()

void exit(int status)

Il processo è terminato restituendo il valore status come codice di uscita. Si ottiene lo stesso effetto se all’interno della funzione main si ha return status. La funzione non ha un valore di ritorno proprio perché non sono eseguite ulteriori istruzioni dopo di essa. Il processo chiamante è informato della terminazione tramite un “segnale” apposito. I segnali sono trattati nei capitoli successivi.

Vettori

I vettori sono sequenze di elementi omogenei (tipicamente liste di dati dello stesso tipo, ad esempio liste di interi o di caratteri). I vettori si realizzano con un puntatore al primo elemento della lista.
Ad esempio con int arr[4] = {2, 0, 2, 1} si dichiara un vettore di 4 interi inizializzandolo: sono riservate 4 aree di memoria consecutive di dimensione pari a quella richiesta per ogni singolo intero (tipicamente 2 bytes, quindi 4*2=8 in tutto).

Esempio

char str[7] = {‘c’, ‘i’, ‘a’, ‘o’, 56,57,0} // 7*1 = 7 bytes

str è dunque un puntatore a char (al primo elemento) e si ha che:
str[n] corrisponde a *(str+n) e in particolare str[0] corrisponde a *(str+0)=*(str)=*str

Stringhe

Le stringhe in C sono vettori di caratteri, ossia puntatori a sequenze di bytes, la cui terminazione è definita dal valore convenzionale '\0' (zero). Un carattere tra apici singoli equivale all’intero del codice corrispondente.
In particolare un vettore di stringhe è un vettore di vettore di caratteri e dunque:

char c; //carattere
char * str; //vettore di caratteri / stringa
char **strarr; //vettore di vettore di caratteri / vettore di stringhe

Si comprende quindi la segnatura della funzione main con **argv.

Array e stringhe

C supporta l’uso di stringhe che, tuttavia, corrispondono a degli array di caratteri.

int nome[DIM];
long nome[] = {1,2,3,4};
char string[] = “ciao”;
char string2[] = {‘c’,’i’,’a’,’o’};
nome[0] = 22;

Gli array sono generalmente di dimensione statica e non possono essere ingranditi durante l’esecuzione del programma. Per array dinamici dovranno essere usati costrutti particolari (come malloc).
Le stringhe, quando acquisite in input o dichiarate con la sintassi "stringa", terminano con il carattere ‘\0’ e sono dunque di grandezza str_len+1

Esempio carattere e argc/argv

#include <stdio.h>
int main(int argc, char **argv) 
{
    int code=0;
    if (argc<2) 
    {
        printf("Usage: %s <carattere>\n", argv[0]);
        code=2;
    } else 
    {
        printf("%c == %d\n", argv[1][0], argv[1][0]);
    };
    return code;
}

Funzioni della libreria string.h

Dato che le stringhe sono riferite con un puntatore al primo carattere non ha senso fare assegnamenti e confronti diretti, ma si devono usare delle funzioni. La libreria standard <string.h> ne definisce alcune come ad esempio:

char * strcat(char *dest, const char *src) // aggiunge src in coda a dest
char * strchr(const char *str, int c) // cerca la prima occorrenza di c in str
int strcmp(const char *str1, const char *str2) // confronta str1 con str2
size_t strlen(const char *str) // calcola la lunghezza di str
char * strcpy(char *dest, const char *src) // copia la stringa src in dst
char * strncpy(char *dest, const char *src, size_t n) // copia n caratteri dalla stringa src in dst

Esempio di parsing manuale degli argomenti

#define MAXOPTL 64
#define MAXOPTS 10
#include <stdio.h>
#include <string.h>
// arrays of options and of values
char opt[MAXOPTS][MAXOPTL];
char val[MAXOPTS][MAXOPTL];
int main(int argc, char **argv) 
{
    int a=0, o=0;
    // loop into arguments:
    while (++a<argc && o<MAXOPTS) 
    {
        if (strcmp("-h", argv[a])==0)
            strcpy(opt[o++], "help");
        …
        if (strcmp("-k", argv[a])==0) 
        {
            strcpy(opt[o++], "key");
            if (a+1<argc)
                strcpy(val[o-1], argv[++a]);
        }
    }
    // dump options (keys/values):
    for (a=0; a<o; a++) 
    {
        printf("opt[%d]: %s,%s\n",a,opt[a],val[a]);
    }
    return 0;
}

  1. In C una stringa è in effetti un vettore di caratteri, quindi un vettore di stringhe è un vettore di vettori di caratteri, inoltre i vettori in C sono sostanzialmente puntatori (al primo elemento del vettore). Per questo la lista di argomenti viene spesso indicata con “char ** argv”. Utile riferimento generale: https://www.gnu.org/software/gnu-c-manual/gnu-c-manual.html ↩︎