フリーキーズ | 独学プログラミング

C言語で実装する動的配列の基礎!サイズ制限のない柔軟なデータ構造を学ぼう

最終更新日
リンドくん

リンドくん

たなべ先生、C言語で配列を使うとき、最初に要素数を決めないといけないのが不便だなと思ってて...。
後から大きさを変えられる配列ってないんですか?

たなべ

たなべ

いい質問だね!確かにC言語の通常の配列は、最初にサイズを固定する必要があるよね。
でも、動的配列という技術を使えば、実行中にサイズを変更できる配列を作ることができるんだ。今日はそれについて詳しく説明するよ!

プログラミングを学んでいると、データを効率的に管理する方法が重要になってきます。
特にC言語では、データ構造の基礎を理解することが、プログラミングスキルを向上させるために不可欠です。

今回は、C言語における「動的配列」について解説します。
通常の配列との違いから実装方法、そして実際の使用例まで、初心者の方にもわかりやすく説明していきます。

動的配列とは?通常の配列との違い

リンドくん

リンドくん

動的配列って、普通の配列とどう違うんですか?

たなべ

たなべ

一言でいうと、サイズが固定されていない配列だね。
普通の配列は「○○個の要素を入れる箱」だけど、動的配列は「必要に応じて大きくなる箱」というイメージだよ。

通常の配列の制限

C言語の通常の配列には、以下のような制限があります。

  • サイズの固定: 宣言時にサイズを指定する必要があり、後から変更できません
  • メモリの事前確保: コンパイル時または実行時の早い段階でメモリが確保されます
  • 柔軟性の欠如: データ量が予測できない場合、メモリの無駄遣いや不足が生じる可能性があります

例えば、以下のように配列を宣言すると、その大きさは変更できません。

int numbers[100]; // 100個の整数を格納できる配列

動的配列の特徴

一方、動的配列には以下のような特徴があります。

  • 可変サイズ: プログラムの実行中にサイズを変更できます
  • 必要に応じたメモリ確保: 必要なときに必要な分だけメモリを確保できます
  • 効率的なメモリ利用: データ量に応じてメモリを調整できるため、無駄が少なくなります

動的配列は、まさに「成長する配列」と言えるでしょう。
データの追加や削除に応じて、自動的にサイズを調整できるのです。

これにより、例えばユーザーから入力されるデータの数が事前にわからない場合でも、柔軟に対応できるようになります。
これは実用的なプログラミングにおいて非常に重要なテクニックです。

C言語で動的配列を実装する方法

リンドくん

リンドくん

でも、C言語にはそんな機能があるんですか?どうやって実装するんですか?

たなべ

たなべ

C言語には直接的な動的配列の機能はないんだけど、malloc関数などのメモリ管理関数を使うことで自分で実装できるんだよ。
ポインタの理解が必要になるけど、基本的な仕組みは意外とシンプルだよ。

メモリ割り当て関数の基本

C言語で動的配列を実装するには、主に以下の関数を使用します。

  • malloc(): 指定したサイズのメモリを割り当てる
  • calloc(): 指定した数と大きさの要素分のメモリを割り当て、0で初期化する
  • realloc(): すでに割り当てられたメモリのサイズを変更する
  • free(): 割り当てられたメモリを解放する

これらの関数を使うには、<stdlib.h>ヘッダーファイルをインクルードする必要があります。

動的配列の基本実装

動的配列の最も基本的な実装は以下のようになります。

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *dynamic_array; // ポインタを宣言
    int size = 5;       // 初期サイズ
    
    // メモリの割り当て
    dynamic_array = (int *)malloc(size * sizeof(int));
    
    if (dynamic_array == NULL) {
        // メモリ割り当てに失敗した場合
        printf("メモリの割り当てに失敗しました\n");
        return 1;
    }
    
    // 配列の使用
    for (int i = 0; i < size; i++) {
        dynamic_array[i] = i * 10;
        printf("dynamic_array[%d] = %d\n", i, dynamic_array[i]);
    }
    
    // サイズの変更
    int new_size = 10;
    dynamic_array = (int *)realloc(dynamic_array, new_size * sizeof(int));
    
    if (dynamic_array == NULL) {
        printf("メモリの再割り当てに失敗しました\n");
        return 1;
    }
    
    // 追加された要素の初期化と表示
    for (int i = size; i < new_size; i++) {
        dynamic_array[i] = i * 10;
        printf("dynamic_array[%d] = %d\n", i, dynamic_array[i]);
    }
    
    // メモリの解放
    free(dynamic_array);
    
    return 0;
}

このコードでは、最初に5つの要素を持つ動的配列を作成し、その後サイズを10に拡張しています。
realloc()関数を使うことで、既存のデータを保持したまま配列のサイズを変更できるのです。

メモリリークを防ぐための注意点

動的配列を使う際の重要なポイントは、使い終わったらメモリを解放することです。
malloc()calloc()で確保したメモリは、free()で解放しないとメモリリーク(メモリの無駄遣い)が発生します。

特に、以下のような場合に注意が必要です。

  • 配列のサイズを変更する際に古いポインタを失わないようにする
  • 関数内で動的に確保したメモリは、関数を抜ける前に解放するか、戻り値として返す
  • プログラム終了時には、すべての動的に確保したメモリを解放する

メモリ管理は、C言語プログラミングにおける最も重要なスキルの一つです。
正しく管理することで、効率的で安定したプログラムを作ることができます。

動的配列の使用例

リンドくん

リンドくん

実際にどんな場面で使うといいんですか?具体的な例があると助かります。

たなべ

たなべ

例えばファイルからデータを読み込むプログラムを考えてみよう。ファイルの内容量が事前にわからない場合、動的配列がとても便利なんだ。
他にも、ユーザー入力を保存するプログラムなど、様々な場面で活躍するよ。

例1)ファイルからのデータ読み込み

ファイルから未知の量のデータを読み込む場合、動的配列は非常に便利です。
例えば、整数値が書かれたファイルから全てのデータを読み込んで処理する例を考えてみましょう。

#include <stdio.h>
#include <stdlib.h>

int* readNumbersFromFile(const char* filename, int* count) {
    FILE* file = fopen(filename, "r");
    if (file == NULL) {
        printf("ファイルを開けませんでした\n");
        *count = 0;
        return NULL;
    }
    
    int capacity = 10;  // 初期容量
    int size = 0;       // 実際のサイズ
    int* numbers = (int*)malloc(capacity * sizeof(int));
    
    if (numbers == NULL) {
        fclose(file);
        *count = 0;
        return NULL;
    }
    
    int num;
    while (fscanf(file, "%d", &num) == 1) {
        if (size >= capacity) {
            // 容量を拡張する必要がある
            capacity *= 2;  // 容量を2倍に
            int* temp = (int*)realloc(numbers, capacity * sizeof(int));
            if (temp == NULL) {
                free(numbers);
                fclose(file);
                *count = 0;
                return NULL;
            }
            numbers = temp;
        }
        
        numbers[size++] = num;
    }
    
    fclose(file);
    *count = size;
    return numbers;
}

int main() {
    int count;
    int* numbers = readNumbersFromFile("numbers.txt", &count);
    
    if (numbers != NULL) {
        printf("読み込んだ数値: ");
        for (int i = 0; i < count; i++) {
            printf("%d ", numbers[i]);
        }
        printf("\n");
        
        // 配列の解放
        free(numbers);
    }
    
    return 0;
}

この例では、ファイルから読み込んだ数値の量に応じて配列のサイズを動的に調整しています。
最初は10個分のメモリを確保し、容量が足りなくなったら2倍に拡張しています。これにより、ファイルの大きさに関係なく効率的にデータを読み込むことができます。

例2)ユーザー入力の保存

ユーザーが入力した文字列を保存する例も見てみましょう。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char** getUserInputLines(int* lineCount) {
    int capacity = 5;    // 初期行数
    int count = 0;       // 実際の行数
    char** lines = (char**)malloc(capacity * sizeof(char*));
    
    if (lines == NULL) {
        *lineCount = 0;
        return NULL;
    }
    
    printf("テキストを入力してください(終了するには空行を入力):\n");
    
    char buffer[1024];
    while (fgets(buffer, sizeof(buffer), stdin) != NULL) {
        // 改行だけの場合(空行)は終了
        if (buffer[0] == '\n') {
            break;
        }
        
        // 改行文字を取り除く
        size_t len = strlen(buffer);
        if (len > 0 && buffer[len-1] == '\n') {
            buffer[len-1] = '\0';
        }
        
        if (count >= capacity) {
            // 容量を拡張
            capacity *= 2;
            char** temp = (char**)realloc(lines, capacity * sizeof(char*));
            if (temp == NULL) {
                // メモリ解放
                for (int i = 0; i < count; i++) {
                    free(lines[i]);
                }
                free(lines);
                *lineCount = 0;
                return NULL;
            }
            lines = temp;
        }
        
        // 文字列のコピーを保存
        lines[count] = strdup(buffer);
        if (lines[count] == NULL) {
            // メモリ解放
            for (int i = 0; i < count; i++) {
                free(lines[i]);
            }
            free(lines);
            *lineCount = 0;
            return NULL;
        }
        
        count++;
    }
    
    *lineCount = count;
    return lines;
}

int main() {
    int lineCount;
    char** lines = getUserInputLines(&lineCount);
    
    if (lines != NULL) {
        printf("\n入力された行:\n");
        for (int i = 0; i < lineCount; i++) {
            printf("%d: %s\n", i+1, lines[i]);
            free(lines[i]);  // 各行のメモリを解放
        }
        free(lines);  // 行ポインタの配列を解放
    }
    
    return 0;
}

この例では、文字列ポインタの動的配列を使用しています。
ユーザーが入力した各行を別々のメモリ領域に保存し、それらのポインタを動的配列で管理しています。入力行数が増えるとともに、配列のサイズも自動的に拡張されます。

これらの例を見ると、動的配列が非常に柔軟でパワフルなデータ構造であることがわかりますね。
実際のプログラミングでは、このような動的なデータ管理の技術が非常に重要になってきます。

動的配列のパフォーマンスと最適化

リンドくん

リンドくん

動的配列を使うと処理が遅くなったりしないんですか?

たなべ

たなべ

鋭い質問だね!確かにメモリの再割り当ては少しコストがかかるんだ。でも、適切に実装すれば、そのオーバーヘッドは最小限に抑えられるよ。
具体的には容量を段階的に拡張するといった工夫が効果的なんだ。

拡張戦略の重要性

動的配列のパフォーマンスを考える上で最も重要なのは、配列の拡張戦略です。
要素が追加されるたびに配列のサイズを1ずつ増やすと、頻繁にメモリの再割り当てが発生し、効率が大幅に低下します。

一般的には、以下のような拡張戦略が効果的です。

  • 倍増戦略: 容量が不足したら、現在のサイズの2倍に拡張する
  • 固定増分戦略: 一定量(例えば1024要素)ずつ拡張する
  • ハイブリッド戦略: 小さいサイズでは倍増し、大きいサイズでは固定割合(例えば1.5倍)で拡張する

倍増戦略を使うと、N個の要素を追加する操作の平均的な計算量はO(1)となり、非常に効率的になります。

メモリ管理の最適化テクニック

動的配列のパフォーマンスを向上させるためのテクニックをいくつか紹介します。

  1. 先行確保: 必要になる前にある程度の余裕をもってメモリを確保しておく
  2. 縮小の抑制: 要素を削除してもすぐにメモリを解放せず、一定の閾値を下回ったときのみ縮小する
  3. アライメント考慮: メモリアクセスの効率化のため、適切なアライメントを考慮する
// 倍増戦略を使った動的配列の実装例
typedef struct {
    int* data;     // 実際のデータを格納する配列
    int size;      // 現在の要素数
    int capacity;  // 確保されているメモリの容量
} DynamicArray;

DynamicArray* createDynamicArray(int initialCapacity) {
    DynamicArray* arr = (DynamicArray*)malloc(sizeof(DynamicArray));
    if (arr == NULL) return NULL;
    
    arr->data = (int*)malloc(initialCapacity * sizeof(int));
    if (arr->data == NULL) {
        free(arr);
        return NULL;
    }
    
    arr->size = 0;
    arr->capacity = initialCapacity;
    return arr;
}

int addElement(DynamicArray* arr, int element) {
    if (arr->size >= arr->capacity) {
        // 容量を2倍に拡張
        int newCapacity = arr->capacity * 2;
        int* newData = (int*)realloc(arr->data, newCapacity * sizeof(int));
        if (newData == NULL) return 0; // 失敗
        
        arr->data = newData;
        arr->capacity = newCapacity;
    }
    
    arr->data[arr->size++] = element;
    return 1; // 成功
}

void destroyDynamicArray(DynamicArray* arr) {
    if (arr) {
        free(arr->data);
        free(arr);
    }
}

この例では、容量不足時に配列のサイズを2倍に拡張することで、効率的なメモリ管理を実現しています。
このような実装により、動的配列は通常の配列に近いパフォーマンスで、柔軟性という大きな利点を得ることができます。

このような動的配列の実装はC++のstd::vectorやJavaのArrayListなど、より高級な言語の標準ライブラリにも採用されています。
C言語で自作することで、これらの高度な機能の内部がどのように動作しているかを深く理解できるのです。

動的配列のラッパー関数を作ろう

リンドくん

リンドくん

毎回malloc()やrealloc()を使うのは面倒そうですね...もっと簡単に扱う方法はないですか?

たなべ

たなべ

その通り!実際のプロジェクトでは、これらの操作をラッパー関数にまとめることが多いんだ。
一度作っておけば、後は簡単に使い回せるようになるよ。

使いやすい動的配列ライブラリの作成

実際のプロジェクトでは、動的配列の操作を簡単に行えるようなラッパー関数群(ライブラリ)を作成することが一般的です。
以下に、基本的な動的配列ライブラリの例を示します。

まず、ヘッダーファイル(dynamic_array.h)を作って必要となる構造体や関数を定義します。

#ifndef DYNAMIC_ARRAY_H
#define DYNAMIC_ARRAY_H

typedef struct {
    void* data;    // 汎用ポインタで任意の型に対応
    int elementSize; // 各要素のサイズ(バイト単位)
    int size;      // 現在の要素数
    int capacity;  // 確保されている容量
} DynamicArray;

// 動的配列を初期化する
DynamicArray* dynarray_create(int elementSize, int initialCapacity);

// 動的配列の末尾に要素を追加する
int dynarray_append(DynamicArray* arr, void* element);

// インデックスで要素を取得する
void* dynarray_get(DynamicArray* arr, int index);

// インデックスで要素を設定する
int dynarray_set(DynamicArray* arr, int index, void* element);

// 動的配列のサイズを変更する
int dynarray_resize(DynamicArray* arr, int newCapacity);

// 動的配列を解放する
void dynarray_destroy(DynamicArray* arr);

#endif

次に、実装ファイル(dynamic_array.c)です。

#include <stdlib.h>
#include <string.h>
#include "dynamic_array.h"

DynamicArray* dynarray_create(int elementSize, int initialCapacity) {
    DynamicArray* arr = (DynamicArray*)malloc(sizeof(DynamicArray));
    if (!arr) return NULL;
    
    arr->data = malloc(elementSize * initialCapacity);
    if (!arr->data) {
        free(arr);
        return NULL;
    }
    
    arr->elementSize = elementSize;
    arr->size = 0;
    arr->capacity = initialCapacity;
    return arr;
}

int dynarray_append(DynamicArray* arr, void* element) {
    if (arr->size >= arr->capacity) {
        // 容量不足の場合は拡張
        int newCapacity = arr->capacity * 2;
        if (!dynarray_resize(arr, newCapacity)) {
            return 0; // 拡張失敗
        }
    }
    
    // 新しい要素をコピー
    char* target = (char*)arr->data + (arr->size * arr->elementSize);
    memcpy(target, element, arr->elementSize);
    arr->size++;
    
    return 1; // 成功
}

void* dynarray_get(DynamicArray* arr, int index) {
    if (index < 0 || index >= arr->size) {
        return NULL; // インデックス範囲外
    }
    
    return (char*)arr->data + (index * arr->elementSize);
}

int dynarray_set(DynamicArray* arr, int index, void* element) {
    if (index < 0 || index >= arr->size) {
        return 0; // インデックス範囲外
    }
    
    char* target = (char*)arr->data + (index * arr->elementSize);
    memcpy(target, element, arr->elementSize);
    return 1; // 成功
}

int dynarray_resize(DynamicArray* arr, int newCapacity) {
    void* newData = realloc(arr->data, arr->elementSize * newCapacity);
    if (!newData) return 0; // 失敗
    
    arr->data = newData;
    arr->capacity = newCapacity;
    return 1; // 成功
}

void dynarray_destroy(DynamicArray* arr) {
    if (arr) {
        free(arr->data);
        free(arr);
    }
}

ライブラリの使用例

このライブラリを使用した具体的な例を見てみましょう。

#include <stdio.h>
#include "dynamic_array.h"

int main() {
    // 整数の動的配列を作成(初期容量10)
    DynamicArray* numbers = dynarray_create(sizeof(int), 10);
    if (!numbers) {
        printf("配列の作成に失敗しました\n");
        return 1;
    }
    
    // 要素を追加
    for (int i = 0; i < 20; i++) {
        int value = i * 10;
        if (!dynarray_append(numbers, &value)) {
            printf("要素の追加に失敗しました\n");
            dynarray_destroy(numbers);
            return 1;
        }
    }
    
    // 要素を表示
    printf("動的配列の内容:\n");
    for (int i = 0; i < numbers->size; i++) {
        int* value = (int*)dynarray_get(numbers, i);
        printf("numbers[%d] = %d\n", i, *value);
    }
    
    // 配列の解放
    dynarray_destroy(numbers);
    
    return 0;
}

このような汎用的なライブラリを一度作成しておけば、以後のプロジェクトでも簡単に動的配列を使用することができます。
型に関係なく使えるようにvoid*を活用することで、様々なデータ型に対応できる柔軟性も備えています。

実際のプロジェクトでは、このようなラッパー関数を作成することで、低レベルのメモリ管理の詳細を隠蔽し、より高レベルな抽象化を提供することが一般的です。
これは、コードの再利用性と可読性を大幅に向上させます。

まとめ

リンドくん

リンドくん

動的配列の仕組みがよくわかりました!これでプログラムがもっと柔軟になりそうです。

たなべ

たなべ

動的配列をマスターすることで、プログラミングの可能性が広がるんだ。
最初は少し難しく感じるかもしれないけど、一度理解すれば強力な武器になるよ。ぜひ実際に試してみてね!

この記事では、C言語における動的配列の基本から応用までを解説してきました。
動的配列は、固定サイズの制限を超えて、より柔軟なプログラムを作成するための重要な技術です。

動的配列を使うことで得られる主なメリットは以下の通りです。

  • 必要に応じたメモリ割り当て:データ量が事前にわからない場合でも効率的にメモリを使用できます
  • 柔軟なデータ管理:実行時にサイズを変更できるため、様々な状況に対応できます
  • メモリの効率的な使用:必要な分だけメモリを確保するため、無駄が少なくなります

C言語でのプログラミングスキルを向上させるためには、このような基本的なデータ構造の理解と実装が非常に重要です。動的配列は比較的シンプルでありながら、非常に強力なデータ構造です。
これをマスターすることで、より複雑なプログラムを効率的に開発するための土台を築くことができます。

関連するコンテンツ