スタックの巻き戻しとは何ですか?検索しましたが、啓発的な答えが見つかりませんでした!
スタックの巻き戻しは、通常、例外処理に関連して説明されています。次に例を示します。
void func( int x )
{
char* pleak = new char[1024]; // might be lost => memory leak
std::string s( "hello world" ); // will be properly destructed
if ( x ) throw std::runtime_error( "boom" );
delete [] pleak; // will only get here if x == 0. if x!=0, throw exception
}
int main()
{
try
{
func( 10 );
}
catch ( const std::exception& e )
{
return 1;
}
return 0;
}
ここで、pleak
に割り当てられたメモリは、例外がスローされると失われますが、s
に割り当てられたメモリは、どのような場合でもstd::string
デストラクタによって適切に解放されます。スタックに割り当てられたオブジェクトは、スコープが終了すると「巻き戻され」ます(ここでは、スコープは関数func
です)。これは、自動(スタック)変数のデストラクターへの呼び出しを挿入するコンパイラーによって行われます。
現在、これは RAII と呼ばれる手法につながる非常に強力な概念です。つまり、Resource Acquisition Is Initialization、C++でメモリ、データベース接続、オープンファイル記述子などのリソースを管理するのに役立ちます。
これで、 例外安全保証 を提供できるようになりました。
これはすべてC++に関連しています。
Definition:オブジェクトを静的に(ヒープメモリに割り当てるのではなくスタック上に)作成し、関数呼び出しを実行すると、オブジェクトは「スタック」されます。 。
スコープ({
および}
で区切られたもの)が終了すると(return XXX;
を使用する、スコープの最後に到達する、または例外をスローする)、そのスコープ内のすべてが破棄されます(すべてに対してデストラクターが呼び出されます)。 ローカルオブジェクトを破棄し、デストラクタを呼び出すこのプロセスは、スタックアンワインドと呼ばれます。
スタックの巻き戻しに関連する次の問題があります。
メモリリークの回避(ローカルオブジェクトによって管理されず、デストラクタでクリーンアップされる動的に割り当てられたものはすべてリークされます)-ニコライによるRAII 参照 、および ブーストのドキュメント: :scoped_ptr または boost :: mutex :: scoped_lock を使用するこの例。
プログラムの一貫性:C++仕様では、既存の例外が処理される前に例外をスローしないでください。これは、スタック巻き戻しプロセスが例外をスローしないことを意味します(デストラクタをスローしないことが保証されているコードのみを使用するか、デストラクタ内のすべてをtry {
で囲み、 } catch(...) {}
)。
スタックのアンワインド中にデストラクタが例外をスローすると、未定義の動作のランドになり、プログラムが予期せず終了する(最も一般的な動作)またはユニバースが終了する(理論的に)可能ですが、実際にはまだ観察されていません)。
一般的な意味では、スタックの「巻き戻し」は、関数呼び出しの終了とそれに続くスタックのポップとほぼ同義です。
ただし、特にC++の場合、スタックの巻き戻しは、C++がコードブロックの開始以降に割り当てられたオブジェクトのデストラクタを呼び出す方法に関係しています。ブロック内で作成されたオブジェクトは、割り当ての逆の順序で割り当て解除されます。
スタックの巻き戻しは、ほとんどがC++の概念であり、スコープが終了したときにスタックに割り当てられたオブジェクトが(通常または例外によって)破棄される方法を扱います。
次のコードの断片があるとします:
void hw() {
string hello("Hello, ");
string world("world!\n");
cout << hello << world;
} // at this point, "world" is destroyed, followed by "hello"
これをまだ読んだかどうかはわかりませんが、 コールスタックに関するウィキペディアの記事 には十分な説明があります。
巻き戻し:
呼び出された関数から戻ると、スタックから一番上のフレームがポップされ、おそらく戻り値が残ります。スタックから1つ以上のフレームをポップしてプログラムの別の場所で実行を再開するより一般的な動作はstack unwindingと呼ばれ、非ローカルのときに実行する必要があります例外処理に使用されるような制御構造が使用されます。この場合、関数のスタックフレームには、例外ハンドラを指定する1つ以上のエントリが含まれます。例外がスローされると、スローされた例外のタイプを処理(キャッチ)する準備ができているハンドラーが見つかるまで、スタックは巻き戻されます。
一部の言語には、一般的な巻き戻しを必要とする他の制御構造があります。 Pascalでは、グローバルgotoステートメントを使用して、ネストされた関数から以前に呼び出された外部関数に制御を移すことができます。この操作では、スタックを展開して、必要な数のスタックフレームを削除して、適切なコンテキストを復元し、外側の外部関数内のターゲットステートメントに制御を移す必要があります。同様に、Cには、非ローカルgotosとして機能するsetjmpおよびlongjmp関数があります。 Common LISPでは、unwind-protect特殊演算子を使用して、スタックが巻き戻されたときの動作を制御できます。
継続を適用すると、スタックは(論理的に)巻き戻されてから、継続のスタックで巻き戻されます。これが継続を実装する唯一の方法ではありません。たとえば、複数の明示的なスタックを使用すると、継続のアプリケーションは単にそのスタックをアクティブ化し、渡される値を巻き上げることができます。 Schemeプログラミング言語では、継続が呼び出されたときに、コントロールスタックの「巻き戻し」または「巻き戻し」の指定されたポイントで任意のサンクを実行できます。
検査[編集]
私が理解するのに役立つブログ投稿を読みました。
スタックの巻き戻しとは何ですか?
再帰関数(Fortran 77とBrainf * ckを除くほとんどすべて)をサポートする言語では、言語ランタイムは現在実行中の関数のスタックを保持します。スタックの巻き戻しは、そのスタックを検査し、場合によっては変更する方法です。
なぜそれをしたいのですか?
答えは明白に思えるかもしれませんが、巻き戻しが有用または必要ないくつかの関連する、しかし微妙に異なる状況があります。
- ランタイム制御フローメカニズムとして(C++例外、C longjmp()など)。
- デバッガーで、ユーザーにスタックを表示します。
- プロファイラーで、スタックのサンプルを取得します。
- プログラム自体から(スタックを表示するクラッシュハンドラーからなど)。
これらには微妙に異なる要件があります。これらのいくつかはパフォーマンスに重要ですが、そうでないものもあります。外部フレームからレジスタを再構築する機能が必要なものもあれば、そうでないものもあります。しかし、すぐにそれらすべてについて説明します。
完全な投稿を見つけることができます こちら 。
誰もがC++での例外処理について話しました。しかし、スタックの巻き戻しには別の意味があり、それはデバッグに関連していると思います。デバッガは、現在のフレームの前のフレームに移動することになっているときはいつでも、スタックの巻き戻しを行う必要があります。ただし、これは現在のフレームに戻ったときに巻き戻す必要があるため、一種の仮想巻き戻しです。この例は、gdbのup/down/btコマンドです。
IMO、この下の図 記事 は、次の命令(キャッチされない例外がスローされると実行される)のルートでのスタックの巻き戻しの効果を美しく説明しています。
写真で:
2番目のケースでは、例外が発生すると、関数呼び出しスタックが線形に例外ハンドラを検索します。検索は、例外ハンドラーを使用して関数で終了します。つまり、try-catch
ブロックを囲むmain()
、で、前ではありません前のすべてのエントリを削除します関数呼び出しスタックから。
C++ランタイムは、スローとキャッチの間で作成されたすべての自動変数を破棄します。以下のこの単純な例では、f1()throwsおよびmain()catchsの間に、タイプBとAのオブジェクトがスタック上にこの順序で作成されます。 f1()がスローされると、BとAのデストラクターが呼び出されます。
#include <iostream>
using namespace std;
class A
{
public:
~A() { cout << "A's dtor" << endl; }
};
class B
{
public:
~B() { cout << "B's dtor" << endl; }
};
void f1()
{
B b;
throw (100);
}
void f()
{
A a;
f1();
}
int main()
{
try
{
f();
}
catch (int num)
{
cout << "Caught exception: " << num << endl;
}
return 0;
}
このプログラムの出力は次のようになります
B's dtor
A's dtor
これは、f1()がスローされるときのプログラムのコールスタックが次のようになるためです。
f1()
f()
main()
したがって、f1()がポップされると、自動変数bが破棄され、f()がポップされると、自動変数aが破棄されます。
これがお役に立てば幸いです!
例外がスローされ、制御がtryブロックからハンドラーに渡されると、C++ランタイムは、tryブロックの開始以降に構築されたすべての自動オブジェクトのデストラクターを呼び出します。このプロセスは、スタックの巻き戻しと呼ばれます。自動オブジェクトは、構築の逆の順序で破棄されます。 (自動オブジェクトは、autoまたはregisterとして宣言された、またはstaticまたはexternとして宣言されていないローカルオブジェクトです。xが宣言されているブロックをプログラムが終了するたびに、自動オブジェクトxは削除されます。)
サブオブジェクトまたは配列要素で構成されるオブジェクトの構築中に例外がスローされる場合、デストラクターは、例外がスローされる前に正常に構築されたサブオブジェクトまたは配列要素に対してのみ呼び出されます。ローカルの静的オブジェクトのデストラクタは、オブジェクトが正常に構築された場合にのみ呼び出されます。
Javaスタックでは、(ガベージコレクターで)スタックの解放または解放はそれほど重要ではありません。多くの例外処理の論文で私はこの概念(スタックの巻き戻し)を見ましたが、特にこれらのライターはCまたはC++での例外処理を扱います。 try catch
ブロックを忘れないでください:ローカルブロックの後のすべてのオブジェクトからスタックを解放。