1. ホーム
  2. c++

C++におけるオブジェクトの破壊

2023-10-12 19:20:40

質問

C++でオブジェクトが破棄されるのは正確にはいつですか、そしてそれはどういう意味ですか。ガーベッジコレクタがないので、手動で破棄しなければならないのでしょうか。例外はどのように発生するのでしょうか。

(注意: これは Stack Overflow の C++ FAQ . もし、このような形でFAQを提供するという考えを批判したいのであれば このすべての始まりとなった meta への投稿 がそれを行う場所でしょう。その質問に対する回答は C++チャットルーム で監視されているので、あなたの回答は、このアイデアを思いついた人たちに読まれる可能性が非常に高いのです)。

どのように解決するのですか?

以下の文章で、私は以下のものを区別します。 スコープ付きオブジェクト と、そのスコープ(関数、ブロック、クラス、式)によって破壊のタイミングが静的に決定される ダイナミックオブジェクト であり、その正確な破壊時刻は一般に実行時までわからない。

クラスオブジェクトの破壊のセマンティクスはデストラクタによって決定されますが、スカラオブジェクトの破壊は常にノー・オペレーションです。具体的には、ポインタ変数を破壊することは ではなく はポインタを破壊しません。

スコープされたオブジェクト

自動オブジェクト

自動オブジェクト (一般にローカル変数と呼ばれるもの) は、制御フローがその定義の範囲から離れると、その定義と逆の順序で破壊されます。

void some_function()
{
    Foo a;
    Foo b;
    if (some_condition)
    {
        Foo y;
        Foo z;
    }  <--- z and y are destructed here
}  <--- b and a are destructed here

関数の実行中に例外が発生した場合、例外が呼び出し元に伝えられる前に、それまでに構築されたすべての自動オブジェクトが破壊される。この処理は スタックアンワインド . スタック巻き戻しの間、それ以上の例外は前述の以前に構築された自動オブジェクトのデストラクタから出ることはできない。そうでない場合は、関数 std::terminate が呼び出されます。

これは、C++で最も重要なガイドラインの1つに繋がります。

デストラクタは決して投げてはならない。

非ローカルな静的オブジェクト

名前空間スコープで定義された静的オブジェクト(一般にグローバル変数と呼ばれます)および静的データメンバは、以下の実行後に定義と逆の順序で破棄されます。 main :

struct X
{
    static Foo x;   // this is only a *declaration*, not a *definition*
};

Foo a;
Foo b;

int main()
{
}  <--- y, x, b and a are destructed here

Foo X::x;           // this is the respective definition
Foo y;

異なる翻訳単位で定義された静的オブジェクトの構築(および破壊)の相対的な順序は不定であることに注意してください。

静的オブジェクトのデストラクタから例外が発生した場合、関数 std::terminate が呼び出されます。

ローカル静的オブジェクト

関数内部で定義された静的オブジェクトは、制御フローがその定義を初めて通過する時(そしてその時)構築されます。 1 の実行後、逆順に破棄されます。 main :

Foo& get_some_Foo()
{
    static Foo x;
    return x;
}

Bar& get_some_Bar()
{
    static Bar y;
    return y;
}

int main()
{
    get_some_Bar().do_something();    // note that get_some_Bar is called *first*
    get_some_Foo().do_something();
}  <--- x and y are destructed here   // hence y is destructed *last*

静的オブジェクトのデストラクタで例外が発生した場合、関数 std::terminate が呼び出されます。

1: これは極めて単純化されたモデルです。静的オブジェクトの初期化の詳細は、実際にはもっと複雑です。

ベースクラスサブオブジェクトとメンバサブオブジェクト

制御フローがオブジェクトのデストラクタ本体から離れると、そのメンバーサブオブジェクト(データメンバーとも呼ばれる)は、その定義の逆順に破壊されます。その後、そのベースクラスのサブオブジェクトはベーススペシファイアリストの逆順に破壊されます。

class Foo : Bar, Baz
{
    Quux x;
    Quux y;

public:

    ~Foo()
    {
    }  <--- y and x are destructed here,
};          followed by the Baz and Bar base class subobjects

を実行中に例外が発生した場合 構築 のいずれかの Foo のサブオブジェクトの一つを構築した場合、例外が伝搬する前に、 それ以前に構築された全てのサブオブジェクトが破壊されます。そのため Foo のデストラクタは、一方では ではなく は実行されません。 Foo オブジェクトが完全に構築されなかったためです。

デストラクタ本体はデータメンバ自身を破壊する責任はないことに注意してください。デストラクタを書く必要があるのは、データメンバが、オブジェクトの破壊時に解放する必要があるリソース(ファイル、ソケット、データベース接続、ミューテックス、ヒープメモリなど)のハンドルである場合だけです。

配列要素

配列の要素は、降順で破壊されます。もし 構築 の構築中に例外が発生した場合、例外が伝播する前に n-1 から 0 までの要素が破壊されます。

一時的なオブジェクト

一時オブジェクトはクラスタイプのprvalue式が評価されたときに構築されます。prvalue式の最も顕著な例は、オブジェクトを値で返す関数の呼び出しであり、例えば T operator+(const T&, const T&) . 通常、一時的なオブジェクトは、prvalueを含む完全な式が完全に評価されたときに破棄されます。

__________________________ full-expression
              ___________  subexpression
              _______      subexpression
some_function(a + " " + b);
                          ^ both temporary objects are destructed here

上記の関数呼び出しは some_function(a + " " + b) は、より大きな式の一部ではないので、完全な式です (その代わり、式 - ステートメントの一部です)。したがって、部分式の評価中に構築されたすべての一時的なオブジェクトは、セミコロンで破壊されます。このような一時的なオブジェクトは2つあります。1つ目は最初の加算時に、2つ目は2番目の加算時に構築されます。2番目の一時的なオブジェクトは1番目のオブジェクトの前に破壊されます。

2回目の加算時に例外が発生した場合、1つ目の一時的なオブジェクトは例外を伝播する前に適切に破壊されます。

ローカル参照が prvalue 式で初期化された場合、一時オブジェクトの寿命はローカル参照のスコープまで拡張されるため、ダングリングリファレンスが発生することがありません。

{
    const Foo& r = a + " " + b;
                              ^ first temporary (a + " ") is destructed here
    // ...
}  <--- second temporary (a + " " + b) is destructed not until here

クラス型でないprvalue式が評価された場合、その結果は であり、一時的なオブジェクトではありません。しかし、一時的なオブジェクトである は構築されます。

const int& r = i + j;

動的なオブジェクトと配列

次のセクションで を破壊する X は、"まずXを破壊し、次に基礎となるメモリを解放する"を意味します。 同様に X を作成する は、"まず十分なメモリを確保し、そこにXを構築する"という意味です。

動的オブジェクト

で作成されたダイナミック・オブジェクトは p = new Foo で生成されたオブジェクトは delete p . もし delete p を忘れると、リソースリークが発生します。これらはすべて未定義の動作につながるので、決してやってはいけません。

  • によって動的オブジェクトを破壊する。 delete[] で破壊します (角括弧に注意してください)。 free またはその他の手段
  • ダイナミック・オブジェクトを複数回破壊する
  • 破壊された後にダイナミック・オブジェクトにアクセスする

の実行中に例外が発生した場合 構築 の構築中に例外が発生した場合、その例外が伝搬される前に基礎となるメモリが解放されます。 (デストラクタは ではなく はメモリ解放の前に実行されません。)

動的配列

で作成された動的配列は p = new Foo[n] で作成された動的配列は delete[] p (を経由して破棄されます(角括弧に注意)。もし delete[] p を忘れると、リソースリークが発生します。これらはすべて未定義の動作につながるので、決してやってはいけません。

  • で動的配列を破壊する。 delete , free またはその他の手段
  • 動的配列を複数回破壊する
  • 動的配列が破壊された後、その配列にアクセスする

の実行中に例外が発生した場合 構築 の構築中に例外が発生した場合、n-1 から 0 までの要素が降順で破壊され、 その下のメモリが解放され、例外が伝搬されます。

(一般的には std::vector<Foo> よりも Foo* を使うようにしました。これにより、正しく堅牢なコードを書くことがより簡単になります)。

参照カウント型スマートポインタ

複数の std::shared_ptr<Foo> オブジェクトが破壊される際に、最後の std::shared_ptr<Foo> オブジェクトは、そのダイナミックオブジェクトの共有に関与している最後の オブジェクトの破棄時に破棄されます。

(一般的には std::shared_ptr<Foo> 以上 Foo* を共有オブジェクトのために使用します。これによって、正しく堅牢なコードを書くことがより簡単になります)。