C言語を習得する過程で、多くのプログラマが最初に直面する大きな壁の一つが「関数への配列の受け渡し」です。
C言語において配列は、他の変数型とは異なる特殊な振る舞いを見せます。
特に関数の引数として配列を渡す際、それは単なるデータの集合ではなく、「メモリ上の先頭アドレス」を指すポインタへと姿を変えます。この性質を正しく理解していないと、意図しないメモリ書き換えや、配列のサイズ誤認によるバグを引き起こす原因となります。
本記事では、2026年現在の標準的な開発スタイルに基づき、ポインタの仕組みから要素数の適切な管理方法、そして実践的な実装パターンまでを詳しく解説します。
C言語における配列と関数の基本構造
C言語で配列を関数に渡す際、最も重要な原則は「配列そのものを値としてコピーして渡すことはできない」という点です。
構造体などとは異なり、配列を引数に指定すると、コンパイラはそれを配列の先頭要素へのポインタとして解釈します。
配列がポインタに「劣化」する仕組み
プログラミングの世界では、これを「Array Decay(配列の減衰・劣化)」と呼びます。
例えば、int data[10] という配列を関数に渡すと、関数側で受け取るのは int * 型のポインタです。
この挙動により、大きな配列を関数に渡す際でも、メモリのコピーが発生しないため実行効率が非常に高いというメリットがあります。
しかし、同時に「関数内部では配列の要素数がわからなくなる」という制約も生まれます。
引数宣言のバリエーション
関数のプロトタイプ宣言において、配列を受け取る記述にはいくつかの表現方法があります。
以下の3つは、コンパイラにとってはすべて「int型へのポインタ」として同一に扱われます。
// パターン1:ポインタ表記
void processArray1(int *arr);
// パターン2:配列表記(要素数省略)
void processArray2(int arr[]);
// パターン3:配列表記(要素数指定、ただしコンパイラには無視されることが多い)
void processArray3(int arr[10]);
実務上は、パターン2の int arr[] が好まれます。
これは、読み手に対して「この引数は単一の変数へのポインタではなく、配列として扱われることを意図している」というメッセージを明確に伝えられるからです。
配列の要素数を渡す重要性と実装パターン
前述の通り、関数内では受け取った配列のサイズを知ることができません。
関数内部で sizeof(arr) を実行しても、返ってくるのは「ポインタ変数のサイズ(通常は8バイトまたは4バイト)」であり、配列全体のサイズではありません。
そのため、配列を扱う関数を設計する際は、必ず「要素数を別の引数として渡す」のが鉄則です。
要素数を引数として渡す標準的な実装
以下に、配列とその要素数をセットで渡す最も一般的な実装例を示します。
#include <stdio.h>
// 配列の要素を合計する関数
// arr: 配列の先頭ポインタ, size: 要素数
int sumArray(int arr[], int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += arr[i]; // ポインタ経由で各要素にアクセス
}
return sum;
}
int main() {
int numbers[] = {10, 20, 30, 40, 50};
// 配列全体のサイズ / 先頭要素のサイズ で要素数を算出
int count = sizeof(numbers) / sizeof(numbers[0]);
int total = sumArray(numbers, count);
printf("配列の合計値: %d\n", total);
printf("要素数: %d\n", count);
return 0;
}
配列の合計値: 150
要素数: 5
このコードにおいて、sizeof(numbers) は main 関数内では正しく 20 (4バイト * 5要素) を返しますが、sumArray 関数の中では期待した動作になりません。
必ず呼び出し元でサイズを計算し、引数として渡す習慣をつけましょう。
多次元配列を引数に渡す方法
2次元配列などの多次元配列を関数に渡す場合は、さらに注意が必要です。
C言語の多次元配列はメモリ上で一列に並んでいますが、コンパイラが array[i][j] というアクセスを正しく計算するためには、「列の数(2番目以降の添字のサイズ)」が固定されている必要があります。
2次元配列の宣言ルール
2次元配列を引数にする場合、最初の添字のサイズは省略可能ですが、2番目以降は明示しなければなりません。
#include <stdio.h>
// 2次元配列を受け取る関数(列サイズは3と固定)
void printMatrix(int matrix[][3], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", matrix[i][j]);
}
printf("\n");
}
}
int main() {
int myMatrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
printMatrix(myMatrix, 2);
return 0;
}
このように、列数が固定される実装は汎用性に欠けるという課題があります。
これを解決するためには、後述する「ポインタのポインタ」を利用するか、可変長配列(VLA)の機能を検討する必要があります。
応用パターン:より柔軟な配列操作の実装
2026年現在のC言語開発では、従来の固定長配列だけでなく、より安全で柔軟な実装パターンが推奨されます。
可変長配列(VLA: Variable Length Array)の活用
C99規格以降(およびそれ以降の標準)では、関数の引数リストの中で、先に渡した引数を配列のサイズ指定に使うことができます。これにより、動的なサイズの多次元配列も扱いやすくなります。
#include <stdio.h>
// VLAを利用した柔軟な行列表示関数
void printDynamicMatrix(int rows, int cols, int matrix[rows][cols]) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%3d ", matrix[i][j]);
}
printf("\n");
}
}
int main() {
int r = 3, c = 4;
int table[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
printDynamicMatrix(r, c, table);
return 0;
}
この記述方法のメリットは、matrix[i][j] という直感的なアクセスを維持しつつ、関数の汎用性を高められる点にあります。
ただし、環境(一部の組み込み向けコンパイラなど)によってはVLAがサポートされていない場合もあるため、ターゲット環境の確認が必要です。
構造体に配列をラップして渡すパターン
「配列のコピーが発生しても構わない」「要素数とセットで管理したい」という場合には、配列を構造体のメンバーに含めるという手法もあります。
構造体は関数に渡す際に「値渡し(コピー)」されるため、配列そのものが関数内に複製されます。
typedef struct {
int data[10];
int length;
} ArrayWrapper;
void modifyWrapper(ArrayWrapper w) {
w.data[0] = 999; // コピーを操作しているため、呼び出し元には影響しない
}
この方法は、データのカプセル化には役立ちますが、大きな配列を扱う場合はスタックメモリを大量に消費するため、通常は構造体のポインタ(ArrayWrapper *w)を渡すのが一般的です。
メモリ管理とセキュリティのベストプラクティス
配列とポインタを扱う以上、バッファオーバーフローなどの脆弱性には細心の注意を払わなければなりません。
const修飾子の積極的な活用
関数内で配列の値を書き換える必要がない場合は、必ず const 修飾子を付与しましょう。
これにより、意図しないデータの変更をコンパイル時に検知でき、コードの堅牢性が向上します。
// 読み取り専用であることを明示
void displayArray(const int *arr, int size) {
for (int i = 0; i < size; i++) {
// arr[i] = 0; // コンパイルエラーになるため安全
printf("%d ", arr[i]);
}
}
境界チェックの徹底
C言語自体には配列の境界チェック機能がありません。
そのため、関数内でループを回す際には、必ず引数で受け取った size を超えないよう厳密に管理する必要があります。
また、呼び出し側で size に負の値や異常に大きな値が入らないようバリデーションを行うことも重要です。
実装パターンの比較表
これまでに紹介した主な実装パターンの特徴を以下の表にまとめました。
| 手法 | メリット | デメリット | 主な用途 |
|---|---|---|---|
| ポインタ + 要素数引数 | 最も標準的で高速。汎用性が高い。 | 要素数の渡し忘れや誤指定のリスク。 | 一般的な1次元配列の処理。 |
| 可変長配列 (VLA) | 多次元配列を直感的に扱える。 | 一部の古い環境や制約下で非対応。 | 行列演算などの数値計算。 |
| 構造体ラップ | 要素数とデータを一体化できる。 | コピーコストが発生する場合がある。 | 小規模な固定データ群の管理。 |
まとめ
C言語において配列を関数に渡すという行為は、実際には「配列の先頭要素へのポインタを渡す」ことに他なりません。
この基本原則を理解することが、C言語プログラミングにおけるメモリ操作をマスターする第一歩となります。
記事のポイントを振り返ると、以下の3点が特に重要です。
- 関数内では配列のサイズを
sizeofで取得できないため、必ず要素数を別の引数として渡す。 - 多次元配列を渡す際は、列サイズを固定するか、VLA(可変長配列)を活用して構造を明示する。
- データの保護のために
const修飾子を活用し、バッファオーバーフローを未然に防ぐ。
これらのパターンを適切に使い分けることで、バグが少なく、メンテナンス性の高いコードを記述できるようになります。
配列とポインタの関係性を「メモリ上の連続したデータへのアクセス方法」として捉え直し、安全なプログラム設計を心がけましょう。
