OOP – La programmazione orientata agli oggetti

Java é un linguaggio di programmazione orientata agli oggetti, vale a dirsi che la soluzione al problema concreto passa attraverso la definizione per astratto di una serie di oggetti che cooperando, raggiungono il risultato computazionale previsto.

L’oggetto

Secondo la definizione di Booch un oggetto é un elemento caratterizzato da uno stato, un comportamento e un’identità. Per stato si intende l’insieme di attributi e proprietà, per comportamento l’insieme dei suoi metodi. Con identità si intende il tipo e l’identificatore.

La classe

Con la parola classe si intende lo schema di costruzione degli oggetti. Viene definita attraverso la parola chiave class e laddove non altrimenti specificato essa gode di visibilità di default package, cioé e visibile da tutte le classi appartenenti allo stesso package. La sintassi di costruzione di una classe é la seguente:

class Identificatore {
    elenco attributi
    elenco metodi
}

Gli attributi di una classe possono essere dati primitivi o oggetti, e devono essere scritti prima dei metodi.

Le istanze di una classe

Con il termine istanziare si intende l’operazione attraverso la quale viene creato un oggetto a partire da una classe. Essa avviene attraverso l’operatore new che carica dinamicamente in memoria operando su di essa per riferimento (by reference).

La sintassi per istanziare un oggetto (NomeOggetto) di tipo NomeClasse é la seguente:

NomeClasse NomeOggetto = new nomeClasse( );

A partire da questo momento l’oggetto sarà una entità distinta tanto dalla classe da cui é stato costruito quanto dagli altri oggetti dello stesso tipo.

I metodi

I metodi svolgono una funzione analoga alle funzioni e possono dunque essere invocati allo scopo di determinare una variazione degli attributi dell’oggetto, potendo anche restituire un valore. Esso é costituito da:

  • una visibilità
  • un tipo
  • un nome ed un elenco di parametri (o argomenti) accettati
  • un blocco di istruzioni chiamati anche implementazione

Qualora il metodo non restituisca alcunché viene definito come void, mentre qualora emetta un risultato visibile all’esterno del metodo, la parola chiave return consente di proiettare all’esterno questo risultato, terminando l’esecuzione del metodo. La sintassi é la seguente:

scope tipo identificatore ( argomenti ) {
    istruzioni;
}

Il metodo main

Per poter essere eseguibile un programma deve sempre contenere una classe, detta classe principale, di tipo public, contenente un metodo main.

public class MioSoftware {
    public static void main( String [ ] args ){
        /.../
    }
}

Invocazione dei metodi e passaggio di argomenti

Il metodo può essere invocato solo qualora l’oggetto definito dalla classe sia stato istanziato, cioé deve essere esistente a livello logico. Una volta che l’oggetto é stato istanziato é possibile invocarne i metodi attraverso la seguente sintassi:

NomeOggetto.nomeMetodo(argomenti);

Gli argomenti passati possono essere sia tipi primitivi che oggetti, con la differenza che nel primo caso sono passati per valore (by value) nel secondo per riferimento (by reference), ne viene passato cioé un puntatore all’area di memoria che contiene l’oggetto.

La classe System

La classe System è un contenitore di diverse funzioni essenziali del linguaggio Java come la virtuale machine, le funzionalità di sicurezza e del sistema. Fornisce inoltre l’accesso alle funzioni di I/O del sistema:

  • IN: gli input (i dati che entrano)
  • OUT: gli output ( i dati che escono)
  • ERR: il canale dedicato agli errori

I Packages

I packages sono pacchetti di classi già pronte all’uso, contenute all’interno di un file con estensione .jar. Si tratta di librerie a disposizione del programmatore che possono essere incluse nel codice attraverso la parola chiave import. Il package java.lang é il più importante di Java e racchiude le classi fondamentali del linguaggio. Questo però, a differenza dei normali packages viene incluso anche senza che venga specificato.

//Utilizzare l’asterisco * per includere tutte le classi contenute in uno specifico pacchetto

import java.awt.*;

//Oppure specificare solo le classi di interesse, per risparmiare spazio e memoria

import java.awt.Frame;

Visualizzazione dei dati

L’output dei dati a video é disponibile attraverso le funzionalità presenti nella libreria di Java. Lo standard output é accessibile attraverso l’oggetto System.out

La classe System raccoglie alcune funzionalità di sistema, uno dei suoi attributi é out, un oggetto che contiene diversi metodi per produrre output.

Lettura dei dati

La lettura dell’input avviene mediante la classe InputStream contenuta in System. Il metodo readLine() della classe BufferedReader legge sequenze di caratteri che restituisce sotto forma di stringhe. La classe BufferedReader deve essere inizializzata fornendo un input, ad esempio System.in di classe InputStream. Si può istanziare un oggetto di classe BufferedStreamReader in questo modo:

InputStreamReader leggi = new InputStreamReader(System.in);

A questo punto, potremo istanziare BufferedReader con l’oggetto leggi, che ci metterà a disposizione il metodono readLine() in esso contenuto, come nel seguente caso:

BufferedReader input = new BufferedReader(leggi);

Sarà quindi possibile leggere una linea di testo istanziando il metodo readLine() dell’oggetto input:

input.readLine();

I commenti

Come in tutti i linguaggi di programmazione, anche in Java i commenti osservano la seguente sintassi:

  • Doppio slash // ad inizio commento
  • Slash e asterisco /* ad inizio commmento e */ a fine commento
  • Slash e doppio asterisco /** e **/ a fine commmento

Relativamente alla terza categoria ci sono i commenti Javadoc che permettono di scrivere la documentazione delle classi affinché possa essere letta da un browser web. La documentazione può essere generata con il comando javadoc seguito dal nome del file contenente la classe di cui si vuole generare la documentazione e supporta l’inserimento di informazioni specifiche come l’autore e la versione del software, come nel seguente esempio:

//**
* Nome classe

* Descrizione classe

*

* @author Luca Scandroglio

* @version 0.1

*/

Le enumerazioni

Un enumerazione é un tipo di dato intero che può contenere un tipo di dato.

Il loro utilizzo é molto simile a quello già visto per le strutture e per le unioni con la differenza che un enumerazione può contenere solo costanti intere (int o char), che i suoi membri non vengono allocati sequenzialmente in memoria e condividono lo stesso spazio dei nomi delle altre variabili (e non anche delle strutture e delle unioni, per cui si possono generare conflitti). Vengono utilizzate laddove ci sia la necessità di organizzare i nomi delle variabili per insiemi o valori significativi. Ad esempio:

enum tag_enum {
    NORD = 0;
    EST = 90;
    SUD = 180,
    OVEST = 270
};

Qualora un enumerazione non venga inizializzata essa assume il valore 0 per il primo membro, 1 per il secondo, 2 per il terzo. In caso di inizializzazione mista si osserva lo stesso principio, incrementando di 1 il valore precedentemente dichiarato.

Le unioni

Le unioni sono tipi di dato derivato costituite da un insieme di elementi, allocati in memoria in modo sovrapposto.

Il loro funzionamento é molto simile a quello delle strutture con la differenza però che mentre per la strutture viene riservata una congrua quantità di memoria necessaria alla memorizzazione di tutti i loro membri, le unioni riservano solo la quantità di memoria necessaria al più capiente dei suoi membri. Ne consegue che un’unione può contenere, alternativamente, solo 1 dei membri dichiarati.

La dichiarazione di un unione avviene attraverso la keyword union, in questo modo:

union tag_unione {
    int membro1;
    long membro2;
} identificatore_unione;

//La struttura avrà dimensioni pari al tipo di variabile più grande, cioè long

L’accesso, la scrittura, o la manipolazione dei membri avviene allo stesso modo delle strutture. Ne consegue che:

  • le unioni possono essere copiate
  • si possono definire con typedef
  • si possono dichiarare senza nome
  • possono essere annidate
  • possono contenere array e possono essere contenute in array
  • possono essere puntate
  • possono essere argomenti di funzione o return
  • possono avere membri anonimi
  • possono essere campi di bit

Casi d’uso

Le unioni offrono il vantaggio di consentire un certo risparmio di memoria e risorse come anche quello di definire un tipo che può contenere tipi di dato differenti.

Le strutture

Una struttura é un tipo di dato derivato costituito da un insieme di elementi anche di diverso tipo.

La dichiarazione di un struttura avviene attraverso la seguente sintassi:

struct tag_struttura {
    tipo identificatore;
    tipo identificatore;
    tipo identificatore;
} identificatore_struttura;

Può essere dichiarato anche senza identificatore come schema tipo-struttura da applicare ad una successiva creazioni di una variabile di tipo struttura

struct tag_struttura identificatore_struttura;

Le strutture hanno uno spazio dei nomi riservato per cui si possono utilizzare nomi di tag e di membri anche dello stesso nome di quelli già utilizzati nel programma, avendo a tutti gli effetti uno scope separato per ogni struttura.

Inizializzazione

I membri di una struttura vengono inizializzati con una lista di inizializzatori sequenziale, corrispondente nell’ordine delle assegnazioni a quello contenuto nella lista dei membri indicata in fase di dichiarazione.

struct tag_struttura {
    tipo identificatore;
    tipo identificatore2;
    tipo identificatore3;
} identificatore_struttura = { valore1, valore2, valore3 };

In alternativa é possibile utilizzare gli inizializzatori designati, esplicitati nella seguente sintassi:

struct tag_struttura identificatore_struttura = {
    .identificatore = valore,
    .identificatore2 = valore
};

Facendo precedere l’operatore . al nome del membro della funzione da inizializzare. In questo caso l’inizializzazione può avvenire in modo selettivo. Tutto ciò che non viene espressamente inizializzato viene inizializzato a 0, o, nel caso dei puntatori, con un puntatore a NULL. Questo ci consente quindi operare sui singoli membri anche attraverso l’utilizzo dell’identificatore della struttura:

//Per la lettura
tipo identificatore_variabile = identificatore_struttura.identificatore_membro
//Per la scrittura
identificatore_struttura.identificatore_membro = valore;

Vale la pena ricordare il corretto modo di copiare una stringa all’interno di una struttura, servendoci della funzione strcpy (contenuta nell’header <string.h>):

struct tag_struttura {
    int variabile_intera;
    char variabile_stringa[MAX];
} identificatore_struttura;

strcpy( identificatore_struttura.variabile_stringa, “valore” );

E’ altresì possibile assegnare tutti i membri di una struttura ad un’altra struttura, purché siano uguali, cioè abbiano lo stesso tipo-struttura, oppure siano assegnate contestualmente alla dichiarazione di una struttura senza tag. Tra strutture non é possibile fare operazioni di confronto (==, >=).

La keyword typedef

Per rendere più agevole la dichiarazione di una struttura è possibile utilizzare la keyword typedef affinché si possa “invocare” una struttura-tipo in modo molto semplice:

typdef struct tag_struttura {
    int variabile_intera;
    char variabile_stringa[MAX];
} alias;

//Invocando con:
alias identificatore_struttura = { 150, “stringa\n” };

Dimensione in memoria delle strutture

Le strutture occupano una dimensione di memoria corrispondente a quella che sarebbe stata altrimenti necessaria per ognuna delle sue variabili, più la quantità di holes necessari all’allineamento dei dati. Gli holes sono quelle aree di memoria necessarie al riempimento degli spazi di memoria inutilizzati affinché il dato abbia una una dimensione e distribuzione uniforme nelle aree di memoria che utilizza.

Allo scopo di ottimizzare l’allineamento é preferibile ordinare le variabili nella dichiarazione della struttura in modo decrescente sulla base della quantità di memoria dedicata.

Strutture senza nome

Dallo standard C99 é possibile dichiarare strutture “senza nome” da utilizzare on-demand.

typedef struct {
    int variabile_intera;
    char variabile_stringa[MAX];
} alias;

alias identificatore_struttura = (tag_struttura){ 150, “stringa\n” };

Strutture annidate

Una struttura annidata é una struttura dove i suoi membri sono a loro volta delle strutture.

struct struttura_figlio identificatore_figlio = {
    tipo identificatore;
    tipo identificatore2;
};

struct struttura_padre identificatore_padre = {
    struct struttura_figlio identificatore_figlio;
    .identificatore1 = valore;
    .identificatore2 = valore;
};

//Inizializzazione
struct struttura_padre identificatore_padre = {
    { valore1, valore2 },
    150
};

//Accesso
printf(“%d”, identificatore_padre.identificatore_figlio.identificatore2 );

Array e strutture

Un array di strutture é un vettore all’interno del quale ogni elemento é a sua volta una struttura.

#define MAX 5
struct tag_struttura {
    tipo identificatore;
    tipo identificatore2;
};

struct tag_struttura identificatore_struttura[MAX] = {
    { valore01, valore02, valore03, ecc },
    //Sintassi alternative
    [1] = { valore11, valore12, valore13, ecc }, 
    {.identificatore = valore21, .identificatore2 = valore31 }
};

//Accesso
struct tag_struttura identificatore_variabile = identificatore_struttura[0]

Strutture e puntatori

Una struttura possiede un proprio indirizzo di memoria a partire dal quale vi sono memorizzati tutti i suoi dati. E’ possibile utilizzare questo indirizzo all’interno di un puntatore a struttura.

struct tag_struttura {
    tipo identificatore_variabile;
    tipo identificatore_variabile2;
} identificatore_struttura;

struct tag_struttura *puntatore_struttura = &identificatore_struttura

//Accesso
valore = puntatore_struttura->identificatore_variabile;

//Sintassi alternativa
valore = (*puntatore_struttura).identificatore_variabile;

//Caso array
struct *puntatore_struttura = &identificatore_struttura[0]

//Oppure interessante
puntatore_struttura += (MAX - 1) //Punta all’ultimo elemento

Strutture e funzioni

Le strutture possono essere utilizzate come argomenti di funzione, o come tipi di ritorno delle funzioni stesse. Se da un lato questo garantisce un certo grado di protezione dei dati della funzione passata, dall’altro strutture di una certa dimensione possono determinare un certo overhead di risorse, salvo che non si utilizzi un puntatore, ma in questo caso verrebbe meno il vantaggio della protezione. Vediamo la sintassi:

struct punto {
    int x;
    int y;
    int somma;
};

//Prototipi di funzione
struct punto nuovoPunto(struct punto p1, struct punto p2);

int main(void){
    /.../
}

struct point nuovoPunto{
    return (struct point){ .x = x, .y = y};
}

Strutture e VLA

Le strutture non possono avere VLA come membri, ma a partire dallo standard C99 sono stati introdotti i membri array flessibili, la cui dimensione può essere omessa, a patto che:

  • siamo dichiarati come ultimi membri della struttura
  • non siano gli unici membri della struttura
  • siano i soli array della struttura
  • non devono contenere alcun valore tra le parentesi quadre [ ]
struct punto {
    int x;
    int y;
    int array[];
};

Una volta dichiarato un membro array flessibile all’interno di una struttura, viene ad esso dedicato un indirizzo di memoria a partire dal quale saranno allocati i suoi elementi. Per compiere questa operazione viene utilizzata la funzione malloc:

int array_flessibile = 4;

struct punto {
    int x;
    int y;
    int array[];
};

struct punto *punto1 = malloc(sizeof(struct punto) + array_flessibile * sizeof(int));

Oltre a questa operazione é bene ricordare anche che i membri array flessibili:

  • non vengono copiate in altre strutture
  • non si devono inizializzare con una lista di inizializzatori
  • non si devono utilizzare più volte tra più strutture

Strutture anonime

A partire dallo standard C11 è possibile dichiarare anche strutture anonime, cioè prive di tag o di identificatore, quali membri di altre strutture:

struct punto {
    int x;
    int y;

    struct { //Struttura anonima
        int x1;
        int x2;
    }
};

L’inizializzazione avviene poi come se x1 ed x2 siano membri della struttura contenitrice:

struct punto {
    0,
    0,
    { 1 , 2 }
};

Campi di bit

E’ possibile operare sulle strutture anche bit-per-bit, attraverso i cosiddetti “campi di bit”, specificando per ogni membro della struttura il numero di bit necessari al contenimento del dato, in modo da evitare sprechi di memoria:

struct tag_struttura {
    tipo identificatore_membro : numero_bit;
    tipo identificatore_membro : numero_bit;
    tipo identificatore_membro : numero_bit;
};

A seconda del numero di bit utilizzati potremo così contenere variabili adatte ad ospitare dati più piccoli. Tale procedura é utilizzabile solo per le variabili di tipo int, signed int, unsigned int e Bool. La dimensione del dato viene esplicitata mediante l’utilizzo dell’operatore : seguito dal numero di bit necessari, ottenibile elevando 2 al numero_bit.

Per via delle sue dimensioni atipiche, l’area di memoria occupata da un campo di bit non é referenziabile attraverso l’operatore di indirizzamento &.

I Puntatori

I puntatori sono variabili che contengono come valore un indirizzo di memoria appartenente ad un oggetto.

La loro utilità risiede nel fatto che la dimensione di memoria occupata da un indirizzo é nettamente inferiore a quella occupata dal dato puntato comportando un risparmio di risorse computazionali e mnemoniche. Generalmente la quantità di memoria necessaria ad un puntatore é 4 byte (su un sistema a 32 bit, 8 su un sistema a 64 bit). La sintassi utilizzata per la dichiarazione di un puntatore é:

tipo *identificatore;

Una volta dichiarato al puntatore deve essere assegnato un indirizzo di memoria valido, e ciò é possibile attraverso l’operatore di indirizzamento espresso dal carattere & come nella seguente sintassi:

identificatore = &identificatore_oggetto_puntato;

Pertanto:

int valore = 10;
int *puntatore = &valore; // puntatore conterrà l’indirizzo di variabile

Allo stesso modo, l’utilizzo nel carattere * detto operatore di deriferimento, permette di accedere al contenuto di un oggetto puntato.

int valore = 10;
int *puntatore = &valore;
int variabile = *puntatore // Il valore di variabile é 10.

Così come:

int valore = 10;
int *puntatore = &valore;
*puntatore = 1000; // la variabile valore sarà 1000.

Puntatore come parametro di funzione

I puntatori possono essere utilizzati come argomenti di funzione, in questo modo:

//Dichiarazione o prototipo di funzione
tipo nome_funzione(tipo *puntatore);

//oppure
tipo nome_funzione(tipo *); //senza identificatore

Come già detto, dal momento che gli argomenti delle funzioni sono passati “by value” ossia, per creazione di variabili temporanee locali interne alla funzione, l’uso dei puntatori permette di operare sui valori esterni alla funzione stessa.

E’ altresì possibile stampare l’indirizzo di memoria di un puntatore, attraverso lo specificatore di tipo %#p (oppure 0x%p).

printf(“L’indirizzo di memoria é: %#p”, identificatore_puntatore);

Puntatori come return di funzioni

Le funzioni possono avere return dei puntatori, é sufficiente che ne venga dichiarato il return in sede di dichiarazione e prototipazione.

int *value(void) {
/…/
}

Tuttavia, nel caso si cerchi di ritornare il valore di una variabile privata della funzione stessa, si riscontrerà un errore causato dal fatto che la variabile ha cessato di esistere al termine della funzione.

Array e puntatori

E’ allo stesso modo possibile utilizzare i puntatori con gli array, attraverso la seguente sintassi:

int identificatore_array[] = { 1, 2, 3, 4, 5, 6, 7, 8, , 9, 10 };
int *puntatore_array = identificatore_array;

E’ importante sottolineare che in questo caso il puntatore punterà all’elemento 0 della predetta array, come messo in evidenza da questa sintassi equivalente ed alternativa:

int *puntatore_array = &identificatore_array[0];

L’accesso ai restanti elementi dell’array resta possibile sommando o sottraendo al puntatore tante unità di memoria (dove con unità di memoria intendiamo l’area di memoria occupata da ogni singolo elemento dell’array) quante quelle che risultano necessarie al raggiungimento della posizione nell’array del dato che desideriamo manipolare. Ad esempio:

int identificatore_array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int *puntatore_array = identificatore_array;
puntatore_array += 2 // *puntatore_array = 3;

Aritmetica dei puntatori

Alla luce di quanto sopra esposto, diviene altresì possibile compiere sui puntatori vere e proprie operazioni aritmetiche come somma e sottrazione tra puntatori o valori interi:

int a = 2;
int b = 4;
int identificatore_array[] = { 1, 2, 3, 4, 5, 6, 7, 8, , 9, 10 };
int *puntatore_array = &identificatore_array[a];
int *puntatore_array2 = &identificatore_array[b];

ptrdiff_t somma_puntatori = *puntatore_array - *puntatore_array2 // 6
ptrdiff_t differenza_puntatori = *puntatore_array - *puntatore_array2 // -2

E’ bene notare che il tipo utilizzato per contenere le variabili somma_puntatori e differenza_puntatori sia di tipo ptrdiff_t e contenuta nella libreria <stddef.h>. Può essere in questo modo usata per stampare il suo contento la funzione printf mediante lo specificatore di formato %td.

Oltre alle operazioni di somma e sottrazione é possibile compiere anche operazioni di confronto ( >, <, >=, <=, ==) ottenendo come risultato true (1) a false (0).

Puntatori ad array come parametri di funzione

La definizione di un argomento di tipo array comporta sempre il passaggio dello stesso sotto forma di puntatore, proprio perché ciò che viene passato é sempre l’indirizzo di memoria dell’elemento 0. Sintassi:

int main(int array[]) { /…/ }

Scorrimento di un array con l’aritmetica dei puntatori

Per scorrere gli elementi di un array vengono solitamente utilizzati i cicli iterativi. E’ tuttavia possibile utilizzare l’aritmetica dei puntatori per raggiungere lo stesso scopo. Nel seguente modo:

#define SIZE 6
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

for (int *puntatore = array; puntatore < SIZE; puntatore++) {
/.../
}

Puntatore ad un array multidimensionale

Bisogna innanzitutto ricordare che un array multidimensionale é un array di array, dove una dimensione è un array in cui ogni elemento é esso stesso un array. Per quanto concerne l’utilizzo di puntatori é assai più semplice rappresentare il loro funzionamento nel seguente modo:

int array[] = {
{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 },
{ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }
};

Per accedere ad un elemento, farlo con la notazione classica degli array:

valore = array[0][0] // valore = 1

Per utilizzarli come argomento di funzione utilizzare la sintassi comune o quella dei puntatori:

void nome_funzione (int array[0][10]);
// Oppure, usando un puntatore
void nome_funzione (int (*array[10]));

NB: ricordarsi le parentesi intorno ad *array[3], altrimenti anziché indicare un puntatore ad un array di tre elementi, indicheremmo un array di 3 puntatori, per via della più alta precedenza degli operatori [ ] rispetto a *.

Una sintassi del tipo int array [ ][ ] non sarebbe in ogni caso accettata, perché, non dando indicazioni circa il numero di colonne, non si saprà in quale area di memoria inizia la successiva riga, essendo la scrittura dei dati, sequenziale. Per questo non é obbligatorio indicare il numero delle righe mentre invece va sempre indicato quello delle colonne.

Array di puntatori

Negli array di puntatori ciascun elemento é un puntatore. Come già accennato dal paragrafo precedente, il suo utilizzo é il seguente:

int *array[] = {
    (int[]) { 1, 2 }, //2 Colonne
    (int[]) { 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 } //10 Colonne
};

Si usano per creare i cosiddetti array triangolari ossia degli array aventi una struttura irregolare cioè un numero di colonne differente per ciascuna riga. E, dato che per definizione il nome di un array non é altri che un puntatore al suo primo elemento, un array di puntatori contiene gli indirizzi di puntamento relative agli array che contiene.

Puntatori a VLA

I puntatori possono essere usati anche con le VLA (Variable lenght array) salvo alcune restrizioni che restano valide per le VLA in generale: possono essere dichiarati solo nelle funzioni o nei prototipi e non possono e sono possono puntare a strutture o unioni.

Puntatori a puntatori

Il puntatore ad un puntatore é una variabile atta a memorizzare l’indirizzo di un puntatore il quale punta ad un valore determinato.

//Dichiarazione di un doppio puntatore

int variabile = 10;
int *puntatore = &variabile;
int **puntatore_a_puntatore = &puntatore;

//Ritornare indirizzo di memoria di un doppio puntatore
*nome_identificatore = *puntatore

//Ritornare il valore della variabile puntata da un doppio puntatore
int valore = **puntatore

La loro utilità si spiega nelle funzioni dove i parametri sono sempre passati by value, qualora si abbia la necessità di manipolare il riferimento in luogo del valore puntato attuando, di fatto, un passaggio by reference, non nativamente supportato in C.

Puntatori a funzione

I puntatori possono anche essere utilizzati come riferimenti a funzioni, osservando la seguente sintassi:

//Dichiarazione puntatore
tipo (*identificatore_puntatore)(tipo argomenti);
identificatore_puntatore = identificatore_funzione;

//Invocazione funzione puntata
(*identificatore_puntatore)(argomenti)

Typedef come alias di puntatori a funzione

L’apposizione delle parentesi tonde serve a distinguerlo dall’invocazione di funzione e agisce come alias. Resta tuttavia possibile invocare una funzione puntata anche senza l’apposizione delle parentesi resta uno standard stilistico utile a distinguere i puntatori a funzione dalle funzioni stesse. Trovano impiego quando le funzioni sono utilizzate quali argomenti di altre funzioni o nella definizione delle cd. callback, cioè quelle funzioni invocate da altre funzioni al verificarsi di un determinato evento.

Risultando a questo punto piuttosto contorta la programmazione mediante puntatori a funzione, la keyword typedef ci viene in aiuto permettendoci di semplificare il codice. Per esempio:

typedef int (*puntatore_a_funzione)(int,int) //Creiamo l’alias puntatore_a_funzione
puntatore_a_funzione nome_funzione;

Sarà quindi possibile creare un puntatore alla funzione in modo più semplice.

Puntatori a void

E’ possibile anche creare puntatori che non puntano a nessun tipo particolare, oppure puntatori a qualsiasi tipo (detti puntatori generici). Questi restano estranei all’utilizzo dell’operatore di deriferimento o all’aritmetica dei puntatori, non essendo definibile a monte la quantità di memoria occupata. Tuttavia un puntatore a un tipo determinato (esempio int *) può essere assegnato a void, e viceversa. L’utilità dei puntatori generici (puntatori a void) é presto spiegata quando é necessario costruire funzioni generiche i cui argomenti non sono di tipo noto.

N.B: è possibile dichiarare solo puntatori a void ma mai variabili di tipo void, proprio per definizione.

Puntatori NULL

Un puntatore a NULL é un puntatore che non punta ad alcun indirizzo di memoria.

Puntatori costanti

La keyword const anteposta ad un puntatore definirà un puntatore costante. Ne esistono di due tipi, quelli riferiti al dato puntato (1° caso), che sarà accessibile attraverso il puntatore in sola lettura, e quelli riferiti al puntatore stesso, il cui indirizzo di memoria non potrà essere modificato (2° caso) mantenendo tuttavia la possibilità di modificare il dato. Puntatori costanti relativi a costanti (3° caso) che non permettono la modifica né dell’indirizzo di memoria né del dato riferito. La loro utilità consiste nell’ottimizzazione delle risorse computazionali, che non prevedendo modifiche, saranno risparmiate.

//Puntatore a costante
const tipo *identificatore_puntatore

//Puntatore costante a variabile
tipo *const identificatore_puntatore

//Puntatore costante a costante
const tipo *const identificatore_puntatore

La keyword restrict

Dallo standard C99 esiste anche la Keyword restrict che applicata ad un puntatore, lo rende un puntatore ristretto,i cui effetti restano limitati al puntatore e non all’oggetto puntato. Questa keyword comunica al compilatore che l’utilizzo dei puntatori é univoco, cioè non sono presenti più puntatori dello stesso per la stessa area di memoria, dando luogo ad un compilazione ottimizzata in termini di risorse computazionali.

Conversione di puntatori

Come le variabili, così anche i puntatori seguono delle regole di conversione e promozione in sede di inizializzazione, assegnamento e comparazione. E’ necessario prestare attenzione in modo particolare nell’utilizzo combinato di puntatori a void con altri tipi di puntatori in quanto causa di comportamenti non definiti.

Le Funzioni

Una funzione é un blocco di codice contenente istruzioni atte a svolgere uno specifico obbiettivo computazionale.

Offrono tutta una serie di vantaggi di tipo pratico, come ad esempio:

  • Modularità: con le funzioni in codice può essere scomposto in più piccole unità di elaborazione che agiscono come moduli che hanno il compito di svolgere un isolato scopo, rendendo nel contempo più semplice e leggibile il codice.
  • Riutilizzo: le funzioni possono essere scritte una sola volta ed invocate infinite volte, rendendo più snello il codice.
  • Occultamento dei dati: le funzioni consentono di definire dei dati che rimangono accessibili solo all’interno della funzione stessa. Consentono inoltre di utilizzare gli stessi identificatori di variabili o costanti già utilizzate all’esterno senza che questo influisca sul funzionamento del programma (scope).

Definizione di una funzione

tipo nome_funzione(argomento1,argomento2){
    dichiarazioni;
    espressioni;
    return;
}

Il listato sopra evidenziato descrive la struttura sintattica di una funzione. Essa é caratterizzata da un tipo di valore di ritorno (int, double, long, etc etc), un proprio nome identificativo, una coppia di parentesi aperte che possono contenere gli argomenti passati all’interno della funzione, e la parola chiave “return” che restituisce il dato elaborato alla funzione chiamante. Tuttavia non é obbligatorio che la funzione restituisca qualcosa, così come non é obbligatorio che abbia dei valori in ingresso.

Invocazione di una funzione

Il procedimento con cui una funzione viene eseguita é definito “invocazione” e si esprime in questo modo:

nome_funzione(argomento1, argomento2);

Viene semplicemente scritto il nome di una funzione precedentemente dichiarata nel punto del codice in cui vogliamo che restituisca il risultato o produca i suoi effetti.

Dichiarazione di una funzione

Una funzione, per poter essere eseguita deve essere stata precedentemente dichiarata. Questo può avvenire in due modi corretti:

  • Può essere scritta per esteso prima della funzione principale main;
  • Può essere solo dichiarata attraverso un prototipo prima della funzione principale main, per poi essere scritta per esteso successivamente a questa.

La ragione di ciò risiede nel fatto che il compilatore deve “conoscere” la funzione che si sta invocando, pena la dichiarazione implicita, attraverso la quale il compilatore assume che la funzione invocata sia di tipo int e priva di argomenti, dando luogo a risultati non definiti.

I prototipi di funzione

Come già accennato é possibile indicare al compilatore soltanto la dichiarazione della funzione, esplicitandone il corpo successivamente alla funzione principale main. Tale procedura é detta prototipo.

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

int radice_quadrata(int numero);

int main(void){
    int numero = 4
    int radice = radice_quadrata(numero);
    printf(“La radice quadrata di %d é %d”, numero, radice);
    return (EXIT_SUCCESS);
}

int radice_quadrata(int numero){
    radice = sqrt(numero);
    return radice;
}

NB: Per dichiarare un prototipo di funzione che non accetta argomenti utilizzare sempre la keyword void es: int nome_funzione(void).

Parametri di funzione

Come é noto, le variabili rappresentano posizioni di memoria contenenti dei dati, tuttavia, quando vengono passate come argomenti ad un funzione, questi diventano delle copie, per cui é possibile operare su di essi senza il timore di alterare le posizioni di memoria a cui essi si riferiscono. Tale modalità é detta per valore (by value).

Promozione degli argomenti

Quando si invoca una funzione é possibile che gli argomenti ad essa passati non concordino con il tipo previsto in sede di definizione della funzione stessa. In tali circostanze assisteremo ad una conversione o ad una promozione del dato in quello previsto dal prototipo o dalla funzione. Ciò può dare luogo ad una perdita del dato o a comportamenti non definiti.

Passaggio di parametri array alla funzione

Una funzione può operare anche su degli oggetti di tipo array, sia monodimensionali che multidimensionali, attraverso le seguenti sintassi:

tipo nome_funzione( tipo nome_identificatore [] );

Occorre però tenere presente che, quando si opera su un array all’interno di una funzione si sta operando però su un puntatore all’array stesso, cosa che può dare luogo a difficoltà di cui l’uso dell’operatore sizeof può rappresentare un esempio. Utilizzando infatti sizeof per conoscere le dimensioni di un array all’interno di una funzione, ci verrà restituita la lunghezza del puntatore, corrispondente a 4 byte.

Array multidimensionali come parametri di funzione

Per passare ad una funzione un array multidimensionale si utilizza la seguente sintassi:

//Array bidimensionale
tipo nome_funzione( tipo nome_identificatore [] [NUMERO_COLONNE] );

//Array tridimensionale
tipo nome_funzione( tipo nome_identificatore [] [NUMERO_COLONNE] [NUMERO_PAGINE]);

La regola generale é che prima dimensione può sempre essere omessa, mentre vanno indicate tutte le dimensioni successive.

VLA come parametri di funzione

Nel caso di passaggio di VLA come argomenti, va invece utilizzata la seguente sintassi:

//Solo nei prototipi
//l é la variabile di lunghezza
tipo nome_funzione( int l, tipo identificatore_array [*] );

//Definizione della funzione
//l viene inserita nelle quadre
tipo nome_funzione( int l, tipo identificatore_array [l] ); 

Vengono sempre passati almeno 2 parametri, dove il primo rappresenta la variabile che definisce la lunghezza del array, ed il secondo il nome dell’array avente tra parentesi quadre il primo argomento. Nei prototipi quest’ultima accortezza può essere sostituita con il carattere * posto nelle parentesi quadre.

Passaggio di un numero indeterminato di argomenti ad una funzione

Fino adesso abbiamo sempre passato ad una funzione un numero predefinito di argomenti, tuttavia può accadere che tale numero non sia noto, o quanto meno non sia ancora. Utilizziamo a questo scopo la libreria <stdarg.h> che fornisce tipi e macro utili a gestire le funzioni con un numero indeterminato di argomenti. Vediamo come:

  1. Inserire il file header <stdarg.h>
  2. Definire un funzione avente almeno un parametro, seguito da tre puntini di sospensione …
  3. Dichiarare nella funzione una variabile di tipo va_list (non int, double, float etc etc)
  4. Utilizzare la macro va_start usando come primo parametro la variabile di tipo va_list, e come secondo, la variabile che abbiamo utilizzato come primo argomento della funzione
  5. Utilizzare la macro va_arg fornendo come primo parametro sempre la variabile di tipo va_list, ma come secondo parametro il tipo di dato da processare
  6. Chiudere con la macro va_end, fornendo come parametro sempre va_list, per chiudere il costrutto.
#include <stdarg.h> //1 Da mettere nel header
int nome_funzione(int numero, ...){ //2 Aggiungo … nei parametri
    va_list argomenti; //3 Variabile argomenti
    va_start(argomenti, numero); //4 Inizializzo argomenti
    int argomento1 = va_arg(argomenti, int); //5 Ricavo primo argomento
    int argomento2 = va_arg(argomenti, int); //7 Ricavo secondo argomento
    va_end(argomenti); //8 Chiudo e clean up
}

L’istruzione return

Con l’istruzione return si termina l’esecuzione di una funzione riportando il controllo alla funzione chiamante. Si può altresì utilizzare per restituire alla funzione chiamante il risultato delle operazioni eseguite dalla funzione, che deve essere dello stesso tipo definito dalla funzione stessa (pena la conversione con possibile perdita di dati), tuttavia se una funzione é di tipo void, può anche non restituire alcun valore, e l’istruzione return può essere posta in qualunque punto della funzione, o, in alternativa, attendere l’esecuzione della funzione raggiunga il termine rappresentato dalla }.

Tipo FunzioneTipo di returnComportamento
non-voidno-returnnon definito
non-voidvoidnon definito
voidnon-voidnon definito

Le funzioni _Noreturn

A partire dallo standard C11 é possibile utilizzare lo specificatore _Noreturn per indicare che una funzione non restituisce alcun dato. Questo si diversifica dall’indicare una funzione void in quanto in questo caso non viene restituito il controllo alla funzione chiamante.

Noreturn void ciao(){
    printf(“Ciao”);
    exit(0);
}

Le funzioni inline

Lo standard C99 rende possibile definire funzioni “in linea” evitando l’overhead della macchina, ossia tutte quelle operazioni legate al passaggio di controlli e dati determinati dall’esecuzione di una funzione. E’ vantaggioso nel caso di piccole funzioni.

static inline int ciao { printf(“ciao”); }

Ricorsione

Con il termine ricorsione si intende il procedimento con cui viene reiterata l’invocazione di una funzione dall’interno della funzione stessa. Essa può essere anche un efficace procedura di iterazione dei dati. E, per evitare cicli infiniti, si definisce nella ricorsione un cosidetto “caso base” che rappresenta il punto di uscita della ricorsione stessa, ed un “passo ricorsivo” all’interno del quale la funzione continuerà ad essere invocata.

voidcountdown(numero){
    if (numero == 0){ // Caso base
    return;
}
printf(“%n”, numero);
countdown(numero); // Passo ricorsivo

I vantaggi offerti dalla ricorsione sono dati dalla leggibilità del codice e dalla sua agevolezza, per contro, l’utilizzo della ricorsione incrementa la quantità di risorse richieste alla macchina indi per cui può appesantire i processi.

La funzione main

Abbiamo finora utilizzato la funzione main senza preoccuparci troppo del suo significato che tuttavia può risultare molto interessante qualora desiderassimo che i nostri programmi abbiamo un utilità inter-operativa. Dobbiamo innanzitutto dire che la funzione main é invocata nel momento in cui il programma é avviato e a questa possono essere passati dei parametri da riga di comando. La sintassi osservata é la seguente:

int main(int argc, char *argv[]) { /…/ }

Qualora volessimo eseguire un programma passando a questo degli argomenti da riga di comando, sarà possibile fornire alla funzione main qualunque argomento, agendo sull’argomento argc (arguments counter, il numero di argomenti che stiamo passando) e *argv ossia un vettore contenente i parametri passati sotto forma di stringa. Alla posizione 0 di questo array avremo il nome del programma e dalla 1 in avanti gli argomenti passati. Esempio:

iMac:~ user$ chmod 755 index.html

In questo esempio possiamo osservare il lancio da terminale di un comando con il quale si modificano i permessi di un file, dove “chmod” é il nome del programma, mentre “755” e “index.html” sono gli argomenti, contenuti rispettivamente in argv[1] e argv[2].

Le strutture di controllo

Elemento comune di tutti i linguaggi di programmazione, le istruzioni condizionali consentono di eseguire un blocco di codice al determinarsi di una determinata condizione. Sono tipicamente composte dalla presenza di keyword specifiche seguite da una coppia di parentesi tonde, nelle quali é posta la condizione da verificare. Qualora questa sia verificata, il blocco condizionale viene eseguito.

L’istruzione condizionale If

La istruzione condizionale if esegue un determinato blocco di codice solo se la condizione contenuta tra parentesi é verificata (true).

Sintassi:

if (espression) {statement};

Istruzione condizionale if/else

Funzionalmente molto simile ad if, alterna alla condizione true un codice da eseguire in caso che la condizione sia false.

Sintassi:

if (expression) {
    statement_true; //Eseguito se la condizione tra parentesi é true
}
else {
    statement_false; //Eseguito se la condizione tra parentesi é false
};

E’ altresì possibile utilizzare più istruzioni if annidate, e, nel tal caso é bene utilizzare le parentesi graffe { } senza parsimonia in quanto il compilatore associa l’istruzione else all’istruzione if più vicina, producendo potenziali effetti indesiderati. Esempio:

#include <stdio.h>
#include <stdlib.h>
int main (void) {
int a = 3;
if (a >= 10) {
    printf(“a >= 10/n”);
}
else {
    if (a >=5 ) {
        printf(“a >= 5 e a < 10\n”);
    }
    else {
        if (a>=0) {
           printf(“a>= 0 e a<5\n”);
        }
    }
}
return (EXIT_SUCCESS);
}

Istruzione di selezione multipla switch

Similmente a if/else, il costrutto switch esegue le istruzioni di un blocco di codice se la condizione espressa fra parentesi tonde é verificata. Sebbene possa sembrare simile a If/else, switch si utilizza per corrispondenze esatte. Gli operandi della condizione da verificare devono essere di tipo intero. (no float e double)

Sintassi:

switch (espressione intera) {
    case costante_intera_1:
        statement_1;
    break;
    case costante_intera_2:
        statement_2;
    break;
    case costante_intera_3:
        statement_3;
    break;
    default:
        statement_default;
    break;
}

Per scrivere un’istruzione di selezione multipla si procede come segue:

  1. Si scrive la keyword switch seguita da parentesi tonde con la condizione da valutare.
  2. Si scrivono le keyword case che indicano le costanti, seguite eventualmente dalla keyword break, che esce dal costrutto condizionale.
  3. Se opportuno si scrive la keyword default che indica le operazioni da eseguire qualora le condizioni precedenti non siano verificate.

I cicli iterativi

Il ciclo for

Il costrutto iterativo for consente di ripetere un blocco di istruzioni fintanto che la condizione posta tra parentesi é verificata. Sono largamente utilizzati nelle array, allo scopo di operare in modo iterativo sui dati contenuti:

Sintassi:

for (dichiarazione_variabile; espressione_1; espressione_2) {
    statement;
}

Per l’utilizzo di una struttura di tipo for, si inserisce la relativa keyword seguita da una coppia di parentesi tonde, all’interno delle quali generalmente viene posto un contatore, che ad ogni incremento del conteggio, esegue lo statement.

  1. Si dichiara ed inizializza una o più variabili, che viene verificata prima dell’espressione di controllo;
  2. Nell’espressione_1 viene effettuato il controllo della variabile inizializzata
  3. L’espressione_3 indica l’espressione da valutare dopo l’esecuzione dello statement.

Si parla di ciclo infinito quando la condizione espressa tra parentesi non raggiunge mai lo stato di false.

Il ciclo while

Il costrutto di iterazione while esegue lo stesso blocco di codice fintanto che una determinata espressione é vera.

while (condizione) {
    statement
}

Il ciclo do/while

Analogamente a quanto avviene con while, il ciclo do/while esegue un blocco di codice fintanto che l’espressione di di controllo risulta vera, ma, a differenza di while, questa verifica viene fatta successivamente all’esecuzione del blocco di codice, indi per cui il suddetto codice sarà eseguito almeno una volta.

Istruzioni di salto

L’istruzione break

L’istruzione break consente di interrompere l’esecuzione di un codice contenuto in un ciclo while, do/while o for uscendo dal ciclo. Può anche essere usato in un costrutto switch, all’interno di un case.

for (int a = 0; a < 10; a++){
    if (a == 5){
        break;
    }
    printf(“%d”, a);
}

a = 1 2 3 4;

L’istruzione continue

L’istruzione continue permette di saltare tutte le istruzioni poste dopo di esso, fino alla fine del ciclo, senza tuttavia determinare l’uscita dal ciclo stesso. Può essere utilizzata in qualunque ciclo iterativo (for, while, do/while). Nel caso di un ciclo while o do/while, la keyword continue salta alla valutazione dell’espressione di controllo, mentre nel caso di un ciclo for, salta alla terza espressione di controllo.

for (int a = 0; a < 10; a++){
    if (a == 5){
        continue;
    }
    printf(“%d”, a);
}

a = 1 2 3 4 6 7 8 9

L’istruzione goto

L’istruzione goto permette di saltare l’esecuzione verso un punto di codice contrassegnato da una label contenuta nella stessa funzione.

int a = 1;
goto label;

label:
    a = 10;
printf(“a = %d”, a);
a = 10;

In linea generale il suo utilizzo é sconsigliato poiché produce il fenomeno dello “spaghetti coding” ossi un codice disordinato e difficilmente gestibile. Può tuttavia essere utile nel caso di voglia uscire da una serie di cicli molto annidati o da un istruzione switch.

L’istruzione return

L’istruzione return permette di terminare l’esecuzione di una funzione trasferendo il processo alla funzione chiamante. Può anche ritornare un valore dello stesso tipo definito in sede di dichiarazione.

Operatori

Un operatore è un istruzione che agisce sui dei dati, detti operandi, allo scopo di ottenere un risultato.

Quando in un’istruzione si incontrano diversi operatori, l’ordine di esecuzione é tipicamente da sinistra verso destra, sebbene vi siano anche operatori che agiscono nel modo opposto. L’apposizione di parentesi tonde ( ) attribuisce all’operatore ivi contenuto priorità d’esecuzione rispetto ad altri operatori contenuti nella stessa espressione. In caso di doppia parentesi ( 1 + 1 + ( 1 – 1)) la priorità viene assegnata alle parentesi più interne. Ciò premesso, vi sono casi in cui gli operatori non obbediscono a criteri logici di precedenza, ma tale precedenza é stabilita in fase di implementazione sulla base di criteri di efficienza, anche sulla base dell’hardware utilizzato.

In generale valgono le stesse precedenze valide in matematica, in base alle quali moltiplicazioni e divisioni hanno la precedenza su addizione e sottrazione.

Operatore di assegnamento ( o assegnazione)

Evitiamo esercizi dialettici squisitamente e meramente definitori: l’operatore di assegnamento = viene tipicamente utilizzato per assegnare un valore ad una variabile.

Operatori aritmetici

Gli operatori aritmetici sono quelli tipici dell’aritmetica ( + – * / ) ed operano allo stesso identico modo, ma si aggiunge l’operatore modulo ( % ) che serve restituisce il resto di una divisione, ampiamente utilizzato per valutare se un numero é pari o dispari.

Operatori unari

Vengono tipicamente utilizzati per restituire lo stesso valore di un operando senza alternarne il segno, o alterandolo.

Operatori unari di incremento o decremento

Gli operatori unari di incremento (++) o decremento (–) sono utilizzati per sottrarre 1 all’operando. Trovano largo impiego nei contatori. Questi operatori possono essere prefissi (preincremento) o postfissi (postincremento). Nel primo caso l’operazione di incremento avviene prima della valutazione della variabile, nel secondo avviene dopo. E’ bene valutare il loro utilizzo ed il loro inserimento in modo ragionato perché quando situati nella stessa espressione, possono dare luogo ad anomalie date dall’incerta priorità di valutazione della variabile stessa.

Operatori relazionali

Sono rappresentato dai segni maggiore o minore cui si aggiunge il segno uguale, così come avviene in matematica. Il risultato dell’operazione può essere true o false. Così se a > b il risultato sarà true, in caso contrario sarà false. Opportunamente abbinati al simbolo ( = ), producono un’operazione di confronto che si estende all’uguaglianza tra gli operandi.

Operatori di uguaglianza

A differenza degli operatori di assegnamento, gli operatori di uguaglianza si rappresentano con due simboli (==). E’ un errore comune che ne venga indicato uno solo. Lo scopo di questo operatore è effettuare il confronto tra due operandi e restituire true in caso di uguaglianza.

Operatore di disuguaglianza

Molto utilizzato, al pari del precedente ha un funzionamento diametralmente opposto, restituisce true in caso di disuguaglianza.

Operatori Logici

Operano con le logiche tipiche dell’algebra booleana. Sono definite anche “porte logiche” e sono: OR ( | |, somma), AND (&&, moltiplicazione), NOT ( ! , negazione, inversione). Il risultato di queste operazioni é tipicamente definito attraverso una tabella della verità:

Porta AND &&

1 ° operando2° operando&& AND
FalseFalseFalse
FalseTrueFalse
TrueFalseFalse
TrueTrueTrue

NB: l’espressione é true solo se entrambi gli operandi sono true.

Porta OR | |

1 ° operando2° operando| | OR
FalseFalseFalse
FalseTrueTrue
TrueFalseTrue
TrueTrueTrue

NB: l’espressione é true se anche solo uno degli operandi è true.

Porta NOT !

operando! NOT
FalseTrue
TrueFalse

NB: restituisce l’inverso di ciò che riceve.

Operatori Condizionali

Gli operatori condizionali sono composti da tre operando e sono rappresentati dalla struttura:

Operando_1 ? Operando_2 : Operando_3;

Se il primo operando é true, valuta il secondo, se é false, valuta il terzo.

Operatori bit per bit

Gli operatori bit per bit svolgono le medesime operazioni degli operatori sopra elencati, ma operano a basso livello, sui singoli bit memorizzati in memoria. Il loro utilizzo può rivelarsi molto utile quando si deve andare ad intervenire su un valore in memoria in modo molto selettivo.

~Operatore di complemento ad uno
&Operatore and bit per bit
|Operatore OR
^Operatore XOR
<<Scorrimento a sinistra bit per bit
>>Scorrimento a destra bit per bit

Tabella della verità

AB~AA&BA|BA^B
001000
100011
011011
110110

Operatore di complemento a uno

Produce la negazione (l’inverso) del suo operando a livello di bit per bit.

Operatore AND bit per bit

L’operatore AND bit per bit si identifica con il simbolo & e svolge la medesima operazione dell’operatore AND, ma bit per bit, il che produce un risultato differente da quest’ultimo. I bit relativi all’operatore in memoria vengono valutati uno per uno e il risultato é prodotto booleano di questa comparazione.

Operatore OR bit per bit

L’operatore OR per bit inclusivo, si identifica con il simbolo | e svolge un funzione OR bit per bit. Il risultato è la somma booleana degli operatori, cioè restituisce true se almeno uno dei suoi operando è true.

Operatore XOR bit per bit

Simile al precedente, ma restituisce true solo nel caso che uno solo dei suoi operandi sia true.

Operatore di scorrimento a sinistra per per bit

L’operatore di scorrimento a sinistra bit per bit, scorre a sinistra i bit dell’operando di sinistra di tante posizioni quante quelle indicate dall’operando di destra. I valori introdotti da destra saranno 0. Se il primo operando é di segno negativo il risultato non sarà definito.

Operatore di scorrimento a destra per per bit

L’operatore di scorrimento a destra bit per bit, scorre a destra i bit dell’operando di sinistra di tante posizioni quante quelle indicate dall’operando di destra. I valori introdotti da destra saranno 0. Se il primo operando é di segno negativo il risultato non sarà definito. Il comportamento potrà dipendere dall’implementazione.

L’operatore virgola

In alcuni casi si tratta di un comune separatore, in altri invece agisce come un combinatore di espressioni, garantendo che i sequence point abbiano avuto effetto (che le operazioni siano state effettivamente eseguite). Sono utilizzati spesso nei cicli for per aggiornare 2 o più variabili.

Operatori di assegnamento composti

Aggiungendo il simbolo ( = ) ad uno dei precedenti operatori (es: &=) si svolge contestualmente all’operazione prevista anche l’assegnamento del valore. Occorre valutare che la priorità con cui viene eseguita è subordinata ad altre operazioni, come ad esempio quelle di tipo aritmetico, per cui può tornare utile servirsi della seguente tabella:

Tabella di precedenza

PrioritàOperatoreNomeAssociazione
1[]ArraySx → Dx
1()Funzione o raggruppamentoSx → Dx
1.Accesso membro strutturaSx → Dx
1->Accesso membro struttura puntatoreSx → Dx
1++Incremento PostfissoSx → Dx
1Decremento PostfissoSx → Dx
1(type)(list)Letterale compostoSx → Dx
2++Incremento prefissoDx → Sx
2Decremento prefissoDx → Sx
2+Più unarioDx → Sx
2Meno unarioDx → Sx
2&IndirizzoDx → Sx
2*DeriferimentoDx → Sx
2~Complemento a 1Dx → Sx
2!NOTDx → Sx
2(type)CastDx → Sx
2sizeofDimensione di un operandoDx → Sx
2_AlignofAllineamento di un operandoDx → Sx
3*MoltiplicazioneSx → Dx
3/DivisioneSx → Dx
3%ModuloSx → Dx
4+AddizioneSx → Dx
4SottrazioneSx → Dx
5<<Scorrimento a sinistra bit per bitSx → Dx
5>>Scorrimento a destra bit per bitSx → Dx
6<Minore diSx → Dx
6<=Minore o uguale aSx → Dx
6>Maggiore diSx → Dx
6>=Maggiore o uguale aSx → Dx
7==Uguale aSx → Dx
7!=Diverso daSx → Dx
8&And bit per bitSx → Dx
9^OR bit per bit esclusivoSx → Dx
10|OR bit per bit inclusivoSx → Dx
11&&ANDSx → Dx
12||ORSx → Dx
13?:Operatore condizionaleDx → Sx
14=Assegnamento sempliceDx → Sx
14+= -= *= /=Operatori di assegnamento compostiDx → Sx
14%= |= ^=Operatori di assegnamento compostiDx → Sx
14<<= >>=Scorrimento e assegnamentoDx → Sx
15,VirgolaSx → Dx

Array di lunghezza variabile (VLA)

Abbiamo considerato finora array di dimensione fissa, sebbene un array possa contenere un numero variabile di valori e per i problemi già considerati, può risultare indubbiamente più utile conferire all’array una dimensione congrua al numero di valori da contenere. Parliamo dunque di VLA (variable lenght array), array a lunghezza variabile che assumono la dimensione derivanti da espressioni che non forniscono necessariamente un valore costante.

…
// Sommare resistenze in parallelo
int n_resistenze;
printf(“Inserisci il numero di resistenze in parallelo che vuoi sommare: “);
scanf(“%d”, &n_resistenze);

//Utilizzo della variabile n_resistenze per dimensionare array
int resistenze[n_resistenze];
…

E’ bene ricordare però che in questo caso:

  • l’array non può essere inizializzata contestualmente alla dichiarazione;
  • non possono contenere lo specificatore di classe static o extern.
  • Possono essere utilizzati solo in funzioni ,anche come parametri, in qualsiasi blocco di codice o come parametri nei prototipi di funzioni.

Array Costanti

E’ anche possibile utilizzare il qualificatore const per dichiarare gli elementi che dopo l’inizializzazione non possono subire modifiche. Questo comporta che l’array sia in sola lettura.

Array Tridimensionali

Un array tridimensionale é una struttura di dati composta un insieme di variabili. Al pari della matrice può concettualmente rappresentare una tabella, dove terza dimensione rappresenta le pagine della stessa.

L’utilizzo dell’ array tridimensionale é molto simile a quanto già visto in precedenza, con la differenza che viene aggiunta una ulteriore coppia di parentesi quadre a rappresentare la terza dimensione.

data_type identificatore[pagina][riga][colonna];

Inizializzazione

Anche l’array tridimensionale può essere inizializzata contestualmente alla sua dichiarazione:

//Dichiarazione ed inizializzazione di array tridimensionale
data_type identificatore[numero_righe][numero_colonne] = {
    {value_0, value_1,value_2,…,value_n},
    {value_0, value_1,value_2,…,value_n}
, /* NB: inserire virgola */
    {value_0, value_1,value_2,…,value_n},
    {value_0, value_1,value_2,…,value_n}
};

Accesso all’array (subscripting)

//Scrittura
identificatore[indice_pagina][indice_riga][indice_colonna] = value;
//Lettura
altra_variabile = identificatore[indice_pagina][indice_riga][indice_colonna];

Le regole di accesso ad un array tridimensionale sono sostanzialmente identiche a quelle delle array bidimensionali, a cui si aggiunge una ulteriore coppia di parentesi. Anche i clicli iterativi comportano la sola aggiunta di un ulteriore ciclo contenuto nei due già previsti.