yoppa.org


メディア芸術演習 VI - メディア・アート II

openFrameworksで、オブジェクト指向プログラミング(OOP) 後編

オブジェクトの生成とメモリ

openFrameworksの入門の授業の際に、変数を定義するということは、値を入れる「箱」のようなものを用意することだと説明しました。また、前回の授業では、オブジェクトを実体化するにはまずクラスをインスタンス化する必要があるという説明をしました。

実はクラスのインスタンス化と、変数の宣言は本質的には同じことをしています。それは、「値や動作を表現するための記憶域を確保する」ということです。int型やfloat型などの宣言に関しても、型からオブジェクトを生成しています。

コンピュータの中では、個々の箱は独立して存在するのではなく、コンピュータに内蔵された広大な記憶領域の中の一部分を使用しています。多くのオブジェクトがこの記憶域を共有して雑居しています。そのため、個々のオブジェクトの格納されている「場所」を表す必要が出てきます。コンピュータではこのオブジェクトが格納された「番地」のことを「アドレス (address)」と呼んでいます。

オブジェクトとアドレス

オブジェクトを生成した際に格納された記憶域のアドレスは、コンピュータによって自動的に割り当てられます。オブジェクトのアドレスは、アドレス演算子「&」によって取りだすことが可能です。

以下のプログラムは、アドレス演算子「&」を使用して、オブジェクトが格納されたアドレスを表示しています。プログラムを実行してみると、int型のオブジェクトi、float型のオブジェクトx、そしてofPointクラスのオブジェクト(インスタンス)posには、それぞれ別個の独立したアドレスが割り振られているはずです。ここでの実行例のアドレスはあくまで一例であり、実行するコンピュータやその状態によってアドレスは変化します。

testApp.cpp

#include "testApp.h"

int n; //int型のオブジェクト n を生成
float x; //float型のオブジェクト x を生成
ofPoint pos; //ofPointクラスからオブジェクト pos を生成

//--------------------------------------------------------------
void testApp::setup(){

}

//--------------------------------------------------------------
void testApp::update(){

}

//--------------------------------------------------------------
void testApp::draw(){
    ofSetColor(0, 0, 0);
    //それぞれのオブジェクトの格納されたアドレスを表示
    ofDrawBitmapString("&n = " + ofToString(int(&n), 0), 10, 20);
    ofDrawBitmapString("&x = " + ofToString(int(&x), 0), 10, 40);
    ofDrawBitmapString("&pos = " + ofToString(int(&pos), 0), 10, 60);
}

/* 後略 */

実行結果 – オブジェクトの格納されたアドレスが表示される。

ポインタ

オブジェクトのアドレスそれ自体を取得して表示しても、プログラムには何の役にもたちません。オブジェクトのアドレスを有効に活用するには、「ポインタ (pointer)」を使用します。ポインタとは、オブジェクトの内容が格納されている場所の位置情報を保持する変数です。わかりやすい例えで考えると、オブジェクトのデータそのものではなく、オブジェクトの格納された住所が格納された箱というイメージです。つまりポインタptrの値がオブジェクトnのアドレスであるとき、「ptrはnを指す」というように表現されます。オブジェクトのアドレスを指し示す(point)機能ということで、ポインタ(pointer)なのです。

では実際にポインタを使ってみましょう。まず、様々な型やクラスのアドレスをポインタに格納しています。例えば、int型のオブジェクトのアドレスを格納するには、「int* 型」を使用します。またfloat型のオブジェクトのアドレスには「float* 型」を使用しています。このような型は「ポインタ型」と呼ばれています。

ポインタ型「ptr」が値「n」のアドレスであるとき、「ptrはnを指す」といいます。下記の図のようなイメージです。

そして、ptrがnを指しているとき、*pはnのエイリアス(別名)となります。図であらわすと下記のようになります。

このオブジェクトとポインタの関係を実際にプログラムで確かめてみましょう。

testApp.cpp

#include "testApp.h"

int n = 10; //int型のインスタンス n を生成
float x = 12.34; //float型のインスタンス x を生成
ofPoint pos; //ofPointクラスからインスタンス pos を生成

//--------------------------------------------------------------
void testApp::setup(){

}

//--------------------------------------------------------------
void testApp::update(){

}

//--------------------------------------------------------------
void testApp::draw(){
    pos.x = 100.0;
    pos.y = 200.0;
    
    int* ptr_n = &n;
    float* ptr_x = &x;
    ofPoint* ptr_pos = &pos;
    
    ofSetColor(0, 0, 0);
    //それぞれのオブジェクトの格納されたアドレスを表示
    ofDrawBitmapString("&n = " + ofToString(int(&n), 0), 10, 20);
    ofDrawBitmapString("&x = " + ofToString(int(&x), 0), 10, 40);
    ofDrawBitmapString("&pos = " + ofToString(int(&pos), 0), 10, 60);
    
    //ポインタ変数の指し示す先のオブジェクトの中身を表示(参照外し)
    ofDrawBitmapString("*ptr_n = " + ofToString(*ptr_n, 0), 10, 100);
    ofDrawBitmapString("*ptr_x = " + ofToString(*ptr_x, 2), 10, 120);

    //ポインタ変数のプロパティにアクセスするには、アロー演算子(->)を使用する
    ofDrawBitmapString("ptr_pos->x = " + ofToString(ptr_pos->x, 2), 10, 140);
    ofDrawBitmapString("ptr_pos->y = " + ofToString(ptr_pos->y, 2), 10, 160);
}

/* 後略 */

ポインタと配列

ポインタと配列はとても密接な関連があります。まず重要な規則として以下の法則があります。

  • 配列名は、その配列の先頭要素のポインタとして解釈される

このことから、aが配列であった場合に以下の式が成り立ちます

  • *a = a[0]; → 配列の値
  • a = &a[0]; → 配列の先頭要素のアドレス

また、このことから、ポインタpを用意して配列aの要素eを指した場合、以下の操作が可能となります。

  • p + i は、要素eのi個だけ後方の要素を指す
  • p – i は、要素eのi個だけ前方の要素を指す

例えば以下のようにpがa[0]を指していたとすると

int a[5];
int* p = a;
  • *p = a[0];
  • *(p + 1) = a[1];
  • *(p + 2) = a[2];
  • *(p + 3) = a[3];
  • *(p + 4) = a[4];

また、例えば以下のようにpがa[2]を指していたとすると下記のようになります

int a[5];
int* p = &a[2];
  • *(p – 2) = a[0];
  • *(p – 1) = a[1];
  • *p = a[2];
  • *(p + 1) = a[3];
  • *(p + 2) = a[4];

この配列とポインタの関係はC++でプログラムする上でとても重要な概念となります。以下のプログラムでポインタと配列の関係について一覧してみましょう。

testApp.cpp

#include "testApp.h"

int a[5];

//--------------------------------------------------------------
void testApp::setup(){
    ofSetColor(0, 0, 0);
}

//--------------------------------------------------------------
void testApp::update(){

}

//--------------------------------------------------------------
void testApp::draw(){
    a[0] = 0;
    a[1] = 1;
    a[2] = 2;
    a[3] = 3;
    a[4] = 4;

    //p は a[0] を指す。
    int* p = a;
    
    for (int i = 0; i < 5; i++) {
        ofDrawBitmapString("a[" + ofToString(i, 0)+ "] = " + ofToString(a[i], 0), 10, i * 20 + 20);
        ofDrawBitmapString("*(a+" + ofToString(i, 0)+ ") = " + ofToString(*(a+i), 0), 100, i * 20 + 20);
        ofDrawBitmapString("&a[" + ofToString(i, 0)+ "] = " + ofToString(int(&a[i]), 0), 220, i * 20 + 20);
        ofDrawBitmapString("a+" + ofToString(i, 0)+ " = " + ofToString(int(a+i), 0), 400, i * 20 + 20);

        ofDrawBitmapString("p[" + ofToString(i, 0)+ "] = " + ofToString(p[i], 0), 10, i * 20 + 150);
        ofDrawBitmapString("*(p+" + ofToString(i, 0)+ ") = " + ofToString(*(p+i), 0), 100, i * 20 + 150);
        ofDrawBitmapString("&p[" + ofToString(i, 0)+ "] = " + ofToString(int(&p[i]), 0), 220, i * 20 + 150);
        ofDrawBitmapString("p+" + ofToString(i, 0)+ " = " + ofToString(int(p+i), 0), 400, i * 20 + 150);

    }
}

/* 後略 */

配列aを定義して、その配列の先頭要素を指すポインタpを定義すると、様々な関係が成り立つことがわかります。

ポインタの利用例 1:ポインタの値渡し

ここまで、ポインタの基本について概観してきました。ここまでのプログラムは、あくまでポインタ理解のためのサンプルプログラムでした。では、ポインタは実際のプログラムの中でどのように役に経つのか実例をみてみましょう。

まず最初にあげる例は、関数を実行して、その結果から複数の値を戻したい場合です。関数の実行結果を戻したい場合には、普通は return を使用して戻り値として、値そのものを受け渡します。しかし、戻り値は常に1つしか使用することができません。2つ以上の戻り値を関数に持たせたい場合には、ポインタを使用して、引数のポインタの値渡しという手法を使用します。

これは、関数の引数として結果をやりとりする際に、値を戻すための引数の宣言をポインタ型にします。関数を呼びだす側では、引数そのものではなく、その参照(アドレス)を渡すようにします。関数の中ではそのポインタの参照外しをして値をとりだし、処理を行います。こうすることで、関数の呼び出し元と外部の関数の内部で全く同じ情報を操作することになります。この方法を用いることで、複数の値を処理の結果として受けとることができるのです。この手法を「ポインタの値渡し」と呼びます。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP


#include "ofMain.h"

class testApp : public ofBaseApp{
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    //2つの数の足し算とかけ算をする関数
    //結果をポインタの値渡しで返すようにする
    void sumAndMul(int x, int y, int *sum, int *mul);
};

#endif

testApp.cpp

#include "testApp.h"

//--------------------------------------------------------------
void testApp::setup(){

}

//--------------------------------------------------------------
void testApp::update(){

}

//--------------------------------------------------------------
void testApp::draw(){
    int a = 10, b = 20;
    int tasizan = 0, kakezan = 0;
    
    //足し算と掛け算をして、結果をポインタの値渡しで2つ受け取る
    sumAndMul(a, b, &tasizan, &kakezan);

    ofSetColor(0, 0, 0);
    ofDrawBitmapString("sum = " + ofToString(tasizan, 0), 10, 20);
    ofDrawBitmapString("mul = " + ofToString(kakezan, 0), 10, 40);
}

//--------------------------------------------------------------
void testApp::sumAndMul(int x, int y, int *sum, int *mul)
{
    *sum = x + y;
    *mul = x * y;
}

ポインタの利用例 2:関数間の配列の受け渡し

次に、関数間で配列のデータをやりとりする場合を考えてみましょう。openFrameworksの基盤となっているC++では、関数の引数として配列を使用することができません。ですので、関数間で複数のまとまった値を配列としてやりとりしようとする際に工夫が必要となります。

引数の値を関数間でやりとりする場合には、その引数として配列そのものではなく、配列の先頭要素を指すポインタ型として指定します。そうすると、そのポインタが外部の関数ではあたかも呼び出しもとの配列そのものであるように振る舞うことが可能となるです。例えば、配列aの先頭要素を指すポインタcを定義し、cを配列の引数とすることで、関数側では配列cを呼び出し元の配列aと同じデータとして共有することが可能となります。これによって、関数間で配列の値を出し入れるすことが可能となるのです。

次の例は、配列の値全てに対して、それぞれの値を2乗する関数を定義しています。呼び出し元の配列xの先頭要素を指すポインタ型aを配列の引数にして、関数内では配列aを操作することで、配列内の値をやりとりしています。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP


#include "ofMain.h"

class testApp : public ofBaseApp{
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    //配列の全ての値を2乗する関数
    //第1引数には配列を受け取る
    void arrayTwoPow(int *a, int n);
};

#endif

testApp.cpp

#include "testApp.h"

//--------------------------------------------------------------
void testApp::setup(){
    ofSetColor(0, 0, 0);
}

//--------------------------------------------------------------
void testApp::update(){

}

//--------------------------------------------------------------
void testApp::draw(){
    int a[5] = {1, 2, 3, 4, 5};
    int n = 5;

    //第1引数に配列、第2引数にその数を設定
    arrayTwoPow(a, n);

    for (int i = 0; i < n; i++) {
        ofDrawBitmapString("a[" + ofToString(i, 0) + "] = " + ofToString(a[i], 0), 10, i * 20 + 20);
    }
}

//--------------------------------------------------------------

//配列の全ての値を2乗する関数
void testApp::arrayTwoPow(int *a, int n)
{
    for (int i = 0; i < n; i++) {
        a[i] = a[i] * a[i];
    }
}

/* 後略 */

オブジェクトの動的な生成

ポインタを利用して、プログラムの実行時に自由なタイミングでオブジェクト用の領域を確保したり解放したりすることができます。

前回の講義で試したように、オブジェクトは冒頭で変数の宣言と同様にクラスの種類とオブジェクトの名前を宣言することで、自動的に生成され記憶領域に格納されます。この際、オブジェクトの寿命はプログラムの流れに委ねられておりコントロールすることはできませんでした。

「new演算子」を使用することで、オブジェクトを任意のタイミングで動的に生成することが可能となります。new演算子は、記憶領域の空き領域(ヒープ heap)から指定したクラスを大きさの領域を確保します。その際、new演算子は確保した領域のアドレスを返します。この動的な記憶領域のエイリアスを使用するために、ポインタを利用します。

例えば、先週とりあげたMyCircle型のクラスを動的に生成してみましょう。MyCircleクラスからオブジェクトを動的に生成し、そのアドレスをポインタ型circleに格納するとすると、下記のようになります。

MyCircle* circle = new MyCircle();

この際、ポインタcircleは、MyCircleクラスのオブジェクトそのものではなく、MyCircleクラスのオブジェクトを指しているポインタであることに注意してください。

動的に生成したオブジェクトのポインタから、そのメンバであるメソッドやプロパティを参照するには、ドット演算子「.」の代わりに、アロー演算子「->」を用います。例えば、MyCircleクラスに、radiusというプロパティとdraw()というメソッドが定義されていたとすると、そのプロパティとメソッドには下記のようにアクセスします。

MyCircle* circle = new MyCircle();
circle->radius = 100; //半径に100を代入
circle->draw(); //draw()メソッドを実行

では、動的なオブジェクトの生成を利用して、先週のMyCircleクラスで円を描くプログラムを書き直してみましょう。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "MyCircle.h"

class testApp : public ofBaseApp{
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    //MyCircleクラスへのポインタを定義
    MyCircle *circle;
};

#endif

testApp.cpp

#include "testApp.h"

//--------------------------------------------------------------
void testApp::setup(){
    ofBackground(0, 0, 0);
    ofSetCircleResolution(64);
    
    //MyCircleのオブジェクトcircleを動的に生成
    circle = new MyCircle();
}

//--------------------------------------------------------------
void testApp::update(){

}

//--------------------------------------------------------------
void testApp::draw(){
    //アロー演算子「->」を用いて、
    //MyCircleクラスのインスンタンスcircleのdraw()メソッドを実行
    circle->draw();
}

MyCircle.h

#ifndef _MY_CIRCLE
#define _MY_CIRCLE

#include "ofMain.h"

class MyCircle {

public: 
    void draw();
    
private:

};

#endif

MyCircle.cpp

#include "MyCircle.h"

void MyCircle::draw()
{
    ofEnableAlphaBlending();
    //色を指定
    ofSetColor(31, 127, 255, 127); 
    //画面の中央に外周の円を描く
    ofCircle(ofGetWidth()/2, ofGetHeight()/2, 100);
    //色を指定
    ofSetColor(255, 31, 0, 200); 
    //画面の中央に円の核を描く
    ofCircle(ofGetWidth()/2, ofGetHeight()/2, 20);
}

オブジェクトのライフサイクル

オブジェクトを動的に生成するにはnew演算子を用いてポインタにそのアドレスを格納しました。

動的に生成されたオブジェクトは、delete演算子を用いて破棄することが可能ですが。detele演算子でオブジェクトを破棄することで、確保された領域は解放されまたヒープ領域(空領域)に戻され、再利用できるようになります。

new演算子とdelete演算子を適切なタイミングで使用することで、メモリを効率良く使用しながらプログラムを実行することが可能となります。画像や映像、音声などの大容量のデータを扱う際や、大量のオブジェクトを生成する際などには、動的なオブジェクトの生成と破棄というオブジェクトのライフサイクルをうまく管理することで、効率良く処理能力の高いプログラムを作成できます。

コンストラクタとデストラクタ

クラスは、インスタンス化される際に、オブジェクトの初期化を行うための特別な関数を定義することができます。この初期化のための関数を「コンストラクタ (constructer)」と呼びます。コンストラクタはクラス名と同じ名前の関数名にするという規則があります。またコンストラクタは戻り値をとることはできません。例えば、MyCicrleクラスのコンストラクタは、MyCircle() となります。

コンストラクタには、引数を利用して初期化の際のパラメータを指定することも可能です。引数は、new演算子を用いてオブジェクトを動的に生成する際に併わせて指定します。例えば、MyCircleクラスのコンスラクタの引数として、円の中心位置(ofPoint pos)とその半径(float radius)を引数として与える場合は、下記のようになります。

MyCircle (ofPoint pos, float radius);

このコンストラクタはnew演算子でオブジェクトを生成する際に一緒に引数を指定します。例えば、位置(100, 200)、半径120で生成する場合には、下記のようになります。

MyCircle* circle = new MyCircle(ofPoint(100, 200), 120);

また、クラスにはそのクラスがdelete演算子で消去される際に実行される特別な関数を定義することも可能です。この消去の際の関数を「デストラクタ (destructer)」と呼びます。デストラクタはクラス名の先頭にチルダ「~」をつけて関数名とします。例えば、MyCiecleクラスの場合は、~MyCircle() となります。

では、先程の円を描くプログラムを改良して、コンストラクタで位置と半径と指定して生成できるように改良してみましょう。マウスをクリックしたタイミングでもしまだオブジェクトが生成されていなければ、、マウスの位置に半径100で円を描くMyCircleクラスのオブジェクトcricleを動的に生成します。またクリックした時に素手にcircleオブジェクトを生成済みの場合は、circleオブジェクトを消去しています。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "MyCircle.h"

class testApp : public ofBaseApp{
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    //MyCircleクラスへのポインタを定義
    MyCircle *circle;
    bool drawCircle;
};

#endif

testApp.cpp

#include "testApp.h"

//--------------------------------------------------------------
void testApp::setup(){
    ofBackground(0, 0, 0);
    ofSetCircleResolution(64);
    
    //円の描画をOFFに
    drawCircle = false;
}

//--------------------------------------------------------------
void testApp::update(){

}

//--------------------------------------------------------------
void testApp::draw(){
    if (drawCircle) {
        //アロー演算子「->」を用いて、
        //circleのdraw()メソッドを実行
        circle->draw();
    }
}

//--------------------------------------------------------------

/* 中略 */

//--------------------------------------------------------------
void testApp::mouseReleased(int x, int y, int button){
    if (button == 0 && !drawCircle) {
        //左ボタンクリックで、マウスの位置に円を生成
        circle = new MyCircle(ofPoint(x, y), 100);
        //円の描画をONに
        drawCircle = true; 
    } else if (button == 2 && drawCircle) {
        //マウス右クリックで、オブジェクトを解放
        delete circle;
        //円の描画をOFFに
        drawCircle = false;
    }
}

/* 後略 */

testApp.h

#ifndef _MY_CIRCLE
#define _MY_CIRCLE

#include "ofMain.h"

class MyCircle {
    
public: 
    //コンストラクタ
    MyCircle(ofPoint pos, float radius);
    //デストラクタ
    ~MyCircle();
    //円を描く
    void draw();
    //アクセサ
    void setPos(ofPoint pos);
    void setRadius(float radius);
    
private:
    //プロパティはprivateで宣言
    //円の位置
    ofPoint pos;
    //円の半径
    float radius;
};

#endif

testApp.cpp

#include "MyCircle.h"

//コンストラクタ
MyCircle::MyCircle(ofPoint _pos, float _radius) 
{
    //位置と半径を設定
    pos = _pos;
    radius = _radius;
}

//デストラクタ
MyCircle::~MyCircle()
{

}

void MyCircle::draw()
{
    ofEnableAlphaBlending();
    //色を指定
    ofSetColor(31, 127, 255, 127); 
    //画面の中央に外周の円を描く
    ofCircle(pos.x, pos.y, radius);
    //色を指定
    ofSetColor(255, 31, 0, 200); 
    //画面の中央に円の核を描く
    ofCircle(pos.x, pos.y, radius*0.2);
}

void MyCircle::setPos(ofPoint _pos)
{
    pos = _pos;
}

void MyCircle::setRadius(float _radius)
{
    radius = _radius;
}

動的配列を利用したオブジェクトの管理

では次にオブジェクトを動的に大量に生成して、配列に格納して使用してみようと思います。動的に生成や消去されるオブジェクトを配列で管理するには、配列も動的に使用できる必要があります。

このサンプルでは、「vector (動的配列)」という新たな概念が導入されています。

たくさんの要素を一度に扱う際には、今までは配列(Array)を使用してきました。例えば、100個のMyCircleのインスタンスを扱いたいのであれば、今までのやり型ですと配列(Array)を使用して次のように定義していたでしょう。

MyCircle circles[100];

今回のサンプルでは、ユーザがクリックした回数だけ制限なくMyCircleを複製していきたいと考えています。ところが、配列はまず生成する際に確保する要素の数を指定しなければなりません。先程の配列の例では、100個以上はオブジェクトを格納できません。これを避けるために、例えば、最初から大量の数を確保する(たとえば10000個の領域を確保する)というのも一つの方法です。しかし、確保した10000という数字には根拠がなく、さらにメモリを無駄に消費してしまうという問題もあり、あまり賢い方法ではありません。

そこで、「vector (動的配列)」というものを使用します。vectorは通常の配列と違って、最初に要素数を宣言する必要がありません。要素数は必要な際に後から追加していくことが可能となっています。vectorを使うことで今回のケースのように要素の数があらかじめわからないような場合でも効率良くプログラムすることが可能です。

vectorの宣言は、下記のような書式になっています。

vector <《要素の型》> 《vector名》;

MyCircleのポインタ型を、動的配列(vector)であるcirclesに格納するには、下記のような書式になります。

vector <MyCircle *> circles;

動的配列であるcirclesは、通常の配列と同じように使用することが可能です。例えば、特定の要素の内容を参照したければ、下記のように通常の配列と同じ書式で値を取りだすことができます。下記の例では、circles配列の5番目の要素のプロパティradiusを参照しています。circles配列はポインタ型なので、プロパティを取りだすにはアロー演算子「->」を使用しているところに注意してください。

radius = circles[4]->radius;

通常の配列としての機能の他、vectorには様々な関数が用意されています。

  • at() 指定した位置の要素を返す
  • back() 最終要素を返す
  • front() 先頭要素を返す
  • clear() 全ての要素を削除する
  • front() 先頭要素を返す
  • insert() 要素をベクタに挿入する
  • pop_back() 最終要素を削除する
  • push_back() ベクタの末尾に要素を追加する
  • size() ベクタ中の要素数を返す

例えば、動的配列のcirclesの最後の要素に新規にCircleクラスのオブジェクトを追加するには、以下のように指定します。

circles.push_back(new MyCircle(ofPoint(mouseX, mouseY), 100));

では、マウスを左クリックすると新規にMyCircleクラスのオブジェクトを生成し、動的配列circlesに追加し、右クリックするとcirclesの最後の要素を消去するプログラムを作成してみましょう。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "MyCircle.h"

class testApp : public ofBaseApp{
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    //MyCircleクラスへのポインタの動的配列
    vector <MyCircle *> circles;
};

#endif

testApp.cpp

#include "testApp.h"

//--------------------------------------------------------------
void testApp::setup(){
    ofBackground(0, 0, 0);
    ofSetCircleResolution(64);
}

//--------------------------------------------------------------
void testApp::update(){

}

//--------------------------------------------------------------
void testApp::draw(){
    //動的配列「circles」内の
    //全てのMyCircleのオブジェクトのdrawメソッドを実行
    for (int i = 0; i < circles.size(); i++) {
        circles[i]->draw();
    }
}

/* 中略 */

//--------------------------------------------------------------
void testApp::mouseReleased(int x, int y, int button){
    if (button == 0) {
        //左クリックで、新規のMyCircleオブジェクトを生成し、
        //動的配列「circles」の末尾に追加する
        circles.push_back(new MyCircle(ofPoint(mouseX, mouseY), ofRandom(10, 200)));
    } else if (button == 2 && circles.size()>0) {
        //右クリックで動的配列「cirlces」の末尾の一つを消去
        delete circles[circles.size()-1];
        circles.pop_back();
    }
}

//--------------------------------------------------------------
void testApp::windowResized(int w, int h){

}

MyCircle.h

#ifndef _MY_CIRCLE
#define _MY_CIRCLE

#include "ofMain.h"

class MyCircle {
    
public: 
    //コンストラクタ
    //第1引数:位置、第2引数:半径
    MyCircle(ofPoint pos, float radius);
    //デストラクタ
    ~MyCircle();
    //円を描く
    void draw();
    //アクセサ
    void setPos(ofPoint pos);
    void setRadius(float radius);
    
private:
    //プロパティはprivateで宣言
    //円の位置
    ofPoint pos;
    //円の半径
    float radius;
};

#endif

MyCircle.cpp

#include "MyCircle.h"

//コンストラクタ
MyCircle::MyCircle(ofPoint _pos, float _radius) 
{
    //位置と半径を設定
    pos = _pos;
    radius = _radius;
}

//デストラクタ
MyCircle::~MyCircle()
{

}

void MyCircle::draw()
{
    ofEnableAlphaBlending();
    //色を指定
    ofSetColor(31, 127, 255, 127); 
    //画面の中央に外周の円を描く
    ofCircle(pos.x, pos.y, radius);
    //色を指定
    ofSetColor(255, 31, 0, 200); 
    //画面の中央に円の核を描く
    ofCircle(pos.x, pos.y, radius*0.2);
}

void MyCircle::setPos(ofPoint _pos)
{
    pos = _pos;
}

void MyCircle::setRadius(float _radius)
{
    radius = _radius;
}

配列オブジェクトの動的生成のサンプルでアニメーションを作成

最後に大量に生成したオブジェクトにアニメーションを加えてみましょう。MyCircleクラスに、updateメソッドを追加します。そして、円がランダムな方向にゆっくりと移動しながら、収縮運動をするようにプログラムしています。

マウスのクリックではなく、ドラッグをすると次々にオブジェクトが動的に生成されるようにしています。また[d]キーを入力すると、生成した全ての円を消去してメモリから解放しています。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "MyCircle.h"

class testApp : public ofBaseApp{
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    //MyCircleクラスへのポインタの動的配列
    vector <MyCircle *> circles;
};

#endif

testApp.cpp

#include "testApp.h"

//--------------------------------------------------------------
void testApp::setup(){
    ofBackground(0, 0, 0);
    ofSetCircleResolution(16);
    ofSetFrameRate(60);
}

//--------------------------------------------------------------
void testApp::update(){
    for (int i = 0; i < circles.size(); i++) {
        circles[i]->update();
    }
}

//--------------------------------------------------------------
void testApp::draw(){
    for (int i = 0; i < circles.size(); i++) {
        circles[i]->draw();
    }
}

//--------------------------------------------------------------
void testApp::keyPressed(int key){
    //dキーでcircle配列をクリア
    if (key == 'd') {
        for (int i=0; i<circles.size(); i++) {
            delete circles[i];
        }
        circles.clear();
    }
    //fキーでフルスクリーン
    if (key == 'f') {
        ofToggleFullscreen();
    }
}

//--------------------------------------------------------------
void testApp::keyReleased(int key){
    
}

//--------------------------------------------------------------
void testApp::mouseMoved(int x, int y ){
    
}

//--------------------------------------------------------------
void testApp::mouseDragged(int x, int y, int button){
    if (button == 0) {
        //マウス左ボタンドラッグで新規にMyCircleのオブジェクトを追加
        for (int i = 0; i < 4; i++) {
            circles.push_back(new MyCircle(ofPoint(mouseX, mouseY), ofRandom(4, 16)));
        }
    } else if (button == 2 && circles.size()>0) {
        //マウス右ボタンのドラッグでMyCircleオブジェクトを一つづつ消去
        delete circles[circles.size()-1];
        circles.pop_back();
    }
}

//--------------------------------------------------------------
void testApp::mousePressed(int x, int y, int button){
    
}

//--------------------------------------------------------------
void testApp::mouseReleased(int x, int y, int button){
    
}

//--------------------------------------------------------------
void testApp::windowResized(int w, int h){
    
}

MyCircle.h

#ifndef _MY_CIRCLE
#define _MY_CIRCLE

#include "ofMain.h"

class MyCircle {
    
public: 
    //コンストラクタ
    MyCircle(ofPoint pos, float radius, float maxSpeed = 0.2, float phaseSpeed = 0.06);
    //デストラクタ
    ~MyCircle();
    //半径の更新
    void update();
    //円を描く
    void draw();
    //アクセサ
    void setPos(ofPoint pos);
    void setRadius(float radius);
    
    //円の移動スピード
    ofPoint speed;
    //移動スピードの最大値
    float maxSpeed;
    //収縮を制御する正弦波の位相
    float phase;
    //収縮スピード
    float phaseSpeed;
    
private:
    //プロパティはprivateで宣言
    //円の位置
    ofPoint pos;
    //円の半径
    float radius;
};

#endif

MyCircle.cpp

#include "MyCircle.h"

//コンストラクタ
MyCircle::MyCircle(ofPoint _pos, float _radius, float _maxSpeed, float _phaseSpeed) 
{
    //位置と半径を設定
    pos = _pos;
    radius = _radius;
    phaseSpeed = _phaseSpeed;
    maxSpeed = _maxSpeed;
    
    //スピードを設定
    speed.x = ofRandom(-maxSpeed, maxSpeed);
    speed.y = ofRandom(-maxSpeed, maxSpeed);
    //初期位相
    phase = ofRandom(0, PI);
}

//デストラクタ
MyCircle::~MyCircle()
{

}

void MyCircle::update()
{
    //円の半径の伸び縮みの位相を更新
    phase += phaseSpeed;
    if (phase > PI) {
        phase -= PI;
    }
    
    //位置を更新
    pos += speed;
    if (pos.x < 0 || pos.x > ofGetWidth()) {
        speed.x *= -1;
    }
    if (pos.y < 0 || pos.y > ofGetHeight()) {
        speed.y *= -1;
    }
}

void MyCircle::draw()
{
    //半径の収縮を計算
    float r = sin(phase)*radius;
    //色を指定
    ofEnableAlphaBlending();
    ofSetColor(31, 127, 255, 100); 
    //画面の中央に外周の円を描く
    ofCircle(pos.x, pos.y, r);
    //色を指定
    ofSetColor(255, 31, 0, 200); 
    //画面の中央に円の核を描く
    ofCircle(pos.x, pos.y, radius*0.2);
}

void MyCircle::setPos(ofPoint _pos)
{
    pos = _pos;
}

void MyCircle::setRadius(float _radius)
{
    radius = _radius;
}

サンプルファイルのダウンロード

今回の授業でとりあげた全てのサンプルファイルは下記のリンクからダウンロード可能です。