cv.cm

コーディング · プログラミング言語

C言語入門チュートリアル(初心者向け)

約10分で読めます

結論

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 の書式
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;
}

配列を関数に渡すと、先頭要素へのポインタに変換されます。そのため 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 のようにバージョンを明示すると、動作が一定になります。

コーディング講座の一覧