コーディング · プログラミング言語
C言語入門チュートリアル(初心者向け)
結論
Cは小さく高速なコンパイル言語で、ポインタを通じてメモリに直接アクセスできます。.c ファイルに main を書き、gcc hello.c -o hello でコンパイルし、./hello で実行します。固定サイズの型、関数、配列とヌル終端文字列、ポインタとアドレス、malloc と free による手動のメモリ管理、戻り値によるエラー処理が中心的な内容です。
Cとは何か、何に使われるか
Cは1970年代初めにBell研究所のDennis RitchieがUNIXオペレーティングシステムを書くために作りました。ISOによって標準化されており、主な版はC99、C11、C17、C23です。Cは実行時のオーバーヘッドがごく小さい機械語にコンパイルされるため、ほぼあらゆる環境で動きます。OSカーネル(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;
}配列を関数に渡すと、先頭要素へのポインタに変換されます。そのため 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による動的メモリ
ローカル変数はスタックに置かれ、関数が戻ると消えます。サイズが実行時にしか分からないデータや、関数より長く存在させたいデータは、malloc でヒープに確保し、free で解放します。malloc が成功するたびに、free をちょうど1回呼ぶ必要があります。
#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 は char ではなく int であることに注意してください。そのため特別な値 EOF を保持できます。
初心者がよくやる間違い
- 条件式で
==の代わりに=を使う。警告(-Wall)でほとんどの場合を検出できます。 - 文字列バッファのサイズを決めるときに、終端の
'\0'の分を忘れる。 getsや、長さを制限しないscanf("%s", buf)を使う。代わりにfgets(buf, sizeof buf, stdin)を使います。- ローカル変数へのポインタを返す。関数から戻った後、その変数はもう存在しません。
printfの書式と型を一致させない。たとえばsize_tに%dを使う。
次のステップ
言語と標準ライブラリのリファレンスとして、cppreference.comのCのセクションを使い、規格そのものはISOのワーキンググループ(WG14)のページで読みましょう。構造体、連結リスト、ヘッダファイル、make でビルドする複数ファイルのプログラムで練習します。デバッガ(gdb または lldb)とValgrindやAddressSanitizerがあれば、何時間も節約できます。その後は、同じ考え方を土台にしてより安全にしたC++やRustにも進んでみましょう。
よくある質問
Cは今でも学ぶ価値がありますか?
はい。今でもOS、組み込み機器、性能が重要なライブラリで使われています。メモリやポインタ、コンパイル済みコードの動作を学べるので、ほかのどの言語を使うときにも役立ちます。
CとC++の違いは何ですか?
C++はCの拡張として始まり、クラス、テンプレート、参照、例外、大規模な標準ライブラリを追加しました。Cは小さく手続き型のままです。ほとんどのCコードは有効なC++に近いですが、規格は別々です。
Cのプログラムがセグメンテーション違反でクラッシュするのはなぜですか?
自分が所有していないメモリにアクセスしたためです。多くは、null、未初期化、解放済みのポインタ経由や、配列の末尾を越えたインデックスが原因です。デバッガやAddressSanitizerで実行すると、正確な行が分かります。
どのC規格を使うべきですか?
C17は現行のコンパイラで広くサポートされており、C23が最新の規格です。-std=c17 のようにバージョンを明示すると、動作が一定になります。