學習目標
- 學會宣告與初始化變數,理解「宣告 = 向編譯器登記名字與型別」。
- 認識基本型別:
char、int、long、long long、float、double,以及signed/unsigned修飾。 - 會使用
sizeof查詢型別大小,並用<limits.h>、<float.h>查詢各型別的數值範圍。 - 理解常數的各種形式:字面值(literal)、字元常數、跳脫序列、
const變數、#define符號常數。 - 知道整數溢位與浮點數精度的基本陷阱。
對照 K&R 章節
- K&R §1.2 Variables and Arithmetic Expressions
- K&R §1.4 Symbolic Constants
- K&R §2.1 Variable Names、§2.2 Data Types and Sizes、§2.3 Constants、§2.4 Declarations
觀念講解
1. 變數宣告與初始化
變數是「有名字的記憶體位置」。使用前必須先宣告,讓編譯器知道它的名字與型別:
int count; /* 宣告:有一個 int 叫 count(值尚未定義!) */
int total = 0; /* 宣告 + 初始化:好習慣 */
int x = 1, y = 2; /* 一行宣告多個(初學建議一行一個,較清楚) */
重點:區域變數不初始化就使用是未定義行為的常見來源。 它的初始值是記憶體殘留的垃圾值。 養成「宣告時就給初值」的習慣。
命名規則(K&R §2.1):由字母、數字、底線組成;不能以數字開頭;大小寫視為不同;
不可使用保留字(int、return…)。慣例上變數用小寫(total_price),符號常數用大寫(MAX_SIZE)。
2. 基本型別總覽
| 型別 | 典型大小(LP64,如 macOS/Linux) | 典型範圍 | 用途 |
|---|---|---|---|
char |
1 byte | -128 ~ 127 或 0 ~ 255 | 字元、最小整數 |
short |
2 bytes | -32768 ~ 32767 | 省空間的小整數 |
int |
4 bytes | 約 ±21 億 | 預設整數型別 |
long |
8 bytes(Windows 為 4) | 約 ±9.2×10^18 | 大整數 |
long long |
8 bytes | 約 ±9.2×10^18 | 保證至少 64 位元 |
float |
4 bytes | 約 6 位十進位有效數字 | 單精度浮點 |
double |
8 bytes | 約 15 位十進位有效數字 | 雙精度浮點(預設選這個) |
標準只保證「相對大小」:char ≤ short ≤ int ≤ long ≤ long long,
以及最低位元數(int 至少 16 位元、long 至少 32 位元)。確切大小依平台而定,
所以才需要 sizeof 與 <limits.h>。
3. signed 與 unsigned
unsigned int u = 4000000000u; /* 無號:0 ~ 約 42.9 億 */
signed char sc = -1; /* 有號 char:明確要求可存負數 */
- 有號(signed)型別可表示負數;無號(unsigned)把同樣的位元全部拿來表示非負數,範圍加倍。
int預設是 signed;但char是 signed 還是 unsigned 由實作決定(x86 通常 signed、ARM 常是 unsigned)。- 無號整數溢位是「明確定義」的環繞(模 2^n);有號整數溢位是未定義行為。
- 有號與無號混用比較是經典陷阱(見下方陷阱 2)。
4. sizeof 運算子
sizeof 在編譯期求出型別或運算式所占的位元組數,型別是 size_t(印出時用 %zu):
printf("int : %zu bytes\n", sizeof(int));
printf("double: %zu bytes\n", sizeof(double));
int arr[10];
printf("array: %zu bytes\n", sizeof arr); /* 對運算式可省略括號 */
5. 型別範圍:limits.h 與 float.h
#include <limits.h>
#include <float.h>
printf("INT_MAX = %d\n", INT_MAX); /* int 最大值 */
printf("INT_MIN = %d\n", INT_MIN); /* int 最小值 */
printf("CHAR_BIT = %d\n", CHAR_BIT); /* 一個 byte 有幾個位元(幾乎都是 8) */
printf("DBL_DIG = %d\n", DBL_DIG); /* double 的十進位有效位數 */
printf("FLT_EPSILON = %g\n", FLT_EPSILON); /* 1.0f 與下一個 float 的差 */
6. 常數(Constants)
整數字面值的型別與進位法:
int d = 1234; /* 十進位 */
int o = 0777; /* 0 開頭:八進位(= 511) */
int h = 0xFF; /* 0x 開頭:十六進位(= 255) */
long l = 1234L; /* 後綴 L:long */
unsigned u = 42u; /* 後綴 u:unsigned */
long long ll = 9000000000LL; /* 後綴 LL:long long */
浮點字面值預設是 double;後綴 f 表示 float:
double d1 = 3.14; /* double */
float f1 = 3.14f; /* float */
double sci = 1.5e-3; /* 科學記號:0.0015 */
字元常數是用單引號括起的一個字元,本質是小整數(它的字元編碼值):
char c = 'A'; /* 'A' 的 ASCII 值是 65 */
char newline = '\n'; /* 跳脫序列也是字元常數 */
常用跳脫序列表:
| 序列 | 意義 | 序列 | 意義 |
|---|---|---|---|
\n |
換行 | \\ |
反斜線 |
\t |
水平定位 | \' |
單引號 |
\0 |
空字元(字串結尾) | \" |
雙引號 |
\r |
歸位(carriage return) | \xhh |
十六進位字元值 |
\a |
響鈴 | \ooo |
八進位字元值 |
const 變數 vs #define 符號常數:
#define MAX_USERS 100 /* 前置處理器文字替換:沒有型別、沒有作用域 */
const double TAX_RATE = 0.05; /* 真正的變數:有型別、有作用域、可除錯 */
| 比較 | #define |
const |
|---|---|---|
| 處理時機 | 前置處理(文字替換) | 編譯期(真正的物件) |
| 型別檢查 | 無 | 有 |
| 作用域 | 從定義處到檔案結尾 | 遵守一般作用域規則 |
| 除錯器可見 | 否 | 是 |
K&R 風格常用 #define;現代 C 建議數值常數優先考慮 const(陣列大小等場合仍常用 #define 或 enum)。
範例程式
| 檔案 | 內容 | 編譯與執行 |
|---|---|---|
01_variables.c |
變數宣告、初始化、指定、基本算術(K&R 溫度轉換精神) | cc -std=c11 -Wall -Wextra -o 01_variables 01_variables.c && ./01_variables |
02_sizeof_limits.c |
用 sizeof 與 <limits.h>、<float.h> 印出本機所有基本型別的大小與範圍表 |
cc -std=c11 -Wall -Wextra -o 02_sizeof_limits 02_sizeof_limits.c && ./02_sizeof_limits |
03_constants.c |
各種常數:進位法、後綴、字元常數、跳脫序列、const 與 #define 對比 |
cc -std=c11 -Wall -Wextra -o 03_constants 03_constants.c && ./03_constants |
04_overflow_precision.c |
整數溢位(unsigned 環繞)與浮點精度誤差的可視化示範 | cc -std=c11 -Wall -Wextra -o 04_overflow_precision 04_overflow_precision.c && ./04_overflow_precision |
常見錯誤與陷阱
陷阱 1:使用未初始化的變數
int sum; /* 忘了 = 0 */
sum = sum + 10; /* 垃圾值 + 10 還是垃圾值 */
修正:int sum = 0;。開了 -Wall 編譯器多半會警告 variable 'sum' is uninitialized。
陷阱 2:有號 / 無號混用比較
unsigned int u = 10;
int i = -1;
if (i < u) { ... } /* 直覺上成立,實際上「不成立」! */
比較時 i 被轉成 unsigned,-1 變成極大的正數(4294967295),因此 i < u 為假。
-Wextra 會警告 sign-compare。修正:避免混用,或先明確轉型並確認語意。
陷阱 3:整數除法悄悄截斷
double ratio = 5 / 9; /* ratio = 0.0,不是 0.555... */
5 / 9 兩邊都是 int,先做「整數除法」得 0,再轉成 double。
修正:讓至少一邊是浮點數,5.0 / 9 或 (double)5 / 9。
陷阱 4:以為 char 一定是 signed
char c = -1; /* 在 ARM 平台上 char 可能是 unsigned! */
if (c < 0) { ... } /* 可攜性炸彈 */
修正:需要存放小整數時,明確寫 signed char 或 unsigned char;
char 只拿來存「字元」。
陷阱 5:用 == 比較浮點數
double x = 0.1 + 0.2;
if (x == 0.3) { ... } /* 幾乎必定不成立:x 實際上是 0.30000000000000004 */
修正:比較「差的絕對值是否小於容忍度」:fabs(x - 0.3) < 1e-9。
練習題
- 基本題:宣告三個變數存放你的年齡(
int)、身高公尺(double)、血型(char),初始化後用一個printf印成一句話。 - 溫度轉換:寫程式把攝氏 37.5 度轉成華氏(F = C × 9/5 + 32),分別用「整數運算」與「浮點運算」各算一次並印出,觀察差異。
- 型別偵查:不看本單元的表格,寫程式印出你機器上
char、short、int、long、long long、float、double的sizeof,再與<limits.h>的INT_MAX、LONG_MAX對照驗證。 - 進位法:宣告
int x = 255;,用%d、%o、%x分別印出十進位、八進位、十六進位。再反過來:宣告0x1F、037用%d印出十進位值。 - 字元即整數:印出
'A'、'A' + 1、'a' - 'A'的整數值,並解釋為什麼'a' - 'A'可以用來做大小寫轉換。 - 挑戰題(溢位觀察):宣告
unsigned char uc = 255;再uc = uc + 1;,印出結果並解釋。然後宣告int i = INT_MAX;,思考(不要真的依賴)i + 1為什麼是未定義行為。
延伸閱讀
- K&R 2/e §2.2「Data Types and Sizes」、§2.3「Constants」
man limits.h、C11 標準 §5.2.4.2(數值極限)- 經典文章〈What Every Computer Scientist Should Know About Floating-Point Arithmetic〉(浮點數深入必讀)
<stdint.h>的固定寬度型別(int32_t、uint64_t)——現代 C 實務常用,高階單元會再介紹- 下一單元預告:運算子與運算式,讓變數真正動起來