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 と呼ぶ.
malloc, new
などにより,プログラム実行中に動的に確保・解放される.
領域の大きさを「実行中に」自由に変更できる.static
で定義された変数などが配置されており,プログラム領域に続いて確保される.プログラムの実行中は移動しない.
例:100個のdouble
型配列の場合,1個あたり 8byte × 100個分 のデータを格納するメモリが,
double x[100];
では,スタック領域に確保される.static double x[100];
では,静的領域(ヒープ領域)に確保される.
重要:通常スタック領域は数MB程度なので,大きな配列(数MB以上)を確保しようとすると実行時にStack overflow エラーが出る.
そのような場合は,
static
をつけて定義する.
このなかで,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 */
メモリを(ヒープ領域に)動的に確保するために,以下のような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++では,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;
}
スマートポインタは「所有権」という考え方でポインタを管理しているので,
通常のポインタのような安易なコピーができない仕組みとなっている.
関数の引数として渡す場合など,詳しくは別途解説する.
配列を確保する目的で,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;
}