第3回まで無料

C 言語入門コース

コンパイルの仕組みと gcc の警告から、変数と制御構文・関数と配列・ポインタ・malloc と free・構造体と連結リスト・ファイルと make・システムコールまで。全50レッスンで「ポインタとメモリを自分で管理し、簡易シェルを 1 本書ける」ところまで進みます。コード・警告・サニタイザの報告はすべて gcc 16.2.0(C23)で実際に動かしたものです。

カリキュラム

全50レッスンを7つの章に分けています。第1章から順に進めるのがおすすめですが、 気になるところだけ拾い読みしてもかまいません。 ※ C はブラウザ内で実行できないため、第2回で用意する gcc 16 の Docker コンテナか、Linux・WSL の gcc で試してください。第7章のシステムコールは Linux が前提です。

Chapter 1 — C を始める(第1〜7回)

コンパイルの 4 段階と gcc の警告から始め、変数と型・演算子・標準入出力・条件分岐・ループまで進みます。書いたコードを gcc に通し、警告とサニタイザに間違いを見つけてもらう習慣をここで作ります。

1

C とは何か(コンパイルの 4 段階)

C がどこで動いているかを整理し、はじめてのプログラムを gcc でコンパイル。-E・-S・-c で前処理・コンパイル・アセンブル・リンクを 1 段ずつ止めて中身を見る。

Free
2

gcc の警告を味方にする(-Wall -Wextra と C23)

練習用の Docker コンテナを用意し、gcc が黙って通す間違いを -Wall -Wextra で見つける。宣言の無い関数がエラーになった gcc 14 と、既定が C23 になった gcc 15 の変化も確かめる。

Free
3

変数と型(sizeof で大きさと範囲を測る)

型は「何バイト使い、どう読むか」の約束。sizeof と limits.h で大きさと範囲を測り、stdint.h の固定幅の型、0.1 + 0.2 の誤差、C23 の constexpr、初期化忘れの警告まで確かめる。

Free
4

演算子と型変換(割り算・unsigned・未定義動作)

整数の割り算は 0 の方向へ切り捨て、キャストは付ける場所で結果が変わる。-1 < 1u が偽になる理由と、signed のあふれが未定義動作であることを UBSan で確かめる。

🔒 ベーシック
5

標準入出力(printf の書式と scanf・fgets)

printf の幅・左寄せ・0 埋め・桁数で出力をそろえる。fgets で大きさを渡して 1 行を読み、scanf は戻り値を必ず見る。「数だけか」を fgets と sscanf で確かめる。

🔒 ベーシック
6

条件分岐(if・switch と [[fallthrough]])

else if は上から順に最初の 1 つだけ。&& と || の短絡評価、switch と break、うるう年の判定。わざと下へ続けるところは C23 の [[fallthrough]] で示し、break 忘れを警告で見分ける。

🔒 ベーシック
7

ループ(for・while・do-while と break・continue)

回数が決まっていれば for、条件の間なら while、最低 1 回なら do-while。FizzBuzz と九九の表、0 を 1 桁と数える do-while、break と continue で抜ける・飛ばすを確かめる。

🔒 ベーシック

Chapter 2 — 関数と配列(第8〜14回)

処理を関数に分け、変数の見える範囲と寿命を押さえます。配列・文字列・多次元配列まで進み、「配列の外を読む」間違いをサニタイザで見つけるところまで確かめます。

8

関数(プロトタイプ宣言・値渡し・[[nodiscard]])

処理に名前を付けて切り出す関数の形。プロトタイプ宣言で main を先に書き、引数が値のコピーであることを確かめる。C23 の [[nodiscard]] と、() が「引数なし」になった変化まで。

🔒 ベーシック
9

スコープと記憶域(static の 2 つの意味)

名前が使える範囲(スコープ)と、変数が生きている期間(記憶域期間)。グローバル変数・関数の中の static・ふつうの変数を並べて比べ、シャドーイングを -Wshadow で見つける。

🔒 ベーシック
10

配列(sizeof で個数を数え、外を読む間違いを捕まえる)

同じ型を添字で並べる配列。sizeof で個数を数え、C23 の {} で全部 0 にする。i <= 5 の書き間違いで配列の外を読むと合計 65550。AddressSanitizer と UBSan で捕まえる。

🔒 ベーシック
11

配列を関数に渡す(渡るのは先頭の場所)

配列を関数に渡すと、中身のコピーではなく先頭の要素の場所が渡る。関数の中の sizeof が 8 になる理由と、個数を一緒に渡す決まりごと、const で書き換えないと約束する書き方。

🔒 ベーシック
12

文字列は char の配列('\0' と strlen)

C の文字列は、char の配列の最後に '\0' を置いたもの。sizeof と strlen の違い、UTF-8 の日本語が 1 文字 3 バイトになること、'\0' の無い文字列が配列の外まで読まれる様子を確かめる。

🔒 ベーシック
13

文字列の関数(strcmp・snprintf・strchr)

string.h の関数で、文字列を比べる・コピーする・つなぐ・探す。== が場所の比較になる理由、strcpy と strncpy の落とし穴、snprintf の戻り値で「切れたか」を確かめる方法。

🔒 ベーシック
14

多次元配列(行優先と可変長配列の引数)

int m[3][4] は「int 4 つの配列」が 3 つ。メモリの中では行ごとに 1 列に並ぶ(行優先)。行と列の数を先に受け取る可変長配列の引数で行列を関数に渡し、三目並べの勝ちを判定する。

🔒 ベーシック

Chapter 3 — ポインタ(第15〜23回)

アドレスとポインタ、ポインタ演算、配列や文字列との関係、const、関数ポインタ、ポインタのポインタを順に扱います。最後は null や行き先の無いポインタがどう壊れるかを、サニタイザで確かめます。

15

アドレス(& で場所を取り、%p で番地を見る)

メモリは 1 バイトごとに番号の付いた箱の並び。&x で変数の場所を取り出し、%p で番地を見る。配列のとなりが型の大きさだけ離れていること、ASLR で番地が毎回変わっても差は変わらないことを確かめる。

🔒 ベーシック
16

ポインタ(int *p と *p・swap)

アドレスを入れておく変数がポインタ。int *p = &x で x を指し、*p で読み書きする。* の 2 つの意味を整理し、場所を渡す swap と出力用の引数で、呼んだ側の変数を書き換える。

🔒 ベーシック
17

ポインタ演算(p + 1 と ptrdiff_t)

p + 1 は 1 バイト先ではなく 1 つ先の要素。進むバイト数は型の大きさで決まり、ポインタどうしの差は要素の数(ptrdiff_t)。指してよいのは配列の中と「1 つうしろ」だけ、という決まりまで。

🔒 ベーシック
18

配列とポインタ(a[i] は *(a + i))

式の中の配列の名前は先頭の要素の場所に変わり、a[i] は *(a + i) の書きかえ。それでも sizeof と & では変わらず、配列はポインタではない。配列に代入できない理由と、引数の int a[] と int *a が同じである理由。

🔒 ベーシック
19

文字列とポインタ(char[] と const char *)

文字列を配列で持つか、ポインタで指すか。文字列リテラルは読むだけの領域(.rodata)に置かれ、書き換えると Segmentation fault。-Wwrite-strings と const char *、*dst++ = *src++ の読み方まで。

🔒 ベーシック
20

const とポインタ(const int * と int *const)

ポインタには「ポインタそのもの」と「指す先の値」の 2 つがあり、const を付ける場所で意味が変わる。右から読む覚え方と、約束を破ったときのエラー、引数の const が「中身を壊さない」約束になること。

🔒 ベーシック
21

関数ポインタ(表から選ぶ・qsort に渡す)

関数の場所を変数に入れて、あとから呼ぶ関数ポインタ。int (*op)(int, int) の読み方と typedef、記号と関数の表、コールバック、qsort に「比べ方」を渡す書き方と a - b を避ける理由。

🔒 ベーシック
22

ポインタのポインタ(int ** と argv)

ポインタの場所を入れる int **。* を付けるたびに 1 つたどり、int ** を受け取れば呼んだ側のポインタを書き換えられる。最後に nullptr を置く文字列の配列と、main の argv の正体。

🔒 ベーシック
23

nullptr と危ないポインタ(null・ぶら下がり・未初期化)

何も指していない null ポインタ、指していたものがもう無いぶら下がりポインタ、どこを指すか決まっていない未初期化のポインタ。C23 の nullptr、Segmentation fault とサニタイザの報告、gcc が 0 を返す機械語にしていた例まで。

🔒 ベーシック

Chapter 4 — メモリを自分で管理する(第24〜31回)

スタックとヒープの地図を描き、malloc・calloc・realloc・free で自分でメモリを借りて返します。リーク・解放後の使用・二重解放・バッファオーバーフローを、LeakSanitizer・valgrind・AddressSanitizer で見つけるところまで進みます。

24

スタックとヒープ(メモリの地図)

コード・読むだけ・データ・bss・ヒープ・スタックがメモリのどこにあるかを番地で確かめる。スタックは速いが小さく(既定で 8 MB)、終わらない再帰で使い切ると Segmentation fault。ヒープは借りて返す責任が自分にある。

🔒 ベーシック
25

malloc と free(nullptr の確かめと ckd_mul)

動かしてから人数を聞き、その人数分の場所を malloc で借りて free で返す。戻り値の nullptr を必ず確かめ、途中で終わるときも返す。-1 が size_t の最大に化ける入力を C23 の ckd_mul で止める。

🔒 ベーシック
26

calloc と realloc(伸びる配列を作る)

いくつ来るか分からないものを入れる「伸びる配列」。len と cap を持ち、いっぱいになったら realloc で 2 倍に。data = realloc(data, …) を避ける理由と、0 で埋めて借りる calloc。

🔒 ベーシック
27

メモリリーク(LeakSanitizer と「だれが free するか」)

借りた場所を返さないまま、指すポインタを失うとメモリリーク。LeakSanitizer の報告の読み方と、報告のあとに標準出力が消える落とし穴。借りた場所の持ち主を決めて関数の説明に書く。

🔒 ベーシック
28

valgrind(作り直さずにメモリの間違いを見つける)

プログラムを「まねの CPU」の上で動かし、メモリの読み書きを見張る valgrind。definitely lost・Invalid read・uninitialised value の 3 つの報告の読み方と、--error-exitcode でテストに組み込む方法。

🔒 ベーシック
29

解放後の使用と二重解放(use-after-free・double free)

free で返した場所を使う use-after-free と、同じ場所を 2 回返す double free。止まらずに変な値が出る怖さと、AddressSanitizer・glibc の止め方。free したら nullptr を入れる習慣で両方を防ぐ。

🔒 ベーシック
30

バッファオーバーフロー(strcpy の危なさと snprintf)

入れ物より長いものを、長さを確かめずに書き込むバッファオーバーフロー。strcpy が危ない理由と、-fstack-protector・_FORTIFY_SOURCE・AddressSanitizer の止め方。頼り切らずに snprintf で大きさを渡す。

🔒 ベーシック
31

動的な文字列(getline と strdup)

第4章の総まとめ。長さの分からない行を getline であふれずに読み、行数に合わせて char ** を realloc で伸ばし、strdup で各行を保存する。2 段階で返す free と、先に長さを数えて 1 回で借りる join。

🔒 ベーシック

Chapter 5 — 構造体とデータ構造(第32〜38回)

構造体でデータをまとめ、パディング・共用体・列挙型でメモリの中の並びを見ます。最後は連結リスト・スタック・キューを C で自作し、ポインタとメモリ管理を組み合わせて使います。

32

構造体(. と -> と qsort)

関係する値を 1 つの型にまとめる struct。名前を指定した初期化と複合リテラル、値渡しとポインタ渡しの違いと ->。= でまるごとコピーでき、== では比べられない。構造体の配列を qsort で並べる。

🔒 ベーシック
33

構造体とメモリ配置(パディングとアラインメント)

構造体の大きさはメンバを足しただけとは限らない。char・int・char は 12 バイト、int・char・char は 8 バイト。アラインメントとすき間(パディング)の理由、static_assert での大きさの固定、-Wpadded。

🔒 ベーシック
34

列挙型と共用体(enum・union)

名前の付いた整数の enum と、メンバが同じ場所を共有する union。switch の case 漏れを -Wall で見つけ、union でリトルエンディアンと float のビットを見る。enum と組み合わせたタグ付き共用体まで。

🔒 ベーシック
35

連結リスト(節を next でつなぐ)

malloc した節(ノード)を、次の節の場所でつないだ連結リスト。先頭に足すと head が変わるので struct node ** を受け取る。全部返すときは free の前に next を覚える。for でたどりながら free すると use-after-free。

🔒 ベーシック
36

連結リストの操作(struct node ** で分けずに書く)

うしろへの追加・小さい順の挿入・削除・逆順。「次を指すポインタの場所」struct node ** を使うと、先頭も途中も同じ書き方になる。*pp = victim->next の 1 行で消し、prev・cur・next の 3 つで向きを変える。

🔒 ベーシック
37

スタックを作る(かっこの対応を調べる)

あとに入れたものから出すスタック(LIFO)を、第26回の伸びる配列で作る。開きかっこを push、閉じかっこで pop して比べ、対応しない・開きが足りない・閉じていないの 3 つの NG を見分ける。

🔒 ベーシック
38

キューを作る(リングバッファ)

先に入れたものから出すキュー(FIFO)を、輪になった配列(リングバッファ)で作る。% cap で配列の端から先頭へ戻り、head と len を持って、いっぱいと空をはっきり見分ける。

🔒 ベーシック

Chapter 6 — ファイルと分割コンパイル(第39〜45回)

テキストとバイナリのファイル入出力、ヘッダでの分割コンパイル、make、プリプロセッサ、静的・共有ライブラリを扱います。最後はコマンドライン引数を getopt で読み、道具としてのプログラムに仕上げます。

39

ファイル入出力(fopen・fgetc と errno)

fopen で開いて FILE * を受け取り、fclose で閉じる。fgetc が int を返す理由と EOF、自作の wc を本物と比べる。失敗の理由を errno・strerror・perror で出し、"r"・"w"・"a" のモードを使い分ける。

🔒 ベーシック
40

バイナリファイル(fwrite・fread と fseek)

構造体をメモリの中の形のまま保存するバイナリの読み書き。fwrite で書いたファイルを od でのぞき、リトルエンディアンの並びを確かめる。fseek・ftell で好きな件を拾い読みし、持ち運ぶときの注意まで。

🔒 ベーシック
41

分割コンパイルとヘッダ(undefined reference を読む)

ヘッダに宣言、.c に定義を書き、ファイルごとにコンパイルしてリンクでつなぐ。インクルードガードと extern・static。リンクのエラー undefined reference と multiple definition の意味と直し方。

🔒 ベーシック
42

make(変えたところだけビルドする)

Makefile に「作るもの: 材料」とタブで始まる作り方を書き、変えたところに関係するところだけビルドし直す make。自動変数とパターンルール、-MMD でヘッダの依存関係を書き出す方法、.PHONY。

🔒 ベーシック
43

プリプロセッサ(#define・#if・#embed)

# で始まる行をコンパイルの前に文字として処理するプリプロセッサ。( ) で囲まないマクロの落とし穴を gcc -E で確かめ、# と ##、#ifdef と -DDEBUG、C23 の #embed でファイルを埋め込む。

🔒 ベーシック
44

ライブラリ(静的 .a と共有 .so)

自分の関数を 2 種類のライブラリにまとめる。ar で作る静的ライブラリ(.a)は実行ファイルにコピーされ、-fPIC と -shared で作る共有ライブラリ(.so)は動かすときに読み込まれる。LD_LIBRARY_PATH と nm・readelf。

🔒 ベーシック
45

コマンドライン引数(getopt と strtol で head を作る)

-n 3 file… のようなオプションと引数を getopt で読み、数は atoi ではなく strtol で安全に読む。head のようなコマンドを作り、使い方の間違い(2)と実行時の失敗(1)で終了ステータスを分ける。

🔒 ベーシック

Chapter 7 — システムに触れる(第46〜50回)

open・read・write のシステムコールを strace でのぞき、fork・exec・pipe・シグナルでプロセスを操ります。最後は全50回の内容をまとめて、パイプとリダイレクトの使える簡易シェルを作ります。

46

システムコール(open・read・write と strace)

OS にしかできないことをたのむ入口がシステムコール。open・read・write・close でファイルをコピーし、strace で OS への依頼をそのまま見る。ファイルディスクリプタ 0・1・2 と、stdio との違い。

🔒 ベーシック
47

プロセス(fork と wait)

動いているプログラムがプロセス。fork は 1 回呼んで 2 回返り、子には 0、親には子の pid が返る。waitpid で子の終わりを待って終了ステータスを受け取り、fork の前に fflush する理由と、行の順番が決まらないことまで。

🔒 ベーシック
48

exec とパイプ(dup2 でプログラムをつなぐ)

fork した子を execvp で別のプログラムに置きかえ、親は wait で待つ。exec は成功すると戻らない。pipe で 2 つの口を作り、dup2 で標準入出力を付けかえて seq 5 | grep 3 を自分で組み立てる。

🔒 ベーシック
49

シグナル(SIGINT を受け取って後始末する)

OS やほかのプロセスから届く合図がシグナル。sigaction で SIGINT・SIGTERM のハンドラを登録し、volatile sig_atomic_t に「来た」と印を付けるだけにして、後始末は main で。SIGINT で終わると終了ステータスは 130。

🔒 ベーシック
50

簡易シェルを作る(総まとめ)

これまでの道具を全部使って、小さなシェル msh を作る。strtok で 1 行を argv に分け、cd・exit は組み込み、外部コマンドは fork と execvp、| は pipe と dup2、Ctrl+C は signal で扱う。足りない機能は練習課題に。

🔒 ベーシック

全50レッスンを終えたら、次は C で手で管理していたメモリを言語が守ってくれる Rust 入門コース や、システムコールとシェルの先にある Linux 入門コース・Linux システム管理入門コース へ。メンバーシップで全コースが解放されます。