閉じる

【C言語】配列インデックスは0から 範囲外アクセスを防ぐコツ

C言語で配列を扱う際、添字(インデックス)の数え方と範囲の考え方を正しく理解することが、安全で読みやすいプログラム作成の第一歩です。

本記事では、インデックスが0から始まる理由と基本操作、範囲外アクセスを防ぐ実践的なコツ、文字配列の注意点まで丁寧に解説します。

インデックス範囲の確認は、バグやクラッシュ、情報漏えいを防ぐ最重要ポイントです

C言語の配列アクセスの基本

インデックスとは

定義と数え方

配列は同じ型の要素を連続して並べた入れ物です。

配列の各要素には0から始まる連番の番号(インデックス)が付いています

例えば長さ5の配列なら、有効なインデックスは0, 1, 2, 3, 4です。

人間の数え方(1から)ではなく、C言語は0から数える点に慣れてください。

図で理解する

次の配列int a[5]に対して、インデックスと値の対応を表にします。

インデックスは先頭が0、末尾は要素数 - 1です。

インデックスa[インデックス]の値
010
120
230
340
450

サンプルコード(特定の要素にアクセス)

C言語
#include <stdio.h>

int main(void) {
    int a[] = {10, 20, 30, 40, 50};

    // 先頭、真ん中、末尾にアクセスして表示
    printf("a[0] = %d\n", a[0]); // 先頭
    printf("a[2] = %d\n", a[2]); // 真ん中
    printf("a[4] = %d\n", a[4]); // 末尾(要素数5なら最大インデックスは4)

    // 注意: a[5] などの範囲外アクセスは未定義動作(危険)です
    return 0;
}
実行結果
a[0] = 10
a[2] = 30
a[4] = 50

配列の読み書きは必ず「0から要素数−1まで」に限定するのが基本です。

先頭と末尾の取り出し

末尾の計算ルール

末尾のインデックスは要素数 - 1です。

要素数がわかっているなら、末尾の要素はa[要素数 - 1]で取り出します。

わからない場合はsizeofで要素数を計算します(後述)。

実例(先頭と末尾を表示)

C言語
#include <stdio.h>

int main(void) {
    int a[] = {3, 1, 4, 1, 5, 9};
    size_t n = sizeof(a) / sizeof(a[0]); // 配列の要素数(同一スコープ内でのみ有効)

    // 先頭と末尾を安全に参照
    printf("first = %d\n", a[0]);
    printf("last  = %d (index %zu)\n", a[n - 1], n - 1);
    return 0;
}
実行結果
first = 3
last  = 9 (index 5)

末尾アクセスは「n−1」を使うのが鉄則で、「n」や「n+1」は必ず範囲外です。

forで配列要素を順にアクセス

基本形

インデックスを0から要素数 - 1まで増やしていくループが基本です。

条件はi < nと書くのが読みやすく安全です。

C言語
#include <stdio.h>

int main(void) {
    int a[] = {8, 6, 7, 5, 3, 0, 9};
    size_t n = sizeof(a) / sizeof(a[0]);

    // 先頭から末尾まで順にアクセス
    for (size_t i = 0; i < n; i++) {
        printf("a[%zu] = %d\n", i, a[i]);
    }
    return 0;
}
実行結果
a[0] = 8
a[1] = 6
a[2] = 7
a[3] = 5
a[4] = 3
a[5] = 0
a[6] = 9

ループ条件は「i < n」の形で書き、境界に等号を入れないことが範囲外アクセス防止に直結します。

範囲外アクセスを防ぐコツ

有効範囲は0 <= i < 要素数

なぜ0からn−1なのか

配列は要素がメモリ上で連続して並び、先頭要素の直後に2番目、その次に3番目…という配置になります。

先頭から数えて「i個分先」にある要素がa[i]なので、iは0から始まります。

有効範囲は常に0 <= i && i < nです。

実用チェック関数

配列アクセス前に、添字が有効かどうかを関数で判定すると安全です。

C言語
#include <stdio.h>

int in_range_int(int i, size_t n) {
    // iが負でも弾けるようintで受け、両側チェックを行う
    return (i >= 0) && ((size_t)i < n);
}

int main(void) {
    int a[] = {10, 20, 30, 40, 50};
    size_t n = sizeof(a) / sizeof(a[0]);

    for (int i = -1; i <= 5; i++) {
        if (in_range_int(i, n)) {
            printf("i=%d: OK a[%d]=%d\n", i, i, a[i]);
        } else {
            printf("i=%d: NG (valid range: 0 <= i < %zu)\n", i, n);
        }
    }
    return 0;
}
実行結果
i=-1: NG (valid range: 0 <= i < 5)
i=0: OK a[0]=10
i=1: OK a[1]=20
i=2: OK a[2]=30
i=3: OK a[3]=40
i=4: OK a[4]=50
i=5: NG (valid range: 0 <= i < 5)

範囲外アクセスは未定義動作で、クラッシュや誤動作、セキュリティ問題につながります。

常に範囲チェックを意識しましょう。

ループ条件の基本

よくある誤りと正解

配列の反復で、次のような書き方は避けます。

  • 誤りになりやすい例: for (i = 0; i <= n; i++) (最後にi == nで範囲外)
  • 正しい基本形: for (i = 0; i < n; i++)
  • 読みやすいが注意が必要: for (i = 0; i <= n - 1; i++) (論理的に正しいが、n - 1の計算ミスが入りやすい)

条件は常に「i < n」の形に統一し、等号(=)を入れないことを習慣化すると、オフバイワンを減らせます。

入力から得た添字は必ずチェック

例: ユーザー入力で要素を参照

ユーザーが指定したインデックスで要素を表示する例です。

入力値の検証と範囲チェックを必ず行います

C言語
#include <stdio.h>

int main(void) {
    int a[] = {4, 8, 15, 16, 23, 42};
    size_t n = sizeof(a) / sizeof(a[0]);

    printf("0〜%zu の範囲でインデックスを入力してください: ", n - 1);
    int idx;
    if (scanf("%d", &idx) != 1) {
        // 読み取り失敗(数値以外など)
        printf("入力エラーです。\n");
        return 1;
    }

    if (idx < 0 || (size_t)idx >= n) {
        printf("範囲外です。valid range: [0, %zu)\n", n);
        return 1;
    }

    printf("a[%d] = %d\n", idx, a[idx]);
    return 0;
}

入力例と出力例:

実行結果
0〜5 の範囲でインデックスを入力してください: 2
a[2] = 15

別の入力例:

実行結果
0〜5 の範囲でインデックスを入力してください: 6
範囲外です。valid range: [0, 6)

ユーザー入力は常に疑う

読み取りの戻り値チェックと範囲チェックの2段構えで守りを固めます。

オフバイワンを避ける書き方

境界を「半開区間」で考える

Cでは配列の範囲を[0, n)(0以上n未満)の半開区間として捉えると、末尾が「含まれない」ことが明確になります。

「含む/含まない」を曖昧にしない表現が重要です。

NG/OKのコード例

C言語
// NG例: <= n だと最後が範囲外になる
// for (size_t i = 0; i <= n; i++) { ... }

// OK例: 半開区間の原則に従う
for (size_t i = 0; i < n; i++) {
    // 要素 a[i] を安全に扱える
}

添字の型にも注意

負の値を扱う可能性がある入力ならintで受けてからチェックし、配列アクセス時にsize_tへ変換します。

負のままsize_tへ暗黙変換すると大きな値に化けるため、必ず先にidx >= 0で検証してからキャストします。

要素数の安全な求め方と管理

sizeofで要素数を求める

基本式

同じスコープ内で定義された「真の配列」なら、sizeofで要素数を求められます。

C言語
#include <stdio.h>

int main(void) {
    int a[] = {1, 2, 3, 4, 5};
    size_t n = sizeof(a) / sizeof(a[0]);
    printf("要素数 n = %zu\n", n);
    return 0;
}
実行結果
要素数 n = 5

注意: 関数引数では使えない

配列を関数に渡すと「ポインタ」として扱われるため、sizeofで要素数は求められません。

C言語
#include <stdio.h>

void show_count(int a[], size_t n_given) {
    size_t wrong = sizeof(a) / sizeof(a[0]); // aはint*として扱われる
    printf("渡されたn = %zu, sizeofで求めたwrong = %zu (誤り)\n", n_given, wrong);
}

int main(void) {
    int a[] = {10, 20, 30, 40, 50};
    size_t n = sizeof(a) / sizeof(a[0]);
    show_count(a, n);
    return 0;
}

実行結果の一例(環境により異なります):

実行結果
渡されたn = 5, sizeofで求めたwrong = 2 (誤り)

関数内では必ず要素数を別引数で渡すか、構造体等にセットして一緒に扱うのが基本です。

定数で配列サイズを一元管理

enumで決める(推奨)

配列サイズを複数箇所で使うなら、1か所で定義して全体で参照します。

Cではenumを使うと「コンパイル時定数」になり、配列長にも使えて便利です。

C言語
#include <stdio.h>

enum { N = 8 }; // 配列サイズを一元管理

int main(void) {
    int a[N] = {0}; // Nは配列長として使える
    for (size_t i = 0; i < N; i++) {
        a[i] = (int)(i * i); // 0,1,4,9,16,...
    }

    // Nを使って安全に走査
    for (size_t i = 0; i < N; i++) {
        printf("a[%zu] = %d\n", i, a[i]);
    }
    return 0;
}

マクロやconstの使い分け

  • #define N 8: プリプロセッサ置換。単純で広く使われますが、型がなくデバッグ時に追いにくい欠点があります。
  • enum { N = 8 };: 型安全で、配列長に使えるコンパイル時定数。初心者にも扱いやすく推奨です。
  • const int N = 8;: 実装や文脈によっては配列長に使えない場面があります。配列長に使う定数はenum#defineが堅実です。

サイズは「1か所で決めて全体で使う」。

これが修正時の不整合とバグを防ぎます。

文字配列のアクセス注意

終端文字を考慮してインデックスを決める

文字列は最後に’\0’が入る

Cの文字列は'\0'(ヌル文字)で終端します。

「文字数+1」が必要で、例えば"Hello"は5文字ですが、配列はchar s[6]が適切です。

C言語
#include <stdio.h>

int main(void) {
    char s[6] = "Hello"; // s[5] には '
#include <stdio.h>
int main(void) {
char s[6] = "Hello"; // s[5] には '\0' が入る
for (size_t i = 0; i < sizeof(s) / sizeof(s[0]); i++) {
// 文字と数値(ASCIIコード)を併記して確認
printf("s[%zu] = '%c' (code=%d)\n", i, s[i], (unsigned char)s[i]);
}
return 0;
}
' が入る for (size_t i = 0; i < sizeof(s) / sizeof(s[0]); i++) { // 文字と数値(ASCIIコード)を併記して確認 printf("s[%zu] = '%c' (code=%d)\n", i, s[i], (unsigned char)s[i]); } return 0; }
実行結果
s[0] = 'H' (code=72)
s[1] = 'e' (code=101)
s[2] = 'l' (code=108)
s[3] = 'l' (code=108)
s[4] = 'o' (code=111)
s[5] = ' ' (code=0)

終端'\0'は文字としては表示されませんが、確実に存在します。

ここを書き潰すと文字列が壊れます。

書き込み時は終端分の余裕を残す

例: 終端を保証しながらコピーする

固定長の文字配列に書き込むときは、最大でも容量−1文字までにして、最後に'\0'を入れます。

C言語
#include <stdio.h>

// src を dest に最大 cap-1 文字だけコピーし、必ず終端 '
#include <stdio.h>
// src を dest に最大 cap-1 文字だけコピーし、必ず終端 '\0' を付ける
void safe_copy(char dest[], size_t cap, const char src[]) {
size_t i = 0;
if (cap == 0) return;            // 容量0なら何もしない
while (i + 1 < cap && src[i] != '\0') {
dest[i] = src[i];
i++;
}
dest[i] = '\0'; // 必ず終端する
}
int main(void) {
char dest[8]; // 7文字 + 終端の合計8
const char *src = "Programming"; // かなり長い
safe_copy(dest, sizeof(dest), src);
printf("dest = \"%s\"\n", dest); // 長すぎる分は切り詰められる(例: "Program")
return 0;
}
' を付ける void safe_copy(char dest[], size_t cap, const char src[]) { size_t i = 0; if (cap == 0) return; // 容量0なら何もしない while (i + 1 < cap && src[i] != '
#include <stdio.h>
// src を dest に最大 cap-1 文字だけコピーし、必ず終端 '\0' を付ける
void safe_copy(char dest[], size_t cap, const char src[]) {
size_t i = 0;
if (cap == 0) return;            // 容量0なら何もしない
while (i + 1 < cap && src[i] != '\0') {
dest[i] = src[i];
i++;
}
dest[i] = '\0'; // 必ず終端する
}
int main(void) {
char dest[8]; // 7文字 + 終端の合計8
const char *src = "Programming"; // かなり長い
safe_copy(dest, sizeof(dest), src);
printf("dest = \"%s\"\n", dest); // 長すぎる分は切り詰められる(例: "Program")
return 0;
}
') { dest[i] = src[i]; i++; } dest[i] = '
#include <stdio.h>
// src を dest に最大 cap-1 文字だけコピーし、必ず終端 '\0' を付ける
void safe_copy(char dest[], size_t cap, const char src[]) {
size_t i = 0;
if (cap == 0) return;            // 容量0なら何もしない
while (i + 1 < cap && src[i] != '\0') {
dest[i] = src[i];
i++;
}
dest[i] = '\0'; // 必ず終端する
}
int main(void) {
char dest[8]; // 7文字 + 終端の合計8
const char *src = "Programming"; // かなり長い
safe_copy(dest, sizeof(dest), src);
printf("dest = \"%s\"\n", dest); // 長すぎる分は切り詰められる(例: "Program")
return 0;
}
'; // 必ず終端する } int main(void) { char dest[8]; // 7文字 + 終端の合計8 const char *src = "Programming"; // かなり長い safe_copy(dest, sizeof(dest), src); printf("dest = \"%s\"\n", dest); // 長すぎる分は切り詰められる(例: "Program") return 0; }
実行結果
dest = "Program"

終端を忘れると、その後の表示や比較でゴミを読み続けてしまいます

常に終端の1バイトを確保・設定しましょう。

まとめ

配列の要素アクセスは、C言語における基礎でありながら、境界条件を間違えると重大な不具合になりやすい箇所です。本記事の要点を最後に整理します。配列インデックスは0から始まり、有効範囲は[0, n)です

ループ条件はi < nを徹底し、ユーザー入力など外部から得た添字は必ずチェックします。

要素数はsizeof(a)/sizeof(a[0])で求め(同一スコープ内に限る)、配列サイズはenum等で一元管理すると保守性が高まります。

文字配列は終端'\0'の分を常に見積もることを忘れず、書き込み時は容量−1文字までに抑えて終端を保証してください。

境界を守る小さな習慣が、安全で信頼できるCプログラムを支えます

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!