學習目標

  • 學會宣告與初始化變數,理解「宣告 = 向編譯器登記名字與型別」。
  • 認識基本型別: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。

練習題

  1. 基本題:宣告三個變數存放你的年齡(int)、身高公尺(double)、血型(char),初始化後用一個 printf 印成一句話。
  2. 溫度轉換:寫程式把攝氏 37.5 度轉成華氏(F = C × 9/5 + 32),分別用「整數運算」與「浮點運算」各算一次並印出,觀察差異。
  3. 型別偵查:不看本單元的表格,寫程式印出你機器上 char、short、int、long、long long、float、double 的 sizeof,再與 <limits.h> 的 INT_MAX、LONG_MAX 對照驗證。
  4. 進位法:宣告 int x = 255;,用 %d、%o、%x 分別印出十進位、八進位、十六進位。再反過來:宣告 0x1F、037 用 %d 印出十進位值。
  5. 字元即整數:印出 'A'、'A' + 1、'a' - 'A' 的整數值,並解釋為什麼 'a' - 'A' 可以用來做大小寫轉換。
  6. 挑戰題(溢位觀察):宣告 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 實務常用,高階單元會再介紹
  • 下一單元預告:運算子與運算式,讓變數真正動起來