Appunti per Scuola e Università
humanisticheUmanistiche
Appunti e tesine di tutte le materie per gli studenti delle scuole medie riguardanti le materie umanistiche: dall'italiano alla storia riguardanti le materie umanistiche: dall'italiano alla storia 
sceintificheScientifiche
Appunti, analisi, compresione per le scuole medie suddivisi per materie scientifiche, per ognuna troverai appunti, dispense, esercitazioni, tesi e riassunti in download.
tecnicheTecniche
Gli appunti, le tesine e riassunti di tecnica amministrativa, ingegneria tecnico, costruzione. Tutti gli appunti di AppuntiMania.com gratis!
Appunti
informatica
CComputerDatabaseInternetJava
Linux unixReti


AppuntiMania.com » Informatica » Appunti di c » Linguaggio c e portabilitÀ

Linguaggio c e portabilitÀ




Visite: 1822Gradito:apreciate 4-stela [ Medio appunti ]
Leggi anche appunti:

Problemi di mutua esclusione nel modello a memoria comune


Problemi di mutua esclusione nel modello a memoria comune VI) Scrivere una

L'i/o e la gestione dei file


L'I/O e la gestione dei file Per Input/Output (I/O) si intende l'insieme delle

Due file sono il medesimo file?


Due file sono il medesimo file? La domanda è formulata in modo fuorviante. Il
immagine di categoria

Scarica gratis Linguaggio c e portabilitÀ

Linguaggio C e portabilità

Una caratteristica di rilievo del linguaggio C consiste nella portabilità. In generale, si dice portabile un linguaggio che consente di scrivere programmi in grado di funzionare correttamente su piattaforme hardware diverse e sotto differenti sistemi operativi, richiedendo semplicemente la ricompilazione dei sorgenti nel nuovo ambiente (e dunque, implicitamente, con una differente implementazione del compilatore).

Tutto ciò è reso possibile, nel caso del C, dalla standardizzazione del medesimo operata dall'ANSI e dal fatto che si tratta di un linguaggio basato in massima parte su routine (funzioni) implementate in librerie esterne al compilatore, e dunque sempre disponibili con caratteristiche coerenti a quelle dei differenti ambienti in cui il compilatore deve operare.

Tali caratteristiche non sono però, da sole, sufficienti a rendere portabile qualsiasi programma C: molto dipende dalla cura spesa nella realizzazione del medesimo nonché, in ultima analisi, dagli scopi che il programmatore si prefigge. In molti casi è impossibile, o sostanzialmente inutile, eliminare quelle caratteristiche del programma che lo rendono più o meno dipendente dallo hardware, dal compilatore e dal sistema operativo, e ciò soprattutto quando si desideri controllare e sfruttare a fondo le prestazioni dell'ambiente in cui il programma deve operare

Dipendenze dallo hardware

Un programma può risultare hardware‑dipendente per molte cause, dalle più scontate a quelle di più difficile individuazione.

Esempio di banalità mostruosa: un programma che assuma aprioristicamente la presenza di un disco rigido non è portabile (fatta salva la possibilità di modificare il sorgente) su macchine che non ne siano dotate.

E ancora: l'accesso diretto al buffer video è un ottimo metodo per rendere molto efficienti le operazioni di output, ma comporta la necessità di conoscerne l'indirizzo fisico, che può variare a seconda dello hardware installato.

Più sottili considerazioni si possono fare sulle relazioni intercorrenti tra i tipi di dati gestiti dal programma e il microprocessore installato sulla macchina. Il tipo di dato forse più 'gettonato' nei programmi C è l'integer, e proprio l'integer può essere fonte di fastidiosi grattacapi. Il C consente tre modi di dichiarare integer una variabile

short s;

long l;

int i;

Va osservato che non è possibile specificarne la dimensione in bit; il compilatore garantisce soltanto che la variabile dichiarata short integer ha dimensione minore o uguale a quella long, mentre la variabile dichiarata semplicemente int viene gestita in modo da ottimizzarne la manipolazione da parte del processore. Se la compilazione avviene su una macchina a 16 bit essa risulta equivalente a quella short, ma la compilazione su macchine a 32 bit la rende equivalente a quella long. Non è difficile immaginare i problemi che potrebbero manifestarsi portando ad una macchina a 16 bit un programma compilato su una a 32 bit. E' dunque opportuno utilizzare (quando le dimensioni in bit o byte delle variabili assumano rilevanza) l'operatore sizeof() (vedere pag.  ) e le costanti manifeste definite in base allo standard ANSI negli header file LIMITS.H e FLOAT.H

In LIMITS.H troviamo, ad esempio, CHAR_BIT (numero di bit in un char INT_MIN (minimo valore per un int INT_MAX (massimo valore per un int UINT_MAX (massimo valore per un unsigned int). A queste si aggiungono minimi e massimi per gli altri tipi: SCHAR_MIN SCHAR_MAX e UCHAR_MAX per signed char e unsigned char CHAR_MIN e CHAR_MAX per i char SHRT_MIN SHRT_MAX e USHRT_MAX per gli short LONG_MIN LONG_MAX e ULONG_MAX per i long

In STDDEF.H è definito un gruppo di costanti manifeste il cui simbolo è costituito da un prefisso indicante il tipo di dato (FLT_ per float DBL_ per double LDBL_ per long double) e da un suffisso al quale è associato il significato della costante medesima. Presentiamo un elenco dei soli suffissi (per brevità) precisando che l'elenco completo delle costanti manifeste si ottiene unendo ogni prefisso ad ogni suffisso (ad es.: FLT_DIG DBL_DIG LDBL_DIG; etc.):

Suffissi per le costanti manifeste definite in STDDEF.H

SUFFISSI

SIGNIFICATI

SUFFISSI

SIGNIFICATI

MAX

Massimo valore

MIN

Minimo valore positivo

MAX_10_EXP

Max.esponente decimale

MIN_10_EXP

Min.esponente decimale

MAX_EXP

Max.esponente binario

MIN_EXP

Min.esponente binario

DIG

N.cifre di precisione

MANT_DIG

N.di bit in mantissa

EPSILON

Min.valore di macchina

RADIX

Radice dell'esponente

Con riferimento ai tipi in virgola mobile, va ancora ricordato che lo standard ANSI ha definito il tipo long double, stabilendo che esso deve essere di dimensione maggiore o uguale al double, senza fissare altri vincoli all'implementazione. Sulle macchine 80x86 esso occupa 80 bit , ma su altre piattaforme hardware i compilatori possono gestirlo diversamente.

Le difficoltà non si limitano alla dimensione dei tipi di dato. Appare rilevante, ai nostri fini, anche il modo in cui il processore ne gestisce l'aritmetica. Esistono infatti processori basati sull'aritmetica in complemento a due (come quelli appartenenti alla famiglia Intel 80x86) ed altri basati sull'aritmetica in complemento a uno . Le differenze sono notevoli: questi ultimi, a differenza dei primi, gestiscono due rappresentazioni dello zero (una negativa e una positiva). Inoltre varia la rappresentazione binaria interna dei numeri negativi: ad esempio,  ad otto bit è  in complemento a due e  in complemento a uno

Infine, alcune considerazioni sul metodo utilizzato dal processore per ordinare i byte in memoria. Gli Intel 80x86 lavorano nella modalità cosiddetta backwords , cioè a parole rovesciate: ogni coppia di byte (detta word, o parola) è memorizzata in modo che al byte meno significativo corrisponda la locazione di memoria di indirizzo inferiore; quando un dato è formato da due parole (ad esempio un long a 32 bit), un analogo criterio è applicato ad ognuna di esse (la word meno significativa precede in memoria quella più significativa). La conseguenza è che i dati sono memorizzati, in sostanza, a rovescio. Altri processori si comportano in modo differente, e ciò può rendere problematico portare da una macchina all'altra programmi che assumano come scontata una certa modalità di memorizzazione dei byte

Dipendenze dai compilatori

Anche le differenze esistenti tra le molteplici implementazioni di compilatori hanno rilevanti riflessi sulla portabilità. E' del tutto palese che costrutti sintattici ammessi da un compilatore ma non rientranti negli standard (ancora una volta il punto di riferimento è lo standard ANSI) possono non essere ammessi da altri, con ovvie conseguenze. Un macroscopico esempio è rappresentato dallo inline assembly , cioè dalle istruzioni di linguaggio assembler direttamente inserite nel codice C.

Talvolta, problemi di portabilità possono sorgere tra successive release della medesima implementazione di compilatore: la famigerata opzione ‑k‑ insegna (pag.  e seguenti).

Il numero dei caratteri significativi nei simboli (nomi di variabili, di funzioni, di etichette) non è fissato da alcuno standard. Ad esempio, alcuni compilatori sono in grado di distinguere tra loro nomi (ipotetici) di funzioni quali ConvertToUpper() e ConvertToLower(), mentre altri non lo sono. Inoltre, nonostante il C sia un linguaggio case sensitive (che distingue, cioè, le lettere minuscole da quelle maiuscole), e dunque lo siano tutti i compilatori, possono non esserlo alcuni linker (quantomeno per default): pericoloso, perciò, includere nei sorgenti simboli che differiscono tra loro solo per maiuscole e minuscole (es.: DOSversion e DosVersion

Anche in tema di compilatori vi è spazio, comunque, per considerazioni più particolari. Il C riconosce gli operatori unari di incremento e decremento (vedere pag.  ), che possono essere anteposti o, in alternativa, posposti alla variabile che si intende incrementare (decrementare): vale la regola generale che quando esso precede la variabile, questa è incrementata (decrementata) prima di valutare l'espressione di cui fa parte; quando esso la segue, l'incremento (il decremento) avviene dopo la valutazione dell'intera espressione. Consideriamo, però, le due seguenti chiamate a funzione:

funz(++a);

funz(a++);

Contrariamente a quanto si potrebbe supporre, non è garantito che nella prima la variabile a sia incrementata prima di passarne il valore alla funzione e, viceversa, nella seconda essa sia incrementata successivamente. Lo standard del linguaggio non prevede, al riguardo, vincoli particolari per l'implementazione, coerentemente con la generale libertà che ogni compilatore C può concedersi nella valutazione delle espressioni

A tutti i programmatori C è noto, inoltre, il problema degli effetti collaterali (side‑effect ) che possono verificarsi quando una variabile in autoincremento (o decremento) costituisce il parametro di una macro, piuttosto che di una funzione. Al riguardo precisiamo che, a seconda dell'implementazione del compilatore, una funzione C può benissimo essere in realtà una macro: è meglio dunque documentarsi a fondo , al minimo curiosando nei file .H

Ancora sulle funzioni: le implementazioni recenti di compilatori ammettono (ed è lo stile di programmazione consigliato) che nelle dichiarazioni di funzione siano specificati tipo e nome dei parametri formali:

int funzione(int *ptr,long parm);

Ciò consente controlli precisi sul tipo dei parametri attuali e limita le possibilità di errore. I compilatori obsoleti individuano in tali dichiarazioni un errore di sintassi e precisano che i parametri possono essere specificati solo nelle definizioni di funzione (non nelle dichiarazioni).

E' ora il momento di affrontare i puntatori . In C è del tutto lecito e normale calcolare la differenza tra due puntatori: ci si potrebbe aspettare che il risultato sia un integer (o unsigned integer). In realtà esso può essere un long: dipende, ancora una volta, dall'implementazione del compilatore. Lo standard ANSI definisce il tipo ptrdiff_t , il quale garantisce la coerenza della dimensione delle variabili ad esso appartenenti con il comportamento del compilatore nel calcolo di differenze tra puntatori.

Aggiungiamo, per rimanere in tema, che il puntatore nullo NULL non è necessariamente uno zero binario in tutte le implementazioni.

Per quanto riguarda i tipi di dato, occorre prestare attenzione al trattamento riservato dal compilatore ai char, i quali possono essere considerati signed o unsigned  char per default. Ignorare le convenzioni in base alle quali il compilatore utilizzato si comporta può essere fonte di bug inaspettati e molto difficili da scovare: ci si ricordi che se, per esempio, i char sono trattati come segnati, un'espressione del tipo (a < 0x90), dove a è dichiarata semplicemente char, è sempre vera; analogamente, se i char sono considerati privi di segno, (a < 0x00) è sempre e comunque falsa. Le costanti esadecimali , infatti, sono sempre considerate prive di segno. Altre 'stranezze' si verificano nel caso di assegnamento del valore (negativo) di una variabile char ad una variabile int, come risulta dal seguente esempio:

.

char c;

int i;

.

c = -1;

i = c;

.

Quanto vale i? Se il compilatore considera i char grandezze senza segno, allora i vale  ; in caso contrario assume il valore

Dipendenze dal sistema operativo

Il sistema operativo mette a disposizione del programmatore un insieme di servizi che possono costituire una comoda interfaccia tra il software applicativo e il ROM BIOS o lo hardware. Sistemi operativi progettati per fornire ambienti differenti sono (è ovvio) interinsecamente diversi; ciononostante è spesso possibile portare programmi dall'uno all'altro senza particolari difficoltà. E' il caso, ad esempio, di Unix e DOS, quando si conoscano entrambi i sistemi appena un poco in profondità e si rinunci ad ottimizzazioni a basso livello del codice. Al riguardo intendiamo ricordare solo alcune delle differenze più notevoli tra DOS e Unix, quale spunto per meditazioni più approfondite.

In primo luogo Unix è, contrariamente al DOS, un sistema multiuser/multitask; un programma scritto sotto DOS con 'ambizioni' di portabilità deve essere in grado di gestire la condivisione delle risorse con altri processi.

Un'altra differenza di rilievo consiste nell'assenza, in Unix, del concetto di volume (cioè di disco logico): pertanto un pathname , sotto Unix, non può includere un identificativo di drive. Vi sono poi sistemi operativi (CP/M , antesignano dello stesso DOS) che non gestiscono file systems gerarchici (in altre parole: non consentono l'uso delle directory). Inoltre la backslash ') che in DOS separa i nomi di directory e identifica la root è una slash (' ') in Unix, il quale considera il punto (' '), nei nomi di file, alla stregua di un carattere qualsiasi, mentre in DOS esso ha la funzione di separare nome ed estensione. In ambiente DOS, infine, i nomi di file possono contare un massimo di undici caratteri (otto per il nome e tre per l'estensione); Unix ammette fino a quattordici caratteri; OS/2 (se installato con HPFS ) fino a 256.

Unix gestisce le unità periferiche come device : tale caratteristica è stata in parte ripresa dal DOS ed il linguaggio C consente di sfruttarla proficuamente attraverso le funzioni basate su stream (pag.  ). Particolarmente interessanti sono gli stream standard, resi disponibili dal sistema, ed usati da funzioni e macro come printf() puts() gets(), etc.: si tratta di stdin (standard input), stdout (standard output), stderr (standard error), stdaux (standard auxiliary) e stdprn (standard printer). La portabilità tra Unix e DOS del codice che ne fa uso è quasi totale , ma vi sono sistemi operativi che gestiscono le periferiche in modo assai differente.

Approfondimenti circa problemi di portabilità tra Unix e DOS di sorgenti che implementano controllo e gestione di processi si trovano a pag. 142.

Se, da una parte, è prevedibile incontrare problemi di portabilità tra sistemi di differente concezione tecnica, dall'altra sarebbe un errore ritenere che lo scrivere codice portabile tra sistemi fortemente analoghi sia privo di ogni difficoltà: si pensi, ad esempio, alle differenze esistenti tra versioni successive del DOS. Il codice che utilizzi servizi DOS non presenti in tutte le versioni non può dirsi completamente portabile neppure nell'ambito del medesimo ambiente operativo. La realizzazione di codice portabile tra ogni release di DOS implica la rinuncia alle funzionalità introdotte via via con le nuove versioni : si tratta, con ogni probabilità, di un prezzo troppo alto e spetta pertanto al programmatore scegliere un opportuno compromesso tra il grado di portabilità da un lato e il livello di efficienza, unitamente al contenuto innovativo del codice, dall'altro . Le funzioni implementate dalle librerie standard dei recenti compilatori C si basano su servizi DOS disponibili a partire dalla versione 2.0 o 3.0.

Va aggiunto che esistono versioni di DOS modificate da produttori di hardware per migliorarne la compatibilità con le macchine da essi commercializzate; possono così riscontrarsi diversità non solo tra release successive, ma anche tra differenti 'marchi' nell'ambito della medesima release. Quasi sempre si tratta, in questi casi, di differenze nelle modalità con le quali il DOS interagisce con BIOS e hardware; pertanto difficilmente esse hanno reale influenza sulla portabilità del codice, eccetto i casi in cui questo incorpori o utilizzi servizi implementati a basso livello. Con un approccio forse un po' grossolano si può inoltre osservare che il DOS è costituito da un insieme di routine, ciascuna in grado di svolgere un compito piuttosto elementare (aprire un file, visualizzare un carattere). Alcune di esse formano l'insieme dei servizi resi disponibili da quella particolare release: sono, pertanto descritte nella documentazione tecnica e la loro permanenza in future versioni è garantita. Altre, realizzate quali routine di supporto alle precedenti, sono riservate ad uso 'interno' da parte del sistema operativo stesso: i manuali tecnici non vi fanno cenno e non è possibile contare sulla loro presenza nelle versioni future




 In effetti questo è proprio lo scopo, più o meno manifesto, di gran parte degli esempi presentati nel testo: dal loro esame appare evidente come spesso la portabilità sia stata sacrificata, a fronte di altri vantaggi.

 In tal modo esso è particolarmente adatto ad essere elaborato nei registri interni dei coprocessori matematici 80x07. Il float e il double occupano invece, ripettivamente, 32 e 64 bit.

 Quando si abbiano esigenze di portabilità è dunque preferibile evitare il ricorso diffuse prassi quali inizializzare a un char o int per utilizzarlo come maschera avente tutti i bit a 

 Vediamo un esempietto. Su una macchina equipaggiata con un Intel 80x86 si può validamente utilizzare il costrutto seguente:

union farptr ptr;

} point;

Esso consente di referenziare un puntatore far point.pointer) oppure la sua word segmento (point.ptr.segment), oppure ancora la word offset (point.ptr.offset) semplicemente utilizzando l'opportuno elemento della union: su macchine equipaggiate con il processore Z8000 tale costrutto maniene la propria validità sintattica, ma point.ptr.offset contiene il segmento del puntatore e, viceversa, point.ptr.segment ne contiene l'offset, in quanto lo Z8000 non utilizza la tecnica backwords.

 Ovviamente ogni compilatore C è tenuto al rispetto delle priorità algebriche; tuttavia la valutazione di un'espressione può procedere, a parità di priorità algebrica tra gli elementi valutati, indifferentemente da sinistra a destra o viceversa (per alcuni operatori non sono definiti vincoli di associatività: vedere pag.  ), a tutto vantaggio dell'efficienza complessiva del programma.

 Esempio, tratto dalla realtà. In tutte le implementazioni standard, toupper(char c) converte in maiuscolo, se minuscolo, il carattere contenuto in c. Se la libreria utilizzata implementa toupper() come funzione, la riga di codice:

a = toupper(++c);

può dare luogo alle ambiguità concernenti il momento in cui c è incrementata. Ma portando il sorgente ad un compilatore che implementi toupper() come macro:

#define toupper(c)      (islower(c) ? c - 'a' + 'A' : c)

la riga di codice viene espansa dal preprocessore nel modo seguente:

a = (islower(++c) ? ++c - 'a' + 'A' : ++c);

con le immaginabili conseguenze sui valori finali di c e di a

 In STDDEF.H

 High Performance File System.

 Dispositivi hardware pilotati da apposite interfaccia software (device driver: pag.  ), che comunicano con le periferiche attraverso un protocollo dedicato e con il sistema mediante flussi (stream) di dati; i programmi applicativi risultano così, in larga misura, indipendenti dal dispositivo fisico (stampante, disco, console) dal quale provengono (o al quale sono diretti) i dati.

 'Quasi' significa che comunque vi sono alcune differenze tra il comportamento dei due sistemi operativi in questione. Ad esempio, Unix consente di redirigere lo standard error, al contrario del DOS.

 Ogni versione di DOS affianca i nuovi servizi a quelli preesistenti, anche quando questi ultimi siano resi obsoleti dai primi: ne deriva una sostanziale compatibilità verso il basso.

 Inoltre è lecito ipotizzare che le nuove versioni del DOS abbiano, nel tempo, rimpiazzato quelle originariamente installate.

 Inoltre, qualora esse siano presenti in altre versioni del DOS, nulla assicura che il contenuto dei registri della CPU o la struttura dei dati gestiti rimangano invariati. E' un vero peccato: di solito si rivelano utilissime.

Scarica gratis Linguaggio c e portabilitÀ
Appunti su: appunti sulle caratteristiche del linguaggio in c,



Scarica 100% gratis e , tesine, riassunti



Registrati ora

Password dimenticata?
  • Appunti superiori
  • In questa sezione troverai sunti esame, dispense, appunti universitari, esercitazioni e tesi, suddivisi per le principali facoltà.
  • Università
  • Appunti, dispense, esercitazioni, riassunti direttamente dalla tua aula Universitaria
  • all'Informatica
  • Introduzione all'Informatica, Information and Comunication Tecnology, componenti del computer, software, hardware ...