メモリ割り当てによる従来のSTLプログラムのクラッシュを防止

5482 ワード

問題の説明
ほとんどのC++開発者は、コードの中でSTLを広く使用しています.STLとVisusal Studio 6.0を直接使用すると、メモリが低い場合にプログラムがクラッシュする可能性があります.なぜならnew操作の結果を検証しなかったからです.さらに悪いことに、new操作が確かに失敗した場合、得られたフィードバックにも基準がありません.あるコンパイラは空のポインタを返し、あるコンパイラは異常を投げ出すことがあります.
要するに、MFCのプロジェクトでSTLを使う場合は、MFCには独自のルールがあることに注意してください.この記事では、これらの問題について主に説明し、最新のVisual C++コンパイラのデフォルトの動作がどのように変更されたかを説明し、Visual C++6.0を使用する場合に必要な変更点を概説します.これにより、new操作に失敗した場合でもSTLを安全に使用できます.
原因分析
new操作が失敗したかどうかを確認するプログラマーは何人いますか?このような検査をよくする必要がありますか?Visual C++6.0で書かれた膨大で複雑なC++プロジェクトを見たことがありますが、newの戻り結果がNULLかどうかをチェックしたところはありません.newに対してNULLを返すチェックであることに注意してください.Visual C++6.0では、new操作に失敗した場合のデフォルトの動作は、例外を放出するのではなくNULLポインタを返すことです.Visual C++2003では、Cランタイムライブラリ(C Runtime Library)のnewが失敗した場合はNULLを返しますが、標準C++ライブラリ(Standard C++Library)のnewが失敗した場合は例外が放出されます.Newが失敗したときの動作は、linkerで標準C++ライブラリが前にあるか、Cランタイムライブラリが前にあるかによって異なります.標準C++ライブラリが前面にあると、異常が放出されます.一方、Cランタイムライブラリが前面にある場合はNULLのみが返されます.この動作を書き換え、異常を投げ出すnewを強制的に使用するには、表示するリンクthrownewが必要です.obj.Visual C++2005、2008および2010では、リンクnothrownewが表示する場合を除く.obj,そうでなければCランタイムライブラリでも標準C++ライブラリでも異常が投げ出される.なお、本明細書で説明する動作は、管理コードまたは管理コードには関与しない.NETフレーム.既存のVisual C++6.0スタイルのコードがnew操作で異常が発生すると予想されなかった場合、これらのコードをすべて高バージョンのコンパイラに移植した後、その中のnewが異常を放出した場合、発生したプログラムは実行時に意外に終了する可能性が高い.この点には注意しなければなりません.
C++標準では、newオペレータは失敗時に異常を投げ出さなければならないことが規定されています.具体的には、この異常はstd::bad_alloc.これはただの基準ですが、具体的にはVisual C++の場合は下表を参照してください.
バージョン#バージョン#
純C++
MFC
Visual C++ 6.0
NULLを返します
CMemoryException
> 6.0
std::bad_alloc
CMemoryException
MFC環境において,放出された異常はC++標準では要求されないことが分かる.STLでcatch(std::bad_alloc)でメモリ割り当てに失敗した場合は、これはMFCがない環境でしかできません.Visual C++6.0のSTLは、newの失敗をcatch(…)で処理します.この書き方は、MFCで正常に動作します.
ソリューション
(一般的には具体的な解決策が必要)
MFC環境において,放出された異常はC++標準では要求されないことが分かる.STLでcatch(std::bad_alloc)でメモリ割り当てに失敗した場合は、これはMFCがない環境でしかできません.Visual C++6.0のSTLは、newの失敗をcatch(…)で処理します.この書き方は、MFCで正常に動作します.
NULLを返すnewオペレータ
通常、両方の場合、newが返すポインタがNULLであるかどうかを確認する必要はありません.newは失敗しないか、newが異常を投げ出すことはありません.
newは決して失敗しないと思っても、戻り値をチェックしないのは悪いプログラミング習慣です.デスクトップアプリケーションは、メモリが消費される可能性が低いのが一般的です.しかし、一部のサーバで24時間実行する必要があるプログラムでは、特に共有アプリケーションサーバでメモリが消費される可能性があります.アプリケーションが1バイトも漏らさないことを保証できない場合は、メモリによってエラーが発生する確率が高くなります.
戻ってきたポインタがNULLであるかどうかをチェックしないと、newが異常を投げ出すため、これも当然です.結局、C++標準ではnewが失敗した場合に例外を投げ出すことが規定されていますが、これはVisual C++6.0のデフォルトのやり方ではなく、NULLポインタが1つしか返されません.以降のバージョンではC++標準がサポートされていますが、6.0の方法(特にSTLと一緒に使用する場合)で問題が発生します.STLでは、どのコンパイラを使用しているかにかかわらず、newが失敗した場合に例外が投げ出されると仮定します.実際、newがこのような動作を示さず、メモリ割り当てに失敗してNULLポインタを得た場合、STLの次の動作は予測不可能であり、プログラムもクラッシュする可能性が高い.
標準テンプレートライブラリ
開発者はC++開発の過程でSTLにますます依存している.STLはC++テンプレートに基づいて多くのクラスと関数を提供している.STLを使用すると、いくつかのメリットがあります.まず、このライブラリはさまざまな汎用タスクに一貫したインタフェースを提供します.次に、このコードは広くテストされているので、バグがないと考えられます.最後に、中のアルゴリズムも最高です.
STLを使用できるように、コンパイラはC++標準をサポートします.Visual C++コンパイラにはSTLがプリインストールされており、他メーカーでも使用可能です.
Visual C++6.0およびnewオペレータ
newが失敗したときにNULLを返すと、この動作はBugであり、標準と一致しないためと考えられる.Visual C++に付属するすべてのSTLの実装では、newオペレータが失敗したときに例外を放出することが予想されます.newの動作を変更してエラーが発生したときに異常を投げ出すことができますが、これはより多くの不規範をもたらします.次のコードで問題を説明します.
#include 
void Foo()
{
    std::string str("A very big string");
}
	  

Visual C++6.0では、上記のコードは最終的にSTLの次の関数に呼び出されます(説明の便利さのために余分なコードが取り除かれました):
void _Copy(size_type _N)
{
    ...
    _E *_S;
    _TRY_BEGIN 
        _S = allocator.allocate(_Ns + 2, (void *)0);
    _CATCH_ALL 
        _Ns = _N; 
        _S = allocator.allocate(_Ns + 2, (void *)0); 
    _CATCH_END 
    ...
    _Ptr = _S + 1;
    // ACCESS VIOLATION
    _Refcnt(_Ptr) = 0;
    ...
}
	  

try文ブロックでallocator.allocateの戻り値はローカル変数に割り当てられます.S、そしてallocator.allocateはnewを使用します.Visual C++6.0のデフォルトの動作は、newオペレータが失敗した場合にNULLが返され、_Sの値はNULLです.次の行は_S+1の値付与_Ptr.もし_SはNULL,Ptrは最終的に0 x 00000001となる.次の文_Refcnt(_Ptr)=0は事実上_を返します.Ptr-1(すなわち_Ptr[-1])は、実際には最初に返されたNULLに対して計算されている._RefcntはNULLポインタを返し、次に0を割り当てます(*NULL=0).これにより、すぐにアクセス競合エラーが発生します.これはBugのように見えますが、STLのコードには問題はありません.正しい動作を得るためには、newが異常を投げ出す必要があります.
newが失敗したときに異常な実行フローを投げ出すのをもう一度見てみましょう.まずallocatorを実行する.allocate,このうちnewが失敗するとstd::bad_alloc異常、続いて_CATCH_ALLはもう一度やってみます.2回目の割り当てに失敗した場合、もう1つのstd::bad_alloc異常は投げ出され、これは私たちのコードに伝播され、最終的にstd::sttingオブジェクトは定義されているが空の状態になる.
修正newオペレータ
#include 
#include 
#pragma init_seg(lib)
namespace
{
int new_handler(size_t) 
{
    throw std::bad_alloc();
    return 0;
}

class NewHandler
{
public:
    NewHandler() 
    {
        m_old_new_handler = _set_new_handler(new_handler);
    }   
    ~NewHandler() 
    {
        _set_new_handler(m_old_new_handler);
    }
private:
    _PNH m_old_new_handler;
} g_NewHandler; 
}   // namespace
	  

上記のコードを私たちのプロジェクトに含めると、newが失敗したときのエラー処理が自動的に変更され、例ではstd::bad_が投げ出されます.alloc.
新(std::nothrow)投げ出しエラー
Visual Studio 6.0では、上記のコードを含めてメモリを割り当てるときにnew(std::nothrow)を使い、releaseを実行すると逆にエラーが出て「Abnormal program termination」と表示されます.これはコンパイラの最適化による比較的詳細な問題です.この問題を回避するためにProject Settings|C/C++|General|Optimizationsに最適化をオフにするか、自分でnew(std::nothrow)を書くこともできます(ソースコードNewNoThrow.cppを参照).
まとめ
Visual C++6.0のデフォルトで提供されるnew操作はSTLと互換性がありません.いくつかの解決策が前述されていても、サードパーティのライブラリまたはSTLで他の関数を個別に使用する場合に問題が発生する可能性があります.VC 6.0ではnew、new(std::nothrow)とSTLの不釣り合いが完全に解決できないが、上記の方法を使わないと、かなり面倒になるに違いない.
MFCプロジェクトでは、STLでnewを使っているところが異常な試練に耐えられるかどうかは、あなたが使っているSTLでのエラー処理時にどのように書かれているかにかかっています.ほとんどはcatch(std::bad_alloc)ではなくcatch(...)を使用しますが、これは必須ではありません.
最後に、最初に述べたように、Visual C++2005から2010までにこれらの問題が修正されました.