メモリの動的確保(配列など)

目次

C言語の配列

C言語では,配列を定義する際,「要素数(つまり配列のサイズ)は定数でなればならない」と習うはずである.
例えば,

#include <stdio.h>
    
int main(void)
{
    double data[10];
    return 0;
}

または

#include <stdio.h>
    
int main(void)
{
    const int N = 10;   //  配列サイズ
    double data[N];
    return 0;
}

のような配列定義が必要とされている.
基本的に,プログラムの作成時点(=コンパイル時)で, 配列のサイズが確定されていなければならない.

最近のC99以降では,要素数にconstでない変数を指定できる.
(古いCコンパイラでは,以下のコードはエラーとなる.)

#include <stdio.h>

int main(void)
{
    int N;
    scanf("%d", &N);  // Keyboard入力
    double data[N];
    return 0;
}

静的と動的の意味

(旧式の)C言語の配列の要素数は,整数定数またはconst修飾された整数型変数でなければならない制約があった. ところが,処理の内容によってはプログラムの(コンパイル時にではなく)実行時に配列のサイズを決めたい場合がある.
例えば,ユーザーからの入力をもとに配列サイズを決めたい場合や,サイズのわからない or サイズがバラバラなデータファイルを読み込んで配列に格納する場合がこれにあたる.

このような「コンパイル時」「実行時」にあたる状況を,プログラミングでは「静的」「動的」と呼ぶ.

自動変数,1次元配列,2次元配列は全て「スタック」メモリと呼ばれるメモリに確保され, プログラム実行中はスタックのサイズは固定である. これを「静的確保」という.
短いプログラムで,充分なメモリ容量がある場合には静的確保で別段問題にはならないが,メモリ容量のごく限られているマイコンや,長時間連続動作するプログラムでは,そうはいかない場合がある.
メモリは,「必要なときに,必要な分だけ確保し,使い終わったら解放する」のが望ましい.

これまで扱ってきた通常のローカル変数は,プログラム中のスコープに応じてメモリの所定の領域に配置されるものであった.
ところがプログラムが大きくなり扱うデータが大きくなると,このスタック領域を使い切ってしまい,プログラムの実行が継続できなくなってしまう.
そこで,必要なときにメモリを確保し,処理が終わったら次のステップのためにその内容を解放すればよい.
このような方法をメモリの「動的確保」と呼ぶ.

プログラム内でのメモリ確保

プログラムが実行されるとき,プログラム内の変数の定義によってさまざまなメモリが確保される. これを「メモリアロケーション」, memory allocation と呼ぶ.

いろいろなメモリ領域(使い道による分類)

プログラム領域(コード領域)
プログラムの実行コードがロードされて格納される場所で,メモリアドレスの先頭に配置されることになり,プログラム実行中は基本的に移動しない.
スタック領域
ローカル変数,自動変数,関数の引数,戻り値などが置かれる. プログラムの実行中に自動的に確保・解放(開放?)される.
一般的には数百kB〜数MB程度のサイズである.
ヒープ領域
malloc, newなどにより,プログラム実行中に動的に確保・解放される. 領域の大きさを「実行中に」自由に変更できる.
一般的にはOSの許す限り,数GB以上の大きな領域を確保できる.
静的領域
グローバル変数や定数,staticで定義された変数などが配置されており,プログラム領域に続いて確保される.プログラムの実行中は移動しない.

例:100個のdouble 型配列の場合,1個あたり 8byte × 100個分 のデータを格納するメモリが,

重要:通常スタック領域は数MB程度なので,大きな配列(数MB以上)を確保しようとすると実行時にStack overflow エラーが出る.
そのような場合は,

このなかで,staticなメモリは,プログラム実行中はずっと解放できないが, 数値シミュレーションなどの計算プログラムでは簡便で使いやすい.
その他,一般的には「動的確保」が望ましいが,解放し忘れるとメモリリークを引き起こす.

int array1[1000];                  /* sizeof(int)*1000 = 4000 byte.をスタック領域に確保.この程度のサイズは問題ない. */
int array2[10*1000*1000];          /* sizeof(int)*10M = 40 Mbyte. スタック領域が溢れる可能性大! */
static int array2[10*1000*1000];   /* static ならOK */

C言語でのメモリ動的確保

メモリを(ヒープ領域に)動的に確保するために,以下のようなstdlib.h 内のライブラリ関数が用意されている.

malloc関数

メモリの確保には malloc 関数を用いる. (malloc = エム・アロックかんすう,または,マロック?と読む.)

変数の型 *ポインタ名 = (キャスト)malloc( 確保するByte数 );

配列では,一般に以下のように用いる.

変数の型 *ポインタ名 = (キャスト)malloc( 要素数 * sizeof(型) );

注:引数は「要素数」ではなく「Byte数」. 配列を使用する場合には,その変数の型に応じて,要素数をsizeof(型) 倍する.

具体例を示すと,

#include <stdlib.h>

int n = 100;    /* これはconstである必要はない.ここが重要! */
	
char   *c_array = (char*)  malloc(n * sizeof(char) );
int    *i_array = (int*)   malloc(n * sizeof(int) );
double *d_array = (double*)malloc(n * sizeof(double) );

となる.

malloc関数の戻り値は void*(voidポインタ)なので,使用する型にキャストする.

一番下の行を例に説明すると,まずd_arrayという名前の double型を指すポインタを定義し, malloc()関数によりdouble型のデータ n 個分のバイト数 (ここでは800バイト)分のメモリを確保し, 戻り値として先頭のアドレスが返ってくるので, それをポインタ変数d_arrayに代入する,と言う意味である.

calloc関数

calloc()関数も,malloc関数と同様に,

変数型 *動的配列名 = calloc(要素数, sizeof(型));   /* 引数2つ! */

として定義される.

引数はmalloc()関数と良く似ているが,引数が異なる. calloc()関数では,要素数と1個あたりのサイズを別々に指定するところである.

また,calloc()関数は配列の全要素をゼロで初期化する.
(正確には,すべてのビットを0にするだけなので,整数では数値の0になるが,実数やポインタは0にならないかもしれない.)
従って,少々処理に余分な時間がかかるため,ゼロ初期化が不要で,少しでも高速化したい場合はmallocを使う.

free 関数

上記,いずれかの関数で確保したメモリ領域であっても,使用が終わったら

free(動的配列へのポインタ);

として,メモリーを解放する必要がある.

具体的には,

free(d_array);
d_array = NULL;          /* これは必須ではない */

とすれば,領域を解放することができる. これ以降,ポインタ変数は無効となるので,このポインタが指していた配列などは使用できなくなる.
この例のように,freeしても,NULLポインタが代入されるとは限らないので,解放したら直後にNULLを代入しておくと安全.

サンプル

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

int main(void)
{
    size_t i;
    double *d = NULL;          /* malloc,calloc戻り値用ポインタ*/

    size_t n = 100;        /* 配列の要素数.constでなくてもOK.size_tは符号なし整数 */

    d = (double *)calloc(n, sizeof(double) );
//  d = (double *)malloc(n * sizeof(double) );  //  mallocの場合.引数の違いに注意.

    if(d == NULL) {            /* 何らかの理由でメモリ確保に失敗した場合 */
        printf("メモリ不足!");
        exit(1);
    }

    /* 普通に配列として使用できる */
    d[0] = 1.23456;
    d[n-1] = 7.89012;
    for(i=0; i<5; i++) {
        printf("d[%03lu] = %lf\n", i, d[i]);
    }

    printf("...\n");

    /* もちろんポインタ変数としても使える */
    for(i=n-5; i<n; i++) {
        printf("d[%03lu] = %lf\n", i, *(d+i) );
    }
    
    free(d);    /* メモリ解放を忘れずに.忘れるとメモリリークを起こす. */
    return 0;
}

この例では,可能な限り大きなメモリサイズまで配列を確保できる. 但し,コンパイラの種類やOSのビット数(32bit, 64bit)により,その最大サイズは異なる.

例えば,size_t n = 1LL*1000*1000*1000; としてみよう(1Giga). 8GBのメモリを確保しようと試みるので,bcc32cにおいてはmalloc()が失敗し,「メモリ不足」と表示される.
64bitOSで,64bitコンパイラ(bcc64など)を用い,PCにメモリが充分たくさん積まれていれば, さらに大きなサイズのメモリも問題なく確保できる.

C++でのメモリ動的確保

C++では,malloc(), free() の代わりに new, delete[]演算子を使用する. (もちろん,C++でも引き続きmalloc,freeが使用可能)

注意:配列の解放は delete ではなく delete[] でないとダメらしいです!

#include <stdio.h>
#include <iostream>

int main(void)
{
    double *p = NULL;     // NULL でなく nullptrを使うのがモダン.
    size_t n = 100;       // 配列の要素数.constでなくてもOK
    size_t i;

    p = new double[n];       // メモリの確保

    // 普通に配列として使用できる
    p[0] = 1.23456;
    p[n-1] = 7.89012;
    for(i=0; i<5; i++) {
        printf("p[%03lu] = %lf\n", i, p[i]);
    }

    printf("...\n");

    // ポインタ変数としても使える
    for(i=n-5; i<n; i++) {
        printf("p[%03lu] = %lf\n", i, *(p+i) );
    }

    delete[] p;    // メモリ解放を忘れずに.忘れるとメモリリークを起こす.
    return 0;
}

この例で,とても大きなメモリサイズを指定してみよう. 例えば,size_t n = 1000LL*1000*1000*1000;とする.

new演算子はメモリ確保に失敗するとstd::bad_alloc例外をthrowする. (何のことかわからないときは,「C++例外処理」を参照)
例えば,メモリ不足の場合,以下のようなメッセージと共にプログラムが終了する.

gcc(Windows上)
terminate called after throwing an instance of 'std::bad_alloc'
what():  std::bad_alloc

clang(Mac OS 上)
libc++abi: terminating with uncaught exception of type std::bad_alloc: std::bad_alloc

例外処理を使わない場合は,p = new(std::nothrow) double[n]; のように書くと,失敗した場合に素直にNULLポインタを返すようになるので,以下のように簡単に判定できる.

size_t n = 1000LL*1000*1000*1000;   // 1TB,とても大きなサイズ
p = new(std::nothrow) double[n];       // メモリの確保,例外を投げないバージョン

if(p == NULL) {
    printf("Memory allocation error\n");
    return 1;
}

スマートポインタの使用

最近のC++(C++11以降)では,従来からあるポインタ(raw pointer,生ポインタと呼ばれる)にかわって, 「スマートポインタ」と呼ばれる新しいポインタが使われる.
スマートポインタには3種類あるが,ここではunique_ptr を紹介する.

ポインタ定義の記述が少々面倒であるが,このように定義さえすれば,配列の削除が自動的に行われる

注:コンパイル時にオプションが必要な場合がある.

clang(Apple clang version 13.0.0, Mac OS)の場合
% c++ ????.cpp -std=c++11
または
% c++ ????.cpp -std=c++14
#include <iostream>
#include <memory>

int main(void)
{
    size_t n = 100L;        // 配列の要素数
    size_t i;

    //  c++14以降
    std::unique_ptr<double[]> p;          // スマートポインタの定義
    p = std::make_unique<double[]>(n);    // メモリの確保

//  このようにまとめて書いてもOK
//  c++11以降
//  std::unique_ptr<double[]> p(new double[n]);

    // 普通に配列として使用できる
    p[0] = 1.23456;
    p[n-1] = 7.89012;

    // そのまま旧式のポインタ変数としては使えない. get()を使う.
    double *raw_ptr = p.get();
    *(raw_ptr+3) = -1.0;

    for(i=0; i<5; i++) {
        printf("p[%lu] = %lf\n", i, p[i]);
    }
    printf("...\n");
    for(i=n-5; i<n; i++) {
        printf("p[%lu] = %lf\n", i, p[i] );
    }

    // pが指すメモリが自動的に解放される
    return 0;
}

スマートポインタは「所有権」という考え方でポインタを管理しているので, 通常のポインタのような安易なコピーができない仕組みとなっている.
関数の引数として渡す場合など,詳しくは別途解説する.

STLのvector

配列を確保する目的で,new, delete[] を使用すると,どうしても解放のし忘れが発生してしまう. また,面倒な配列のコピーや,関数への引き渡しの際,配列サイズを別途管理しなければならないなど,いろいろ手間がかかる.
そこで,Standard Template Library とよばれる新しいライブラリ群が追加され,その中に配列の代わりに使用可能なstd::vectorという機能が追加された.

通常のC言語の配列はスタック領域に確保されるのに対し,vectorはヒープ領域に領域を確保するため,とても大きな配列を確保することができる.

また,resize()メンバ関数で処理途中で配列のサイズを自由に変更したり, 別のvector=演算子だけで中身をすべてコピーできるなど,いろいろ便利な機能がある.

#include <iostream>
#include <vector>

int main(void)
{
    size_t n = 100L;            // 配列の要素数
    size_t i;
    
    std::vector<double> a;     // vectorの定義
    a.resize(n);               // メモリの確保

//  このように書くこともできる
//  std::vector<double> a(n);     // vectorの定義

    // 普通に配列として使用できる
    a[0] = 1.23456;
    a[99] = 7.89012;

    // 先頭へのポインタを得る方法. 
    double *ptr = &a[0];     // ポインタの取得
    *(ptr+3) = -1.0;

    for(i=0; i<5; i++) {
        printf("a[%03lu] = %lf\n", i, a[i] );
    }
    printf("...\n");
    for(i=a.size()-5; i<a.size(); i++) {  // size()で要素数が得られる
        printf("a[%03lu] = %lf\n", i, a[i] );
    }

    // vectorで確保されたメモリが自動的に解放される
    return 0;
}