動的メモリ

概要
配列型の要素数は定数または定数式でなければならない.逆にいうと,実行中に動的に変化させることはできない.現実には,動的に要素数を変化させたい場合が多々あるので,ここでは動的にメモリを確保する方法を学ぶ.
目次

はじめに

配列型は,コンパイルする前に要素数が決定されている必要がある.このため,実行時にならないと要素数を決定できないような場合は配列型を定義できない.例えば関数の引数によって配列の要素数が渡される場合などである[1] .このような場合には メモリを動的に確保 する必要がある.ここで「動的」は「実行時に」という意味である.動的メモリは ヒープ と呼ばれるメモリ領域に確保される. 動的メモリを使う際の注意点は, 必ず,一度だけ,確保した領域を解放すること である.解放しなければメモリがずっと確保され続けることになり,関数に与えられているメモリ領域を使い果たすことになる(メモリリーク).逆に同じ領域を2回以上解放すると,発見しにくいバグとなり,予想外の動作をする原因となる(セグメントフォルトなどのエラーが起こらない場合もある). 確保したメモリを,必ず,一度だけ解放すること はプログラマの責任である.

  • [1] 実はgcc/g++ではそのようなコードをコンパイルし,実行できてしまうのだが,ほかの多くのコンパイラでは使えないし,ISO/JIS規格も配列の宣言/定義において要素数は定数式によって指定されなければならないと規定している.移植性のないコードは書くべきでない.また,確保できるサイズにも(ヒープに確保できるサイズよりも小さな)上限がある.

動的メモリを使うケース

  • プログラム作成時に配列のサイズがわからないとき
  • システムコール(OSが提供する関数)を呼び出すときにヒープ領域を渡すことが指定されている場合 など.

確保と解放の方法

Cでは malloc 関数 を,C++では new 演算子 を使って確保する.Cの malloc 関数 は単純にメモリを 指定されたバイト数確保する だけなのに対して,C++の new 演算子 は 指定した型を指定した数だけ生成する. 両者の違いを理解するためには,コンストラクタやデストラクタなど class (オブジェクト指向)の考え方を理解しておく必要があるが,一言で言うなら new 演算子の方が安全 である.この理由は,動的メモリ生成のためのコードをコンパイラが( malloc よりも)余分に書いてくれるからである.ちなみにC++でも malloc は使うことができるが,普通使わないし,安全性を損なうから使うのは止めた方がよい(コンストラクタが呼ばれなくなる).

ある型 X を n 個分確保するためには,

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  X *p(NULL);
  p = new X[n];

ひとつだけでいい場合は,

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  X *p(NULL);
  p = new X;

と書く. new はヒープに X を n 個分のメモリを確保して,そのメモリへのポインタを返す.

確保したメモリは,配列とほぼ同じ使い方ができる.つまり, [] 演算子が使えるということだ.例えば20個目の要素にアクセスしたければ,

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  p[20] = 10;

とする.しかしあくまでポインタであることを忘れてはならない(このあたりの事情はポインタと参照型 -「ポインタと配列」 を参照).

確保したメモリを解放するには, n 個確保した場合,

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  delete[] p;
  p=NULL;

ひとつだけ確保した場合,

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  delete p;
  p=NULL;

と書く. p=NULL; の一文は,なくても構わない(それ以降のコードで利用するなら別だが).しかし,安全に生きたければ書く癖をつけておいた方がよい.

サンプルプログラム:

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  #include <iostream>
  using namespace std;
  int main(int argc, char**argv)
  {
    int n=100;
    double *p(NULL);
    p = new double[n];
    p[0] = 1.0;
    p[1] = 1.0;
    for (int i(2); i<n; ++i)
      p[i] = p[i-1] + p[i-2];  // フィボナッチ数列
    cout << "Fibonacci sequence:";
    for (int i(0); i<n; ++i)
      cout << "  " << p[i];
    cout << endl;
    delete[] p; p=NULL;
    return 0;
  }

結果

 Fibonacci sequence:  1  1  2  3  5  8  13  21  34  55  89  144
 233  377  610  987  1597  2584  4181  6765  10946  17711
 28657  46368  75025  121393  196418  317811  514229  832040
 1.34627e+06  2.17831e+06  3.52458e+06  5.70289e+06  9.22746e+06
 1.49304e+07  2.41578e+07  3.90882e+07  6.3246e+07  1.02334e+08
 (以下略)

最後にもう一度. 確保したメモリは,必ず,一度だけ解放すること

二次元配列の確保

二次元配列とポインタ

一次元の, X 型の配列 a に対して式中で a[10] と書くと,これは *(a+10) と等価であった.ここで a は X 型のポインタとして振る舞う.このルールは二次元配列でも同様に適用される.

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  X b[N][M];

のように二次元配列 b が宣言されていたとき, b[i][j] は配列 b の (i,j) 番目の要素を表す.この要素は,メモリ上では b の先頭アドレスから (M*i+j)*sizeof(X) バイト目に確保されている.このことは,既に学習している.

さて, b[i] は X 型の配列(要素数 [M] ).「配列は,式中ではポインタとして振る舞う」ルールから, b[i] をひとつの配列とみれば式中では X 型のポインタとして振る舞うことがわかる.したがって, b[i]+j は b[i][j] が格納されているアドレスを表す.よって,

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  *(b[i]+j)  ==  b[i][j]

は常に真である.さらに, b は X[M] 型の配列(要素数 [N] )である(二次元配列が「配列の配列の...」である理由).したがって, *(b+i) は b[i] と等価である.これを上の等式に代入すると,

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  *(*(b+i)+j)  ==  b[i][j]

が成立する.実際,コンパイラは b[i][j] を解釈するとき, b[i] を *(b+i) に置き換えて (*(b+i))[j] とし,これをさらに **1+j) に置き換える.つまり,二次元配列を単なるポインタと整数の演算に置き換えてしまうわけだ.

ちなみに,ポインタと整数の演算 b+i において,整数 i の単位は sizeof(X[M]) だから, M*sizeof(X) バイト,ポインタと整数の演算 b[i]+j において,整数 j の単位は sizeof(X) バイトだから,アドレス b[i]+j ( b[i][j] のアドレス)は, b から数えて i*M*sizeof(X)+j*sizeof(X) バイトである.

二次元配列の動的確保

二次元配列を動的に確保するときは,少なくとも b[i] が X 型のポインタで, b[i]+j が b[i][j] を指すポインタであればよい.逆に,このように確保しさえすれば,どのように確保してもよい.

ひとつ目の例は,double を N*M 個分動的に確保して p_in にそのポインタを代入し,さらに p を double へのポインタ N 個分動的に確保したものに p[r] = p_in+r*M を代入することで, p[r]+c が p[r][c] を指すポインタになることを実現している.

二次元配列の動的確保:

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  #include <iostream>
  using namespace std;
  int main(int argc, char**argv)
  {
    int N=10,M=4;
    // 確保
    double **p(NULL), *p_in(NULL);
    p_in = new double[N*M];
    p = new double*[N];
    for (int r(0); r<N; ++r)

    p[r] = p_in+r*M;

    // 処理
    for (int r(0); r<N; ++r)
      for (int c(0); c<M; ++c)
        p[r][c] = (r+1)+(c+1)/10.0;
    for (int r(0); r<N; ++r)
    {
      for (int c(0); c<M; ++c)
        cout << "  " << p[r][c];
      cout << endl;
    }
    // 解放
    delete[] p; p=NULL;
    delete[] p_in; p_in=NULL;
    return 0;
  }

結果

   1.1  1.2  1.3  1.4
   2.1  2.2  2.3  2.4
   3.1  3.2  3.3  3.4
   4.1  4.2  4.3  4.4
   5.1  5.2  5.3  5.4
   6.1  6.2  6.3  6.4
   7.1  7.2  7.3  7.4
   8.1  8.2  8.3  8.4
   9.1  9.2  9.3  9.4
   10.1  10.2  10.3  10.4

これは,行列を二次元配列で表現する場合,もっともよい方法である.このように確保しておくことで行列の足し算をp_in のリニアアクセスによって高速に実現できる.例えば,上の p[r][c] のすべての要素に1加える処理は,

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  double *ptr=p[0];
  for (int i(0); i<N*M; ++i,++ptr)
    *ptr += 1.0;

によって高速に実現できる.

二つ目の例は,行ごとに列の数だけメモリを確保して行く方法である.メモリの解放の方法に注意.

二次元配列の動的確保2:

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  #include <iostream>
  using namespace std;
  int main(int argc, char**argv)
  {
    int N=10,M=4;
    // 確保
    double **p(NULL);
    p = new double*[N];
    for (int r(0); r<N; ++r)
      p[r] = new double[M];
    // 処理
    for (int r(0); r<N; ++r)
      for (int c(0); c<M; ++c)
        p[r][c] = (r+1)+(c+1)/10.0;
    for (int r(0); r<N; ++r)
    {
      for (int c(0); c<M; ++c)
        cout << "  " << p[r][c];
      cout << endl;
    }
    // 解放
    for (int r(0); r<N; ++r)
      {delete[] p[r]; p[r]=NULL;}
    delete[] p; p=NULL;
    return 0;
  }

二重解放の危険性

二重解放 とは,同じメモリ領域を二度解放することである.gcc/g++ではこの 二重解放(double free) を検出してくれる(glibが検出してくれるようだ).しかし,検出してくれないコンパイラも存在するし,gcc/g++でも二重解放が検出できないケースがある.ひとつ,例を示そう.

二重解放

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  #include <iostream>
  using namespace std;
  #define PRINTPTR(p)  {cout << #p"(*"#p")= " << p\
                        << " (" << *p << ")" << endl;}
  int main(int argc, char**argv)
  {
    int *p1=NULL, *p2=NULL;
    p1 = new int;      // line 1
    cout << p1 << " is allocated" << endl;
    (*p1)=100;         // line 2
    PRINTPTR (p1);
    delete p1;         // line 3
    cout << p1 << " is freed" << endl;
    cout << endl;
    p2 = new int;      // line 4
    cout << p2 << " is allocated" << endl;
    (*p2)=200;         // line 5
    PRINTPTR (p2);
    PRINTPTR (p1);
    cout << endl;
    delete p1;         // line 6
    cout << p1 << " is freed again!" << endl;
    cout << endl;
    p1 = new int;      // line 7
    (*p1)=100;         // line 8
    PRINTPTR (p1);
    PRINTPTR (p2);
    delete p1;         // line 8
    delete p2;         // line 9
  }

結果

 0x804a008 is allocated
 p1(*p1)= 0x804a008 (100)
 0x804a008 is freed
 0x804a008 is allocated
 p2(*p2)= 0x804a008 (200)
 p1(*p1)= 0x804a008 (200)
 0x804a008 is freed again!
 p1(*p1)= 0x804a008 (100)
 p2(*p2)= 0x804a008 (100)
 *** glibc detected *** double free or corruption (fasttop):
 0x0804a008 ***
 アボート (coreを出力しました)

順を追って説明して行く.

  • line 1: メモリが動的に確保される.結果をみると,そのアドレスは 0x804a008 のようだ.
  • line 2: 0x804a008 に 100 が代入される.
  • line 3: 0x804a008 が解放される.しかし,この時点で p1 には 0x804a008 が代入されている.
  • line 4: 新たにメモリを確保し, p2 に代入する.
    • 0x804a008 は先ほどまで確保されていたが, line 3 で既に解放されているので,空いている(誰も使っていない).
    • だから, *システムがこのアドレスをもう一度確保しようとするのは自然なこと* だ.したがって, 0x804a008 が確保され, p2 に代入される.
  • line 5: 0x804a008 に 200 が代入される.この時点で *p2 は当然 200 である. *p1 も同じアドレスを指しているのだから 200 である.
  • line 6: うっかり間違って ,既に解放したはずの p1 を解放してしまう.
    • つまり, 0x804a008 が解放される.何が起こるか? **確保されている(と思っている)はずの p2 が解放される** のである.その結果,
  • line 7: では,再び 0x804a008 が確保され,
  • line 8: で 0x804a008 に 100 が代入される. *p1 が 100 なのは期待通りだが, *p2 も 100 なのはとんでもない期待外れだ.
  • line 9: 0x804a008 が解放される.
  • line 10: 0x804a008 が再び解放される.gcc/g++ではここでエラーを出力する.しかし,しないコンパイラもあるし,このコードを書かなかったら,気づかない.

このプログラムで間違っているコードは, line 6 のたった一行である.これをコメントアウトしてコンパイルし,実行してみよう.何も問題は起こらない.

ちなみに, line 7 がなくても, line 8 はエラーにならない.これもポインタが危険な理由だ.

二重解放は気づきにくく,致命的なバグとなりやすいことが理解できたと思うが,実際にこのようなコードを書くときはどんな場合か.例えば,動的に確保された配列を要素に持つ構造体(行列とかベクタ,グラフ)を,更に10個ほど動的に確保するとしよう(動的メモリが入れ子構造になっている).この変数をコピーしたり,解放したりするプログラムを間違わずに書くことができるかどうか,ぜひ試してみよう.

Standard Template library

C++には STL (Standard Template library) と呼ばれるテンプレートクラスのライブラリがある. クラスもテンプレートも説明していないので,ここで解説はしないが,ベクタやリストと言った構造のテンプレート(ひながた)が用意されていて,メモリの確保や解放を自分で記述する必要がなくなる,と思えばよい.その結果,コード量は大幅に減る,二重解放と言ったわけのわからないバグも減る,コードのメンテナンスが簡単と言った恩恵を受けられる.しかし,STLを効率的に使うためには,少なくとも一度は自分でこれらの構造を設計してみるべきだし, コピーコンストラクタ などの重要な概念も知っておく必要がある.

参考: Cで動的メモリを確保する方法

Cでは malloc 関数を使って,動的にメモリを確保する. ある型 X を n 個分確保するためには,

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  X *p(NULL);
  p = malloc (sizeof(X)*n);

と書く. new 演算子と同じように, malloc は,欲しいバイト数を指定すると,ヒープにその領域を確保してそのメモリへのポインタを返す. 注意点としては, malloc は バイト数 で指定しなければならない点である.このため X 型ひとつあたりのサイズ sizeof(X) に n を掛けて, X を n 個分のサイズを計算している. 確保したメモリを解放するには,

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  free (p);
  p=NULL;

と書く.

サンプルプログラム:

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  #include <stdlib.h>
  #include <stdio.h>
  int main(int argc, char**argv)
  {
    int n=100, i;
    double *p=NULL;
    p = malloc (sizeof(double)*n);
    p[0] = 1.0;
    p[1] = 1.0;
    for (i=2; i<n; ++i)
      p[i] = p[i-1] + p[i-2];  // フィボナッチ数列
    printf ("Fibonacci sequence:");
    for (i=0; i<n; ++i)
      printf ("  %g", p[i]);
    printf ("\n");
    free (p); p=NULL;
    return 0;
  }

結果

 Fibonacci sequence:  1  1  2  3  5  8  13  21  34  55  89  144
 233  377  610  987  1597  2584  4181  6765  10946  17711
 28657  46368  75025  121393  196418  317811  514229  832040
 1.34627e+06  2.17831e+06  3.52458e+06  5.70289e+06  9.22746e+06
 1.49304e+07  2.41578e+07  3.90882e+07  6.3246e+07  1.02334e+08
 (以下略)

最後にもう一度. 確保したメモリは,必ず,一度だけ解放すること


*1 *(b+i

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2018-08-30 (木) 07:17:05 (19d)