4

a がメモリ内でどのように見えるかは理解できますvoid**が、それを正しく使用しているかどうか疑問に思っています。以下に説明する内容に根本的な欠陥はありますか? たとえば、「自分には合っている」とは言えますが、何らかの方法で悪い/移植性のないコードを作成していませんか?

だから私は小惑星のクローンを持っています。弾丸を発射できるエンティティは、プレイヤー ( SHIP *player_1SHIP *player_2) と UFO ( UFO *ufo) の 3 つです。弾丸が発射されたとき、誰が弾丸を発射したかを知ることが重要です。プレーヤーの場合、何かに当たったときにスコアを増やす必要があります。したがって、箇条書きには、それが属するエンティティの種類 ( owner_type) と、所有者への直接のポインタ( ) が格納されますowner

enum ShipType
{
    SHIP_PLAYER,
    SHIP_UFO
};

typedef struct Bullet
{ 
    // ...other properties
    enum ShipType owner_type;
    void **owner;
} BULLET;

次に、プレイヤーがボタンを押すか、UFO がターゲットを検出すると、次の関数のいずれかが呼び出されます。

void ship_fire(SHIP **shipp)
{
    BULLET *bullet = calloc(1, sizeof(BULLET));
    bullet->owner_type = SHIP_PLAYER;
    bullet->owner = (void**)shipp;
    // do other things
}

void ufo_fire(UFO **ufop)
{
    BULLET *bullet = calloc(1, sizeof(BULLET));
    bullet->owner_type = SHIP_UFO;
    bullet->owner = (void**)ufop;
    // do other things
}

...たとえば、次のように呼び出すことができます。

ship_fire(&player_1);

最後に、弾丸がターゲット (小惑星など) に命中すると、所有者を逆参照します。船の場合は、その場でスコアをインクリメントできます。

void hit_asteroid(ASTEROID *ast, BULLET *bullet)
{
    SHIP *ship_owner;
    if (bullet->owner_type == SHIP_PLAYER && *bullet->owner != NULL)
    {
        ship_owner = (SHIP*)*bullet->owner;
        ship_owner->score += 1000;
    }
}

それは合理的なアプローチだと思いますか?私が言うように、それは私にとってはうまくいきますが、C の経験は数か月しかありません。

最後の注意: なぜ a のvoid*代わりに a を使用しないのvoid**ですか? ダングリングポインターを避けたいからです。言い換えれば、player_1死んで解放されたが、弾丸は進み続けて小惑星に衝突したとします。しかない場合void*、関数はそれが割り当て解除されたメモリを指してhit_asteroidいることを知る方法がありません。bullet->ownerしかし、 を使用するとvoid**、それが NULL かどうかを有効に確認できます。player_1が NULL の場合は、これ*bullet->ownerも NULL になります。

編集:これまでのすべての回答者は、(たとえば、ベースオブジェクトを静的に割り当てるだけで) ダングリングポインターの問題を回避できるため、ここではおそらく void** を使用する必要はないことに同意しています。それらは正しいので、リファクタリングします。しかし、メモリ割り当て/キャストなどの点で何かを壊す可能性のある方法で void** を使用したかどうかを知りたいと思っています。しかし、誰も手を空中に投げ出して欠陥を宣言しなかったとしたら、少なくとも技術的には機能するものに似ていると思います.

ありがとう!

4

6 に答える 6

6

そのまま使い続けたい場合でも、使用する必要はありませんvoid **(使用すべきではありません)。

void *ジェネリック ポインター型ですが、ジェネリック ポインターツーポインター型ではありません。常に本物のオブジェクトを指す必要がvoid **ありますvoid *。あなたのコードは、型の左辺値を介してSHIP **またはポインターを逆参照しています。これは、技術的に動作することが保証されていません。(これはあなたがするときに起こります)。UFO **void **(SHIP*)*bullet->owner

void *ただし、良いニュースは、プレーンを使用して仕事を行うこと で、引き続きダブルポインター方式を使用できることです。void *ポインターからポインターへのポインターを喜んで格納できます (結局のところ、それは単なる別の種類のポインターであるため)。に変更ownerするとvoid *、次のようにship_fireなります。

bullet->owner = shipp;

そして、hit_asteroidあなたはこれをします:

ship_owner = *(SHIP **)bullet->owner;

一般に、ポインター キャストを操作するためのルールは、次のとおりです。まず、ポインターを実際に知っているポインター型にキャストし直してから、参照します。

于 2010-02-08T23:28:19.420 に答える
4

The linux kernel does this in an interesting way. It would be something like

/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:        the pointer to the member.
 * @type:       the type of the container struct this is embedded in.
 * @member:     the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({                      \
        const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
        (type *)( (char *)__mptr - offsetof(type,member) );})


typedef struct Ship {
     void (*fire)(struct Ship * shipp);
     /* ...other methods...*/
} SHIP;

#define playership_of(shipp) container_of(shipp, PLAYERSHIP, ship)
#define ufoship_of(shipp) container_of(shipp, UFOSHIP, ship)

typedef struct PlayerShip {
    /* PlayerShip specific stuff ...*/
    SHIP ship;
    /*...*/
} PLAYERSHIP;

typedef struct UFOShip {
    /*...UFO stuff...*/
    SHIP ship;
    /*...*/
} UFOSHIP;

void ship_fire(SHIP * shipp)
{
     shipp->fire(shipp);
}

void player_fire(SHIP *shipp)
{
    PLAYERSHIP * ps = playership_of(shipp);
    BULLET *bullet = calloc(1, sizeof(BULLET));
    bullet->owner = shipp;
    // do other things
}

void ufo_fire(SHIP * shipp)
{
    UFOSHIP * ufos = ufoship_of(shipp);
    BULLET *bullet = calloc(1, sizeof(BULLET));
    bullet->owner = ufop;
    // do other things
}

UFOSHIP ufoship = { /*...*/ .ship = { .fire = ufo_fire } /* ... */ };
PLAYERSHIP playership = { /*...*/ .ship = { .fire = player_fire } /*...*/ };


/* ... */
ship_fire(&playership.ship);

Read the linux kernel source code for lots of examples of this tecnique.

于 2010-02-08T15:59:31.860 に答える
2

まず、mipadiによって提案されたユニオン構造を確認します。これは、ポリモーフィズムを処理するための非常に読みやすく効率的な方法です。

スニペット/質問の近くで、一見すると、ポインターからポインターへの二重間接参照の必要性/使用法はわかりません。xxxx_fire()メソッドへの引数がxxxxオブジェクトへの[直接]ポインターである場合(およびロジックの残りの型キャストなどがそれに応じて続く場合)、ロジック全体は同じように機能します。

ポインタへのポインタは、ある時点で中間ポインタの値が変更される可能性がある場合に役立ちます。たとえば、基になるオブジェクトが移動された場合、またはオブジェクトが完全に別のオブジェクトに置き換えられた場合(たとえば、ゲームの新しいレベルのより設備の整った船の部分など)

編集:(割り当て解除される可能性のあるオブジェクトの「フリート」を管理するための二重間接参照の使用について
コメントに応じて、オブジェクトが強制終了/破棄されたときにオブジェクトが(メモリから)割り当て解除されないようにリファクタリングしないでください(代わりに、次のようなものを調べてください。これは、ポインターからポインターへの構成が非常に役立つ例です。これがどのように機能するかを次に示します。

  • ゲーム(またはレベル)の初期化時に、時間の経過とともにゲームが割り当てる可能性のあるオブジェクトの総数と同じ数のポインターを含むのに十分な大きさのポインターの配列を割り当てます。すべての値をNULLに初期化します。
  • この配列で次に使用可能な(=これまで使用されていない)ポインターの位置を示すint値インデックスを導入します。
  • 新しいオブジェクト(UFO、船、またはあなたが持っているもの)が作成されると、次の4つのことが起こります。
    • オブジェクト自体に新しいメモリが割り当てられます
    • この新しいメモリのアドレスは、オブジェクトポインタ配列(インデックスで示される場所)に格納されます。
    • インデックスがインクリメントされます
    • 「世界」は、二重間接参照によってのみこのオブジェクトを認識します
  • オブジェクトが破壊されると、2つのことが起こります
    • メモリが解放されます
    • 配列内のポインタがnullに設定されている
  • オブジェクトにアクセスするとき、プログラムは3つのことを行います
    • ポインタからポインタへの最初の逆参照(1回)
    • これがnullかどうかを確認します(これがオブジェクトがもう存在しないことを示している場合、ロジックは、再試行しないように、オブジェクトが格納されている場所からこの参照を削除することを決定する場合がありますが、これはもちろんオプションです)。
    • 中間ポインタを逆参照して実際のオブジェクトにアクセスします(NULLでない場合)

洞察では、C言語の短いスニペットがより明確であった可能性があります。申し訳ありませんが、これを言葉で説明しました...

于 2010-02-08T14:40:39.253 に答える
2

考えられる型は 2 つしかないので、次のようにユニオンを使用します。

typedef struct Bullet {
    enum ShipType owner_type;
    union {
        SHIP *ship;
        UFO *ufo;
    } owner;
} BULLET;

/* and then... */

void hit_asteroid(ASTEROID *ast, BULLET *bullet)
{
    SHIP *ship_owner;
    if (bullet->owner_type == SHIP_PLAYER && bullet->owner.ship != NULL) {
        ship_owner = bullet->owner.ship;
        ship_owner->score += 1000;
    }
}

あなたが使用したポインターからポインターへのスキームを使用しなかったことに注意してください。その必要性については確信が持てず、私が提案したコードはそのような手法を必要としません。

于 2010-02-08T14:27:40.043 に答える
1

弾丸の所有者が頻繁に変更される(たとえば、割り当てが解除される)場合は、ポインターからポインターへのアプローチが適しています。ユニオンソリューションは、この懸念に直接対処していません。提示されているように、それはその船の各弾丸のポインターに触れずに船の割り当てを解除することをサポートしていません。もちろん、これは実際には一部の実装では実用的な解決策になる可能性があります。たとえば、特定のプレーヤーのすべての弾丸を見つける必要がある場合は、それらのリンクリストを維持できます。各弾丸の「next_bullet」ポインターと「last_bullet」 」各プレーヤーのリストの先頭へのポインタ。

また、各弾丸を個別に割り当てる代わりに、mjvの提案に従って、いくつかの弾丸を事前に割り当て、次に使用可能な弾丸を選択します。リンクリストの実装では、同じ「next_bullet」ポインタを使用して、現在使用されていない事前に割り当てられた箇条書きの1つのリストを維持できます。このアプローチの利点は、配列を維持する代わりに、それらを使い果たした場合に簡単に多くを割り当てることができることです。つまり、使用可能な箇条書きのリストが空の場合は、オンデマンドでリストに追加するだけです。同様に、「期限切れ」(爆発?)の弾丸を使用可能な弾丸のリストに戻すと、割り当てられた量が必要な数に自動的に適応します。

頭に浮かぶもう一つのことは、どの特定のUFO(または他の敵)が特定の弾丸を所有しているかを知る必要がないかもしれないということです。所有しているプレイヤー用の単一のポインター(たとえばSHIP **)を持ち、プレイヤー以外のすべての弾丸に対してそれをNULLに設定するだけです。これが適切でない場合は、各所有者のタイプを所有者構造体自体の先頭に格納することも検討できます。例:

enum EntityType { TYPE_PLAYER_SHIP, TYPE_UFO_SHIP, TYPE_BULLET, };

struct GameEntity {
    enum EntityType type;
    // This struct is not actually used, it just defines the beginning
};

struct Ship {
    enum EntityType type; // Set to TYPE_PLAYER_SHIP when allocating!
    …
};

struct UFO {
    enum EntityType type; // Set to TYPE_UFO_SHIP when allocating!
    …
};

struct Bullet {
    enum EntityType type;  // Set to TYPE_BULLET when allocating!
    struct GameEntity *owner;
    …
};

struct Bullet *ship_fire (struct Ship *ship) {
    Bullet *b = get_next_available_bullet();
    b->owner = (struct GameEntity *) ship;
    return b;
}

void hit_asteroid (struct Asteroid *ast, struct Bullet *bullet) {
    if (bullet->owner && bullet->owner->type == TYPE_PLAYER_SHIP) {
        …
    }
}

このトリックは、交換可能なさまざまなタイプの構造体へのポインターと、各タイプの構造体の同じオフセットに格納されている単一の列挙型に依存していることに注意してください。実際には、これらは不合理な仮定ではありませんが、この動作が標準Cで厳密に保証されているかどうかはわかりません(ただし、たとえばstruct sockaddr、同じトリックを使用し、のようなさまざまなPOSIXネットワーク機能で使用されますbind)。

于 2010-02-08T18:15:24.777 に答える
1

I would do like this:

enum _ShipType
    {
    SHIPT_PLAYER,
    SHIPT_UFO, //trailing , is good if you need to add types later
    };

typedef struct _Bullet
    { 
    // ...other properties
    struct _Bullet_Owner
        {
        enum _ShipType type;
        void* ship;
        }owner;
    } Bullet;

void ship_fire(Player* p)
    {
    Bullet* b = malloc(sizeof(Bullet));
    // ...other init
    b->owner.type = SHIPT_PLAYER;
    b->owner.ship = p;
    }

If there's only <constant> players, you would be better off having a dead flag for each and setting when they die. (And having them statically allocated.)

#define PLF_DEAD 0x1
//more stuff

struct _Player
    {
    long flags;
    //other data;
    }player_1,player_2;

Or you could have an array, or...

Edit: Nonconstant players, a horrifically overengineered solution:

typedef struct _PShip
    {
    long nweakrefs;
    void** weakrefs;
    //etc...
    }PShip;

PShip* PShip_new(/* args or void */)
    {
    PShip t;
    t = malloc(sizeof(PShip));
    t->nweakrefs = 1;
    t->weakrefs = malloc(sizeof(void*)*t->nweakrefs);
    //other stuff
    }
void PShip_regref(PShip** ref)
    {
    void** temp;
    temp = realloc((*ref)->weakrefs,(*ref)->nweakrefs);
    if(!temp){/* handle error somehow */}
    (*ref)->weakrefs = temp;
    (*ref)->weakrefs[(*ref)->nweakrefs++] = ref;
    }
void PShip_free(PShip* ship)
    {
    long i;
    for(i=0;i<ship->nweakrefs;i++)
        {
        if(ship->weakrefs[i]){*(ship->weakrefs[i]) = 0;}
        }
    //other stuff
    }

Alternatively, a reference count might work well, without the O(n) memory.

typedef struct _PShip
    {
    long refs;
    //etc...
    }PShip;

void Bullet_free(Bullet* bullet)
    {
    //other stuff
    if(bullet->owner.type == SHIPT_PLAYER)
        {
        if(--(((PShip*)(bullet->owner.ship))->refs) <= 0)
            {PShip_free(bullet->owner.ship);}
        }
    }

Also, neither of these is threadsafe.

于 2010-02-08T15:04:40.937 に答える