[C 語言] 程式設計教學:資料型別 (Data Type)

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    前言

    絕大部分的程式語言都有資料型別 (data type) 的特性。資料型別是資料的標註 (annotation),用來規範電腦程式對該資料的合理操作。在本文中,我們會介紹 C 語言的資料型別。

    C 語言的資料型別

    以下是 C 語言的資料型別:

    • 基礎型別 (fundamental types)
      • 布林數 (boolean) (C99)
      • 整數 (integer)
      • 浮點數 (floating-point number)
      • 複數 (complex number) (C99)
      • 字元 (character)
      • 列舉 (enumeration)
    • 衍生型別 (derived types)
      • 陣列 (array)
      • 結構體 (structure)
      • 聯合體 (structure)
      • 指標 (pointer)
      • 函式 (function)

    基礎型別用來標註資料本身的形態,像是整數、浮點數、字元等。不同資料型別在記憶體中有不同的儲存方式。

    相對來說,衍生型別則是基於其他型別所建立的型別,像陣列是資料的容器等。衍生型別的特質在於保留使用者自訂的彈性。

    附帶一提,雖然我們可以用結構體等型別來模擬其他高階語言的類別 (class),嚴格來說,C 語言沒有類別的概念。我們用類物件的手法來寫 C 程式碼只是一種整理程式碼的方式。

    布林數 (Boolean) (C99)

    C 語言沒有原生的布林數 (boolean),但 C 語言有布林語境 (boolean context),像是 5 > 3 等關係運算。在這個例子中,5 > 3 為真,故回傳 1。相對來說,3 > 5 為偽,會回傳 0。而 1 在條件句中視為真,0 視為偽。

    C99 之前,常見的手法是用巨集自行宣告真值和偽值,如下例:

    #define TRUE   1
    #define FALSE  0
    

    C99stdbool.h 函式庫,就是把巨集宣告這件事標準化,避免各軟體專案各自為政的情形。在 C 程式碼引入該函式庫後,可以得到以下三個巨集宣告:

    • true:展開為常數 1
    • false:展開為常數 0
    • bool:展開為 _Bool 型別

    由於 stdbool.h 是標準函式庫的一部分,只要自己使用的 C 編譯器有支援,應優先使用。

    整數 (Integer)

    根據正負號的有無和記憶體容量的大小,C 語言的整數細分為數個型別:

    • 無號整數 (unsigned integer)
      • unsigned short
      • unsigned int
      • unsigned long
      • unsigned long long (C99)
    • 帶號整數 (signed integer)
      • shortsigned short
      • intsigned int
      • longsigned long
      • long longsigned long long

    會分那麼細主要是為了節約系統資源。實際使用時,只要知道該整數型別的範圍即可。一開始不會用時,先一律用 int 型別,之後再慢慢練習細分即可。

    實際上整數型別的範圍大小會隨系統而異,並非一成不變。在 C 語言的 limits.h 提供數個和整數型別上下限相關的巨集宣告。我們可以利用這些巨集宣告來檢查自己系統的整數型別的上下限。參考以下範例程式:

    #include <limits.h>
    #include <stdio.h>
    
    int main(void)
    {    
        printf("Max of signed char: %d\n", CHAR_MAX);
        printf("Min of signed char: %d\n", CHAR_MIN);
    
        printf("Max of signed short: %d\n", SHRT_MAX);
        printf("Min of signed short: %d\n", SHRT_MIN);
    
        printf("Max of signed int: %d\n", INT_MAX);
        printf("Min of signed int: %d\n", INT_MIN);
    
        printf("Max of signed long: %ld\n", LONG_MAX);
        printf("Min of signed long: %ld\n", LONG_MIN);
    
        printf("Max of signed long long: %lld\n", LLONG_MAX);
        printf("Min of signed long long: %lld\n", LLONG_MIN);
    
        printf("\n");  /* Line separator. */
    
        printf("Max of unsigned char: %u\n", UCHAR_MAX);
    
        printf("Max of unsigned short: %u\n", USHRT_MAX);
    
        printf("Max of unsigned int: %u\n", UINT_MAX);
    
        printf("Max of unsigned long: %lu\n", ULONG_MAX);
    
        printf("Max of unsigned long long: %llu\n", ULLONG_MAX);
    
        return 0;
    }
    

    對於無號整數來說,最小值一律為 0,故未列出。

    筆者自己在測試時,longlong long 的範圍是相同,但在其他系統上這兩者可能會有差異。讀者可以在自己的電腦上試跑這個小程式看看。

    細心的讀者可能有發現我們在這裡列入字元型態 char。因為 char 內部仍然是以整數來儲存,我們可以把 char 當成小範圍的整數來用,可節約系統資源。

    如果我們需要更大位數的整數呢?這時候就要透過大數 (big number) 函式庫來運算。大數是以軟體模擬的,不受 CPU 的先天限制,但速度會比基礎數字型態慢一些。像 GMP 就是一個大數函式庫。

    固定寬度整數 (C99)

    原本 C 語言不保證整數的寬度,會因系統而異。著眼於這項議題, C99 新增 stdint.h 函式庫,提供固定寛度的整數。有需要的讀者可自行參考。

    浮點數 (Floating-Point Number)

    C 語言的浮點數細分為三種:

    • float
    • double
    • long double (C99)

    三種浮點數在最大值、精準位數等會有一些差異。一開始時,先一律用 double 即可,之後再學著依情境細分。

    同樣地,不要硬背浮點數的範圍、精準度等資訊。可以試著寫小程式在自己系統上面跑。參考以下範例程式:

    #include <float.h>
    #include <stdio.h>
    
    int main(void)
    {
        printf("Max of float: %e\n", FLT_MAX);
        printf("Min pos of float: %e\n", FLT_MIN);
    
        printf("Max of double: %e\n", DBL_MAX);
        printf("Min pos of double: %e\n", DBL_MIN);
    
        printf("Max of long double: %e\n", LDBL_MAX);
        printf("Min pos of long double: %e\n", LDBL_MIN);
    
        printf("\n");  /* Line separator. */
    
        printf("Digit of float: %u\n", FLT_DIG);
        printf("Digit of double: %u\n", DBL_DIG);
        printf("Digit of long double: %u\n", LDBL_DIG);
    
        return 0;
    }
    

    那麼,如何用 C 語言表示無窮大 (infinity) 和非數字 (not a number) 呢?C 語言沒有內建的表示法,但我們可以透過簡單的運算取得這些特殊數字。參考以下範例程式:

    #include <stdio.h>
    
    int main(void)
    {
        /* Positive infinity. */
        double PosInf = 1.0 / 0.0;
        /* Negative infinity. */
        double NegInf = -1.0 / 0.0;
        /* Not a number. */
        double NaN = 0.0 / 0.0;
        
        printf("%e\n", PosInf);
        printf("%e\n", NegInf);
        printf("%e\n", NaN);
        
        printf("\n");  /* Line separator. */
        
        printf("inf + inf = %e\n", PosInf + PosInf);
        printf("-inf + -inf = %e\n", NegInf + NegInf);
        printf("inf + -inf = %e\n", PosInf + NegInf);
        
        printf("inf + nan = %e", PosInf + NaN);
    
        return 0;
    }
    

    在一些舊的 C 編譯器上,這樣的範例程式會引發程式的錯誤。不過,近年來新的 C 編譯器都不再把除以 0.0 視為程式錯誤,所以可以用這種方法來取得這些特殊數字。

    複數 (Complex Number) (C99)

    C99 之前,C 語言沒有原生的複數。當時的手法是自己用其他型別來模擬,像是以下的結構體宣告:

    struct complex_t {
        double real;
        double imag;
    };
    

    在此結構體宣告中,real 表示實部,imag 表示虛部。

    C99 後,C 語言支援原生的複數,我們就不用自己手刻複數型別了。在使用複數時,會引入 complex.h 函式庫,可得到額外的巨集宣告和複數運算相關函式。

    以下是一個簡短的範例程式:

    #include <assert.h>
    #include <complex.h>
    #include <math.h>
    
    int main(void)
    {
        complex p = 3 + 4 * I;
    
        assert(creal(p) == 3);
        assert(cimag(p) == 4);
    
        complex o = 0 + 0 * I;
    
        double d = sqrt(pow(creal(p) - creal(o), 2) + pow(cimag(p) - cimag(o), 2));
        assert(d == 5.0);
    
        return 0;
    }
    

    在此程式中,我們分別以 creal() 函式和 cimag() 函式取出複數的實部和虛部,再計算兩複數的距離。

    字元 (Character)

    字元代表單一的字母 (letter) 或符號 (symbol),像是 'c' 代表英文字母的 c。C 語言中有三種字元型別:

    • 一般字元 char
    • 多位元組字元 char
    • 寬字元 wchar_t

    初學 C 語言時,重點在於核心概念,先會用 char 即可。後兩種字元主要用於國際化 (internationalization) 相關議題,一開始不用急著馬上學。

    字串 (String)

    C 語言沒有真正的字串型別,而是用以零結尾的字元陣列 (null-terminated string) 來表示字串。比起其他的高階語言,這種字串表示法相對低階,處理起來比較瑣碎。

    在以下範例程式中,我們用兩種方式來撰寫相同的字串:

    #include <assert.h>
    #include <string.h>
    
    int main(void)
    {
        char s1[] = "Hello World";
        char s2[] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\0'};
        
        assert(strcmp(s1, s2) == 0);
    
        return 0;
    }
    

    在這個範例中,我們直接以 "Hello World" 字串實字對 s1 賦值,但刻意用字元陣列來對 s2 賦值。以 strcmp() 函式比較兩個字串,確認其回傳值為 0,表示兩字串相等。

    列舉 (Enumeration)

    列舉是由使用者自訂的型別,用來表達有限數量、離散的資料。像是性別 (gender)、星期幾 (day of week) 等。在下列例子中,我們用列舉定義交通號誌:

    enum traffic_light_t {
        TRAFFIC_LIGHT_GREEN,
        TRAFFIC_LIGHT_YELLOW,
        TRAFFIC_LIGHT_RED
    };
    

    現行的交通號誌有綠、黃、紅三種,故在本範例中我們定義三個值。

    在這個例子中,TRAFFIC_LIGHT_ 是我們自訂的前綴。因為 C 語言沒有命名空間也沒有物件的概念,所以我們用自訂的前綴來模擬命名空間。這在 C 語言不是強制的,而是一種撰碼風格。

    列舉所定義的識別字,在程式中視為一種獨一無二的符號。這些識別字本身的值不是重點,而是取其符號上的意義。

    陣列 (Array)

    陣列是由使用者定義的線性容器。陣列的特性是 C 語言內建的,但陣列的型別則由使用者來決定。

    例如,以下範例宣告一個長度為 5 的整數 (int) 陣列:

    int arr[] = {1, 2, 3, 4, 5};
    

    我們將於後文介紹陣列的使用方式,故這裡不詳談。

    結構體 (Structure)

    結構體是由使用者定義的複合型別。結構體內部會包含一至多個欄位 (field),這些欄位有可能是同質或異質的。

    例如,下列結構體用來模擬平面座標上的點 (point):

    struct point_t {
        double x;
        double y;
    };
    

    在此結構體宣告中,我們定義了兩個屬性 xy,分別表示點的 x 座標和 y 座標。

    聯合體 (Union)

    聯合體是另一種由使用者定義的複合型別。聯合體內部包含一至多個欄位 (field),這些欄位有可能是同質或異質。但聯合體內的屬性是共用的,在同一時間內同一聯合體只能用其欄位中其中一個欄位。

    例如,以下聯合體定義了兩個欄位:

    union data_t {
        double dl;
        int i;
    };
    

    使用者可選擇使用 dli,但兩者不能共存。

    指標 (Pointer)

    指標 (pointer) 用來儲存資料在記憶體中的虛擬位址,在 C 或 C++ 等系統程式語言中都有指標的概念。這項特性主要的用途是管理記憶體。我們會在後續文章中介紹指標,故此處不重覆說明。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk