第 10 回 動的なメモリ確保

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

10-1. 動的なメモリ確保

変数とメモリ

C 言語では情報処理は変数により行います。 変数はプログラム実行時にメモリ上に配置されます。 変数のメモリ上のアドレスは前述したように &変数名で得ることが できます。 一方、変数がメモリを使用する量は、それぞれの型により変わります。 sizeof 演算子は引数に型名、あるいは変数名を与えることで、占有するメモ リ量をバイト数で返します。

例10-1

以下のプログラムでは基本的な型とそのポインタ型のメモリ使用量を表示しま す。 なお、下記のように #define でカッコで引数を付けて定義すると、定義を呼 び出すときに定義内にある引数部分が引数に置き換えられます(マクロ機 能)。 また、#y のように引数に # を付けると、置き換え時に""(ダブルクォーテショ ンマーク)で括られます。


#include <stdio.h>
#define printsize(y) printf("%sのメモリ使用量は %d Byte\n",#y,sizeof(y));
int main(void){
  printsize(char);
  printsize(int);
  printsize(float);
  printsize(double);
  printsize(char*);
  printsize(int*);
  printsize(float*);
  printsize(double*);
  return 0;
}

例10-2

以下のプログラムでは構造型とそのポインタ型のメモリ使用量を表示しま す。


#include <stdio.h>
#define printsize(y) printf("%sのメモリ使用量は %d Byte\n",#y,sizeof(y));
typedef struct s1 {
  double d1;
} S1;
typedef struct s2 {
  double d1,d2;
} S2;
typedef struct s3 {
  double d1,d2,d3;
} S3;
int main(void){
  printsize(S1);
  printsize(S2);
  printsize(S3);
  printsize(S1*);
  printsize(S2*);
  printsize(S3*);
  return 0;
}

このように、それぞれの型の占有メモリ量は異なります。 しかし、ポインタ型の占有メモリ量は、どの型に対してもポインタはアドレス の計算のみなので、全て同じ量になります。

動的なメモリの確保

malloc 関数はプログラムの実行中にメモリを確保する関数です。 malloc 関数に必要なバイト数を引数にして呼ぶと、OS にメモリを確 保させ、確保したメモリの先頭の番地を値として返します。 一方、 free 関数はメモリの番地を引数とすると、そのメモリを OS に返します。 なお、利用できるメモリがない場合に malloc 関数が呼ばれた場合、メモリは 確保されず、 NULL が返されます。 NULL が返された時に返ってきた値(NULL)を用いて処理を続けるとプログラム が異常動作を起こし、最悪プログラムが暴走(操作不能な誤動作)を起こします。 そのため、malloc 関数の戻り値が NULL かどうかを検査する必要があります。

なお、 malloc 関数、 free 関数を使うには stdlib.h ヘッダファイルをイン クルードする必要があります。

通常の変数

malloc 関数で確保したメモリの領域を C 言語の変数として使うことができま す。 そのためには次の操作をします。

  1. 変数の型を定め、その型のポインタ型の変数をあらかじめ宣言しておきま す。
  2. malloc 関数でその型に必要なメモリ量を確保します。 malloc 関数は確保したメモリの先頭番地を (void *)型で返しま す。ポインタ型変数に代入すると、自動的に型変換されます。
  3. あとはそのポインタ型変数を使って malloc で確保した領域にアクセスします。

例10-3

malloc の使用例を次に示します。


#include <stdio.h>
#include <stdlib.h>
int main(void){
  char *c;
  int *i;
  double *d;
/* 実際に文字、整数、実数を入れられる場所はまだない */
  if((c=malloc(sizeof(char)))==NULL){
    fprintf(stderr,"メモリーが確保できませんでした\n");
    return 1;
  }
  if((i=malloc(sizeof(int)))==NULL){
    fprintf(stderr,"メモリーが確保できませんでした\n");
    free(c);
    return 1;
  }
  if((d=malloc(sizeof(double)))==NULL){
    fprintf(stderr,"メモリーが確保できませんでした\n");
    free(i);
    free(c);
    return 1;
  }
  *c='A';
  *i=10;
  *d=3.14;
  printf("%c %d %f\n",*c,*i,*d);
  free(d);
  free(i);
  free(c);
  return 0;
}

例10-4

malloc で構造体を作る例を示します。


#include <stdio.h>
#include <stdlib.h>
typedef struct st {
  char c;
  int i;
  double d;
} ST;
int main(void){
  ST *s;
  if((s=malloc(sizeof(ST)))==NULL){
    fprintf(stderr,"メモリーが確保できませんでした\n");
    return 1;
  }
  s->c='A';
  s->i=10;
  s->d=3.14;
  printf("%c %d %f\n",s->c,s->i,s->d);
  free(s);
  return 0;
}

ポインタ変数を宣言しても、通常は単独ではポインタ先の内容を変数として使 えません。 しかし、このように malloc 関数でメモリを確保することにより、変数領域を 確保することができます。

10-2. 配列として使用

malloc でメモリを確保する際に、型に必要なメモリ量の整数倍の領域を確保 すると、得られた領域を配列変数として使うことができます。 ポインタであっても、y[x] と配列の表現も使用できます。 これまでは、配列変数は宣言時に領域量を決めておかなければなりませんでし たが、この方法を使うと実行文中で配列のサイズを決めることができます。

例10-5


#include <stdio.h>
#include <stdlib.h>
int main(void){
  int x;
  int *y;
  int n;
  printf("確保する量は?");
  scanf_s("%d",&n);
  if((y=malloc(n*sizeof(int)))==NULL){
    fprintf(stderr,"メモリを確保できませんでした\n");
    return 1;
  }
  for(x=0; x<n; x++){
    printf("%d ",y[x]=x);
  }
  printf("\n");
  for(x=0; x<n; x++){
    printf("%d ",++y[x]);
  }
  printf("\n");
  free(y);
  return 0;
}

二次元配列

二次元配列は一次元配列を複数管理する物と考えることができます。 一次元配列、つまり通常の配列は特定の型の連続領域ですので、その型に対す るポインタ型の変数で制御します。 つまり、 int 型の配列は int* 型の変数で制御します。 したがって、二次元配列はそのポインタ型の変数を複数持つような配列を用意 すれば可能です。 つまり、 int 型の二次元配列を作るには、まず int* に対するポインタ、つ まり int** 型の配列を作り、その 配列の要素に int の配列を指すポインタ(int* 型)を入れていけば良いことに なります。

例10-6

二次元配列の例を示します。 free する際、確保した領域を開放順に気を付けなければなりません。 一次元の後に二次元、つまり、管理される側を先にし、管理する側は後にしま す。 もし、逆に管理する側を先に free してしまうと、管理される側をアクセスで きなくなります。


#include <stdio.h>
#include <stdlib.h>
int main(void){
  int **mat;
  int i,j,n;
  printf("行列の大きさを入れてください ");
  scanf_s("%d",&n);
  if((mat=malloc(sizeof(int*)*n))==NULL){
    fprintf(stderr,"メモリーが確保できませんでした\n");
    return 1;
  }
  for(i=0;i<n;i++){
    if((mat[i]=malloc(sizeof(int)*n))==NULL){
      fprintf(stderr,"メモリーが確保できませんでした\n");
      for(j=0;j<i;j++){
	free(mat[j]);
      }
      free(mat);
      return 1;
    }
  }
  for(i=0;i<n;i++){
    for(j=0;j<n;j++){
      mat[i][j]=i*n+j;
    }
  }
  for(i=0;i<n;i++){
    for(j=0;j<n;j++){
      printf(" %d",mat[i][j]);
    }
    printf("\n");
  }
  for(j=0;j<n;j++){
    free(mat[j]);
  }
  free(mat);
  return 0;
}

10-3. 演習問題

演習10-1

次のようなプログラムを作りなさい。

  1. データ数を scanf_s で質問します
  2. 整数値をその数だけ入力します
  3. すると入力した数値を逆順に出力します

演習10-2

文字列二つを結合するプログラムを作りなさい。但し、結合した結果を入れる 文字配列は malloc で作りなさい。


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void){
  char x[]="abcdef";
  char y[]="ghijklmn";
  char *z;
  int i,j;
  int lx,ly;
  lx=strlen(x);
  ly=strlen(y);
  printf("文字列 x=%s(長さ%d) と文字列 y=%s(長さ%d)を結合すると",x,lx,y,ly);
  /* z の領域を確保して、x と y をコピーする*/
  printf("z=%s(長さ%d)になる\n",z,strlen(z));
  free(z);
  return 0;
}

発展問題

人数を入力されたら、下記に示すような構造体の配列を作り、 学籍番号と名前を入力させなさい。 そして、全員入力が終えたら、名簿を出力しなさい。


typedef struct meibo {
    char *id;
    char *name;
} MEIBO;

ヒント

文字列を新たな領域にコピーするには、 _strdup 関数を使用するのが便利です。 string.h をインクルードすると使えます。

10-4. C++ のメモリ管理

C++ でメモリを確保する場合、通常は new 演算子を使用し、 malloc は使用 しません。

例10-7

例10-5を C++ の構文で書くとつぎのようになります。 なお、下記のプログラムでは new に対してもエラーの対策をしています。 但し、この様に対策をしなくても、C++ ではメモリー確保に失敗した場合、例 外が発生します。 つまり、例外が発生した場合、 try で囲わなければ、プログラムが停止しま す。 したがって、C 言語では対策しないと暴走の危険性がありましたが、 C++ で は対策しなくても暴走の危険性はありません。


#include <iostream>
#include <new>
int main(void){
  int x;
  int *y;
  int n;
  std::cout << "確保する量は?";
  std::cin >> n;
  try{
    y = new int[n];
  } catch (std::bad_alloc& e) {
    std::cerr << "メモリを確保できませんでした" << std::endl;
    return 1;
  }
  for(x=0; x<n; x++){
    std::cout << (y[x]=x) << " ";
  }
  std::cout << std::endl;
  for(x=0; x<n; x++){
    std::cout << ++y[x] << " ";
  }
  std::cout << std::endl;
  delete[] y;
  return 0;
}

また、 C++ で std::malloc 関数を使う必要がある場合、戻り値である *void 型を別の型へ自動的に変換させようとするとエラーが出ます。 どうしても必要な場合は static_cast を使用します。


#include <iostream>
#include <cstdlib>
int main(void){
  int x;
  int *y;
  int n;
  std::cout << "確保する量は?";
  std::cin >> n;
  if((y = static_cast<int*>(std::malloc(sizeof(int)*n)))==NULL){
    std::cerr << "メモリを確保できませんでした" << std::endl;
    return 1;
  }
  for(x=0; x<n; x++){
    std::cout << (y[x]=x) << " ";
  }
  std::cout << std::endl;
  for(x=0; x<n; x++){
    std::cout << ++y[x] << " ";
  }
  std::cout << std::endl;
  std::free(y);
  return 0;
}
参考
アンドレイ・アレキサンドレスク"Moderan C++ Design"ピアソン・エデュ ケーション(2001)の 6.8 章では std::malloc を使用する例があります。


坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科