JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

型別、變數,以及你的第一支真正的程式

你已經跑過了「hello, world」,也見識過了編輯—編譯—執行的循環。現在我們放慢腳步,認識 C 真正的原料:稱為變數、帶有型別的小盒子。讀完本篇,你就能寫出、編譯並推敲一支真正會運算的小程式——而且清楚知道每一行到底在做什麼。

我們爬到階梯的哪一階了

在「基礎」那一階,你看過了全局:電腦是一台把自己的指令儲存在記憶體裡的機器,而要跑你自己的程式,你走過了編輯—編譯—執行的循環——打出原始碼、交給編譯器、再執行它產出的可執行檔。你甚至印出了 hello, world。那支程式是一句客氣的問候,但它並沒有「運算」任何東西。本階「學習 C 語言」要補上這一塊:在五篇導引裡,我們會學到足夠的 C,足以寫出真正可用的小程式。

我們從最底層開始,從每支程式賴以建構的原料著手:資料,以及盛裝它的、帶有型別的盒子。後面幾篇會加上運算子與判斷、接著是函式、再來是陣列與結構,最後是標準函式庫巡禮。我們刻意停在指標之前——位址、以及為它們命名的東西,自有專屬的一整階,因為它們威力強大卻也確實容易出錯,操之過急正是初學者受傷的方式。眼下,我們寫的一切都住在單純、具名的變數裡。

變數是一個貼了標籤、帶有型別的盒子

把記憶體想像成「基礎」階所描述的樣子:一條很長、格子有編號的層架。變數就是你用來認領其中幾格、給它們一個能唸出口的名字、並向編譯器承諾那裡將住著「哪一種」東西的方式。那個承諾就是型別。當你寫下 `int age = 27;`,你是在告訴 C:替一個整數留位置,叫它 `age`,並從 27 開始。編譯器於是知道要空出幾格、又該怎麼把它們讀回來。

為什麼非得堅持要有型別?因為記憶體裡同一串位元樣式,會隨著你怎麼讀它而代表不同的東西——這是個深刻的概念,你會在「位元與位元組」那一階再度遇見。型別就是那副鏡片。`int` 把一叢格子讀成整數;`double` 把(大小不同的)一叢格子讀成帶有小數部分的數;`char` 把單一格子讀成一個小整數,通常用來代表一個文字字元。C 的核心基本型別正是這份簡短的菜單:`char`、`int`、`float`、`double`,加上 `short`/`long` 這些大小變體——以及 `_Bool`(`bool`)與 `void`,我們很快會碰到。

宣告、定義,以及空盒子的危險

`int age = 27;` 這一行其實一口氣做了兩件事,值得把它們拆開來看。首先它宣告了名字 `age` 與它的型別,讓編譯器知道這個盒子存在。接著它定義了那個盒子——真正留下那些格子——並把它初始化,把 27 寫進去。C 在宣告與定義之間劃下了真實的界線;對一個簡單的區域變數來說,這兩者通常發生在同一行,但等你遇到函式與多個檔案時,就會看到它們被拆開。

那句話裡最重要的詞是初始化。如果你只寫 `int age;` 就停手,你擁有了一個盒子,卻從沒往裡頭放任何東西。C「並不會」貼心地替你歸零(至少對區域變數而言)。此刻讀取 `age`,得到的是那些格子裡原本就坐著的任何位元樣式——前面某段程式留下的殘渣垃圾。這就是經典的未初始化變數 bug,而它比聽起來更惡劣:那團垃圾今天碰巧是 0、明天卻可能是四百萬,於是 bug 看似隨機地時隱時現。解法是一種紀律,而非什麼花招:在你建立每個變數的當下,就給它一個值。

名字、生命期,以及有號與無號

變數不會永遠活著,它的名字也不是處處可見。圍住一段程式的大括號 `{ ... }` 標記出一個作用域:在其中宣告的變數,於控制流進入時誕生、於控制流離開時不復存在。這就是它的作用域與生命期。正因如此,你才能在兩個不同的地方都用 `i` 當迴圈計數器而不相撞——各自在自己的區塊裡生與死。這也意味著回傳一個區域變數的位址(指標日後會誘惑你這麼做)是個陷阱,因為等有人去看時,那個盒子早已消失。

每個整數型別還背負著一個選擇:它能不能是負的。普通的 `int` 是有號的——它花一個位元在符號上,於是涵蓋大約負二十億到正二十億。同樣大小的 `unsigned int` 沒有負值,因此正向能伸得高出一倍。這就是有號/無號之別。對於確實不可能為負的東西(計數、大小、位元樣式),就用無號;但要保持警覺:無號型別減過零並不會變負,而是回繞成一個巨大的數,這正是它惡名昭彰的意外。

而無論有號或無號,每個整數盒子都有固定數量的格子,因此有個上限。把值推過頂端,你就撞上溢位。對「無號」型別,標準精確地定義了回繞;對「有號」型別,溢位則是未定義行為,一個更微妙的危害,我們會在它專屬的階好好對待。眼下誠實的總結是:整數並非數學裡那種無窮的數——它們是有限的,而認清它的邊界,正是寫出不會騙你的 C 的一部分。

你的第一支真正的程式

把它們組裝起來。每支 C 程式都從一個叫 `main` 的函式開始執行,而 `main` 結束時會交回一個小整數——那是它的結束狀態,依長久以來的慣例,0 代表成功。(`main` 也能透過 argc 與 argv 接收命令列引數,但我們暫時還用不到。)下面這支程式真的會運算:它用公式 F = C * 9 / 5 + 32 把攝氏溫度換算成華氏。

#include <stdio.h>

int main(void) {
    int celsius = 100;
    int fahrenheit = celsius * 9 / 5 + 32;
    printf("%d C is %d F\n", celsius, fahrenheit);
    return 0;
}
攝氏轉華氏:帶型別的盒子、一次運算、印出結果,以及乾淨地結束。
  1. 存成 main.c,然後建置它:`gcc -Wall -Wextra main.c -o temp`——其中 -o 把可執行檔命名為 temp,而非預設的 a.out。
  2. 執行它:`$ ./temp` 會印出「100 C is 212 F」。printf() 裡的 `%d` 佔位符,依序由 `celsius` 與 `fahrenheit` 填入。
  3. 查看結束狀態:緊接著輸入 `$ echo $?` 會顯示 0,也就是我們的 `return 0;` 回傳給 shell、用來說「一切順利」的值。
  4. 現在動手實驗:把 100 改成 0 再重新建置——你應該會得到 32 F。從編輯到結果的整個循環,正是寫 C 的節奏。

這七行裡藏著兩個誠實的註腳。`100` 與 `32` 是字面常數——直接烤進原始碼裡的值。而那段算術是「整數」算術:因為 `celsius`、9、5 全是 `int`,`celsius * 9 / 5` 每一步都朝零取整,於是 37 C 算出來是 98 F,而不是 98.6。這不是 C 的 bug;這是 C 對你的要求精確以對。想要小數部分?把那些數改成 `double`,再用 `%f` 印出——一個小小的改動,悄悄教會了整個階段奠基其上的道理:在 C 裡,你選的型別,決定了你得到的答案。