C言語で配列を扱う際、添字(インデックス)の数え方と範囲の考え方を正しく理解することが、安全で読みやすいプログラム作成の第一歩です。
本記事では、インデックスが0から始まる理由と基本操作、範囲外アクセスを防ぐ実践的なコツ、文字配列の注意点まで丁寧に解説します。
インデックス範囲の確認は、バグやクラッシュ、情報漏えいを防ぐ最重要ポイントです。
C言語の配列アクセスの基本
インデックスとは
定義と数え方
配列は同じ型の要素を連続して並べた入れ物です。
配列の各要素には0から始まる連番の番号(インデックス)が付いています。
例えば長さ5の配列なら、有効なインデックスは0, 1, 2, 3, 4
です。
人間の数え方(1から)ではなく、C言語は0から数える点に慣れてください。
図で理解する
次の配列int a[5]
に対して、インデックスと値の対応を表にします。
インデックスは先頭が0、末尾は要素数 - 1
です。
インデックス | a[インデックス]の値 |
---|---|
0 | 10 |
1 | 20 |
2 | 30 |
3 | 40 |
4 | 50 |
サンプルコード(特定の要素にアクセス)
#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
で要素数を計算します(後述)。
実例(先頭と末尾を表示)
#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
と書くのが読みやすく安全です。
#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
です。
実用チェック関数
配列アクセス前に、添字が有効かどうかを関数で判定すると安全です。
#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」の形に統一し、等号(=)を入れないことを習慣化すると、オフバイワンを減らせます。
入力から得た添字は必ずチェック
例: ユーザー入力で要素を参照
ユーザーが指定したインデックスで要素を表示する例です。
入力値の検証と範囲チェックを必ず行います。
#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のコード例
// 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
で要素数を求められます。
#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
で要素数は求められません。
#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
を使うと「コンパイル時定数」になり、配列長にも使えて便利です。
#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]
が適切です。
#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'
を入れます。
#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プログラムを支えます。