【C言語】配列を宣言する方法や宣言する際の注意点を解説

この記事では、C言語で配列を使う方法について学びます。

配列は、同じ種類のデータをまとめて管理するための便利なツールです。

この記事を読むことで、以下のことがわかります。

  • 配列の基本的な宣言方法
  • 配列に値を入れたり取り出したりする方法
  • 配列を使うときの注意点
  • 配列とポインタの関係

初心者の方でもわかりやすいように、具体的な例やサンプルコードを使って説明します。

これを読めば、配列の使い方がしっかりと理解できるようになります。

目次から探す

配列の宣言方法

C言語において、配列は同じデータ型の複数の要素を格納するためのデータ構造です。

配列を使うことで、複数の変数を一つの名前で管理でき、コードの可読性や保守性が向上します。

ここでは、一次元配列と多次元配列の宣言方法について詳しく解説します。

一次元配列の宣言

基本的な宣言方法

一次元配列は、単純に一列に並んだデータの集合です。

以下のように宣言します。

データ型 配列名[要素数];

例えば、整数型の配列を宣言する場合は次のようになります。

int numbers[5];

この宣言により、numbersという名前の整数型の配列が作成され、5つの要素を持つことになります。

各要素にはインデックス(0から始まる番号)を使ってアクセスできます。

初期化の方法

配列を宣言すると同時に初期化することも可能です。

初期化の方法は以下の通りです。

int numbers[5] = {1, 2, 3, 4, 5};

この場合、numbers配列の各要素はそれぞれ1, 2, 3, 4, 5に初期化されます。

要素数を省略することもできます。

int numbers[] = {1, 2, 3, 4, 5};

この場合、コンパイラが自動的に要素数を計算してくれます。

多次元配列の宣言

多次元配列は、配列の中にさらに配列が含まれる構造です。

ここでは、二次元配列と三次元以上の配列の宣言方法について説明します。

二次元配列の宣言

二次元配列は、行と列で構成される配列です。

以下のように宣言します。

データ型 配列名[行数][列数];

例えば、3行4列の整数型の二次元配列を宣言する場合は次のようになります。

int matrix[3][4];

この宣言により、matrixという名前の3行4列の整数型の配列が作成されます。

各要素には行と列のインデックスを使ってアクセスできます。

初期化する場合は以下のようにします。

int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

この場合、matrix配列の各要素は指定された値に初期化されます。

三次元以上の配列の宣言

三次元以上の配列も同様に宣言できます。

例えば、2×3×4の三次元配列を宣言する場合は次のようになります。

データ型 配列名[次元1][次元2][次元3];

具体的には以下のように宣言します。

int tensor[2][3][4];

この宣言により、tensorという名前の2×3×4の整数型の三次元配列が作成されます。

各要素には次元ごとのインデックスを使ってアクセスできます。

初期化する場合は以下のようにします。

int tensor[2][3][4] = {
    {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    },
    {
        {13, 14, 15, 16},
        {17, 18, 19, 20},
        {21, 22, 23, 24}
    }
};

この場合、tensor配列の各要素は指定された値に初期化されます。

以上が、一次元配列および多次元配列の基本的な宣言方法と初期化の方法です。

次に、配列の操作方法や宣言時の注意点について詳しく解説します。

配列の操作

配列への値の代入

配列を宣言した後、各要素に値を代入する方法を見ていきましょう。

配列の各要素にはインデックスを使ってアクセスします。

インデックスは0から始まるため、最初の要素はインデックス0でアクセスします。

#include <stdio.h>
int main() {
    int numbers[5]; // 配列の宣言
    // 配列への値の代入
    numbers[0] = 10;
    numbers[1] = 20;
    numbers[2] = 30;
    numbers[3] = 40;
    numbers[4] = 50;
    // 配列の内容を表示
    for (int i = 0; i < 5; i++) {
        printf("numbers[%d] = %d\n", i, numbers[i]);
    }
    return 0;
}

このプログラムでは、numbersという名前の配列を宣言し、各要素に値を代入しています。

forループを使って配列の内容を表示しています。

配列からの値の取得

配列から値を取得する方法も、値を代入する方法と同様にインデックスを使います。

以下の例では、配列から値を取得して表示する方法を示します。

#include <stdio.h>
int main() {
    int numbers[5] = {10, 20, 30, 40, 50}; // 配列の宣言と初期化
    // 配列からの値の取得と表示
    for (int i = 0; i < 5; i++) {
        printf("numbers[%d] = %d\n", i, numbers[i]);
    }
    return 0;
}

このプログラムでは、numbers配列を宣言し、初期化しています。

forループを使って各要素の値を取得し、表示しています。

配列の範囲外アクセスの危険性

配列の範囲外アクセスは、プログラムの動作を予期しないものにする可能性があるため、非常に危険です。

C言語では、配列の範囲外アクセスに対するチェックが自動的に行われないため、プログラマが注意する必要があります。

以下の例では、範囲外アクセスがどのような問題を引き起こすかを示します。

#include <stdio.h>
int main() {
    int numbers[5] = {10, 20, 30, 40, 50}; // 配列の宣言と初期化
    // 範囲外アクセス
    printf("numbers[5] = %d\n", numbers[5]); // 配列の範囲外アクセス
    return 0;
}

このプログラムでは、numbers配列の範囲外であるインデックス5にアクセスしようとしています。

範囲外アクセスは未定義の動作を引き起こし、プログラムがクラッシュしたり、予期しない値が表示されたりする可能性があります。

範囲外アクセスを防ぐためには、配列のサイズを超えないようにインデックスを管理することが重要です。

例えば、forループを使う場合は、ループの条件を配列のサイズに基づいて設定することが推奨されます。

#include <stdio.h>
int main() {
    int numbers[5] = {10, 20, 30, 40, 50}; // 配列の宣言と初期化
    // 正しい範囲内でのアクセス
    for (int i = 0; i < 5; i++) {
        printf("numbers[%d] = %d\n", i, numbers[i]);
    }
    return 0;
}

このプログラムでは、forループの条件をi < 5とすることで、範囲外アクセスを防いでいます。

配列の宣言時の注意点

配列のサイズ指定

サイズを定数で指定する利点

配列のサイズを定数で指定することにはいくつかの利点があります。

まず、コードの可読性が向上します。

定数を使うことで、配列のサイズがどこでどのように決定されているかが明確になります。

また、定数を使うことで、配列のサイズを変更する際にコード全体を修正する必要がなくなります。

以下に例を示します。

#include <stdio.h>
#define ARRAY_SIZE 10
int main() {
    int array[ARRAY_SIZE];
    for (int i = 0; i < ARRAY_SIZE; i++) {
        array[i] = i;
    }
    for (int i = 0; i < ARRAY_SIZE; i++) {
        printf("%d ", array[i]);
    }
    return 0;
}

この例では、ARRAY_SIZEという定数を使って配列のサイズを指定しています。

これにより、配列のサイズを変更する際には#defineの値を変更するだけで済みます。

サイズを変数で指定する場合の注意点

C言語では、配列のサイズを変数で指定することも可能ですが、いくつかの注意点があります。

まず、変数で指定する場合、配列のサイズが実行時に決定されるため、スタックメモリの使用量に注意が必要です。

特に大きな配列を宣言する場合、スタックオーバーフローのリスクがあります。

#include <stdio.h>
int main() {
    int n;
    printf("配列のサイズを入力してください: ");
    scanf("%d", &n);
    int array[n];
    for (int i = 0; i < n; i++) {
        array[i] = i;
    }
    for (int i = 0; i < n; i++) {
        printf("%d ", array[i]);
    }
    return 0;
}

この例では、ユーザーが入力した値を使って配列のサイズを指定しています。

配列のサイズが大きすぎると、スタックメモリが不足する可能性があるため注意が必要です。

配列の初期化

初期化しない場合のリスク

配列を宣言しただけでは、その要素は未初期化のままです。

未初期化の配列要素には不定の値が入っているため、これを使用すると予期しない動作やバグの原因となります。

以下に未初期化の配列を使用した場合の例を示します。

#include <stdio.h>
int main() {
    int array[5];
    for (int i = 0; i < 5; i++) {
        printf("%d ", array[i]);
    }
    return 0;
}

この例では、配列arrayの要素が未初期化のまま出力されるため、不定の値が表示されます。

部分的な初期化

配列を部分的に初期化することも可能です。

初期化されていない要素は自動的にゼロで初期化されます。

以下に部分的な初期化の例を示します。

#include <stdio.h>
int main() {
    int array[5] = {1, 2};
    for (int i = 0; i < 5; i++) {
        printf("%d ", array[i]);
    }
    return 0;
}

この例では、配列arrayの最初の2つの要素が1と2で初期化され、残りの要素はゼロで初期化されます。

配列の境界チェック

境界チェックの重要性

配列の境界チェックは非常に重要です。

配列の範囲外にアクセスすると、メモリの不正アクセスが発生し、プログラムがクラッシュする可能性があります。

特にC言語では、配列の範囲外アクセスに対する自動的なチェックが行われないため、プログラマが自分でチェックを行う必要があります。

境界チェックの実装方法

配列の境界チェックを行うためには、配列のインデックスが有効な範囲内にあるかどうかを確認する必要があります。

以下に境界チェックを行う例を示します。

#include <stdio.h>
int main() {
    int array[5] = {1, 2, 3, 4, 5};
    int index;
    printf("インデックスを入力してください: ");
    scanf("%d", &index);
    if (index >= 0 && index < 5) {
        printf("array[%d] = %d\n", index, array[index]);
    } else {
        printf("エラー: インデックスが範囲外です\n");
    }
    return 0;
}

この例では、ユーザーが入力したインデックスが配列の範囲内にあるかどうかをチェックしています。

範囲外の場合はエラーメッセージを表示します。

これにより、配列の範囲外アクセスを防ぐことができます。

配列とポインタの関係

C言語において、配列とポインタは密接な関係があります。

配列は連続したメモリ領域を確保し、その領域にデータを格納します。

一方、ポインタはメモリのアドレスを格納する変数です。

ここでは、配列とポインタの基本的な違い、配列名とポインタの関係、そしてポインタを使った配列操作について詳しく解説します。

配列とポインタの基本的な違い

配列とポインタは似ているようで異なる概念です。

以下にその違いを示します。

  • 配列: 配列は同じ型のデータを連続して格納するためのメモリ領域です。

配列のサイズは宣言時に決定され、変更することはできません。

  • ポインタ: ポインタはメモリのアドレスを格納する変数です。

ポインタを使うことで、メモリの特定の位置にアクセスしたり、操作したりすることができます。

サンプルコード: 配列とポインタの基本的な違い

#include <stdio.h>
int main() {
    int arr[3] = {1, 2, 3}; // 配列の宣言
    int *ptr; // ポインタの宣言
    ptr = arr; // 配列の先頭アドレスをポインタに代入
    // 配列の要素を表示
    for (int i = 0; i < 3; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }
    // ポインタを使って配列の要素を表示
    for (int i = 0; i < 3; i++) {
        printf("ptr[%d] = %d\n", i, *(ptr + i));
    }
    return 0;
}
arr[0] = 1
arr[1] = 2
arr[2] = 3
ptr[0] = 1
ptr[1] = 2
ptr[2] = 3

配列名とポインタの関係

配列名は配列の先頭要素のアドレスを指すポインタとして扱われます。

つまり、配列名はポインタのように使用することができます。

ただし、配列名自体はポインタ変数ではなく、配列の先頭アドレスを示す定数です。

サンプルコード: 配列名とポインタの関係

#include <stdio.h>
int main() {
    int arr[3] = {4, 5, 6};
    printf("配列名 arr はポインタとして扱われます。\n");
    printf("arr = %p\n", arr); // 配列の先頭アドレスを表示
    printf("&arr[0] = %p\n", &arr[0]); // 配列の先頭要素のアドレスを表示
    return 0;
}
配列名 arr はポインタとして扱われます。
arr = 0x7ffee3b5b8a0
&arr[0] = 0x7ffee3b5b8a0

ポインタを使った配列操作

ポインタを使うことで、配列の要素にアクセスしたり、操作したりすることができます。

ポインタを使った配列操作は、特に動的メモリ割り当てを行う場合に便利です。

サンプルコード: ポインタを使った配列操作

#include <stdio.h>
int main() {
    int arr[3] = {7, 8, 9};
    int *ptr = arr; // 配列の先頭アドレスをポインタに代入
    // ポインタを使って配列の要素を変更
    for (int i = 0; i < 3; i++) {
        *(ptr + i) = *(ptr + i) * 2;
    }
    // 配列の要素を表示
    for (int i = 0; i < 3; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }
    return 0;
}
arr[0] = 14
arr[1] = 16
arr[2] = 18

このように、ポインタを使うことで配列の要素にアクセスし、操作することができます。

ポインタを使った配列操作は、特に動的メモリ割り当てや関数間での配列の受け渡しにおいて重要な技術です。

よくある質問(FAQ)

配列のサイズを動的に変更する方法は?

C言語では、配列のサイズを動的に変更するために、標準ライブラリのmallocrealloc関数を使用します。

これにより、実行時に必要なメモリを動的に確保し、配列のサイズを変更することができます。

サンプルコード

#include <stdio.h>
#include <stdlib.h>
int main() {
    int *array;
    int size = 5;
    // メモリの動的確保
    array = (int *)malloc(size * sizeof(int));
    if (array == NULL) {
        printf("メモリの確保に失敗しました\n");
        return 1;
    }
    // 配列に値を代入
    for (int i = 0; i < size; i++) {
        array[i] = i * 2;
    }
    // 配列のサイズを変更
    size = 10;
    array = (int *)realloc(array, size * sizeof(int));
    if (array == NULL) {
        printf("メモリの再確保に失敗しました\n");
        return 1;
    }
    // 新しい要素に値を代入
    for (int i = 5; i < size; i++) {
        array[i] = i * 2;
    }
    // 配列の内容を表示
    for (int i = 0; i < size; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");
    // メモリの解放
    free(array);
    return 0;
}
0 2 4 6 8 10 12 14 16 18

配列の初期化におけるベストプラクティスは?

配列の初期化は、プログラムの予期しない動作を防ぐために重要です。

以下に、配列の初期化におけるベストプラクティスを示します。

  1. 宣言時の初期化: 配列を宣言する際に初期化する方法です。
  2. ループを使った初期化: 配列の各要素をループで初期化する方法です。
  3. memset関数の使用: 標準ライブラリのmemset関数を使用して配列を初期化する方法です。

サンプルコード

#include <stdio.h>
#include <string.h>
int main() {
    // 宣言時の初期化
    int array1[5] = {0, 1, 2, 3, 4};
    // ループを使った初期化
    int array2[5];
    for (int i = 0; i < 5; i++) {
        array2[i] = i;
    }
    // memset関数を使った初期化
    int array3[5];
    memset(array3, 0, sizeof(array3));
    // 配列の内容を表示
    for (int i = 0; i < 5; i++) {
        printf("array1[%d] = %d, array2[%d] = %d, array3[%d] = %d\n", i, array1[i], i, array2[i], i, array3[i]);
    }
    return 0;
}
array1[0] = 0, array2[0] = 0, array3[0] = 0
array1[1] = 1, array2[1] = 1, array3[1] = 0
array1[2] = 2, array2[2] = 2, array3[2] = 0
array1[3] = 3, array2[3] = 3, array3[3] = 0
array1[4] = 4, array2[4] = 4, array3[4] = 0

配列とリストの違いは?

配列とリストは、データを格納するための基本的なデータ構造ですが、それぞれに特徴と利点があります。

特徴配列リスト
メモリ配置連続したメモリ領域非連続なメモリ領域
サイズ変更固定サイズ動的に変更可能
要素へのアクセスインデックスを使用して高速順次アクセス
メモリ効率高い低い(ポインタのオーバーヘッド)
挿入・削除非効率(シフトが必要)効率的(ポインタの変更のみ)

まとめ

この記事では、C言語における配列の宣言方法や操作方法、宣言時の注意点について詳しく解説しました。

配列は、データを効率的に管理するための基本的なデータ構造であり、正しく理解し使用することが重要です。

また、配列とポインタの関係や、動的に配列のサイズを変更する方法についても触れました。

次に学ぶべきトピック

配列の基本を理解した後は、以下のトピックを学ぶことをお勧めします。

  1. 構造体と配列: 構造体と配列を組み合わせて使用する方法。
  2. 動的メモリ管理: mallocfree関数を使った動的メモリ管理の詳細。
  3. 文字列操作: 文字列を配列として扱う方法と標準ライブラリ関数の使用方法。
  4. アルゴリズムとデータ構造: ソートアルゴリズムや検索アルゴリズムの実装。

これらのトピックを学ぶことで、C言語の理解がさらに深まり、より高度なプログラムを作成できるようになります。

目次から探す