5

私は長年 Java で開発を行ってきましたが、C++ に切り替えたいと考えているため、メモリ管理システムを理解するのに苦労しています。

小さな例で状況を説明しましょう。

私の理解では、スタックまたはヒープのいずれかにスペースを割り当てることができます。1 つ目は、次のように変数を宣言することによって行われます。

 int a[5]

また

int size = 10;
int a[size]

逆に、ヒープにメモリを割り当てたい場合は、「new」コマンドを使用して実行できます。たとえば、次のようにします。

int *a = new int[10]; (notice that I haven't tried all the code, so the syntax might be wrong)

2 つの違いの 1 つは、関数が終了したときにスタックに割り当てられている場合、スペースは自動的に割り当て解除されますが、それ以外の場合は、delete() で明示的に割り当てを解除する必要があることです。

さて、次のようなクラスがあるとします。

class A {
  const int *elements[10];

  public void method(const int** elements) {
    int subarray[10];
    //do something
    elements[0] = subarray;
  }
}

さて、いくつか質問があります:

  1. この場合、部分配列がスタックに割り当てられます。関数メソッドが終了した後でも、elements[0] を見ると部分配列のデータがまだ表示されるのはなぜですか? コンパイラはヒープ割り当ての最初の割り当てを変換しましたか (この場合、これは良い習慣ですか)?
  2. サブアレイを「const」として宣言すると、コンパイラはそれを要素に割り当てさせません。なぜだめですか?const はポインターを変更できないことだけに関係していると思いましたが、他には何もありませんでした。
  3. (これはおそらくかなりばかげています) 「要素」を固定の 10 個の要素ではなく、コンストラクターからのパラメーターで割り当てたいとします。スタックに割り当てることはまだ可能ですか、それともコンストラクターは常にヒープに割り当てますか?

そのような質問で申し訳ありませんが (専門の C プログラマーにはばかげているように見えるかもしれません)、C++ のメモリ管理システムは Java とは大きく異なります。また、リークや低速なコードを回避したいと考えています。よろしくお願いします!

4

6 に答える 6

2

a) この場合、部分配列がスタックに割り当てられます。関数メソッドが終了した後でも、elements[0] を見ると部分配列のデータがまだ表示されるのはなぜですか? コンパイラはヒープ割り当ての最初の割り当てを変換しましたか (この場合、これは良い習慣ですか)?

それは「未定義の動作」と呼ばれ、何でも起こる可能性があります。この場合、subarrayおそらく関数が戻った直後にそのメモリにアクセスするため、保持された値はまだそこにあります。ただし、コンパイラは、返す前にこれらの値をゼロにすることもできます。コンパイラは、火を噴くドラゴンを家に送ることもできます。「未定義の動作」-土地では何でも起こり得ます。

b) サブアレイを「const」として宣言すると、コンパイラはそれを要素に割り当てさせません。なぜだめですか?const はポインターを変更できないことだけに関係していると思いましたが、他には何もありませんでした。

これは、この言語のかなり残念な癖です。検討

const int * p1; // 1
int const * p2; // 2
int * const p3; // 3
int * p4;       // 4
int const * const p5; // 5

これはすべて有効な C++ 構文です。1 は、 const intへの変更可能なポインターがあることを示しています。2 は 1 と同じことを言います (これは癖です)。3 は、変更可能な intへのconst ポインターがあることを示しています。4 は、変更可能な intへの単純な古い変更可能なポインターがあることを示しています。5 は、 const intへのconst ポインターがあることを示しています。ルールはおおまかに次のとおりです。右側または左側のいずれかにある最後の const を除いて、 const をright-to-leftから読み取ります。

c) 固定の 10 個の要素ではなく、コンストラクターからのパラメーターを使用して「要素」を割り当てたいとします。スタックに割り当てることはまだ可能ですか、それともコンストラクターは常にヒープに割り当てますか?

動的割り当てが必要な場合、これは通常ヒープ上にありますが、スタックとヒープの概念は実装に依存します (つまり、コンパイラ ベンダーが何を行うかに関係なく)。

最後に、Java のバックグラウンドがある場合は、メモリの所有権を考慮する必要があります。たとえば、メソッド内void A::method(const int**)で、ポインタをローカルに作成されたメモリにポイントしますが、そのメモリはメソッドが戻った後に消えます。あなたのポインタは、誰も所有していないメモリを指しています。実際にそのメモリを新しい領域 (たとえば、クラスのデータ メンバA) にコピーしてから、ポインタがそのメモリの一部を指すようにする方がよいでしょう。また、C++ はポインターを実行できますが、絶対に避けるのが賢明です。たとえば、可能かつ適切な場合はポインターの代わりに参照を使用するように努め、std::vector任意サイズの配列のクラス。ベクトルを別のベクトルに割り当てると、実際にはすべての要素が一方から他方にコピーされるため、このクラスは所有権の問題にも対処します (現在は右辺値参照を除きますが、今のところ忘れてください)。一部の人々は、「裸の」新規作成/削除を悪いプログラミング手法と見なしています。

于 2013-10-01T17:22:59.217 に答える
2

A)いいえ、コンパイラはそれを翻訳しておらず、未定義の動作に踏み込んでいません。Java 開発者との類似点を見つけるには、関数の引数について考えてみてください。あなたがするとき:

int a = 4;
obj.foo(a);

aメソッドに渡されるとどうなりfooますか? コピーが作成され、それがスタック フレームに追加され、関数が戻ると、フレームは他の目的に使用されます。ローカル スタック変数は、呼び出し規則を除いて、通常は同様に扱われるため、引数の続きと考えることができます。スタック (言語に依存しないスタック) がどのように機能するかについて詳しく読むと、この問題がさらに明らかになると思います。

B)ポインターをマークするか、ポインターが指すconstものをマークすることができますconst

int b = 3
const int * const ptr = &b;
^            ^
|            |- this const marks the ptr itself const
| - this const marks the stuff ptr points to const

C)一部の C++ 標準ではスタックに割り当てることができますが、他の標準では割り当てられません。

于 2013-10-01T17:00:07.953 に答える
0

a) スタック領域がまだ回収されていないため、これが表示されます。このメモリは、スタックが拡大および縮小するにつれて上書きされる可能性があります。これを行わないでください。結果は未定義です。

b) サブ配列は整数配列であり、ポインターではありません。const の場合は代入できません。

c) ばかげた質問ではありません。新しい配置でそれを行うことができます。変数を使用して、スタック上の配列の次元を設定することもできます。

于 2013-10-01T17:04:29.603 に答える
0

re a): 関数がデータを返すとき、データはまだスタック上に置かれています。しかし、そこにアクセスするのは未定義の動作であり、そのストレージはすぐに再利用されます。次の関数呼び出し時に確実に再利用されます。これは、スタックの使用方法に固有のものです。

于 2013-10-01T17:08:43.783 に答える