跳到內容

資料型態與範圍

型態決定一個變數佔幾個 bytes,也就決定了它能表示的範圍;算出來的結果超過範圍就是溢位。在 Windows 上 int 和 long 都只有 32 位元,數字可能超過約 21 億(2^31 − 1)時要用 long long。

C 標準只規定每種型態「至少」多大,實際大小由平台決定。下表是這台電腦(Windows x64、clang 23)實際量到的大小:

型態這台電腦標準保證至少大約範圍printf / scanf 格式
char1 byte1 byte−128 ~ 127(見下方說明)%c(字元)、%hhd(數字)
short2 bytes16 位元±3 萬%hd
int4 bytes16 位元±21 億(±2.1 × 10^9)%d
long4 bytes32 位元±21 億%ld
long long8 bytes64 位元±9.2 × 10^18%lld

每種整數型態都有對應的 unsigned 版本,例如 unsigned int。無號型態沒有負數,所以最大值大約是有號版本的兩倍:unsigned int 是 0 ~ 42 億,printf / scanf 格式是 %u、%lu、%llu。

sizes.c
/* 每種型態佔幾個 bytes、幾個位元(bit)。
改改看:在 main 裡多加幾行 SHOW(...),例如 SHOW(long double)、SHOW(_Bool)。 */
#include <limits.h> /* CHAR_BIT:1 個 byte 有幾個位元(幾乎都是 8) */
#include <stdint.h> /* int32_t、int64_t 這些固定大小的型態 */
#include <stdio.h>
#define SHOW(T) printf("%-20s %2zu bytes = %2zu bits\n", #T, sizeof(T), sizeof(T) * CHAR_BIT)
int main(void) {
SHOW(char);
SHOW(short);
SHOW(int);
SHOW(long);
SHOW(long long);
SHOW(float);
SHOW(double);
SHOW(long double);
SHOW(int32_t);
SHOW(int64_t);
SHOW(size_t);
SHOW(void *);
return 0;
}

在這台電腦執行的結果(Windows x64、clang 23、-O2,沒有開 Sanitizer):

執行結果
char 1 bytes = 8 bits
short 2 bytes = 16 bits
int 4 bytes = 32 bits
long 4 bytes = 32 bits
long long 8 bytes = 64 bits
float 4 bytes = 32 bits
double 8 bytes = 64 bits
long double 16 bytes = 128 bits
int32_t 4 bytes = 32 bits
int64_t 8 bytes = 64 bits
size_t 8 bytes = 64 bits
void * 8 bytes = 64 bits
在 Compiler Explorer 開啟 ↗可以線上修改、執行;那邊是 Linux,有些結果會和 Windows 不同
ranges.c
/* 每種整數型態能表示的最小值和最大值(數值來自 <limits.h>)。 */
#include <limits.h>
#include <stdio.h>
int main(void) {
printf("%-19s %-21s %s\n", "type", "min", "max");
printf("%-19s %-21d %d\n", "char", CHAR_MIN, CHAR_MAX);
printf("%-19s %-21d %d\n", "unsigned char", 0, UCHAR_MAX);
printf("%-19s %-21d %d\n", "short", SHRT_MIN, SHRT_MAX);
printf("%-19s %-21d %d\n", "unsigned short", 0, USHRT_MAX);
printf("%-19s %-21d %d\n", "int", INT_MIN, INT_MAX);
printf("%-19s %-21d %u\n", "unsigned int", 0, UINT_MAX);
printf("%-19s %-21ld %ld\n", "long", LONG_MIN, LONG_MAX);
printf("%-19s %-21d %lu\n", "unsigned long", 0, ULONG_MAX);
printf("%-19s %-21lld %lld\n", "long long", LLONG_MIN, LLONG_MAX);
printf("%-19s %-21d %llu\n", "unsigned long long", 0, ULLONG_MAX);
return 0;
}

在這台電腦執行的結果(Windows x64、clang 23、-O2,沒有開 Sanitizer):

執行結果
type min max
char -128 127
unsigned char 0 255
short -32768 32767
unsigned short 0 65535
int -2147483648 2147483647
unsigned int 0 4294967295
long -2147483648 2147483647
unsigned long 0 4294967295
long long -9223372036854775808 9223372036854775807
unsigned long long 0 18446744073709551615
在 Compiler Explorer 開啟 ↗可以線上修改、執行;那邊是 Linux,有些結果會和 Windows 不同

long 的大小因平台而異。 64 位元的 Windows 採用 LLP64 模型,long 是 32 位元;64 位元的 Linux 和 macOS 採用 LP64 模型,long 是 64 位元。所以同一個程式在不同電腦上,long 能裝的數字不一樣。需要 64 位元時,直接用 long long 或 int64_t 最保險。點上面範例的「在 Compiler Explorer 開啟」按鈕到 Linux 上執行,就能看到 long 變成 8 bytes。

char 有沒有正負號由平台決定。 在 x86(一般電腦)上是有號的(−128 ~ 127),在 ARM 的 Linux 上是無號的(0 ~ 255)。要拿 char 存數字時,寫清楚 signed char 或 unsigned char。

想要「不管在哪台電腦上都剛好 32 位元」,就用固定大小的型態:

型態大小說明
int8_t、int16_t、int32_t、int64_t剛好 8 / 16 / 32 / 64 位元有號
uint8_t ~ uint64_t同上無號
size_t這台電腦是 8 bytes無號;sizeof 和 strlen 的結果、陣列大小都用它,格式 %zu

C++ 要用 #include <cstdint>,型態名稱一樣。

型態這台電腦有效位數printf 格式scanf 格式
float4 bytes約 6 ~ 7 位%f%f
double8 bytes約 15 ~ 16 位%f%lf
long double16 bytes(實際精確度 80 位元)約 18 位%Lf(Windows 上有陷阱,見下方)%Lf

浮點數存的是近似值,大部分小數(例如 0.1)在二進位裡是無限循環小數,存進去時就已經有誤差。

floats.c
/* 浮點數:大小、精確度,以及為什麼 0.1 + 0.2 不等於 0.3。 */
#include <float.h>
#include <stdio.h>
int main(void) {
printf("float : %2zu bytes, about %2d significant digits, max %g\n", sizeof(float), FLT_DIG, FLT_MAX);
printf("double : %2zu bytes, about %2d significant digits, max %g\n", sizeof(double), DBL_DIG, DBL_MAX);
printf("long double: %2zu bytes, about %2d significant digits\n", sizeof(long double), LDBL_DIG);
/* float 只有 24 個位元存有效數字,超過 2^24 = 16777216 之後就無法表示每個整數 */
float f = 16777216.0f;
f = f + 1.0f;
printf("16777216.0f + 1.0f = %.1f\n", f);
/* 0.1 和 0.2 在二進位都是無限循環小數,存進電腦時就已經有誤差 */
double x = 0.1 + 0.2;
printf("0.1 + 0.2 = %.17f\n", x);
printf("0.1 + 0.2 == 0.3 ? %s\n", x == 0.3 ? "yes" : "no");
return 0;
}

在這台電腦執行的結果(Windows x64、clang 23、-O2,沒有開 Sanitizer):

執行結果
float : 4 bytes, about 6 significant digits, max 3.40282e+38
double : 8 bytes, about 15 significant digits, max 1.79769e+308
long double: 16 bytes, about 18 significant digits
16777216.0f + 1.0f = 16777216.0
0.1 + 0.2 = 0.30000000000000004
0.1 + 0.2 == 0.3 ? no
在 Compiler Explorer 開啟 ↗可以線上修改、執行;那邊是 Linux,有些結果會和 Windows 不同

C17 用 #include <stdbool.h> 之後就能寫 bool、true、false(到了 C23,這三個直接變成關鍵字)。C++ 本來就有 bool。大小都是 1 byte。

情況C 標準怎麼說
無號整數超過範圍有定義:會「繞回」,例如 UINT_MAX + 1 == 0
有號整數超過範圍未定義行為(undefined behavior,UB):標準不保證任何結果,編譯器可以假設它永遠不會發生
overflow.c
/* 溢位實驗。改改看:換掉 a、b 的值或型態,看結果怎麼變。 */
#include <limits.h>
#include <stdio.h>
int main(void) {
/* 1. 無號整數超過最大值:C 標準規定會「繞回」從 0 開始 */
unsigned int u = UINT_MAX;
u = u + 1;
printf("1. UINT_MAX + 1 = %u\n", u);
/* 2. 有號整數溢位:未定義行為 (undefined behavior)。
volatile 讓編譯器不能事先把答案算好。這台電腦上通常會繞成負數,
但 C 標準不保證任何結果;用 run(Sanitizer 開啟)執行會直接被抓到。 */
volatile int a = 2000000000, b = 2000000000;
int wrong = a + b;
printf("2. int: a + b = %d\n", wrong);
/* 3. 先把其中一個轉成 long long,整個加法就用 long long 計算 */
long long right = (long long)a + b;
printf("3. (long long)a + b = %lld\n", right);
/* 4. 陷阱:結果存進 long long 並不能解決問題。a + b 照規則是用 int 計算的,
溢位一樣是未定義行為,所以結果要看編譯器心情:
這台電腦用 -O0 編譯會印出負數,用 -O2 編譯卻「剛好」印出正確答案。不要依賴它! */
long long still_wrong = a + b;
printf("4. long long x = a + b = %lld\n", still_wrong);
return 0;
}

在這台電腦執行的結果(Windows x64、clang 23、-O2,沒有開 Sanitizer):

執行結果
1. UINT_MAX + 1 = 0
2. int: a + b = -294967296
3. (long long)a + b = 4000000000
4. long long x = a + b = 4000000000
在 Compiler Explorer 開啟 ↗可以線上修改、執行;那邊是 Linux,有些結果會和 Windows 不同

注意第 4 行:在這台電腦用 -O2 編譯(網站上的結果)時,它「剛好」印出正確的 4000000000;可是用 -O0 編譯時,同一行會印出 −294967296。這就是未定義行為可怕的地方:結果會隨編譯選項改變,看起來正確不代表程式是對的。用 run(Sanitizer 開啟)執行這個範例,UBSan 會直接在第 2 行停下來,告訴你 signed integer overflow。

  1. 整數提升:char、short 參與運算時,會先變成 int。
  2. 兩邊型態不同時,往「大」的那邊轉:int + long long 用 long long 計算,int + double 用 double 計算。
  3. 有號和無號混在一起時,有號的會被轉成無號,這常常造成意外:
signed_unsigned.c
/* 有號和無號混在一起運算時,有號的那一邊會被轉成無號,結果常常出乎意料。
編譯時會看到 -Wsign-compare 警告,那就是在提醒這件事。 */
#include <stddef.h>
#include <stdio.h>
int main(void) {
int x = -1;
unsigned int y = 1;
/* -1 被轉成 unsigned int 變成 4294967295,所以「-1 < 1」竟然不成立 */
printf("-1 < 1u ? %s\n", x < y ? "yes" : "no");
/* size_t 是無號整數(陣列大小、strlen 的回傳值都是它),0 - 1 會繞回最大值 */
size_t n = 0;
printf("(size_t)0 - 1 = %zu\n", n - 1);
return 0;
}

在這台電腦執行的結果(Windows x64、clang 23、-O2,沒有開 Sanitizer):

執行結果
-1 < 1u ? no
(size_t)0 - 1 = 18446744073709551615
在 Compiler Explorer 開啟 ↗可以線上修改、執行;那邊是 Linux,有些結果會和 Windows 不同
寫法型態
2000000000int(裝得下就是 int)
3000000000long long(int 裝不下,會自動挑裝得下的型態)
100LL、100ULLlong long、unsigned long long
100Uunsigned int
3.14double
3.14ffloat
1e9double(不是整數!)
  • 一般的計數、索引:int
  • 可能超過 21 億,例如總和、乘積、10^9 等級的數字相加:long long
  • 陣列大小、strlen 的結果:size_t
  • 要固定大小,例如處理檔案格式、位元運算:int32_t、uint64_t
  • 小數:優先用 double;float 的精確度常常不夠
  • long long s = a + b; 不能防止溢位:如果 a、b 是 int,加法先用 int 算完才存進去。要寫成 (long long)a + b,或一開始就把 a、b 宣告成 long long。
  • i * i <= n 也會溢位:i 是 int、n 接近 10^9 時,i * i 可能超過 int。可以把 i 宣告成 long long,或改寫成 i <= n / i。
  • scanf 讀 double 要用 %lf;printf 印 double 用 %f 就可以。印 long long 要用 %lld,用錯格式會讀到或印出錯的數字。
  • 不要用 == 比較浮點數:要改成 fabs(a - b) < 1e-9 這類寫法。
  • 有號和無號混著比較:-1 < 1u 不成立。for (size_t i = n - 1; i >= 0; i--) 是無窮迴圈,因為 size_t 永遠 ≥ 0。
  • Windows 上用 printf 印 long double 會印錯:Windows 的 C 執行環境不認得 80 位元的 long double。
long_double.c
/* long double 在這台電腦上是 80 位元精確度(佔 16 bytes),
但 Windows 的 C 執行環境的 printf 不認得它,用 %Lf 會印出錯的數字。
在 Compiler Explorer(Linux)上執行就會是正確的,可以比較看看。 */
#include <stdio.h>
int main(void) {
long double x = 1.0L / 3.0L;
printf("printf %%Lf : %Lf\n", x);
printf("cast to double : %f\n", (double)x);
return 0;
}

在這台電腦執行的結果(Windows x64、clang 23、-O2,沒有開 Sanitizer):

執行結果
printf %Lf : 0.000000
cast to double : 0.333333
在 Compiler Explorer 開啟 ↗可以線上修改、執行;那邊是 Linux,有些結果會和 Windows 不同

解法是在 #include 之前加上 #define __USE_MINGW_ANSI_STDIO 1,或印之前轉成 double:

long_double_fix.c
/* 解法:在 #include 之前定義 __USE_MINGW_ANSI_STDIO,
改用 MinGW 自己實作的 printf,它認得 80 位元的 long double。 */
#define __USE_MINGW_ANSI_STDIO 1
#include <stdio.h>
int main(void) {
long double x = 1.0L / 3.0L;
printf("printf %%.18Lf : %.18Lf\n", x);
printf("double %%.18f : %.18f\n", (double)x);
return 0;
}

在這台電腦執行的結果(Windows x64、clang 23、-O2,沒有開 Sanitizer):

執行結果
printf %.18Lf : 0.333333333333333333
double %.18f : 0.333333333333333315
在 Compiler Explorer 開啟 ↗可以線上修改、執行;那邊是 Linux,有些結果會和 Windows 不同

(還沒寫)