1

オブジェクトをバッファに保存しています。これで、オブジェクトのメモリレイアウトについて推測できないことがわかりました。

オブジェクトの全体的なサイズがわかっている場合、このメモリへのポインタを作成して関数を呼び出すことはできますか?

たとえば、次のクラスがあるとします。

[int,int,int,int,char,padding*3bytes,unsigned short int*]

1)このクラスのサイズが24であり、メモリ内で開始するアドレスがわかっているが、メモリレイアウトがこれをポインタにキャストして、アクセスするこのオブジェクトの関数を呼び出すことが許容されると想定するのは安全ではない場合これらのメンバー?(C ++は、魔法によってメンバーの正しい位置を知っていますか?)

2)これが安全/問題ない場合、すべての引数を取得し、各引数を一度に1つずつバッファーから引き出すコンストラクターを使用する以外の方法はありますか?

編集:私が求めているものにより適切になるようにタイトルを変更しました。

4

8 に答える 8

3

非仮想関数呼び出しは、C 関数と同様に直接リンクされます。オブジェクト (this) ポインターは、最初の引数として渡されます。関数を呼び出すために、オブジェクトのレイアウトに関する知識は必要ありません。

于 2009-01-06T17:36:33.380 に答える
2

オブジェクト自体をバッファに保存しているのではなく、オブジェクトを構成しているデータを保存しているようです。

このデータがクラス内で定義された順序で(プラットフォームに適切なパディングを使用して)メモリ内にあり、タイプがPODである場合memcpy 、データをバッファーからタイプへのポインターに変換できます(またはキャストできます)。 、ただし、異なるタイプのポインターへのキャストを含むプラットフォーム固有の落とし穴がいくつかあることに注意してください)。

クラスがPODでない場合、フィールドのメモリ内レイアウトは保証されません。再コンパイルのたびに変更できるため、観察された順序に依存しないでください。

ただし、PODからのデータを使用して非PODを初期化することはできます。

非仮想関数が配置されているアドレスに関しては、コンパイル時に、タイプのすべてのインスタンスで同じであるコードセグメント内のある場所に静的にリンクされます。「ランタイム」は含まれないことに注意してください。このようなコードを書くとき:

class Foo{
   int a;
   int b;

public:
   void DoSomething(int x);
};

void Foo::DoSomething(int x){a = x * 2; b = x + a;}

int main(){
    Foo f;
    f.DoSomething(42);
    return 0;
}

コンパイラは、次のようなコードを生成します。

  1. 関数main
    1. fオブジェクト" "のスタックに8バイトを割り当てます
    2. クラス" Foo"のデフォルトの初期化子を呼び出します(この場合は何もしません)
    3. 42引数値をスタックにプッシュします
    4. オブジェクト" f"へのポインタをスタックにプッシュします
    5. 関数を呼び出すFoo_i_DoSomething@4(実際の名前は通常、より複雑です)
    6. 戻り値0をアキュムレータレジスタにロードします
    7. 発信者に戻る
  2. 関数Foo_i_DoSomething@4(コードセグメントの他の場所にあります)
    1. xスタックから""値をロードします(呼び出し元によってプッシュされます)
    2. 2を掛ける
    3. thisスタックから""ポインタをロードします(呼び出し元によってプッシュされます)
    4. オブジェクトa内のフィールド""のオフセットを計算しますFoo
    5. ステップ3でロードされた計算されたオフセットをthisポインタに追加します
    6. ステップ2で計算された製品を、ステップ5で計算されたオフセットに保存します。
    7. xスタックから""値を再度ロードします
    8. thisスタックから""ポインタを再度ロードします
    9. オブジェクトa内のフィールド""のオフセットを再度計算しますFoo
    10. thisステップ8でロードされた計算されたオフセットをポインタに追加します
    11. aオフセットに保存された""値をロードします。
    12. ステップ7でロードされた""値に" a"値、ロードされたintステップ12を追加しますx
    13. thisスタックから""ポインタを再度ロードします
    14. オブジェクトb内のフィールド""のオフセットを計算しますFoo
    15. thisステップ14でロードされた計算されたオフセットをポインタに追加します
    16. ステップ13で計算された合計を、ステップ16で計算されたオフセットに格納します。
    17. 発信者に戻る

つまり、これを記述した場合とほぼ同じコードになります(DoSomething関数の名前やthisポインターの受け渡し方法などの詳細は、コンパイラー次第です)。

class Foo{
    int a;
    int b;

    friend void Foo_DoSomething(Foo *f, int x);
};

void Foo_DoSomething(Foo *f, int x){
    f->a = x * 2;
    f->b = x + f->a;
}

int main(){
    Foo f;
    Foo_DoSomething(&f, 42);
    return 0;
}
于 2009-01-06T18:03:25.667 に答える
2
  1. この場合、POD タイプのオブジェクトは既に作成されており (new を呼び出すかどうかに関係なく、必要なストレージを割り当てるだけで十分です)、そのオブジェクトの関数の呼び出しを含めて、そのメンバーにアクセスできます。ただし、これは、必要な T のアライメント、T のサイズ (バッファーはそれよりも小さくない場合があります)、および T のすべてのメンバーのアライメントを正確に知っている場合にのみ機能します。pod タイプの場合でも、コンパイラは必要に応じて、メンバー間にパディング バイトを配置できます。非 POD 型の場合、型に仮想関数や基本クラスがなく、ユーザー定義のコンストラクター (もちろん) がなく、それが基本とそのすべての非静的メンバーにも適用される場合、同じ運が得られます。

  2. 他のすべてのタイプについては、すべての賭けはオフです。最初に POD で値を読み込んでから、そのデータで非 POD 型を初期化する必要があります。

于 2009-01-06T18:51:00.533 に答える
2

オブジェクトをバッファに格納しています。... オブジェクトの全体的なサイズがわかっている場合、このメモリへのポインタを作成して関数を呼び出すことはできますか?

これは、キャストの使用が許容される範囲で許容されます。

#include <iostream>

namespace {
    class A {
        int i;
        int j;
    public:
        int value()
        {
            return i + j;
        }
    };
}

int main()
{
    char buffer[] = { 1, 2 };
    std::cout << reinterpret_cast<A*>(buffer)->value() << '\n';
}

オブジェクトを raw メモリなどにキャストしてから戻すことは、特に C の世界では、実際にはかなり一般的です。ただし、クラス階層を使用している場合は、メンバー関数へのポインターを使用する方が理にかなっています。

次のクラスがあるとします: ...

このクラスのサイズが 24 であることがわかっていて、メモリ内で開始するアドレスがわかっている場合...

ここが難しいところです。オブジェクトのサイズには、そのデータ メンバー (および任意の基本クラスのデータ メンバー) のサイズに、パディング、関数ポインター、または実装に依存する情報を加えたものから、特定のサイズの最適化 (空の基本クラスの最適化) によって保存されたものを差し引いたものが含まれます。結果の数値が 0 バイトの場合、オブジェクトは少なくとも 1 バイトのメモリを必要とします。これらは、言語の問題と、ほとんどの CPU がメモリ アクセスに関して持つ一般的な要件の組み合わせです。 物事を適切に機能させようとするのは、本当に大変なことです。

オブジェクトを割り当てて生メモリとの間でキャストするだけであれば、これらの問題は無視できます。しかし、オブジェクトの内部を何らかのバッファにコピーすると、オブジェクトはすぐに頭をもたげます。上記のコードは、アラインメントに関するいくつかの一般的なルールに依存しています (つまり、クラス A には int と同じアラインメント制限があるため、配列を安全に A にキャストできることをたまたま知っていますが、必ずしも配列の一部を A にキャストし、一部を他のデータ メンバーを持つ他のクラスにキャストした場合も同様です)。

ああ、オブジェクトをコピーするときは、ポインタを適切に処理していることを確認する必要があります。

Google の Protocol BuffersFacebook の Thriftなどにも興味があるかもしれません。


はい、これらの問題は難しいです。そして、はい、一部のプログラミング言語はそれらを覆い隠しています。 しかし、敷物の下に一掃されているものは非常にたくさんあります。

Sun の HotSpot JVM では、オブジェクト ストレージは最も近い 64 ビット境界に合わせられます。これに加えて、すべてのオブジェクトにはメモリ内に 2 ワードのヘッダーがあります。JVM のワード サイズは通常、プラットフォームのネイティブ ポインター サイズです。(32 ビットの int と 64 ビットの double だけで構成されるオブジェクト -- 96 ビットのデータ -- が必要です) オブジェクト ヘッダーに 2 ワード、int に 1 ワード、double に 2 ワード。それは 5 ワードで、160 ビットです。アラインメントのため、このオブジェクトは 192 ビットのメモリを占有します。

これは、Sun がメモリ アラインメントの問題に対して比較的単純な戦術に依存しているためです (架空のプロセッサでは、char は任意のメモリ位置に存在でき、int は 4 で割り切れる任意の位置に存在でき、double は必要になる場合があります)。は 32 で割り切れるメモリ位置にのみ割り当てられますが、最も制限の厳しい位置合わせ要件は他のすべての位置合わせ要件も満たしているため、Sun は最も制限の厳しい位置に従ってすべてを位置合わせしています)。

メモリ アラインメントの別の戦術では、そのスペースの一部を再利用できます

于 2009-01-06T19:13:11.890 に答える
0

それはあなたが「安全」とはどういう意味かによります。この方法でメモリアドレスをポイントにキャストするときはいつでも、コンパイラによって提供される型安全機能をバイパスし、自分自身に責任を負います。クリスが示唆しているように、メモリレイアウトやコンパイラの実装の詳細について誤った仮定をすると、予期しない結果が生じ、移植性が失われます。

このプログラミングスタイルの「安全性」について懸念しているので、既存のライブラリなどの移植可能でタイプセーフなメソッドを調査したり、目的のためにコンストラクタや代入演算子を記述したりすることは、しばらくの間価値があります。

于 2009-01-06T17:51:38.047 に答える