位元詩人 [C 語言] 程式設計教學:資料型態 (Data Type)

C 語言資料型態
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)
    • 聯合體 (union)
    • 指標 (pointer)
    • 函式 (function)

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

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

雖然我們可以用結構體等型別來模擬其他高階語言的類別 (class),嚴格來說,C 語言沒有類別的概念。所謂的物件導向 C (objective-oriented C) 只是一種利用物件導向程式的概念整理 C 程式碼的方式。

除了陣列以外,C 語言沒有其他的內建資料結構型態。如果 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++ 等系統程式語言中都有指標的概念。這項特性主要的用途是管理記憶體。我們會在後續文章中介紹指標,故此處不重覆說明。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。