変数や配列の動的な確保

配列などは、実行してから要素の数が決まるということがよくあります。

以下のプログラムはコンパイル時にエラーになって実行できません。

//プロジェクト名: dynamicarray1
#include <stdio.h>

int
main()
{
    printf("データの総数を入力してください。\n");
    int num;
    scanf("%d", &num);
    int a[num]; //ここで問題。配列の個数は数字でないといけない。

    for(int i = 0; i < num; i++)
    {
        printf("%d番目の数字を入力してください:", i+1);
        scanf("%d", &a[i]);
    }

    printf("入力されたデータは、");

    for(int i = 0; i < num; i++)
    {
        printf("%d ", a[i]);
    }

    printf("です。\n");

    getc(stdin);
    getc(stdin);
    return 0;
}

このように、データの個数がプログラムの実行時にはじめて決まる場合は、これまでの配列の宣言のやり方ではうまく行きません。

一つの解決方法はnew演算子を使うことです。

//dynamicarray1を変更する。
#include <stdio.h>

int
main()
{
    printf("データの総数を入力してください。\n");
    int num;
    scanf("%d", &num);
    int* a = new int[num]; //要素数がnum個のint型の配列のためのメモリを動的に確保

    for(int i = 0; i < num; i++)
    {
        printf("%d番目の数字を入力してください:", i+1);
        scanf("%d", &a[i]);
    }

    printf("入力されたデータは、");

    for(int i = 0; i < num; i++)
    {
        printf("%d ", a[i]);
    }

    printf("です。\n");

    delete[] a; //newで確保したメモリは必要なくなったら必ずdeleteで開放する

    getc(stdin);
    getc(stdin);
    return 0;
}

上の例では、

int* a = new int[num];

が、実行時に配列の要素を決めるやり方です。num個のint型の配列をメモリに割り当てて、そのメモリの先頭のアドレスをポインタ変数aに入れます。

 

newは配列だけではなく、一個の変数も割り当てることが出来ます。

int* a = new int;

これは、

int* a = new int[1];

と同じことです。

int a[100];

などと宣言した配列は、自動的に必要なくなるとメモリから消滅しますが、newで確保したものはいつまでも(プログラムが終わるまで)残ります。 よって、newで作ったものは消さないといけません。消すためにはdeleteを使います。

int* a = new int[num];

としたら、

delete[] a;

として消します。

 

int* a = new int[1];

としたら、おなじく

delete[] a;

とします。

 

int* a = new int;

としたら、

delete a;

とします。

 

もし、

int* a = new int[100];

としたのに、

delete a;

としてすると、先頭の1個の要素だけが消され、残りの99個はいつまでもメモリに残ることになりますので注意してください。


スコープ

C,C++では、中カッコ{}で囲まれた部分が一つのスコープ(範囲)の単位となっています。

例えば、

int d = 1;

void test1()
{
    int a;
    int b;
    b = d; //dはこの位置からも使える
}

void test2()
{
    int a;//この変数はtest1の変数aとは別物
    int c;
    c = d; //dはこの位置からも使える
}

関数test1の中({}の中)では変数a,b,dにアクセスできます。関数test2の中では変数a,c,dにアクセスできます。

ここで、test1とtest2の変数aは別の変数になることに注意してください。変数dはtest1からもtest2からも使える変数です。

 

void test3()
{
    int a;

    {
        int b[100];
        int*c = new int[100];
   
    } //b[100]はここで消える。cは使えなくなるがメモリの内容は残る
   
   
a = b[0]; //エラー
   
a = c[0]; //エラー
}

上の例では、カッコが関数の中にあります。カッコの内側で宣言した変数はカッコの外に出たときに消えます。ただし、newで確保したメモリは残ります。

void test4()
{
    int* a1;
    int* a2;

    {
        int b[100];
        int*c = new int[100];

        a1 = b;
        a2 = c;
   
    } //b[100]はここで消える。cは使えなくなるがメモリの内容は残る

    int k = a1[1];//NG もう消えている
    int l = a2[1];//OK
}


STLを使って動的な配列を楽に扱う

STLか開発されるまでは配列が実行時に決まるような場合はnewを使うのが一般的でしたがSTLを使うともっと簡単です。STLはテンプレートというC++の機能を使っています。この内容を本当に理解するにはテンプレートを学習する必要がありますが、その詳細は後期に説明したいと思います(今のところあくまでも予定)。ただし使いかを覚えて使うのは簡単です。

vector

vectorは動的に要素の数が変わる配列を操作するのに適したテンプレートクラスです。例えば、10個の要素のint型の配列を宣言するには、

std::vector<int> a(10);

とします。10の部分には変数も使うことが出来ます。

char型の配列の場合は

std::vector<char> a(100);

の様にします。<>の中に型やクラスを指定します。

宣言した後は、通常の配列の様につかえます。

a[1] やa[3]の様に各要素にアクセスできます。また、スコープの外に出ると消えるのも同様です。

上のnewを使ったプログラムをvectorを使って書き直したものが以下のプログラムです。

//プロジェクト名: dynamicarray2
#include <stdio.h>
#include <vector> //STLのvectorを使う

int
main()
{
    printf("データの総数を入力してください。\n");
    int num;
    scanf("%d", &num);
    std::vector<int> a(num); //numの要素をもつint型の配列を宣言する

    for(int i = 0; i < num; i++)
    {
        printf("%d番目の数字を入力してください:", i+1);
        scanf("%d", &a[i]);
    }

    printf("入力されたデータは、");

    for(int i = 0; i < num; i++)
    {
        printf("%d ", a[i]);
    }

    printf("です。\n");

    getc(stdin);
    getc(stdin);
    return 0;
}

このように、配列の宣言以外まったく同じです。

ところで、vector配列の個数を実行中に変更できます。ということは、わざわざ最初に個数を入力してもらわなくても良くなります。vectorはクラスなのでメンバ関数が色々あります。要素を追加するメンバ関数push_backを使うと要素数を増やしながら追加していくことが出来ます。

//dynamicarray2を変更する。
#include <stdio.h>
#include <vector> //STLのvectorを使う

int
main()
{
    std::vector<int> a(0); //空の配列(要素0個)

    int tmp = 0;
    for(int i = 0; tmp > -1; i++)
    {
        printf("%d番目の数字を入力してください(-1で終了):", i+1);
        scanf("%d", &tmp);
        if(tmp != -1)
        {
            a.push_back(tmp); //配列の後ろに入力された数字を入れる
        }
    }

    printf("入力されたデータは、");

    for(int i = 0; i < a.size(); i++) //sizeは要素数を返すメンバ関数
    {
        printf("%d ", a[i]);
    }

    printf("です。\n");

    getc(stdin);
    getc(stdin);
    return 0;
}

vectorのメンバ関数sizeは配列の要素数を返します。上のプログラムでは表示のときの繰り返しの数を決めるのに使っています。

vectorは他にも色々な機能があります。HELPやインターネットで調べてみてください。

 

反復子

STLではvectorのような、何かを格納するクラスをコンテナといいますが、その各要素アクセスする方法が何通りかあります。一番直感的なのが

a[0]

のようにカッコ[ ]でアクセスする要素の番号を指定することですが、この方法は配列の様に要素が隣り合っているときにはうまく行きますが、要素がメモリの中にばらばらに配置されているようなときには使えない場合があります。もっと色々な場合にもうまくいく要素のアクセスの仕方が反復子(iterator)を使ったやり方です。

//プロジェクト名: dynamicarray3
#include <stdio.h>
#include <vector> //STLのvectorを使う

int
main()
{
    std::vector<int> a(0); //空の配列(要素0個)

    int tmp = 0;
    for(int i = 0; tmp > -1; i++)
    {
        printf("%d番目の数字を入力してください(-1で終了):", i+1);
        scanf("%d", &tmp);
        if(tmp != -1)
        {
            a.push_back(tmp); //配列の後ろに入力された数字を入れる
        }
    }

    printf("入力されたデータは、");

    for(
std::vector<int>::iterator i = a.begin(); i != a.end(); i++) //反復子による繰り返し
    {
        printf("%d ", *i); //反復子iに*を付けるとiが指す位置の値になる(ポインタと同じ)
    }

    printf("です。\n");

    getc(stdin);
    getc(stdin);
    return 0;
}

表示の部分のfor文の中が反復子を使った例です。

std::vecotr<int>::iterator i = a.begin();

の青い部分がintやcharと同様の型だと考えてください。ちょっと長いですがこれは「整数が要素のvectorの反復子」という意味です。

そしてiがその型の変数(正しくはインスタンス)です。iは要素の位置を示しています。*iとするとiが示している位置の値になります。これは、ちょうどポインタと同じです。反復子も実はクラスですが、演算子をオーバロードしてポインタと同じ感覚で扱えるように作られているわけです。

そして、vectorのメンバであるbeginは要素の最初の位置を返します。endは最後の位置を返します。endの方は注意してください。正しくは、要素の最後の次の位置を返します。つまりiが一番最後の時には*iとするとエラーになります。この様にしたほうが都合がいい場合が多いのでこの様になっています。

反復子は足すことで次の要素へアクセスできます。

i ++; //次の要素へ

i--; //一つ前の要素へ

i = i + 1; //次の要素へ(コンテナによっては使えない)

i = i -1; //一つ前の要素へ(コンテナによっては使えない)

i = i +3; //3つ先の要素へ(コンテナによっては使えない)

このように書けます。

 

色々なコンテナ

反復子で要素をアクセスできるようになるを、色々なコンテナに同じ方法でアクセスできるようになります。STLにはvector以外にも色々なコンテナクラスがあります。

コンテナとは入れ物のことです。コンテナが違うとは、入れ方が違うということです。入れ方が違うと、得意な操作と不得意な操作が変わります。

例えば、vectorは要素がメモリの中で連続して配置されます。よって、好きな要素を見ることが簡単に出来ます。その代わり、配列の個数を変えたりするときに内部で手間が掛かるのでその分時間が掛かります。

一方listというコンテナでは、要素がメモリの中で次の要素の位置とペアで格納されます。また、要素が連続して配置されているとは限りません。list場合、要素を見るときはいちいち先頭の要素から辿っていかないといけないのでvectorに比べて時間が掛かります。その代わり配列の個数を変えるのがとても早く出来ます。

ここで、重要なのは、コンテによって得意なことと苦手なことがあるということです。よって、用途にふさわしいコンテナを選択すると効率の良いプログラムが書けます。そして、反復子を使うと異なるコンテナを同じ方法で使うことが出来 て、コンテナの入れ替えが簡単に出来るということです。

それでは、上のプログラをvectorからlistに変えてみましょう。

//dynamicarray3を変更する。
#include <stdio.h>
#include <list> //STLのlistを使う

int
main(int argc, char* argv[])
{
    std::list<int> a(0); //空の配列(要素0個)

    int tmp = 0;
    for(int i = 0; tmp > -1; i++)
    {
        printf("%d番目の数字を入力してください(-1で終了):", i+1);
        scanf("%d", &tmp);
        if(tmp != -1)
        {
            a.push_back(tmp); //配列の後ろに入力された数字を入れる
        }
    }

    printf("入力されたデータは、");

    for(
std::list<int>::iterator i = a.begin(); i != a.end(); i++) //反復子による繰り返し
    {
        printf("%d ", *i); //反復子iに*を付けるとiが指す位置の値になる(ポインタと同じ)
    }

    printf("です。\n");

    getc(stdin);
    getc(stdin);
    return 0;
}

このプログラムは、vectorをlistに置き換えただけです。まったく同じ動きをします。

 

アルゴリズム

実は、STLには、並べ替える機能も用意されています。これは、関数で用意されています。ただし、ただの関数ではなくテンプレートの機能が使われていて普通の関数よりも色々なことに使えます。このようにテンプレートの機能を使って定義された関数をかっこよく 「アルゴリズム」と呼びます。ただし、これはSTL独特の呼び方で、普通アルゴリズムとは(有限で終了する)実行手順のことをいいます。

それでは、vectorを使ったプログラムに並べ替えの機能を付けてみましょう。

//プロジェクト名: algorithm1
#include <stdio.h>
#include <vector> //STLのvectorを使う
#include <algorithm> //STLのアルゴリズムを使う

int
main()
{
    std::vector<int> a(0); //空の配列(要素0個)

    int tmp = 0;
    for(int i = 0; tmp > -1; i++)
    {
        printf("%d番目の数字を入力してください(-1で終了):", i+1);
        scanf("%d", &tmp);
        if(tmp != -1)
        {
            a.push_back(tmp); //配列の後ろに入力された数字を入れる
        }
    }

    std::sort(a.begin(), a.end()); //STLなら並べ替えも簡単

    printf("入力されたデータは、");

    for(
std::vector<int>::iterator i = a.begin(); i != a.end(); i++) //反復子による繰り返し
    {
        printf("%d ", *i); //反復子iに*を付けるとiが指す位置の値になる(ポインタと同じ)
    }

    printf("です。\n");

    getc(stdin);
    getc(stdin);
    return 0;
}

真ん中の

std::sort(a.begin(), a.end());

が並べ替えを行っているところです。

このように

引数に、最初と最後の位置を与えます。

このようにSTLについて駆け足で説明しましたが、STLがいかに便利であるかわかってもらえたでしょうか?興味が出た人は、色々なほかのSTLの機能や、STLの基本技術であるテンプレートを各自学習してみてください。

次へ進みます。