資料型態與範圍
型態決定一個變數佔幾個 bytes,也就決定了它能表示的範圍;算出來的結果超過範圍就是溢位。在 Windows 上 int 和 long 都只有 32 位元,數字可能超過約 21 億(2^31 − 1)時要用 long long。
C 標準只規定每種型態「至少」多大,實際大小由平台決定。下表是這台電腦(Windows x64、clang 23)實際量到的大小:
| 型態 | 這台電腦 | 標準保證至少 | 大約範圍 | printf / scanf 格式 |
|---|---|---|---|---|
char | 1 byte | 1 byte | −128 ~ 127(見下方說明) | %c(字元)、%hhd(數字) |
short | 2 bytes | 16 位元 | ±3 萬 | %hd |
int | 4 bytes | 16 位元 | ±21 億(±2.1 × 10^9) | %d |
long | 4 bytes | 32 位元 | ±21 億 | %ld |
long long | 8 bytes | 64 位元 | ±9.2 × 10^18 | %lld |
每種整數型態都有對應的 unsigned 版本,例如 unsigned int。無號型態沒有負數,所以最大值大約是有號版本的兩倍:unsigned int 是 0 ~ 42 億,printf / scanf 格式是 %u、%lu、%llu。
/* 每種型態佔幾個 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 bitsshort 2 bytes = 16 bitsint 4 bytes = 32 bitslong 4 bytes = 32 bitslong long 8 bytes = 64 bitsfloat 4 bytes = 32 bitsdouble 8 bytes = 64 bitslong double 16 bytes = 128 bitsint32_t 4 bytes = 32 bitsint64_t 8 bytes = 64 bitssize_t 8 bytes = 64 bitsvoid * 8 bytes = 64 bits/* 每種整數型態能表示的最小值和最大值(數值來自 <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 maxchar -128 127unsigned char 0 255short -32768 32767unsigned short 0 65535int -2147483648 2147483647unsigned int 0 4294967295long -2147483648 2147483647unsigned long 0 4294967295long long -9223372036854775808 9223372036854775807unsigned long long 0 18446744073709551615long 的大小因平台而異。 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。
固定大小的整數:<stdint.h>
Section titled “固定大小的整數:<stdint.h>”想要「不管在哪台電腦上都剛好 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 格式 |
|---|---|---|---|---|
float | 4 bytes | 約 6 ~ 7 位 | %f | %f |
double | 8 bytes | 約 15 ~ 16 位 | %f | %lf |
long double | 16 bytes(實際精確度 80 位元) | 約 18 位 | %Lf(Windows 上有陷阱,見下方) | %Lf |
浮點數存的是近似值,大部分小數(例如 0.1)在二進位裡是無限循環小數,存進去時就已經有誤差。
/* 浮點數:大小、精確度,以及為什麼 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+38double : 8 bytes, about 15 significant digits, max 1.79769e+308long double: 16 bytes, about 18 significant digits16777216.0f + 1.0f = 16777216.00.1 + 0.2 = 0.300000000000000040.1 + 0.2 == 0.3 ? noC17 用 #include <stdbool.h> 之後就能寫 bool、true、false(到了 C23,這三個直接變成關鍵字)。C++ 本來就有 bool。大小都是 1 byte。
| 情況 | C 標準怎麼說 |
|---|---|
| 無號整數超過範圍 | 有定義:會「繞回」,例如 UINT_MAX + 1 == 0 |
| 有號整數超過範圍 | 未定義行為(undefined behavior,UB):標準不保證任何結果,編譯器可以假設它永遠不會發生 |
/* 溢位實驗。改改看:換掉 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 = 02. int: a + b = -2949672963. (long long)a + b = 40000000004. long long x = a + b = 4000000000注意第 4 行:在這台電腦用 -O2 編譯(網站上的結果)時,它「剛好」印出正確的 4000000000;可是用 -O0 編譯時,同一行會印出 −294967296。這就是未定義行為可怕的地方:結果會隨編譯選項改變,看起來正確不代表程式是對的。用 run(Sanitizer 開啟)執行這個範例,UBSan 會直接在第 2 行停下來,告訴你 signed integer overflow。
運算時的型態轉換
Section titled “運算時的型態轉換”- 整數提升:
char、short參與運算時,會先變成int。 - 兩邊型態不同時,往「大」的那邊轉:
int + long long用long long計算,int + double用double計算。 - 有號和無號混在一起時,有號的會被轉成無號,這常常造成意外:
/* 有號和無號混在一起運算時,有號的那一邊會被轉成無號,結果常常出乎意料。 編譯時會看到 -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常數本身也有型態
Section titled “常數本身也有型態”| 寫法 | 型態 |
|---|---|
2000000000 | int(裝得下就是 int) |
3000000000 | long long(int 裝不下,會自動挑裝得下的型態) |
100LL、100ULL | long long、unsigned long long |
100U | unsigned int |
3.14 | double |
3.14f | float |
1e9 | double(不是整數!) |
- 一般的計數、索引:
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 在這台電腦上是 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.000000cast to double : 0.333333解法是在 #include 之前加上 #define __USE_MINGW_ANSI_STDIO 1,或印之前轉成 double:
/* 解法:在 #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.333333333333333333double %.18f : 0.333333333333333315用到這個主題的題目
Section titled “用到這個主題的題目”- 02|兩數相加 — ✔ 已完成
- 03|時間換算 — ✔ 已完成
- 05|簡易計算機 — 還沒完成
- 07|質數判斷 — 還沒完成
- 08|陣列反轉與統計 — 還沒完成
- 20|串流統計 — 還沒完成
- 21|缺少的數字 — 還沒完成
- cppreference:C 的算術型態(英文)
- cppreference:C++ 的基本型態(英文)
- cppreference:固定大小的整數型態
<stdint.h>(英文) - cppreference:printf 的格式(英文)
(還沒寫)