配列のオーバーフローについて

フォーラム(掲示板)ルール
フォーラム(掲示板)ルールはこちら  ※コードを貼り付ける場合は [code][/code] で囲って下さい。詳しくはこちら
管理人

配列のオーバーフローについて

#1

投稿記事 by 管理人 » 17年前

配列のオーバーフローについて語るトピをこちらに立てさせていただきました。

以下、他トピからうつした記事です。

管理人

Re:配列のオーバーフローについて

#2

投稿記事 by 管理人 » 17年前

(以下コピー)

    ****dixq****

村に入ってからゲームを始めた時、ドレス変身、低速移動時に
レーザーが表示されないというバグ修正しました。

ひぃ、やっぱりオーバーフロー怖いですね~。

#define CH_CHANGE_NUM1 3
#define CH_CHANGE_NUM2 2

int img_cshot1[CH_CHANGE_NUM1][CH_CHANGE_NUM2];

img_cshot1[3][0] = LoadGraph( "dat/img/char/cshot30.png" );//レーザー画像
      ↑

その後、村に入った時、全然関係無いところで宣言したhumanに対して

memset(human,0,sizeof(human_t));

これを処理した瞬間画像が無くなって(呼び出せなくなって)いました。
オーバーフローした時コンパイラが怒ってくれたらいいのに・・。




    **** バグ ****

私もよくやります(苦笑)
配列なのに添え字自体を書き忘れていて、正体不明のアドレスにデータを書き込んでしまい、例外が発生しました。冷静に考えたら、恐ろしいことをしてしまったなぁ…と(^_^;)




    ****GPGA****

C/C++の 配列[要素数] は単純にアドレスの足し算を行うだけなので
配列オーバーの判断はしないのではないでしょうか?
上記のため、以下のソースはコンパイルが通ります。
#include <stdio.h>

int main(void)
{
    printf("%c\n", 3["abcde"]);
    return 0;
}
 

Eclipse+CDTのコンパイルは行ったことがないのですが、上記のソースはコンパイルが通るのですかね?
もし通らないのであれば、言語の規格に沿っていないことになるのかも・・・。




    ****dixq****

>>>GPGAさん
>上記のため、以下のソースはコンパイルが通ります。

奇怪に見えますねw
Eclipsの話をしておきながら、Eclipsは使えないので(JAVAが得意な友人の様子を見ていただけ)確認出来ませんが、
オーバーフローの指摘があっただけでかなりのバグは抑えられる気がするのですが・・。
まぁ単純に管理が出来ていないだけだということもありますが(汗

管理人

Re:配列のオーバーフローについて

#3

投稿記事 by 管理人 » 17年前

    ****GPGA****

Javaとしてコンパイルした場合ですと、エラーを出してくれますね。
これはたぶんJavaの言語仕様で決まっていることなのだと思います。
Javaにはポインタがありませんから。

配列オーバーのチェックですが、私が考えられる方法は3つあります。

一つ目は全てをenumで管理するやり方です。これはチェックというより管理ですね。
enum {
    CH_CHANGE_NUM1_IDX0,
    CH_CHANGE_NUM1_IDX1,
    CH_CHANGE_NUM1_IDX2,

    CH_CHANGE_NUM1,
};

int img_cshot1[CH_CHANGE_NUM1];
img_cshot1[CH_CHANGE_NUM1_IDX0] = 0;
 
このように、全ての配列アクセスをenumで定義した値で行うことで、危険を回避します。


二つ目はstd::vectorのatメソッドを使用することです。
このメソッドは配列アクセスを行う際に、指定されたインデックスのチェックを行い
要素外の値の場合は std::out_of_range を投げます。
#include <iostream>
#include <vector>

int main(void)
{
    std::vector<int> ary(4);
    
    try {
        ary.at(5) = 10;
    } catch (std::out_of_range e) {
        std::cout << "配列オーバー:" << e.what() << std::endl;
    }
    return 0;
}
 
三つ目はstd::vectorを継承して、自分独自のクラスを作成し、そのクラスでoperator[/url] を定義して
チェックを行うというものです。
#include <iostream>
#include <vector>

template<class T>
class myvector : public std::vector<T> {
public :
	myvector(int n) :
	   std::vector<T>(n)
	{
	}
	~myvector() {
		std::vector<T>::~vector();
	}

	T& at(int idx, const char* file, int line) {
		if (idx < 0 || (unsigned int)idx >= size()) {
			std::cout << file << ":" << line << "行目 " << "配列オーバー" << std::endl;
			throw std::out_of_range("invalid myvector<T> subscript");
			//return std::vector<T>::operator[/url](0);
		}
		return std::vector<T>::operator[/url](idx);
	}
	const T& at(int idx, const char* file, int line) const {
		if (idx < 0 || (unsigned int)idx >= size()) {
			std::cout << file << ":" << line << "行目 " << "配列オーバー" << std::endl;
			throw std::out_of_range("invalid myvector<T> subscript");
			//return std::vector<T>::operator[/url](0);
		}
		return std::vector<T>::operator[/url](idx);
	}

	T& operator [/url](int idx) {
		if (idx < 0 || (unsigned int)idx >= size()) {
			std::cout << "配列オーバー:" << idx << std::endl;
			throw std::out_of_range("invalid myvector<T> subscript");
			//return std::vector<T>::operator[/url](0);
		}
		return std::vector<T>::operator[/url](idx);
	}
	const T& operator [/url](int idx) const {
		if (idx < 0 || (unsigned int)idx >= size()) {
			std::cout << "配列オーバー:" << idx << std::endl;
			throw std::out_of_range("invalid myvector<T> subscript");
			//return std::vector<T>::operator[/url](0);
		}
		return std::vector<T>::operator[/url](idx);
	}
};
#define at(n)		at(n, __FILE__, __LINE__)

int main(void)
{
	myvector<int> vec(1);

	try {
		vec[1] = 20;
	} catch (...) {
	}
	try {
		vec.at(1) = 20;
	} catch (...) {
	}

	return 0;
}
 
ただ、本来std::vectorやstd::stringなどのSTLは継承用に作られていないので、継承した場合
元のデストラクタが標準で呼び出されません。(デストラクタにvirtualが付いていない)
ですので、継承先のデストラクタで継承もとのデストラクタの呼び出しを行っているのですが
これが正しいかどうか、わかりません。もし間違っているのであれば、どなたかご指摘ください。

もしこれで問題がない場合、使い方はstd::vectorと何も変わりません。
ただ、このクラスでは[/url]アクセスの際にオーバーした場合 atメソッドと同じように例外を投げてくれます。
また、atメソッドの方にも改良を加え、[/url]の代わりにatメソッドを使用した場合、問題が発生した
ファイルと行数を見れるようにしてあります。ただし、at用のマクロを定義するか、__FILE__と__LINE__を
常に入れる必要があるのが難点です。




    ****dixq****

なるほど、詳しくありがとうございます。
私も必要な配列要素数がよくわかるものにはベクターは使うのでは、これは単なる配列より処理が遅くなるのではと思い、
固定でいいものや小さい配列には普通の配列を使っていました。
問題になるほどの処理速度の違いは無いんでしょうか?




    ****組木紙織****

>元のデストラクタが標準で呼び出されません。(デストラクタにvirtualが付いていない)
基底クラスのポインタでオブジェクトを扱った時
(簡単に言うとポリモーフィズムで扱うとき)
にはこのような状態になりますが、
オブジェクトそのもので扱った時にはきちん基底クラスのデストラクタは呼ばれます。

なのでこの場合にはデストラクタを明示的に呼ぶ必要はないのではないかと思います。
また。オブジェクトの解体は継承された部分から解体していくことになっていたはずなので、
先に継承元のオブジェクトを解体すると、何か悪いことが起こるような気がします。

std::vectorを継承して使わずに、メンバに入れて扱う方が良いのではないかと思います。
#include <iostream>
using namespace std;

class Hoge
{
	public:
	Hoge(){cout << "begin Hoge" << endl;}
	~Hoge(){cout << "end Hoge" << endl;}
	
};

class HogeHoge :public Hoge
{
	public:
	HogeHoge(){cout << "begin HogeHoge" << endl;}
	~HogeHoge(){cout << "end HogeHoge" << endl;}
};

int main()
{
	Hoge *p = new HogeHoge;
	delete p;
	return 0;
}

管理人

Re:配列のオーバーフローについて

#4

投稿記事 by 管理人 » 17年前

    ****GPGA****

確実に配列オーバーしない保障があると思うものに関しては
普通の配列を使用しても問題ないと思います。

vectorに関しては、アセンブル出力をして調べたことがないので、正確な速度はわかりませんが
配列参照一回に付き、関数呼び出し一回分くらいの速度の低下は考えておいたほうがいいかもしれません。
ただ、うまく配列の扱えば速度低下はかなり抑えられると考えております。

まず先ほどの、myvectorは毎回配列チェックを行っているので、かなり重い処理です。
ですが、チェックを行うのはデバッグ時のみで、リリース時に必要ないのであれば
#ifdef _DEBUG
    typedef myvector<int> IntArray;
#else
    typedef std::vector<int> IntArray;
#endif
 
とすることで、リリース版では余計なチェックを入れないようにすることができます。

次に、配列の高速な使い方の例を示します。
まずシューティングの弾の構造体を以下とします。
struct BulletData {
    int x, y, flg;
} bullet[100];
 
そして、以下は処理するプログラムです。
for (int i = 0; i < 100; ++i) {
    BulletData* p = &bullet;
    if (p->flg != 0) {
        p->x = 0;
        p->y = 0;
    }
}
 
処理する前にポインタに格納し、後はポインタから操作を行うだけで
余計な配列アクセスを行わないので、処理を高速化することができます。
以上実装した上でのアクセス数であれば、処理速度はそれほど気にする必要はないと思われます。

GPGA

Re:配列のオーバーフローについて

#5

投稿記事 by GPGA » 17年前

>>組木紙織さん
>オブジェクトの解体は継承された部分から解体していくことになっていたはずなので、
>先に継承元のオブジェクトを解体すると、何か悪いことが起こるような気がします。
>std::vectorを継承して使わずに、メンバに入れて扱う方が良いのではないかと思います。
やはりこうなってしまいますかね。
安全性を考えると、この方法が一番だということはわかっているのですが
今回これをやるということは、vectorのメソッドを全てラップする必要が出てくるんですよね。
myvectorのあり方は、vectorクラスに追加機能を加えるというものですから
vectorを継承して使用せず、メンバに持って全てのvectorクラスのメソッドのラップを作るというのは
オブジェクト指向の考え方に背いているような感じがします。

たかぎ

Re:配列のオーバーフローについて

#6

投稿記事 by たかぎ » 17年前

厳密な意味での安全性という観点では、std::vector からは継承しない方がよいのですが...

std::vector から継承したクラスを定義し、なおかつ動的多相性を実現したいのであれば、boost::shared_ptr を使うとよいでしょう。
これなら、仮想デストラクタがなくても正しく振舞ってくれます。
また、テンプレートを用いて、静的な多相性を発揮させる上では何の問題もありません。

個人的には、セマンティックスが異なるのであれば、異なるインタフェースを持たせるのが筋のように思います。
例えば、非メンバの at 関数を用意するなどです。

GPGA

Re:配列のオーバーフローについて

#7

投稿記事 by GPGA » 17年前

>std::vector から継承したクラスを定義し、なおかつ動的多相性を実現したいのであれば、boost::shared_ptr を使うとよいでしょう。
これはmyvectorをポインタとして扱うとき、以下のようにするということでしょうか?
template<class T>
class myvector : public std::vector<T> {
・・・
};

boost::shared_ptr<myvector<int> > vec;
 

たかぎ

Re:配列のオーバーフローについて

#8

投稿記事 by たかぎ » 17年前

> これはmyvectorをポインタとして扱うとき、以下のようにするということでしょうか?

そうです。
shared_ptr のコンストラクタは、実引数の静的な型に応じて、delete 用の関数を内部で管理します。
そのため、仮想デストラクタがなくても、あたかも仮想デストラクタがあるかのように動作します。

閉鎖

“C言語何でも質問掲示板” へ戻る