編碼 · 程式語言
C 語言入門教學
簡答
C 是小巧、快速的編譯式語言,能透過指標直接存取記憶體。在 .c 檔中撰寫 main,用 gcc hello.c -o hello 編譯,再執行 ./hello。核心概念包括固定大小的型別、函式、陣列與以空字元結尾的字串、指標與位址、以 malloc 和 free 手動管理記憶體,以及透過回傳值處理錯誤。
C 語言是什麼,用來做什麼
C 由 Dennis Ritchie 於 1970 年代初在貝爾實驗室創造,用來撰寫 Unix 作業系統。它由 ISO 標準化(主要版本有 C99、C11、C17 與 C23)。C 編譯成機器碼,幾乎不需要執行階段支援,因此幾乎在所有平台上都能執行。它用於作業系統核心(Linux)、嵌入式韌體、資料庫(SQLite)、語言執行環境(CPython 就是以 C 寫成)與高效能函式庫。學習 C 也能讓你了解記憶體與電腦真正的運作方式。
安裝編譯器並執行 Hello, world
Linux 上安裝 gcc(例如 sudo apt install build-essential);macOS 上執行 xcode-select --install 取得 clang(也能以 gcc 呼叫);Windows 上可使用 MSYS2、WSL 或 Visual Studio。
#include <stdio.h>
int main(void) {
printf("Hello, world\n");
return 0;
}gcc -Wall -Wextra -std=c17 hello.c -o hello
./hello
# Hello, world#include 會引入標頭檔中的宣告,這裡引入的是標準輸入輸出函式庫。main 回傳 0 代表成功。編譯時請務必開啟警告(-Wall -Wextra)。
變數與基本型別
每個變數都有決定其大小與意義的型別。大小取決於平台,除非標準另有規定;<stdint.h> 提供 int32_t 這類固定寬度的型別。
| 型別 | 常見大小 | 範例 | printf 格式 |
|---|---|---|---|
char | 1 位元組 | char c = 'A'; | %c |
int | 4 位元組 | int n = 42; | %d |
long long | 8 位元組 | long long big = 1LL << 40; | %lld |
double | 8 位元組 | double x = 3.14; | %f |
bool | 1 位元組 | bool ok = true;(C99 的 <stdbool.h>) | %d |
size_t | 64 位元平台上為 8 位元組 | size_t len = sizeof(arr); | %zu |
#include <stdio.h>
int main(void) {
int count = 7;
double avg = count / 2; /* 3.0: integer division first */
double avg2 = count / 2.0; /* 3.5 */
printf("%d %.1f %.1f\n", count, avg, avg2); /* 7 3.0 3.5 */
printf("%zu\n", sizeof(int)); /* 4 on most systems */
return 0;
}流程控制與函式
函式必須在使用之前宣告。引數以傳值方式傳遞:函式收到的是副本。
#include <stdio.h>
int square(int x) {
return x * x;
}
int main(void) {
int score = 72;
if (score >= 90) {
printf("A\n");
} else if (score >= 70) {
printf("B\n"); /* B */
} else {
printf("C\n");
}
for (int i = 0; i < 3; i++) {
printf("%d ", square(i)); /* 0 1 4 */
}
printf("\n");
int n = 10;
while (n > 1) n /= 2;
printf("%d\n", n); /* 1 */
return 0;
}C 還有 do/while、帶有 case 標籤的 switch(記得寫 break),以及 goto,後者多半用於錯誤路徑中的清理工作。
陣列與字串
陣列是彼此相鄰儲存的固定數量元素。C 不會檢查邊界,也不會儲存長度,所以長度要另外傳入。字串是以零位元組 '\0' 結尾的 char 陣列。
#include <stdio.h>
#include <string.h>
int sum(const int *values, size_t n) {
int total = 0;
for (size_t i = 0; i < n; i++) total += values[i];
return total;
}
int main(void) {
int nums[] = {3, 1, 2, 5};
size_t n = sizeof nums / sizeof nums[0];
printf("%zu items, sum %d\n", n, sum(nums, n)); /* 4 items, sum 11 */
char name[16] = "Ada";
strncat(name, " Lovelace", sizeof name - strlen(name) - 1);
printf("%s (%zu chars)\n", name, strlen(name)); /* Ada Lovelace (12 chars) */
return 0;
}陣列傳入函式時會退化(decay)成指向第一個元素的指標,這就是 sum 接收 const int * 與長度的原因。
招牌特色:指標
指標存放另一個值的記憶體位址。&x 取得 x 的位址,*p 則讀取或寫入位址 p 上的值。指標讓函式能修改呼叫端的變數、不必複製就能共用大型資料,也能用來建立鏈結式資料結構。
#include <stdio.h>
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
int main(void) {
int x = 1, y = 2;
int *p = &x;
*p = 10; /* changes x */
swap(&x, &y);
printf("x=%d y=%d\n", x, y); /* x=2 y=10 */
int arr[3] = {7, 8, 9};
int *q = arr;
printf("%d %d\n", *(q + 1), q[2]); /* 8 9: pointer arithmetic */
return 0;
}使用 malloc 與 free 的動態記憶體
區域變數存在堆疊(stack)上,函式返回時就會消失。若資料大小要到執行階段才知道,或必須比函式活得更久,就要用 malloc 在堆積(heap)上配置,並用 free 釋放。每一次成功的 malloc 都要剛好對應一次 free。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t n = 5;
int *squares = malloc(n * sizeof *squares);
if (squares == NULL) {
fprintf(stderr, "out of memory\n");
return 1;
}
for (size_t i = 0; i < n; i++) squares[i] = (int)(i * i);
printf("%d\n", squares[4]); /* 16 */
free(squares);
squares = NULL; /* avoid using a dangling pointer */
return 0;
}相關函式還有 calloc(配置並清零)與 realloc(調整大小)。可以用 struct 把相關的值組合在一起,例如 struct point { int x; int y; };。
錯誤處理
C 沒有例外機制。函式透過回傳值回報錯誤(通常是 -1、NULL 或狀態碼),許多函式庫函式也會設定全域變數 errno。每個可能失敗的回傳值都要檢查。
#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(void) {
FILE *f = fopen("missing.txt", "r");
if (f == NULL) {
fprintf(stderr, "cannot open: %s\n", strerror(errno));
return 1; /* cannot open: No such file or directory */
}
fclose(f);
return 0;
}完整的小程式:行數與單字計數器
這個程式的功能類似小型的 wc:讀取命令列指定的檔案,並印出行數、單字數與位元組數。
#include <ctype.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: %s FILE\n", argv[0]);
return 1;
}
FILE *f = fopen(argv[1], "r");
if (f == NULL) {
perror(argv[1]);
return 1;
}
long lines = 0, words = 0, bytes = 0;
int c, in_word = 0;
while ((c = fgetc(f)) != EOF) {
bytes++;
if (c == '\n') lines++;
if (isspace(c)) {
in_word = 0;
} else if (!in_word) {
in_word = 1;
words++;
}
}
fclose(f);
printf("%ld lines, %ld words, %ld bytes\n", lines, words, bytes);
return 0;
}以 gcc -Wall -Wextra wc.c -o wc 編譯,再執行 ./wc notes.txt。請注意 c 是 int 而不是 char,這樣才能容納特殊值 EOF。
初學者常見錯誤
- 在條件中使用
=而不是==。警告選項(-Wall)能抓到大多數情況。 - 設定字串緩衝區大小時,忘了替結尾的
'\0'預留空間。 - 使用
gets或沒有長度限制的scanf("%s", buf)。請改用fgets(buf, sizeof buf, stdin)。 - 回傳指向區域變數的指標,該變數在函式返回後就不存在了。
printf的格式與型別不一致,例如對size_t使用%d。
接下來學什麼
可以把 cppreference.com 的 C 區當作語言與標準函式庫的參考,並閱讀 ISO 工作小組(WG14)的頁面來了解標準本身。多練習 struct、鏈結串列、標頭檔,以及用 make 建置的多檔案程式。除錯器(gdb 或 lldb)與 Valgrind 或 AddressSanitizer 能替你省下大量時間。之後可以探索 C++ 或 Rust,它們建立在相同的概念之上,並提供更多安全性。
常見問題
現在還值得學 C 嗎?
值得。C 仍用於作業系統、嵌入式裝置與效能關鍵的函式庫,而且能讓你了解記憶體、指標與編譯後程式的執行方式,對學習其他語言都有幫助。
C 和 C++ 有什麼差別?
C++ 最初是 C 的擴充,加入了類別、樣板、參考、例外與龐大的標準函式庫。C 則保持精簡並採程序式風格。大多數 C 程式碼接近合法的 C++,但兩者是各自獨立的標準。
為什麼我的 C 程式會因 segmentation fault 當掉?
程式存取了不屬於它的記憶體,通常是透過 null、未初始化或已釋放的指標,或是索引超出陣列結尾。在除錯器中執行,或搭配 AddressSanitizer,就能找出確切的那一行。
該使用哪個 C 標準?
目前的編譯器普遍支援 C17,而 C23 是最新的標準。請明確指定版本,例如 -std=c17,讓行為保持一致。