cv.cm

編碼 · 程式語言

C 語言入門教學

閱讀約 9 分鐘

簡答

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 格式
char1 位元組char c = 'A';%c
int4 位元組int n = 42;%d
long long8 位元組long long big = 1LL << 40;%lld
double8 位元組double x = 3.14;%f
bool1 位元組bool ok = true;(C99 的 <stdbool.h>)%d
size_t64 位元平台上為 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,讓行為保持一致。

全部編碼教學