C言語を習得する上で、避けては通れない非常に重要な概念が「配列」です。
配列を正しく理解し活用することで、大量のデータを効率的に管理し、簡潔なプログラムを記述することが可能になります。
変数を一つひとつ個別に定義するのではなく、同じデータ型の値を一つのグループとして連続したメモリ領域に保持するという配列の考え方は、後のデータ構造やアルゴリズムの学習においても強固な土台となります。
本記事では、配列の基本的な宣言方法から、多次元配列、関数への渡し方、そして動的メモリ確保を用いた応用まで、実例を交えて詳しく解説します。
配列とは何か:基本概念とメモリ構造
C言語における配列とは、同じ型のデータ要素をメモリ上の連続した場所に並べたデータ構造です。
例えば、4バイトのint型の要素を5個持つ配列を定義した場合、メモリ上には合計20バイトの領域が隙間なく確保されます。
配列の主な特徴
配列には、他のデータ構造とは異なるいくつかの重要な特徴があります。
- 同一データ型:一つの配列には、すべて同じ型の要素しか格納できません。
int型の配列にdouble型の値を混在させることは不可能です。 - 添字によるアクセス:配列の各要素には「インデックス(添字)」を使用してアクセスします。C言語のインデックスは必ず0から始まることに注意が必要です。
- 固定長(静的配列の場合):通常の宣言方法では、プログラムの実行中に配列のサイズを変更することはできません。
メモリ上の配置
配列がメモリ上でどのように配置されているかを知ることは、C言語のポインタを理解する助けにもなります。
例えば、int a[3] という配列があるとき、a[0] のアドレスが 1000 番地であれば、a[1] は 1004 番地、a[2] は 1008 番地となります(intが4バイトの場合)。
このように要素が隣接しているため、計算によって目的の要素へ高速にアクセスできるのです。
配列の宣言と初期化
配列を使用するには、まずその配列がどのような型で、いくつの要素を持つかをコンパイラに伝える必要があります。
基本的な宣言
配列の宣言は以下の書式で行います。
データ型 配列名[要素数];
具体例を見てみましょう。
int scores[5]; // 5個の整数を格納できる配列
double prices[10]; // 10個の実数を格納できる配列
char grades[3]; // 3個の文字を格納できる配列
配列の初期化方法
宣言と同時に値を代入することを初期化と呼びます。
いくつかのパターンがあります。
1. 全要素を個別に指定する
int numbers[5] = {10, 20, 30, 40, 50};
2. 要素数を省略する
初期化子がある場合、要素数を省略するとコンパイラが自動的に要素数を数えてくれます。
int numbers[] = {1, 2, 3}; // 要素数は3として扱われる
3. 一部の要素のみ初期化する
指定されなかった残りの要素は、数値型の場合は0で初期化されます。
int numbers[5] = {1, 2}; // {1, 2, 0, 0, 0} となる
4. すべてを0で初期化する
非常によく使われるテクニックです。
int numbers[100] = {0}; // すべての要素が0になる
配列要素へのアクセスと操作
配列の要素を利用するには、配列名[インデックス]という記法を用います。
要素の読み取りと代入
以下のサンプルプログラムは、配列に値を代入し、それを表示する基本的な流れを示しています。
#include <stdio.h>
int main() {
int data[3];
// 値の代入
data[0] = 100;
data[1] = 200;
data[2] = 300;
// 値の参照
printf("data[0]: %d\n", data[0]);
printf("data[1]: %d\n", data[1]);
printf("data[2]: %d\n", data[2]);
return 0;
}
data[0]: 100
data[1]: 200
data[2]: 300
インデックスの範囲外アクセスに注意
C言語において最も注意すべき点は、配列の境界チェックが行われないことです。
例えば要素数5の配列に対し、data[5] や data[10] にアクセスしようとしても、コンパイルエラーにはなりません。
しかし、これはメモリ上の未定義の領域を操作することになり、プログラムの異常終了やセキュリティホール(バッファオーバーフロー)の原因となります。
ループ処理との組み合わせ
配列の真価は、for文などのループ処理と組み合わせたときに発揮されます。
for文による全要素の走査
配列の要素を順番に処理する例を見てみましょう。
#include <stdio.h>
int main() {
int scores[] = {85, 92, 78, 64, 50};
int n = 5; // 要素数
int sum = 0;
for (int i = 0; i < n; i++) {
printf("%d番目のスコア: %d\n", i + 1, scores[i]);
sum += scores[i];
}
double average = (double)sum / n;
printf("合計点: %d\n", sum);
printf("平均点: %.1f\n", average);
return 0;
}
1番目のスコア: 85
2番目のスコア: 92
3番目のスコア: 78
4番目のスコア: 64
5番目のスコア: 50
合計点: 369
平均点: 73.8
sizeof演算子による要素数の取得
配列の要素数をハードコーディングするのではなく、sizeof演算子を使って動的に計算する方法が推奨されます。
int arr[] = {1, 2, 3, 4, 5, 6};
size_t count = sizeof(arr) / sizeof(arr[0]);
sizeof(arr) は配列全体のバイトサイズを返し、sizeof(arr[0]) は要素1つ分のバイトサイズを返します。
これらを割ることで、要素の数を安全に取得できます。
多次元配列の使い方
配列の中に配列を入れることで、行列のような多次元構造を表現できます。
最も一般的なのは「2次元配列」です。
2次元配列の宣言と初期化
2次元配列は、行(Row)と列(Column)の概念で考えると理解しやすくなります。
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
この例では、2行3列の行列を作成しています。
2次元配列の走査
多次元配列を処理するには、ループを入れ子(ネスト)にします。
#include <stdio.h>
int main() {
int table[3][2] = {
{10, 20},
{30, 40},
{50, 60}
};
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 2; j++) {
printf("table[%d][%d] = %d ", i, j, table[i][j]);
}
printf("\n");
}
return 0;
}
table[0][0] = 10 table[0][1] = 20
table[1][0] = 30 table[1][1] = 40
table[2][0] = 50 table[2][1] = 60
配列とポインタの関係
C言語を学ぶ上で避けて通れないのが、「配列名は配列の先頭要素のアドレスを指す」というルールです。
配列名の正体
配列名単体で記述すると、それは &配列名[0] と同じ意味を持ちます。
#include <stdio.h>
int main() {
int arr[3] = {10, 20, 30};
int *p = arr; // 配列の先頭アドレスをポインタに代入
printf("arr[0]のアドレス: %p\n", (void *)&arr[0]);
printf("arrの指すアドレス : %p\n", (void *)arr);
// ポインタ経由でのアクセス
printf("ポインタpが指す値: %d\n", *p); // 10
printf("ポインタp+1が指す値: %d\n", *(p+1)); // 20
return 0;
}
このように、ポインタ演算を用いることで配列要素にアクセスすることも可能です。
ただし、可読性の観点からは、特別な理由がない限り [] 記法を用いるのが一般的です。
配列を関数に渡す方法
関数に配列を引数として渡す場合、配列全体がコピーされるわけではありません。
実際には、配列の先頭アドレスが渡されます。
基本的な関数の定義
配列を引数に取る関数では、通常「配列本体」と「要素数」の2つをセットで渡します。
#include <stdio.h>
// 配列の要素をすべて表示する関数
void printArray(int *arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int myData[] = {1, 3, 5, 7, 9};
int n = sizeof(myData) / sizeof(myData[0]);
printArray(myData, n);
return 0;
}
1 3 5 7 9
関数内での int \*arr は int arr[] と書くこともできますが、どちらもポインタとして扱われます。
そのため、関数内で引数の配列に対して行った変更は、呼び出し元の配列にも反映されることに注意してください。
文字列と文字配列
C言語には独立した「文字列型」は存在しません。
文字列はchar型の配列として扱われます。
ヌル終端文字
C言語の文字列において最も重要なのが、末尾に必ずヌル文字 \0 が含まれることです。
これにより、プログラムはどこで文字列が終わるのかを判断します。
char str1[] = {'H', 'e', 'l', 'l', 'o', '\0'};
char str2[] = "Hello"; // 暗黙的に末尾に \0 が追加される
文字列操作の注意点
文字列の長さを取得するには strlen 関数を、コピーするには strcpy 関数を使用しますが、これらも内部的には配列の要素を一つずつ走査しています。
配列サイズを超えて文字を書き込まないよう、常にバッファサイズを意識する必要があります。
動的メモリ確保による配列の作成
これまでの例はすべて「静的配列」であり、コンパイル時にサイズが決定されている必要がありました。
しかし、ユーザーの入力に応じて配列の大きさを変えたい場合は、malloc 関数を使用して「動的配列」を作成します。
mallocとfreeの使い方
ヒープ領域からメモリを確保し、使い終わったら必ず解放します。
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("配列の要素数を入力してください: ");
scanf("%d", &n);
// 動的にメモリを確保
int *dynamicArr = (int *)malloc(n * sizeof(int));
if (dynamicArr == NULL) {
printf("メモリの確保に失敗しました。\n");
return 1;
}
// データの入力
for (int i = 0; i < n; i++) {
dynamicArr[i] = i * 10;
}
// 表示
for (int i = 0; i < n; i++) {
printf("%d ", dynamicArr[i]);
}
printf("\n");
// メモリの解放
free(dynamicArr);
return 0;
}
動的確保を利用することで、プログラムの柔軟性が大幅に向上します。
ただし、メモリリークを防ぐために、確保したメモリは必ず free する習慣をつけましょう。
配列操作のベストプラクティス
効率的で安全なプログラムを書くために、以下のポイントを意識してください。
1. マクロや定数による要素数管理
要素数を直接数値で書く(マジックナンバー)のは避け、#define や const を使用しましょう。
#define MAX_USERS 100
int user_ids[MAX_USERS];
2. 初期化の徹底
宣言したばかりのローカル変数の配列には、メモリ上に残っていた「ゴミデータ」が入っています。
予期せぬ動作を防ぐため、必ず初期化を行うようにしましょう。
3. 関数の引数にはconstを検討
関数内で配列の内容を書き換える必要がない場合は、引数に const を付けることで、誤った書き換えを防ぐとともに可読性を高めることができます。
void showData(const int *arr, int size) {
// arr[0] = 100; // コンパイルエラーになるため安全
}
よくあるエラーと解決策
配列を扱う際によく遭遇する問題とその対策をまとめます。
| 現象 | 原因 | 対策 |
|---|---|---|
| セグメンテーション違反 | 範囲外アクセスやNULLポインタの参照 | インデックスが 0 ~ 要素数-1 であるか確認する |
| 値がおかしい | 初期化を忘れている | 配列宣言時に {0} で初期化する |
| 配列を代入できない | arr1 = arr2; と書いている | memcpy を使うか、ループで一つずつ代入する |
| 関数の外で配列が消える | 関数内のローカル配列のポインタを返している | 動的確保を使うか、呼び出し側で用意した配列に書き込む |
特に「配列同士の代入はできない」という点は初心者が見落としがちです。
配列名は定数ポインタのような振る舞いをするため、後から別の場所を指すように書き換えることはできません。
まとめ
配列は、C言語におけるデータ操作の中核をなす仕組みです。
- 基本:同じ型のデータを連続したメモリ領域に並べる。
- アクセス:0から始まるインデックスを使用する。
- 安全性:境界チェックがないため、範囲外アクセスには厳重に注意する。
- 応用:多次元配列やポインタとの連携、動的メモリ確保を使いこなす。
これらの概念を理解することで、より複雑なプログラムやデータ構造(リスト、スタック、キューなど)の実装が可能になります。
まずは小さなプログラムから、配列とループを組み合わせた処理に慣れていきましょう。
メモリ構造を意識しながらコードを書くことが、C言語マスターへの近道です。
