C言語を学ぶ上で、多くのプログラミング初学者が最初に行き詰まる難所の一つが「2次元配列の関数への受け渡し」です。
多次元配列は画像処理、行列計算、ゲームのマップデータ保持など、実務的な開発において欠かせないデータ構造です。
しかし、C言語では配列が関数に渡される際に「ポインタ」へと変換されるという特殊な性質を持っているため、記述方法が複雑になりがちです。
本記事では、2026年現在のモダンなC言語開発においても通用する、2次元配列を引数として正しく扱うための具体的な手法を詳しく解説します。
ポインタ、配列記法、そして可変長配列(VLA)といった多様な選択肢の中から、状況に応じた最適な使い分けをマスターしましょう。
C言語における2次元配列のメモリ構造を理解する
2次元配列を正しく関数に渡すためには、まずそのデータがメモリ上でどのように配置されているかを正確に把握する必要があります。
C言語における2次元配列は、概念的には「行」と「列」を持つ表のような構造ですが、実際の物理メモリ上では1次元の連続した領域として確保されます。
例えば、int matrix[2][3]という配列がある場合、メモリ上には6個の整数型データが隙間なく一列に並んでいます。
関数に配列を渡す際、C言語の仕様では「配列の先頭要素へのポインタ」のみがコピーされます。
1次元配列であれば、関数側はint *ptrとして受け取れば十分ですが、2次元配列の場合は「1行のサイズがどれくらいか」という情報がポインタ演算に必要になります。
この「1行の列数」という情報が欠落していると、コンパイラはmatrix[i][j]というアクセスを正しいメモリアドレスに変換することができません。
したがって、引数として2次元配列を渡す際には、列のサイズを明示することが文法上の絶対条件となります。
方法1:固定サイズの配列記法で渡す
最も直感的で古くから使われている手法は、関数の引数宣言で列の要素数を直接指定する方法です。
この方法は、扱うデータのサイズがコンパイル時に確定している場合に非常に有効です。
固定サイズ指定の記述例
以下のサンプルコードは、3×3の行列を表示する関数を定義したものです。
#include <stdio.h>
// 第2次元(列数)のサイズを明示する必要がある
void printMatrix(int arr[3][3]) {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
// arr[i][j]という形式で直感的にアクセス可能
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main() {
int data[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
printMatrix(data);
return 0;
}
1 2 3
4 5 6
7 8 9
この手法のメリットと制限
この記述法の最大のメリットは、コードの可読性が非常に高いという点にあります。
関数側でarr[i][j]という標準的な配列アクセス記法がそのまま使えるため、ロジックの意図が伝わりやすくなります。
しかし、一方で大きなデメリットとして「柔軟性の欠如」が挙げられます。
例えば、引数をint arr[3][3]と定義した関数に対して、4×4の配列を渡すことはできません。
汎用的なライブラリを作成する際には、この「サイズが固定される」という制約がボトルネックとなる場合があります。
方法2:配列ポインタ(Pointer to Array)を利用する
次に紹介するのは、ポインタの概念をより厳密に利用した「配列ポインタ」による方法です。
これは、前述の「配列記法」をポインタ形式で書き直したものであり、内部的な動作は全く同じです。
配列ポインタの文法
配列ポインタを使用する場合、int (*ptr)[3]のように記述します。
これは「3つのint型要素を持つ配列へのポインタ」という意味になります。
ここで重要なのは、(*ptr)のように括弧を忘れないことです。
もし括弧を忘れてint *ptr[3]と書くと、「int型のポインタを3個持つ配列」という全く別の意味になってしまいます。
#include <stdio.h>
// 配列ポインタによる受け取り
// 呼び出し側からは配列名を渡すだけでよい
void showElements(int (*p)[3], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 3; j++) {
// 配列記法でもポインタ演算でもアクセス可能
printf("[%d]", p[i][j]);
}
printf("\n");
}
}
int main() {
int grid[2][3] = {{10, 20, 30}, {40, 50, 60}};
// 行数は引数で渡すが、列数は型情報に含まれている
showElements(grid, 2);
return 0;
}
[10][20][30]
[40][50][60]
この方法は、C言語のメモリアドレス計算の仕組みを明示的に示しているため、低レイヤーのプログラミングでは好まれる傾向があります。
ただし、初心者にとってはポインタの優先順位(括弧の有無)が混乱の元になりやすいため注意が必要です。
方法3:可変長配列(VLA)を活用する(推奨手法)
現代のC言語(C99規格以降、および最新のC23に準拠する環境)において、最も推奨される柔軟な方法が可変長配列(Variable Length Array: VLA)を利用した記述です。
VLAを使用すると、実行時に決定される変数を配列のサイズ指定に使用することができます。
VLAを用いた汎用的な関数設計
VLAを関数の引数に使う場合、サイズを表す引数を、配列の引数よりも先に定義する必要があります。
#include <stdio.h>
// 行と列のサイズを先に引数で受け取る
void processVLA(int rows, int cols, int arr[rows][cols]) {
printf("%d行 %d列の配列を処理中...\n", rows, cols);
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
arr[i][j] *= 2; // 全要素を2倍にする
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main() {
int r = 2, c = 4;
int table[2][4] = {
{1, 1, 1, 1},
{2, 2, 2, 2}
};
processVLA(r, c, table);
return 0;
}
2行 4列の配列を処理中...
2 2 2 2
4 4 4 4
この手法の最大の利点は、一つの関数で任意のサイズの2次元配列を扱えるようになることです。
従来の「サイズ固定」という制約から解放され、再利用性の高いコードを書くことが可能になります。
現在の主要なコンパイラ(GCCやClang)では標準的にサポートされていますが、一部の組み込み環境やMSVC(Microsoft Visual C++)では独自の制約があるため、プロジェクトの対象環境を確認してから採用するのが賢明です。
よくある間違い:ダブルポインタ(**)で渡す
多くの学習者が陥りやすいミスとして、2次元配列をint **ptrという型の引数で受け取ろうとすることがあります。
結論から言うと、静的に確保された2次元配列をダブルポインタで受け取ることはできません。
なぜダブルポインタではいけないのか
int matrix[3][3]はメモリ上に連続して並んでいますが、int **ptrは「ポインタが並んだ配列へのポインタ」を期待しています。
ダブルポインタ経由でアクセスする場合、プログラムはまず最初のポインタを読み取り、そのポインタが指す先にある別のメモリアドレスへジャンプしようとします。
しかし、静的な2次元配列の実体は連続した数値データであり、ジャンプ先を示す「ポインタのリスト」はメモリ上に存在しません。
そのため、int **ptrとして無理やりキャストしてアクセスすると、セグメンテーションフォールト(不正なメモリ参照)が発生してプログラムが強制終了します。
ダブルポインタが適切なケース
ダブルポインタが活躍するのは、malloc関数などを使用して動的に「ポインタの配列」を作成した場合のみです。
以下の表で、それぞれの構造の違いを整理しました。
| 構造の種類 | 引数の型 | メモリ配置 |
|---|---|---|
| 静的2次元配列 | int arr[R][C] / int (*p)[C] | 完全に連続した単一ブロック |
| 動的ポインタ配列 | int **ptr | バラバラのメモリ領域への参照 |
より高度な手法:1次元配列として扱い計算で位置を特定する
パフォーマンスを重視するライブラリ(線形代数計算など)では、あえて2次元配列を「大きな1次元配列」として関数に渡す手法もよく使われます。
この場合、関数側ではint *arrとしてデータを受け取ります。
i行j列目の要素にアクセスするには、arr[i * cols + j]という計算式を手動で行います。
この方法は、コンパイラの配列解釈に頼らず、プログラマが完全にメモリ制御を行うスタイルです。
多次元配列の次元数が動的に変わるような非常に複雑なシステムにおいては、このアプローチが最も確実でバグの混入を防げる場合があります。
2026年におけるベストプラクティス
C言語を取り巻く環境は進化し続けていますが、2次元配列の扱いについては「安全性」と「可読性」のバランスが重視されるようになっています。
現代の開発において推奨される選択基準は以下の通りです。
- 特定の用途でサイズが変わらないことが明白な場合は、方法1(固定サイズ配列)を使用する。
- 関数の汎用性を高めたい場合は、方法3(VLA)を第一選択とする。
- 古いコンパイラや特殊な組み込み環境との互換性が重要な場合は、方法2(配列ポインタ)または「1次元配列へのフラット化」を検討する。
どのような方法を選択するにせよ、「メモリ上でデータがどう並んでいるか」を常に意識することが、デバッグの難しいポインタ関連のバグを防ぐ鍵となります。
まとめ
C言語で2次元配列を関数に渡す際は、単に「配列を渡す」という感覚ではなく、「型情報(特に列数)をいかにコンパイラへ伝えるか」が重要です。
固定サイズによる指定はシンプルで分かりやすい反面、柔軟性に欠けます。
配列ポインタによる指定は、C言語のポインタ演算の仕組みを忠実に反映した記述です。
そしてVLAによる指定は、現代のC言語において最も強力で柔軟な解決策を提供してくれます。
また、ダブルポインタと2次元配列は全く別物であるという事実は、中級者へステップアップするために避けては通れない知識です。
今回解説した各手法の特性を理解し、プロジェクトの要件やターゲット環境に合わせて最適な方法を選択できるようになりましょう。
正しい配列操作を身につけることは、効率的で堅牢なC言語プログラムを書くための第一歩です。
