【C 語言】NaN(非數)的意義與使用徹底解析|判斷方法與應用範例也介紹

1. 前言

C語言中 NaN 的意義與重要性

C語言中,處理浮點數時不可避免的就是「NaN(Not a Number)」的存在。這是一種特殊的數值,在日文中也被譯為「非數」,用來表示無法作為數值處理的結果。例如,除以零或計算負數的平方根等,數學上無法定義的運算結果會返回 NaN。

為什麼需要了解 NaN?

在程式設計中,如果未正確理解 NaN 的存在,可能會導致意想不到的錯誤。特別是在 C 語言中,與含 NaN 的變數進行比較或運算時,可能會出現預期之外的行為,因此必須深入了解其特性並妥善理。 此外,在進行資料處理或數值分析的程式中,將異常值或缺失值以 NaN 表示,能在保持資料正確性的同時持續處理。因此,學習 NaN 的意義與使用方式不僅僅是錯誤處理,更是與打造穩健軟體開發直接相關的重要知識。

本文的目的

本文將全面說明 C 語言中 NaN 的定義、產生方式、判定方法、實際應用案例,以及與 NaN 相關的注意事項。這些在處理浮點數時必備的知識,將以易於理解的方式編排,讓初學者到中級者皆能輕鬆學習。

2. NaN是什麼?

NaN(Not a Number)的基本定義

NaN是「Not a Number(非數值)」的縮寫,屬於數值型資料中包含的特殊值之一。顧名思義,它表示沒有數值意義的資料。這不是錯誤,而是一種明確表示「無法以數值處理的運算結果」的機制。 例如,以下的計算結果在數學上無法定義:
  • 0.0 / 0.0(零除)
  • sqrt(-1.0)(負值的平方根)
  • log(-5.0)(負值的對數)
在此情況下,使用 C 語言的標準函式庫時,會以 NaN 作為返回值,表示「此計算結果不具數值意義」。

IEEE 754 中 NaN 的定位

C 語言的浮點數(floatdouble)是依據 IEEE 754 這一國際標準設計的。此標準將 NaN 定義為一種特殊的位元模式,用以表示超出數值範圍的結果或無效的運算結果。 在 IEEE 754 中,NaN 大致分為以下兩種類型:
  • quiet NaN(qNaN):在大多數環境中常用的 NaN。運算時不會產生錯誤,會作為 NaN 處理。
  • signaling NaN(sNaN):本來用於觸發例外處理,但在 C 語言中多數實作並未支援。
在 C 語言中使用的 NaN 大多屬於 quiet NaN。

NaN 的傳播特性

NaN 若與其他值進行運算,結果基本上也會是 NaN。這基於「包含 NaN 的結果不可可信」的概念。
#include <stdio.h>
#include <math.h>

int main() {
    double a = 0.0 / 0.0;  // NaN
    double b = a + 100.0;  // 也是 NaN
    printf("%f
", b);     // 顯示結果: nan
    return 0;
}
如此一來,一旦 NaN 發生,會連鎖傳播,因此 NaN 的偵測與處理變得相當重要。

3. C語言中產生 NaN 的方法

<math.h> 產生 NaN 的方式

在 C 語言中,為了產生浮點數的特殊值 NaN,通常使用標準函式庫 <math.h> 中定義的函式。代表性的函式如下:
  • nan(const char *tagp)
  • nanf(const char *tagp)
  • nanl(const char *tagp)
這些函式分別回傳 doublefloatlong double 型別的 NaN 值。
#include <stdio.h>
#include <math.h>

int main() {
    double x = nan("");
    printf("%f
", x);  // 輸出: nan
    return 0;
}

tagp 參數的意義

傳遞給這些函式的 tagp 參數是用來指定附加在 NaN 上的標籤資訊,可能用於除錯或診斷用途。但實際行為與是否支援取決於實作(編譯器或函式庫),因此通常會指定空字串 ""
double y = nan("payload");
即使如此指定,輸出的 NaN 幾乎沒有差異,且標籤會明顯顯示的情況很少見。若重視可移植性,使用 "" 是較安全的做法。

除函式呼叫外產生 NaN 的方法

在某些情況下,也可以利用明確回傳 NaN 的運算式來產生 NaN。例如如下,將 0.0 除以 0.0 會產生 NaN。
double nan_val = 0.0 / 0.0;
但此類方法可能導致執行時錯誤或未定義行為,實務上建議使用 nan("") 等函式明確產生。

編譯器依賴的注意事項

NaN 的產生與行為雖遵循 C 語言標準,但依環境可能會有些微差異。特別是在嵌入式開發等浮點運算被特殊處理的環境中,NaN 可能無法正確處理,因此在測試環境中確認其行為相當重要。

4. NaN的種類與特徵

quiet NaN(qNaN)與 signaling NaN(sNaN)的差異

NaN 有 quiet NaN(qNaN)signaling NaN(sNaN) 兩種。兩者皆是表示「非數」的特殊值,但其用途與行為有明顯的差別。

quiet NaN(qNaN)

quiet NaN 是在 C 語言中最常使用的 NaN 形式。顧名思義是「靜默的 NaN」,在程式執行時不會發出任何通知,悄悄地繼續運算處理。 例如,以下程式碼產生的 NaN 為 qNaN:
#include <math.h>
double x = nan("");
此 qNaN 會以數值方式顯示發生異常運算的事實,但不會拋出例外或停止程式。

signaling NaN(sNaN)

signaling NaN 是在運算時可能引發 例外(Floating-Point Exception) 的 NaN。原本設計用於除錯或開發階段的異常偵測。 然而,在 C 標準函式庫以及多數實作(如 GCC、Clang 等)中,sNaN 的明確產生或處理往往不被支援,或會被當作 qNaN 處理。sNaN 主要與硬體層面的控制相關,會在組合語言或低階浮點運算控制的情況下出現。

NaN 的位元模式與辨識

符合 IEEE 754 的 C 語言環境中,NaN 以特定的位元模式表示。例如,在 64 位元的double 型別中,指數部全為 1 且尾數部非零時,即被視為 NaN。
符號位元 1bit | 指數部 11bit(全部為1) | 尾數部 52bit(非零)
透過此位元結構,編譯器或 CPU 能判斷是否為 NaN,並執行適當的行為(傳播、迴避、警告)。

NaN 的行為:不可預測的比較與運算

NaN 具有與數值不同的獨特性質,特別是在比較運算時需特別留意。
double x = nan("");
if (x == x) {
    printf("相等
");
} else {
    printf("不相等
");
}
// 結果:不相等
如上所示,NaN 甚至不等於自身,因此在一般比較運算中難以處理。此特性在偵測 NaN 存在時相當有用,但同時也可能成為錯誤的根源。

5. NaN的判定方法

NaN在一般比較中無法判定

NaN最特異的性質之一是「NaN與任何事物都不相等」這一點。這表示,甚至與自身的比較也不成立的極端性質,使用一般的==運算子也無法判定NaN。
#include <math.h>
#include <stdio.h>

int main() {
    double x = nan("");
    if (x == x) {
        // 若是一般數值則為 true,
        // 若為 NaN 則為 false
    }
}
因此,若在檢查數值同一性的程式碼中混入 NaN,容易產生意外的分支。特別是在複雜的 if 語句或迴圈中使用時,可能成為 bug 的溫床。

isnan() 函式的使用

在 C 語言中,為了判定浮點數是否為 NaN,使用在標準函式庫 <math.h> 中定義的 isnan() 函式。
#include <math.h>
#include <stdio.h>

int>int main() {
    double x = 0.0 / 0.0;  // NaN
    if (isnan(x)) {
        printf("x是 NaN\n");
    } else {
        printf("x是數值\n");
    }
    return 0;
}
此函式若參數為 NaN,則回傳真(非零),否則回傳偽(0)。

各資料型別的對應

isnan() 是針對 double 型的函式,但亦提供以下相同功能的巨集(視環境而定可能無法使用,需注意):
  • isnanf() : 支援 float
  • isnanl() : 支援 long double

判定時的注意事項

NaN 常成為「看不見的 bug」的原因,特別需要注意以下情況:
  • 未初始化的變數在進行浮點運算時,可能會變成 NaN
  • 外部函式庫輸入輸出處理中可能混入 NaN
  • 當比較處理出現預期外結果時,首先懷疑是否為 NaN

NaN 檢測的最佳實踐

  • 在複雜運算處理之後,使用 isnan() 進行確認
  • 除錯時使用 printf("%f", x) 等顯示時,檢查是否輸出「nan
  • 對於存放資料的變數,明確設定初始值,即使在有意使用 NaN 的情況下,也保留註解說明

6. NaN的應用範例

作為異常值與缺失值的表示的 NaN

C語言用於數值計算時,NaN 作為異常值或缺失值的「旗標」運作。例如,在感測器資料或使用者輸入等情況下,並非所有數值都能正確取得,這時可將 NaN 代入取代一般數值,以明確表示「此值無效」。
double temperature = isnan(sensor_reading) ? nan("") : sensor_reading;
透過這樣的方式活用 NaN,即可不中斷處理,並在後續步驟中偵測與排除異常值。

作為計算過程中錯誤偵測用的 NaN

在程式內的數值運算中,即使在途中發生未預期的運算(如除以零或負數的平方根),也不會立即使程式崩潰,而是可以 回傳 NaN 並持續計算。如此可在之後追蹤異常資料產生的來源。
#include <math.h>

double safe_divide(double a, double b) {
    if (b == 0.0) {
        return nan("");  // 錯誤但仍持續處理
    }
    return a / b;
}
此類程式碼構成了 高可靠性的數值處理程式的基本結構

在資料分析與統計計算中對 NaN 的利用

在統計處理中,常將資料的缺失(missing value)視為 NaN。雖然 C 語言本身未內建統計函式庫,但以 NaN 為基礎的統計處理對於函式庫開發與資料處理的實作相當重要。 以下是一個排除 NaN 後計算平均值的範例:
#include <math.h>

double mean_without_nan(double *arr, int size) {
    double sum = 0.0;
    int count = 0;
    for (int i = 0; i < size; i++) {
        if (!isnan(arr[i])) {
            sum += arr[i];
            count++;
        }
    }
    return (count == 0) ? nan("") : (sum / count);
}
透過這樣的方式活用 NaN,即可實現 「不將其納入計算的彈性處理」

在除錯用途中插入 NaN

在程式執行確認時,故意插入 NaN,作為「若存取此值即表示有問題」的標記。此手法屬於測試階段有意加入不正確值以觀察系統回應的「故障注入(Fault Injection)」的一環。

7. NaN相關的注意事項與陷阱

比較運算的誤解:NaN與任何值都不相等

NaN最大的特徵,也是最容易引起誤解的,是「NaN與任何事物都不相等」的性質。這意味著,甚至連與自身的比較都不成立,因此即使使用一般的==運算子也無法判斷NaN。
double x = nan("");
if (x == x) {
    // 若是一般數值則會是 true,
    // NaN 時則為 false
}
因此,若在檢查數值同一性的程式碼中混入 NaN,意外的分支很容易發生。尤其在複雜的 if 語句或迴圈中使用時,可能成為 bug 的溫床。

NaN 參與的運算結果全部為 NaN

NaN 具有「傳染性」,無論與任何數值運算,基本上結果都會是 NaN。例如以下的運算都會回傳 NaN。
double a = nan("");
double b = 100.0;

double result = a + b;  // result 為 NaN
若在不了解此特性的情況下繼續運算,可能會出現 程式後半突然所有值皆變成 NaN 的情況。

NaN 混入卻不會當機反而成問題

在 C 語言中,NaN 通常不會「觸發例外」。也就是說,即使程式執行異常的運算,也不會被偵測為錯誤,會靜默地繼續執行。 因此,可能在不自覺的情況下持續處理異常值,導致 bug 的發現延遲,形成陷阱。對策包括:
  • 在每個步驟加入 isnan() 檢查
  • 測試時刻意提供含 NaN 的資料以確認行為
此類對策是有效的。

平台與編譯器依賴的行為

NaN 的內部位元表示遵循 IEEE 754,但 不同平台或編譯器可能會產生細微的行為差異。尤其以下幾點需要注意:
  • nan("") 的字串參數是否會被解析(有時會被忽略)
  • printf 的輸出格式(nanNaN-nan 等)
  • sNaN 是否可用(多數環境不支援)
因此,在重視可移植性的程式碼中需注意 NaN 的處理,且不要忘記在不同環境進行驗證。

8. 總結

正確理解 NaN 的重要性

NaN(Not a Number)是 C 語言中浮點運算時,用來表示不具數值意義的特殊值。作為除以零或無效運算的結果出現的 NaN,並不是以錯誤方式停止處理,而是用數值來表達「發生了無效狀態」的資訊機制。 具備此類特性的 NaN,乍看可能覺得難以處理,但若能正確理解並善加利用,則有可能大幅提升程式的穩健性與彈性。

本篇文章學到的要點

  • NaN 是什麼? 是無法作為數值處理的特殊值
  • 產生方法nan("") 函數等可明示產生
  • 種類:有 quiet NaN 與 signaling NaN(實作依賴)
  • 判定方法:可透過 isnan() 函數安全檢查
  • 活用例:涵蓋缺失值、錯誤偵測、除錯用標記等多方面
  • 注意點:比較運算始終為 false、傳播性、以及不會導致崩潰的特性

與 NaN 相處之道:面向實務的實踐

NaN 含有程式靜靜地進入錯誤狀態的可能性。因此,重要不是「發現就停止」,而是「在可能發生的前提下設計」。 特別是在需要數值處理或高可靠性的系統中,若在初期階段就妥善設計 NaN 的判定與移除處理,則能大幅減少之後的錯誤與故障。
侍エンジニア塾