前言
絕大部分的程式語言都有資料型態 (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
C99 的 stdbool.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)
short
或signed short
int
或signed int
long
或signed long
long long
或signed 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
,故未列出。
筆者自己在測試時,long
和 long 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;
};
在此結構體宣告中,我們定義了兩個屬性 x
和 y
,分別表示點的 x 座標和 y 座標。
聯合體 (Union)
聯合體是另一種由使用者定義的複合型別。聯合體內部包含一至多個欄位 (field),這些欄位有可能是同質或異質。但聯合體內的屬性是共用的,在同一時間內同一聯合體只能用其欄位中其中一個欄位。
例如,以下聯合體定義了兩個欄位:
union data_t {
double dl;
int i;
};
使用者可選擇使用 dl
或 i
,但兩者不能共存。
指標 (Pointer)
指標 (pointer) 用來儲存資料在記憶體中的虛擬位址,在 C 或 C++ 等系統程式語言中都有指標的概念。這項特性主要的用途是管理記憶體。我們會在後續文章中介紹指標,故此處不重覆說明。