GDIのリソースリークについて

[戻る]
とよ 2009/04/16(木) 16:16:23
WinXP(SP3) VC++2005 MFC でアプリケーション開発している者です。

 親関数で定義したCDCオブジェクトを子関数(親関数内で呼び出し)へポインタで渡し、
子関数では、ペンを定義して、CDCに持たせます(SelectObject)。
このまま子関数から親関数へプログラムが戻ると、ペンはリークしてしまうのでしょう
か?
 具体的には以下のような感じです。

void 親関数() 
{
    CClientDC dc(対象のウインドウハンドル);

    子関数(&dc);
}

void 子関数( CDC *pdc) 
{
    CPen pen( PS_DOT, 1, RGB(0,0,0) );
    CPen *pOldPen = pdc->SelectObject(&pen);

    //描画する。

  //penがリーク?
}

  もし、子関数終了時にpenがリークしていても、親関数が終了するときに、dcが解放さ
れるので、そのときにpenも解放されるのでは?と考えているのですが・・・。

 ちなみに、親関数の中で、子関数が何度も呼び出されている場合は、どんどんpenがリ
ークしていき、親関数終了時のdc解放時に、いっきにpenが解放されるようなイメージを
もっています。どうなんでしょうか?

 よろしくお願いします。

とよ 2009/04/16(木) 16:23:58
すみません・・・。以下のように訂正します。

void 子関数( CDC *pdc) 
{
    CPen pen( PS_DOT, 1, RGB(0,0,0) );
  //CPen *pOldPen = pdc->SelectObject(&pen); ←SelectObjectの戻り値は使わずに
    pdc->SelectObject(&pen);

    //描画する。

  //penがリーク?
}

Blue 2009/04/16(木) 16:27:08
抜ける前にpOldPenをSelectObjectするとどうなりますか?

maru 2009/04/16(木) 16:47:11
オブジェクトがリークするかどうかが問題ではなく、ローカルで宣言したオブジェクト
を呼び出し側のオブジェクトに関連付けさせたまま関数を終了させていることが問題で
しょう。

Blueさんが書いているように抜ける前にpOldPenをSelectObjectすべきでしょう。

とよ 2009/04/16(木) 16:50:45
  pOldPenをSelectObjectすると、子関数終了時にpenが解放されて、リークはしないん
でしょうけど、実際どういうしくみになっているかが、いまいちつかめません。
 dcにSelectObjectされているときはCPenのデストラクタで自動解放できないのでリー
クされるとして、そのdcが解放されるときは、リークしていたpenはどうなるのかな〜と
疑問です。

  出荷済みの製品にこのような書き方をしている部分があって。(自分で書いたんですけ
ど・・・)

 //もう一つ訂正です。
 子関数の引数は CClientDC *pdc でした。 すみません。

Blue 2009/04/16(木) 17:06:05
CGdiObjectクラスは、デストラクタで作成されたGDIオブジェクトを
DeleteObjectメソッドで破棄します。

MSDNのCGdiObject::DeleteObjectには
<quote>
プリケーションでは、デバイス コンテキストに現在選択されている CGdiObject オブ
ジェクトの DeleteObject 関数を呼び出さないでください。
</quote>
と記述があります。
実際に呼び出したらどうなるかと記述がないのでわかりません。
ただ、
>出荷済みの製品にこのような書き方をしている部分
はやってはいけない記述だということでしょう。

CGdiObject::DeleteObject
http://msdn.microsoft.com/ja-jp/library/st2k3wfa.aspx

Blue 2009/04/16(木) 17:13:57
http://social.msdn.microsoft.com/forums/ja-JP/vcgeneralja/thread/33e83792-4079-4b88-86dd-4eef8a9e090e/
より
SelectObject中のGDIオブジェクトをDeleteObjectすると失敗するらしいです。
失敗=リソースが解放されない
ということでしょう。

pOldPenに対してもリソースリークするのかな。

とよ 2009/04/16(木) 17:31:36
[[解決]]
Blueさん
maruさん
ありがとうございます。

 上記URLを見てみたら、「CGdiObject オブジェクトに関連付けられている領域には影
響しません」ともありましたが、この領域って一体なんのことを指してるんでしょうか
ね?

 DeleteObjectを呼び出したらその先は不定ということですね。リークの可能性大のよ
うな気が・・・。恐ろしや。こっそりEXEをなおして、さらにこっそり客先のパソコンに貼
り付けたいんですが、今のところ電話はならないのでうまく動作しているのかな・・・。
 どう対応するか考えます。

 いろいろ調べてみたら、OSのX98とNT系で動きに違いがあるとかなんとかで、その辺が
よく分かりませんが、XPはNT系ということで、もう少し調べてみます。


 勉強になりました。ありがとうございました。

仲澤@失業者 2009/04/16(木) 18:13:00
典型的な「バグ」ですね。深刻度もかなり高いといえます(笑)。

プロセスで作成されたGDIオブジェクトのハンドルはそのプロセス終了後に全て解放
されますが、プロセス動作中はDeleteObject()されないかぎり破棄はされません。
しかし、CPenの寿命が尽きてデストラクタでDeleteObject()しようとしても、DCに
ハンドルが選択されているため、これに失敗してしまいます。

CPenのデストラクタが働いても、そのGDIオブジェクトのハンドルHPEN自体は
依然として有効であることは、以下のコードで簡単に調べることができます。

void BOO( CDC * dc){
  CPen localPen( PS_SOLID, 5, RGB(0xFF,0,0));
  dc->SelectObject( localPen);
  dc->MoveTo(0,0); dc->LineTo(200,200);//ローカルな赤ペンで線を書く
}
XXX::OnPaint(){
  CPaintDC dc(this); // ClientDCでもおんなじです
  BOO( &dc);
  // ペンを選択しないで線を描いてみる
  dc->MoveTo(200,0); dc->LineTo(0,200); // 依然として赤い線が引かれる。
                    // つまりHPENは有効
}
 
従って、とよさんのアプリは動作中にGDIオブジェクトのハンドルが破棄されない
まま、溜まり続けています。
んで、この影響ですが、OSがどのくらい我慢していられるかに依存します。
GDIリソースはシステムの共有リソースなので、他に大量に(キャッシュして)使う
アプリがいればすぐに影響が出るでしょう。逆に、単独でちょこっとだけ使う
アプリなら発覚を免れるかもしれません。

こういうことを防ぐのにCDC(又はHDC)をラップ(CDC_WRAP)してGDIオブジェクトは
CDC_WRAPに作らせる、という技を使う人もいます。

gak 2009/04/17(金) 16:59:00
GDIオブジェクトと言っても確か2つ(論理、実体)あったハズ。

> CPen pen( PS_DOT, 1, RGB(0,0,0) );
これで作られるGDIオブジェクトは「論理」の方。
DeleteObject() で削除されるもの「論理」の方。

「実体」の方はどうなのかというと、DC に SelectObject() で選択された段階で確保さ
れる。そして、DC から非選択された段階で開放される。
つまり、SelectObject() は「DC に GDIオブジェクトを結ぶだけ」という単純なものでは
なく、SelectObject() によっても GDI リソース領域の確保/開放が行われるので、処理
が完了した後は必ず SelectObject() で元に戻すべし。

と俺は認識している。
# ソースは MSDN だったのだけれども、今そのページを見つける事はできなかった…

> CGdiObject オブジェクトに関連付けられている領域には影響しません
CGdiObject* object = new CGdiObject;
object->m_hObject = ::CreatePen(PS_SOLID, 1, 0x000000);
object->DeleteObject(); // m_hObject は無効になるが object はまだ有効
delete object;          // CGdiObject領域無効

という感じの事を言いたいんじゃないかな。

gak 2009/04/17(金) 17:45:52
> そのページを見つける事はできなかった
http://msdn.microsoft.com/en-us/library/ms969928.aspx
多分これ。見直して見ると先の回答は嘘吐いていた。

> 「実体」の方はどうなのかというと、DC に SelectObject() で選択された段階で確保
これは100%正解では無いが大体○。上記ページの「Memory Usage」項に記載がある。

> そして、DC から非選択された段階で開放される。
これ嘘。「Memory Usage」に記されている
> Physical pens and brushes are not deleted from the system until the 
> corresponding object is deleted. The physical object that corresponds to a 
> selected logical object is locked in GDI’s heap. (It is unlocked upon 
> deselection.)
を改変して憶えていたのかもしれない。

じゃ「実体」は何時開放されるのかというと
「Creating vs. Recreating」の内容等
> The speed gains are possible because GDI caches physical objects.
から「ガベージコレクションみたくやってる?」とか思えるが…ぶっちゃけ判んない。

# そもそも1992年に書かれた参照ページの内容が今現在通用するのかも怪しいので
# 根本から違っているかもしれない…

ロマ 2009/04/19(日) 13:47:16
MFC苦手のためxp&APIで実験しました。
WM_CREATEでhdc = GetDC(hWnd);しておいて、

1)コマンドIDM_TESTのハンドラで
HPEN hPen = CreatePen(PS_SOLID, 5, RGB(255, 0, 0));
HGDIOBJ oldPen = SelectObject(hdc, hPen);
MoveToEx(hdc, 0, 0, 0);
LineTo(hdc, 200, 200);
int ret = DeleteObject(hPen);

結果retはTRUE、タスクマネジャーのGDIオブジェクト数は増減なしでした。

2)上記コードのDeleteObject()の前にSelectObject(hdc, oldPen)を挿入
さらにDeleteObject()後に
SelectObject(hdc, hPen); //Delete済みのペンを選択する
MoveToEx(hdc, 200, 0, 0);
LineTo(hdc, 0, 200);

結果赤いXが描けました。

私の結論
xpでは選択中に破棄しても問題ない。
破棄後はゾンビになって動作する。
(直後に10000個位のペンを実体化するとゾンビペンの色などは変化するようです-実験
中)

maru 2009/04/20(月) 10:09:00
> 私の結論
> xpでは選択中に破棄しても問題ない。
> 破棄後はゾンビになって動作する。
ゾンビになるんじゃ『問題ない』ことはないんじゃない?
「xpでは選択中に破棄しても『エラーにはならない』。」
のまちがいじゃないの。

PATIO 2009/04/20(月) 12:06:41
そもそも、Win32のDeleteObjectは、GDIObjectのハンドルを開放する事が
本来の目的です。で、CGDIObjectのそれもハンドル開放が目的ですから
CGDIObject自体の実体はスコープを外れた時に開放されるでしょうけれど、
ハンドルの開放に失敗すれば、ハンドルはそのまま残る事になります。
残ったハンドルは、プログラマが意識的に保存しない限り、
CGDIObjectが破棄されてしまったら把握できなくなりますから
正規の方法では開放できない状態になります。
OSの仕組み上、同時に確保できるハンドル数には上限がありますから
開放しないまま確保を続ければ、そのうち上限に達してハンドルが取れなくなります。
ここまで行ってしまうとWindows全体の描画にすら影響が出てくるはずです。
なので、問題は大ありで救いようがありません。
プロセスが終わってしまえば、そのプロセスで確保しているメモリやハンドルは
強制的に開放されるはずですが、それだとおかしくなる度に終了して、
再度、起動し直しになるわけでこれが仕様とか言ったらユーザーに怒られそうです。

gak 2009/04/20(月) 17:14:07
> int ret = DeleteObject(hPen);
> 結果retはTRUE
http://support.microsoft.com/?scid=kb%3Ben-us%3B136989&x=9&y=11
> In Windows 95, if you try to delete a GDI drawing object while it is selected
> into a DC with DeleteObject(), the call to DeleteObject() succeeds, but the 
> result of the call is a non-functioning GDI object. In Windows NT and Windows 
> 2000, a call to DeleteObject() on a GDI object selected into a DC will fail.
信じ難いが、winxp は win95 と同じ動作をしている可能性もある?

--------
HPEN hPen = CreatePen(PS_SOLID, 5, RGB(255, 0, 0));
HGDIOBJ oldPen = SelectObject(hdc, hPen);
MoveToEx(hdc, 0, 0, 0);
LineTo(hdc, 200, 200);

SelectObject(hdc, oldPen);
// LineTo(hdc, 200, 200);

int ret = DeleteObject(hPen);
VERIFY(SelectObject(hdc, hPen) != NULL); //Delete済みのペンを選択する
MoveToEx(hdc, 200, 0, 0);
LineTo(hdc, 0, 200);

> 結果赤いXが描けました。
確かに描けた。けど↑コードのコメント行を有効にすると違う結果になる。
SelectObject(hdc, hPen) も失敗する。
http://msdn.microsoft.com/en-us/library/ms969928.aspx
> The pen style is recorded in the physical object, but the information is not 
> relevant until the pen is actually used for drawing.
実際に描画処理が行われるまで「physical pen」には反映されないって事らしい。
ロマさんのコードで赤いXが描けたのは oldPen 選択直後に描画処理無しで hPen を再選
択?している為、結果的に「physical pen」は終始変化無しという状況だったのかも。

ロマ 2009/04/20(月) 18:35:41
gakさんのコードを細工して

HPEN hPen = CreatePen(PS_SOLID, 5, RGB(255, 0, 0));
HGDIOBJ oldPen = SelectObject(hdc, hPen);
MoveToEx(hdc, 0, 0, 0);
LineTo(hdc, 200, 200);
int ret = DeleteObject(hPen);               // ここで削除に変更
SelectObject(hdc, oldPen);
// LineTo(hdc, 200, 200);

//int ret = DeleteObject(hPen);
VERIFY(SelectObject(hdc, hPen) != NULL); //Delete済みのペンを選択する
MoveToEx(hdc, 200, 0, 0);
LineTo(hdc, 0, 200);

これでも同じでした

xpではSelect中でもDeleteObjectは有効と思います
もちろんお勧めはしません

PATIO 2009/04/21(火) 11:23:43
うーん、DeleteObjectが成功を返す、結果は機能しないGDIオブジェクトです
というのはSelectObjectしていても削除できてしまうと言う意味みたいですねぇ。
でも、削除されたGDIObjectで描画をしても全く意味が無いと思います。
落っこちたって文句は言えないと思いますし。
偶々動いているのであれば、それを云々言ってもなぁと思います。

基本的なGDIのルールが変わったという話は聞いた事が無いので
実際の実装はどうあれ、Select状態にあるGDIObjectは削除すべきではないし、
削除する必要があるならSelect状態を解除しないといけないというのは
変わらないと思いますよ。

で、DeleteObjectで削除してしまったGDIObjectをSelect状態にして
描画した結果がどうなったとしてもそれは保証される範囲では無いと思うので
あまり意味が無いと思います。その部分は結局実装依存になるでしょうし。
現状動いていても実装方法に変更が加えられた場合、
動きが変わってしまう可能性は十分あります。
保証されていない部分の動作について議論しても意味が無いし、
提供されているAPIは保証された範囲で使用するべきだと思います。

純粋に興味で調べる分には良いと思いますけれど。
使うかどうかとは別問題です。

古いスレッドには追加発言できません。