Funzioni di Confronto per i Numeri in Virgola Mobile in Linguaggio C
- I numeri in virgola mobile devono essere confrontati utilizzando l'errore relativo per evitare risultati inaspettati dovuti a errori di arrotondamento e precisione limitata.
- La libreria standard del linguaggio C fornisce macro specifiche per confrontare i numeri in virgola mobile in modo sicuro, tenendo conto di casi speciali come NaN e infiniti.
- Le funzioni
fmax,fminefdimconsentono di calcolare rispettivamente il massimo, il minimo e la differenza positiva tra due numeri in virgola mobile, gestendo correttamente i casi in cui uno degli argomenti è NaN.
Il problema di confrontare i numeri in virgola mobile
Quando si lavora con i numeri interi, il confronto tra due valori è un'operazione semplice e diretta. Basta adoperare gli operatori relazionali standard come ==, !=, <, >, <= e >=.
Sebbene tali operatori funzionino anche con i numeri in virgola mobile (tipi float, double e long double), il confronto diretto tra questi tipi di dati può portare a risultati inaspettati a causa della natura approssimativa della rappresentazione dei numeri in virgola mobile.
Ad esempio, a causa di errori di arrotondamento e precisione limitata, due numeri che dovrebbero essere uguali potrebbero non esserlo esattamente quando rappresentati in virgola mobile. Questo può causare problemi quando si scrive un codice di questo tipo:
double x;
double y;
// Supponiamo che x e y vengano calcolati in qualche modo
if (x == y) {
// Fai qualcosa se x è uguale a y
} else {
// Fai qualcos'altro se x non è uguale a y
}
Il problema, in questo codice, è che x e y potrebbero non essere esattamente uguali a causa di piccole differenze dovute alla rappresentazione in virgola mobile, anche se matematicamente dovrebbero esserlo. Quindi il confronto di uguaglianza x == y potrebbe restituire false anche quando ci si aspetta che sia true.
Per questo motivo, il modo corretto di confrontare i numeri in virgola mobile è utilizzare l'errore relativo.
Matematicamente, l'errore relativo tra due numeri a e b è definito come:
Questo valore rappresenta il rapporto tra l'errore assoluto (ossia la differenza assoluta tra a e b, b. In soldoni, ci dice in percentuale quanto a si discosta da b.
Per confrontare due numeri in virgola mobile x e y, si può calcolare l'errore relativo tra di essi e verificare se questo errore è inferiore a una soglia di tolleranza predefinita, spesso chiamata "epsilon". Se l'errore relativo è inferiore a questa soglia, si considera che i due numeri siano "praticamente uguali". Quindi il codice di sopra può essere riscritto in questo modo:
double x;
double y;
double epsilon = 1e-10; // Soglia di tolleranza
if (fabs(x - y) / fabs(y) < epsilon) {
// Fai qualcosa se x è praticamente uguale a y
} else {
// Fai qualcos'altro se x non è praticamente uguale a y
}
Per rendersi conto di questo problema, si può considerare il seguente esempio:
#include <stdio.h>
int main() {
double a = 0.1 + 0.2;
double b = 0.3;
if (a == b) {
printf("a e b sono uguali\n");
} else {
printf("a e b non sono uguali\n");
}
return 0;
}
In questo esempio, stiamo effettuando la somma:
dopodiché, confrontiamo a con b, che è uguale a 0.3. Nonostante matematicamente a e b dovrebbero essere uguali, il confronto diretto a == b restituirà false a causa della rappresentazione approssimativa dei numeri in virgola mobile.
Infatti, se si esegue questo codice, l'output sarà:
a e b non sono uguali
La motivazione di questo comportamento risiede nel fatto che 0.1, 0.2 e 0.3 non possono essere rappresentati esattamente in binario, portando a piccole imprecisioni nei calcoli.
Quindi, per confrontare correttamente a e b, si dovrebbe utilizzare l'errore relativo come descritto in precedenza:
#include <stdio.h>
#include <math.h>
int main() {
double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 1e-10; // Soglia di tolleranza
if (fabs(a - b) / fabs(b) < epsilon) {
printf("a e b sono praticamente uguali\n");
} else {
printf("a e b non sono praticamente uguali\n");
}
return 0;
}
I numeri in virgola mobile devono sempre essere confrontati utilizzando l'errore relativo
Poiché i numeri in virgola mobile possono introdurre errori di arrotondamento e precisione limitata, è fondamentale confrontarli utilizzando l'errore relativo piuttosto che gli operatori relazionali standard. Questo approccio aiuta a evitare risultati inaspettati e garantisce che i confronti siano più affidabili:
dove
Fortunatamente, la libreria standard del linguaggio C fornisce delle funzioni specifiche per confrontare i numeri in virgola mobile in modo sicuro e affidabile, tenendo conto delle peculiarità di questi tipi di dati. Queste funzioni sono definite nell'header <math.h>.
Vediamole in dettaglio.
Macro di confronto per i numeri in virgola mobile
La libreria standard del linguaggio C definisce diverse macro per confrontare i numeri in virgola mobile in modo sicuro. Queste macro sono progettate per gestire le peculiarità dei numeri in virgola mobile, come NaN (Not a Number) e infiniti.
Tali macro parametriche sono:
| Macro | Descrizione |
|---|---|
int isgreater(real x, real y); |
Restituisce un valore diverso da zero se x è maggiore di y. |
int isgreaterequal(real x, real y); |
Restituisce un valore diverso da zero se x è maggiore o uguale a y. |
int isless(real x, real y); |
Restituisce un valore diverso da zero se x è minore di y. |
int islessequal(real x, real y); |
Restituisce un valore diverso da zero se x è minore o uguale a y. |
int islessgreater(real x, real y); |
Restituisce un valore diverso da zero se x è minore o maggiore di y (ossia, se x e y non sono uguali). |
int isunordered(real x, real y); |
Restituisce un valore diverso da zero se almeno uno tra x e y è NaN (Not a Number). |
La prima osservazione da fare riguarda il fatto che tali macro accettano come argomenti dei valori di qualunque tipo in virgola mobile: float, double o long double. Il tipo specifico viene dedotto automaticamente dal compilatore in base ai tipi degli argomenti passati. Il tipo di ritorno è sempre int ma il valore restituito è 0 (falso) o diverso da 0 (vero), a seconda del risultato del confronto.
Le macro isgreater, isgreaterequal, isless e islessequal svolgono, sostanzialmente, le stesse operazioni degli operatori relazionali >, >=, < e <=, ma in modo sicuro per i numeri in virgola mobile. Esse tengono conto di casi speciali come NaN e infiniti, garantendo risultati affidabili.
La macro islessgreater è utile per verificare se due numeri in virgola mobile sono diversi tra loro e ha due peculiarità:
- Gestisce correttamente i casi in cui uno o entrambi gli argomenti sono NaN, restituendo
0(falso) in tali situazioni. - Utilizza come
epsilonuna soglia di tolleranza predefinita:DBL_EPSILON,FLT_EPSILONoLDBL_EPSILON, a seconda del tipo di dato in virgola mobile utilizzato.
L'ultima macro, isunordered, è particolarmente utile per verificare se almeno uno dei due numeri in virgola mobile è NaN. Questo è importante perché qualsiasi confronto con NaN restituisce sempre false, quindi questa macro consente di gestire correttamente tali situazioni.
Funzioni di Minimo, Massimo e Differenza Positiva
Oltre alle macro di confronto, la libreria standard del linguaggio C fornisce anche delle funzioni per calcolare il minimo, il massimo e la differenza positiva tra due numeri in virgola mobile. Queste funzioni sono anch'esse definite nell'header <math.h>.
Le funzioni sono:
| Funzione | Descrizione |
|---|---|
double fmax(double x, double y); |
Restituisce il massimo tra x e y. Se uno dei due è NaN, restituisce l'altro valore. |
float fmaxf(float x, float y); |
versione per float di fmax. |
long double fmaxl(long double x, long double y); |
versione per long double di fmax. |
double fmin(double x, double y); |
Restituisce il minimo tra x e y. Se uno dei due è NaN, restituisce l'altro valore. |
float fminf(float x, float y); |
versione per float di fmin. |
long double fminl(long double x, long double y); |
versione per long double di fmin. |
double fdim(double x, double y); |
Restituisce la differenza positiva tra x e y, ossia x - y se x > y, altrimenti 0. Se uno dei due è NaN, restituisce NaN. |
float fdimf(float x, float y); |
versione per float di fdim. |
long double fdiml(long double x, long double y); |
versione per long double di fdim. |
Alcune osservazioni su queste funzioni:
- Ogni funzione ha tre versioni, una per ciascun tipo di dato in virgola mobile:
float,doubleelong double. Il suffissofindica la versione perfloat, mentre il suffissolindica la versione perlong double. - Le funzioni
fmaxefmingestiscono correttamente i casi in cui uno degli argomenti è NaN, restituendo l'altro valore. Questo è utile per evitare risultati imprevisti quando si lavora con numeri in virgola mobile che potrebbero essere NaN. -
La funzione
fdimcalcola la differenza positiva tra due numeri in virgola mobile, restituendo0se il primo numero è minore o uguale al secondo. Anche in questo caso, se uno degli argomenti è NaN, la funzione restituisce NaN. Matematicamente,fdim(x, y)può essere espressa come: