none
MFCのCArrayを使用しているとヒープが無効になる

    質問

  • 現在Visual Studio Professional 2015で既存のMFCアプリケーションを修正しているのですが

    CArrayのRemoveAllやインスタンスの破棄時に"Invalid address specified to RtlValidateHeap"が発生してしまいます。

    Releaseモードの時はこの問題が発生せず、Debugのみ発生します。

    なぜこうなったのか、原因はわかりませんが発生している点はCArray::SetSize()で次の配列数を0にした際に現在のデータを全部破棄するコードで発生していることは確認しています。

    強引にchar配列にしてしまうことも考えたのですが、この配列に挿入するデータはDBからSQLで取得する都合上取得件数が分からない為どうにかしてCArrayを使用しておきたいです。

    どうにかしてこのままCArrayを使用する方法はないでしょうか?

    ビルド環境のOSはwindows8.1、VisualStudio2015はUpdate3適用済です。

    2017年3月28日 1:24

回答

  • CArray<T>の派生クラスを多用してますが、特に問題はありません。
    例えば、Tにクラスポインタ等を使用している場合などには、単位Tの破棄を伴う処理は全て自前で実装しなければなりませんが、そのあたりは万全でしょうか。

    2017年3月28日 2:20
  • tsDataSet変数を操作している個所は

    > dataSet tsDataSet;

    > // データセットは使いまわすので1件以上データが有れば削除する
    > if(tsDataSet.GetCount() > 0){
    >     tsDataSet.RemoveAll();
    > }

    > // SQLを実行
    > nReturn = PRC_Query13(szSql, szOraMsg, tsDataSet);

    > tsDataSet.RemoveAll();

    だけなので何の情報開示にもなっていません。PRC_Query13()が破壊行為を行っているのでしょう。

    なお、typedef CArray<CRows,CRows&> dataSet; がとても危なっかしく感じました。今回の問題の原因もここにある可能性も。

    CArrayの要素がCRow&を持つということになっています。参照ですからそれとは別に実体が存在しなければなりません。ところがPRC_Query13()はtsDataSetを操作した後、関数を戻ってきています。ということはPRC_Query13()が確保したCRowの実体はどこで解放されているのでしょうか? 単純に考えれば、全く解放されず放置か、関数を抜けるタイミングで解放か、2択です。アクセス違反が発生していることから後者の可能性が濃厚です。

    ちなみにC++言語ではこのような危険な行為は認められていません。ただし、Visual C++では/Ze Microsoft拡張機能の有効化により、危険な行為を認めてしまっています。(/Za Microsoft拡張機能を無効化しエラーとするを指定するとどこかしらエラーが発生するはずです。)

    2017年3月28日 2:57
  • dataSet型の定義に致命的な問題はないと思います(Add()からのSetAtGraw()はコピーオペレータを実行しますのでコピー元は破棄されても問題ありません)。
    afxtempl.hを眺めてみると、
    配列の本体  T * CArray::m_pData、 は BYTEの配列として確保されていますが、
    破棄時は必要な部分で適切に ~T() されています。
    ただ、やはりPRC_Query13()の中でどの様な追加処理が行われているのかが気になるところです。

    クラスCRowsにvirtualなデストラクタを明示的に実装し、そこでブレークしてみることをお勧めします。
    もし、破棄済みインスタンスの再破棄などの場合、問題が発見できるかもしれません。
    明示的なコンストラクタ、およびコピーコンストラクタも役に立つかもしれません(コードにあるのはコピーオペレータですね)。

    2017年3月28日 4:05
  • コンストラクター・デストラクター・operator=()の各関数にメッセージ出力をさせて、それぞれの関数が意図したタイミングに実行されているかを確認してみてはどうでしょうか?

    # 意図していないタイミングにデストラクターが動作していて、double-freeが発生しているのではないかと…。

    2017年3月28日 4:57
  • なんで親のメンバーをdeleteするんです?デストラクタは両方呼ばれますよ

    jzkey

    2017年3月28日 6:06

すべての返信

  • 文章で説明されても、コードが提示されないことには何が起こっているか把握できません。

    フォーラムのご利用方法(質問の投稿)についてフォーラムでご質問頂くにあたっての注意点をご確認ください。特にコードを公開できない場合はサポートサービスを受けることをお勧めします。

    2017年3月28日 1:50
  • 申し訳ありません、ソースを提示します。

    class CRows : public CStringArray
    {
    // コピーコンストラクタ
    public:
    	CRows& operator=(const CRows& row) {
    		Copy(row);
    		return *this;
    	}
    };
    
    typedef CArray<CRows,CRows&> dataSet;
    
    // タイマーイベントが起動する毎に実行される関数
    int CMyFormDlg::WatchDBCreateReqest(){
    	int nReturn = 0;
    	Request *tcrRequest;
    	dataSet tsDataSet;
    
    	// リクエスト情報一覧を削除
    	int nReqCount = mMagRequest.GetCount();
    	for(int i = 0; i < nReqCount; i++){
    		tcrRequest = (Request*)mMagRequest[i];
    		delete tcrRequest;
    	}
    	mMagRequest.RemoveAll();
    
    	// 現在の起動プロセスの状態を確認(実行最大値に到達しているか確認)
    	if(nNowWorkProcessCount >= nPracticeProcessCountMax){
    		// 最大数に到達している場合は起動しないようにするのでタイマー処理を終了
    		return 1;
    	}
    
    	int nCount = szaSelectSQL.GetCount();
    	// 設定されたSELECT文の数だけ実行する
    	for(int selectCount = 0; selectCount < nCount; selectCount++){
    		int nNull[20];
    		char szSql[4096];
    		char szOraMsg[2048];
    
    		memset(szSql, 0x00, sizeof(szSql));
    		memset(szOraMsg, 0x00, sizeof(szOraMsg));
    		memset(nNull, 0x00, sizeof(nNull));
    		strcpy(szSql, szaSelectSQL[selectCount]);
    
    		// データセットは使いまわすので1件以上データが有れば削除する
    		if(tsDataSet.GetCount() > 0){
    			tsDataSet.RemoveAll();
    		}
    
    		// SQLを実行
    		nReturn = PRC_Query13(szSql, szOraMsg, tsDataSet);
    
    		// データが存在する場合
    		if(nReturn == 0){
    			// プロセスを立ち上げる為の情報を格納
    		}
    		// Oracleエラー
    		else if(nReturn < 0){
    			return nReturn;
    		}
    	}
    
    	tsDataSet.RemoveAll();
    	return nReturn;
    }

    今回のエラーが発生している箇所はtsDataSet.RemoveAll()を実行した時です。

    また、エラー発生時に出力された行が以下になります。

    HEAP[CntlApp.exe]: Invalid address specified to RtlValidateHeap( 00F60000, 00F736F8 )

    2017年3月28日 2:17
  • CArray<T>の派生クラスを多用してますが、特に問題はありません。
    例えば、Tにクラスポインタ等を使用している場合などには、単位Tの破棄を伴う処理は全て自前で実装しなければなりませんが、そのあたりは万全でしょうか。

    2017年3月28日 2:20
  • 返信ありがとうございます。

    自前で用意すべき破棄処理はスコープが外れる前かアプリケーション終了時に入れています。

    2017年3月28日 2:29
  • tsDataSet変数を操作している個所は

    > dataSet tsDataSet;

    > // データセットは使いまわすので1件以上データが有れば削除する
    > if(tsDataSet.GetCount() > 0){
    >     tsDataSet.RemoveAll();
    > }

    > // SQLを実行
    > nReturn = PRC_Query13(szSql, szOraMsg, tsDataSet);

    > tsDataSet.RemoveAll();

    だけなので何の情報開示にもなっていません。PRC_Query13()が破壊行為を行っているのでしょう。

    なお、typedef CArray<CRows,CRows&> dataSet; がとても危なっかしく感じました。今回の問題の原因もここにある可能性も。

    CArrayの要素がCRow&を持つということになっています。参照ですからそれとは別に実体が存在しなければなりません。ところがPRC_Query13()はtsDataSetを操作した後、関数を戻ってきています。ということはPRC_Query13()が確保したCRowの実体はどこで解放されているのでしょうか? 単純に考えれば、全く解放されず放置か、関数を抜けるタイミングで解放か、2択です。アクセス違反が発生していることから後者の可能性が濃厚です。

    ちなみにC++言語ではこのような危険な行為は認められていません。ただし、Visual C++では/Ze Microsoft拡張機能の有効化により、危険な行為を認めてしまっています。(/Za Microsoft拡張機能を無効化しエラーとするを指定するとどこかしらエラーが発生するはずです。)

    2017年3月28日 2:57
  • dataSet型の定義に致命的な問題はないと思います(Add()からのSetAtGraw()はコピーオペレータを実行しますのでコピー元は破棄されても問題ありません)。
    afxtempl.hを眺めてみると、
    配列の本体  T * CArray::m_pData、 は BYTEの配列として確保されていますが、
    破棄時は必要な部分で適切に ~T() されています。
    ただ、やはりPRC_Query13()の中でどの様な追加処理が行われているのかが気になるところです。

    クラスCRowsにvirtualなデストラクタを明示的に実装し、そこでブレークしてみることをお勧めします。
    もし、破棄済みインスタンスの再破棄などの場合、問題が発見できるかもしれません。
    明示的なコンストラクタ、およびコピーコンストラクタも役に立つかもしれません(コードにあるのはコピーオペレータですね)。

    2017年3月28日 4:05
  • 返信ありがとうございます。

    公開ソース以外の点で解放処理を...という意味と間違えて解釈していました。

    [C/C++]->[言語]->[言語拡張を無効にする]をいいえからはいに変えたところ大量のエラーが出力されました(下記はその一部)

    1>c:\program files\microsoft visual studio 14.0\vc\atlmfc\include\atlcomcli.h(1800): error C2039: 'cVal': 'tagVARIANT' のメンバーではありません。
    1>  c:\program files\windows kits\10\include\10.0.10586.0\um\oaidl.h(455): note: 'tagVARIANT' の宣言を確認してください
    1>c:\program files\microsoft visual studio 14.0\vc\atlmfc\include\atlcomcli.h(1800): error C2065: 'cVal': 定義されていない識別子です。
    1>c:\program files\microsoft visual studio 14.0\vc\atlmfc\include\atlcomcli.h(1810): error C2039: 'bVal': 'tagVARIANT' のメンバーではありません。
    1>  c:\program files\windows kits\10\include\10.0.10586.0\um\oaidl.h(455): note: 'tagVARIANT' の宣言を確認してください
    1>c:\program files\microsoft visual studio 14.0\vc\atlmfc\include\atlcomcli.h(1810): error C2065: 'bVal': 定義されていない識別子です。
    1>c:\program files\microsoft visual studio 14.0\vc\atlmfc\include\atlcomcli.h(1820): error C2039: 'pcVal': 'tagVARIANT' のメンバーではありません。
    1>  c:\program files\windows kits\10\include\10.0.10586.0\um\oaidl.h(455): note: 'tagVARIANT' の宣言を確認してください
    1>c:\program files\microsoft visual studio 14.0\vc\atlmfc\include\atlcomcli.h(1820): error C2065: 'pcVal': 定義されていない識別子です。
    1>c:\program files\microsoft visual studio 14.0\vc\atlmfc\include\atlcomcli.h(1830): error C2039: 'pbVal': 'tagVARIANT' のメンバーではありません。
    1>  c:\program files\windows kits\10\include\10.0.10586.0\um\oaidl.h(455): note: 'tagVARIANT' の宣言を確認してください
    1>c:\program files\microsoft visual studio 14.0\vc\atlmfc\include\atlcomcli.h(1830): error C2065: 'pbVal': 定義されていない識別子です。
    1>c:\program files\microsoft visual studio 14.0\vc\atlmfc\include\atlcomcli.h(1840): error C2039: 'iVal': 'tagVARIANT' のメンバーではありません。
    1>  c:\program files\windows kits\10\include\10.0.10586.0\um\oaidl.h(455): note: 'tagVARIANT' の宣言を確認してください
    1>c:\program files\microsoft visual studio 14.0\vc\atlmfc\include\atlcomcli.h(1840): error C2065: 'iVal': 定義されていない識別子です。
    1>c:\program files\microsoft visual studio 14.0\vc\atlmfc\include\atlcomcli.h(1850): error C2039: 'piVal': 'tagVARIANT' のメンバーではありません。

    >CArrayの要素がCRow&を持つということになっています。
    >参照ですからそれとは別に実体が存在しなければなりません。ところがPRC_Query13()はtsDataSetを操作した後、関数を戻ってきています。
    >ということはPRC_Query13()が確保したCRowの実体はどこで解放されているのでしょうか?
    >単純に考えれば、全く解放されず放置か、関数を抜けるタイミングで解放か、2択です。
    >アクセス違反が発生していることから後者の可能性が濃厚です。

    PRC_Query13()では引数のSQLを実行した結果をCRowに格納し、それをtsDataSetにAddメソッドで追加していました。

    また、PRC_Query13()内ではnewでインスタンスを生成せずにローカル変数としてCRowのインスタンスを作成していました。

    2017年3月28日 4:16
  • 返信ありがとうございます。

    CRowsクラスを以下のように修正、dataSetをforループ内のローカル変数に変更し、RomoveAllを実行しないように修正しました。

    その後Debug実行してみたところデストラクタ内でdeleteを実行した際にアクセス違反例外が発生しました。

    class CRows: public CStringArray { // コピーコンストラクタ public: // CString *m_pData; // INT_PTR m_nSize; // INT_PTR m_nMaxSize; // INT_PTR m_nGrowBy; // コンストラクタ CRows() { this->m_pData = NULL; this->m_nSize = 0; this->m_nMaxSize = 0; this->m_nGrowBy = 0; } virtual ~CRows() { if (m_nSize != 0 && this->m_pData != NULL) {

    // ここでエラー delete[] this->m_pData; } } CRows& operator=(const CRows& row) { Copy(row); return *this; } };


    その際のエラーメッセージは

    [0x0FA1D551 (mfc140d.dll) で例外がスローされました (CntlApp.exe 内): 0xC0000005: 場所 0xF90FBDB0 の読み取り中にアクセス違反が発生しました]

    となっており、m_pDataの先頭アドレスは[0x0117c5c0]でした。

    また、m_pDataに格納されているデータはデバッガ上から確認ができています。
    2017年3月28日 4:50
  • コンストラクター・デストラクター・operator=()の各関数にメッセージ出力をさせて、それぞれの関数が意図したタイミングに実行されているかを確認してみてはどうでしょうか?

    # 意図していないタイミングにデストラクターが動作していて、double-freeが発生しているのではないかと…。

    2017年3月28日 4:57
  • 返信ありがとうございます。

    CRowsのコンストラクタ/デストラクタ/コピーコンストラクタ・dataSetを宣言している関数それぞれにOutputDebugStringを使用してメッセージを出力させたところ以下のように出力されました。

    // ここから
    データセット作成
    データセット作成完了
    SQL実行
    SQL実行終了
    ループ終了、ローカル変数破棄
    // ここまではSQLで1件も取得できなかった
    
    // ここから
    データセット作成
    データセット作成完了
    SQL実行
    コンストラクタ処理-開始
    コンストラクタ処理-終了
    コンストラクタ処理-開始
    コンストラクタ処理-終了
    コピー処理-開始
    コピー処理-終了
    デストラクタ処理-開始
    デストラクタ処理 0026C0A8 破棄開始
    HEAP[TDCAD_AsblCntl.exe]: Invalid address specified to RtlValidateHeap( 001E0000, 0026C088 )

    また、CRowsクラスを以下のように修正しています。

    delete[]で一括処理するのではなく要素数が分かっている以上末尾から1つづつdeleteを掛けていくようにしました。

    class CRows : public CStringArray
    {
    // コピーコンストラクタ
    public:
    	// CString *m_pData;
    	// INT_PTR m_nSize;
    	// INT_PTR m_nMaxSize;
    	// INT_PTR m_nGrowBy;
    
    	// コンストラクタ
    	CRows () {
    		OutputDebugString("コンストラクタ処理-開始\n");
    		this->m_pData = NULL;
    		this->m_nSize = 0;
    		this->m_nMaxSize = 0;
    		this->m_nGrowBy = 0;
    		OutputDebugString("コンストラクタ処理-終了\n");
    	}
    
    	virtual ~CRows () {
    		OutputDebugString("デストラクタ処理-開始\n");
    		if (m_nSize != 0 && this->m_pData != NULL) {
    			for (int i = m_nSize - 1; i > 0; i--) {
    				CString str;
    				str.Format("デストラクタ処理 %p 破棄開始\n", m_pData[i]);
    				OutputDebugString(str);
    				delete m_pData[i];
    				OutputDebugString("デストラクタ処理-破棄完了\n");
    			}
    		}
    		OutputDebugString("デストラクタ処理-終了\n");
    	}
    
    	CRows & operator=(const CSA_Row& row) {
    		OutputDebugString("コピー処理-開始\n");
    		Copy(row);
    		OutputDebugString("コピー処理-終了\n");
    		return *this;
    	}
    };

    2017年3月28日 5:19
  • delete[]で一括処理するのではなく要素数が分かっている以上末尾から1つづつdeleteを掛けていくようにしました。

    壮絶な勘違いが見られるので、本スレッドからは脱線しますが指摘しておきます。

    • deleteはnewで確保したメモリに対して実行します。1要素分だけデストラクターが実行された後にメモリが解放されます。
    • delete[]はnew[]で確保したメモリに対して実行します。new[]で確保した要素数分のデストラクターが実行された後にメモリが解放されます。
      つまりnew[]で確保されていないメモリに対してはデストラクターを実行すべき要素数を決定できません。

    以上のように動作が全く異なりますので、delete[] ⇔ deleteを適当な想いで切り替えることはできません。

    全ての個所で、new deleteペア、new[] delete[]ペアを正しく組み合わせていますでしょうか?

    2017年3月28日 5:37
  • なんで親のメンバーをdeleteするんです?デストラクタは両方呼ばれますよ

    jzkey

    2017年3月28日 6:06
  • 返信ありがとうございます。

    ご指摘頂いた内容を元に今回のクラス[CRows]の基底クラスであるCStringArrayのSetSize等の関数について再度確認をしました。

    その結果なのですが、CString*m_pDataに対してnewをしたデータをコピーしているのですが、そのコピー元はBYTE型の配列をnewしていました。

    BYTE型の配列をnewで生成した後CString*にキャストしてm_pDataに対してコピーを行う、若しくはmemcpy_sを使用してm_pDataの次の要素としてインスタンスをコピーするということをしていたので、下記のように修正を行いました。

    class CRows: public CStringArray
    {
    // コピーコンストラクタ
    public:
    	// CString *m_pData;
    	// INT_PTR m_nSize;
    	// INT_PTR m_nMaxSize;
    	// INT_PTR m_nGrowBy;
    
    	// コンストラクタ
    	CRows() {
    		this->m_pData = NULL;
    		this->m_nSize = 0;
    		this->m_nMaxSize = 0;
    		this->m_nGrowBy = 0;
    	}
    
    	virtual ~CRows() {
    		if (m_nSize != 0 && this->m_pData != NULL) {
    			delete[](BYTE*)m_pData;
    			m_pData = NULL;
    			m_nSize = m_nMaxSize = 0;
    		}
    	}
    
    	CRows& operator=(const CRow& row) {
    		Copy(row);
    		return *this;
    	}
    };

    この状態のソースで実行するとアクセス違反例外が出力されずに実行ができました。

    基底クラスのデストラクタが以下になるのですが、何故この修正で動作するようになったのかが分かりません。

    CStringArray::~CStringArray()
    {
    	AFX_BEGIN_DESTRUCTOR
    
    		ASSERT_VALID(this);
    
    		_DestructElements(m_pData, m_nSize);
    		delete[] (BYTE*)m_pData;
    
    	AFX_END_DESTRUCTOR
    }
    

    もう少しブレークポイント等を入れてみて理解してみようと思います。

    ありがとうございました。

    2017年3月28日 6:17
  • jzkeyさんも指摘されていますが、派生クラスが出しゃばって親クラスのリソースの解放を行う意味がありません。

    たぶん、これに限らず質問のプログラムは全体にわたって手を加えるべきでない操作を多数行っており、手に負えない状況になっていることが予想されます。顕在化しているエラーを解消させたところで、内部はぐちょぐちょに崩れていると思いますので、今何をすべきか考え直すことから始めることをお勧めします。

    2017年3月28日 7:23