C++で作るベクトルクラス その3 〜可変長ベクトル

可変長でN個の要素を持つベクトルを作成するときの注意事項について。

メンバ変数

ベクトルクラスが持つべき情報は、要素の個数と1次元配列の先頭アドレスになります。

class Vector{
private:
	int m_size;
	float* m_val; 

コンストラクタによる初期化について

多次元の場合、コンストラクタの定義には悩まされるところだと思いますが、考えられるパターンとしては、

  1. コンストラクタの引数をベクトルサイズとして定める
  2. 引数を各要素の初期値として定める

という場合がありそうです
一つ目の案を採用した場合は、

Vector v(3);//サイズ3のベクトルを生成
Vector v(3,1,2,3)//サイズ3のベクトルを生成して各要素を1,2,3で初期化する

要素の個数が可変長なので、初期化とともに要素の値を定めるには可変長引数の関数としてコンストラクタを作成しなければなりません。
可変長の関数を定義するには、

  • printf関数など、C言語で用意されている可変長引数リストを利用した方法(stdarg.hを利用)
  • C++0x限定で、テンプレートを使う方法

などがあるみたいです。

2つ目の方法では、引数の個数を取得するような仕組みがないといくつのサイズの領域を確保すればよいのか分かりません。

Vector v(3,4,5);//サイズ3のベクトルを生成して3,4,5で初期化
Vector v(1,3)//サイズ2のベクトルを生成して各要素を1,3で初期化

このように使うことができれば見栄えはいいですが、これを実現するためには生成するベクトルのサイズごとにコンストラクタを定めなければいけません。よく使う次元(例えば、2,3,4次元など)専用のコンストラクタだけ作成して、他は直接各成分の値を書き換えるという方法が妥協案といったところでしょうか。しかし、全ての次元に対応させるためには、サイズの情報をどこかから取得しなければ
ならないので、結局引数をベクトルサイズとして定めるというのは必須のようです。
(うまく実現する方法があるかもしれないので注意。)
というわけで、まず下の2つを実装してみます。

Vector v();//サイズ0
Vector v(3);//サイズ3のベクトルを生成して0で初期化
Vector();
explicit Vector(int sz);
inline Vector::Vector():
m_size(0),m_val(0)
{
}
inline Vector::Vector(int sz):m_size(sz){
	if(m_size==0){   //次元が0の場合
		m_val=0;	
		return;
	}
	m_val=new float[m_size];	//float型m_size個分の領域確保
	for(int i=0;i<m_size;i++){	//初期化
		m_val[i]=0;
	}
}

デストラク

確保した領域を解放して、サイズを0にします。

inline Vector::~Vector(){
	free(); 	//領域解放
	m_size=0;
}
//バッファの解放
inline void Vector::free(){
	if(m_val != 0){
		delete [] m_val;
		m_val = 0;
	}
}

アクセッサー

サイズ

サイズを取得します。

inline int Vector::size()const{
	return m_size;
}
添え字演算子

ベクトルの値にアクセスする演算子です。範囲外の要素にアクセスする場合の例外処理を実装しておきます。

inline float& Vector::operator[](int i)const{ 
   if(i<0 || i>=m_size)throw out_of_range("out of range!");
   return m_val[i];
}

コピーコンストラク

このクラスはインスタンスを生成したあとに動的にメモリを確保しています。コピーコンストラクタを定義せずに、
コピーによる初期化を行うとコピー前の領域を2つのインスタンスが指すことになってしまうため、インスタンスを破棄すると2回deleteしてしまうことになります。したがって、コピーコンストラクタを自前で定義する必要があります。

Vector v(3);//コピーコンストラクタが定義されていない
Vector u=Vector(v);//暗黙のコピーコンストラクタが呼ばれる
	Vector(const Vector& v);	//コピーコンストラクタの宣言
inline Vector::Vector(const Vector& v){
	if(v.size()==0){		//size 0の場合
		m_size=0;
		m_val=0;
		return;
	}
	m_size=v.size();
	m_val=new float[v.size()];	//copy先の領域確保:float型をv.size()個分
	for(int i=0;i<m_size;i++){
		m_val[i]=v[i];		//複製
	}
}

代入演算子

インスタンスを代入する場合も注意が必要です。二つのインスタンスが、それぞれ0xABCDと0x1234にメモリを確保していたとすると、
代入後は先頭アドレスを上書きするため両方とも0x1234を指すことになります。0xABCDの先頭アドレスの情報はもはや誰も持っていないので、最後まで消されることはなくメモリリークになってしまいます。そしてさらに0x1234を2回deleteすることになってしまいます。代入演算子も自前で定義するか禁止するか決めなければいけません。
もうひとつの注意点は、自分自身を引数にしたときで、この場合は処理を行わないようにします。

 x = y;		//複製
 x = x;		//何もしない
inline Vector& Vector::operator=(const Vector& v){
	if(&v == this){		//自分自身が入力されたとき
		return *this;
	}
	if(v.size()==0){	//input size=0の時、メモリ解放,size0にする
		free();
		m_size=0;
		return *this;
	}
	//input size=output sizeのとき(但し0より大きい)
	if(m_size==v.size()){
		for(int i=0;i<m_size;i++){
			m_val[i]=v[i];	//メモリ操作なしにそのままコピー
		}
		return *this;
	}
	//iput size!= output sizeの場合 (但しinput size >= 0の時)
	m_size=v.size();
	free();		//複製する前に複製先の領域を一旦解放
	//複製先に適切な記憶領域確保:float型v.size()個分
	m_val=new float[v.size()];
	for(int i=0;i<m_size;i++){
		m_val[i]=v[i];		//複製
	}
	return *this;
}

サイズの変更

いつでもサイズを変更できるようにしてみます。変更後に元の値を残す場合と、消去する場合が考えられますが、元の値を残しつつサイズ変更できるようにしてみます。まずサイズを比較して、異なる場合は、必要分だけ一時的にコピー元の値を保存します。その後新たに領域を確保して確保していた値を複製します。

	Vector v(4,1,0);	//v=(4,1,3)
	v.resize(6);		//v=(4,1,3,0,0,0)
	v.resize(2);		//v=(4,1)
inline void Vector::resize(int d){
	if(m_size==d){ 		//サイズが同じ場合処理を行わない
		return;
	}
	if(d==0){ //サイズが0の場合
		free();		//このクラスのバッファを解放する
		return;
	}
	//指定サイズの方が大きい場合,余分な成分を0で埋める
	if(m_size<d){
		//このVectorサイズ分の一時領域を確保
		float* tmp;
		tmp=new float[m_size];
		//一時的に現在の値をコピーする
		for(int i=0;i<m_size;i++){
			tmp[i]=m_val[i];
		}
		//このクラスのバッファを解放する
		free();
		//新たにバッファを入力サイズ分だけ確保する
		m_val=new float[d];
		//新たに確保したバッファに先程一時保存した成分の値をコピーする
		for(int i=0;i<m_size;i++){
			m_val[i]=tmp[i];
		}
		//余分な成分は0で埋める
		for(int i=m_size;i<d;i++){
			m_val[i]=0;
		}
		float j=m_val[d-1];
		//一時領域を解放する
		delete[] tmp;
	}
	//指定サイズの方が小さい場合,指定サイズ分だけ値をコピーする	
	else if(m_size>d){
		//入力サイズ分の一時領域を確保
		float* tmp;
		tmp=new float[d];
		//一時領域にこのクラスの値を保存する
		for(int i=0;i<d;i++){
			tmp[i]=m_val[i];
		}
		//バッファを解放する
		free();
		//新たにバッファを確保する
		m_val=new float[d];
		//一時保存した値を新たな領域にコピーする
		for(int i=0;i<d;i++){
			m_val[i]=tmp[i];
		}
		//一時領域を解放する
		delete[] tmp;
	}
	//このクラスのサイズを入力サイズに変更する
	m_size=d;
}

上の例ではメモリが確保できなかった場合の例外処理を行っていないので、本格的に使う場合は必ず実装しなければいけません。

可変個数の引数をもつコンストラク

可変個数のコンストラクタはstdarg.hを使って実装することができます。
まず可変個数部分を"..."と記述します。引数の個数を取得することはできないのでサイズを引数にひとついれなければなりません。

#include <stdarg.h>

class Vector{
public:
Vector( int size, ...);//コンストラクタの宣言

次に、va_listで引数のリストを定義し、引数を取り出す部分をva_startとva_endで挟みます。後は順次va_argを呼び出すと自動的に
引数を取得することができます。

	va_list args;//可変個数引数リスト
	va_start(args, size);

	for (int i = 0; i < size; i++){
		//ここで順次引数の値を取得//??? = va_arg(args, float);
	}

	va_end(args);

inline Vector::Vector( int size, ...){
   //次元が0の場合
	if(size<=0){
		m_size=0;
		m_val=0;	
		return;
	}
	va_list args;	//可変個数引数リスト
	va_start(args, size);

	m_size=size;
	m_val=new float[m_size];	//float型m_size個分の領域確保
	for(int i=0;i<m_size;i++){
		m_val[i]=0;
	}
	for (int i = 0; i < m_size; i++){
		m_val[i] = va_arg(args, float);	// 引数を取得する
	}
	va_end(args);
}