4

我可以理解 avoid**可能在内存中的样子,但我想知道我是否正确使用它。我在下面描述的内容是否存在任何基本缺陷?例如,虽然我可以说“它对我有用”,但我是否以某种方式创建了糟糕/不可移植的代码?

所以我有一个小行星克隆。可以发射子弹的实体有 3 个,玩家 ( SHIP *player_1, SHIP *player_2) 和 UFO ( UFO *ufo)。当子弹发射时,知道是谁发射的子弹很重要;如果是玩家,当它击中某些东西时,他们的分数需要增加。所以,子弹将存储它属于什么类型的实体 ( 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 经验。

最后一点:为什么我不使用 avoid*而不是 a void**?因为我想避免悬空指针。换句话说,假设它player_1死了并且被释放了,但他们的子弹继续前进并击中了一颗小行星。如果我只有一个void*,则该hit_asteroid函数无法知道bullet->owner指向已取消分配的内存。但是使用 a void**,我可以有效地检查它是否为 NULL;如果player_1是 NULL,那么*bullet->owner也将是 NULL。

编辑:到目前为止,所有受访者都同意在这里使用 void** 可能不是必需的,因为我可以避免悬空指针问题(例如,仅通过静态分配基础对象)。他们是正确的,我会重构。但是我仍然有点想知道我是否以某种方式使用了 void** 可能会破坏某些东西,例如在内存分配/强制转换方面。但我想如果没有人举手宣布它有故障,它至少类似于技术上可行的东西。

谢谢!

4

6 回答 6

6

即使您想继续按照以前的方式进行操作,也不需要void **(也不应该)使用。

虽然void *是通用指针类型,但不是void **通用指针到指针类型 - 它应该始终指向真正的对象。您的代码通过类型的左值取消引用 a或指针- 从技术上讲,这不能保证有效。(当你这样做时会发生这种情况)。void *SHIP **UFO **void **(SHIP*)*bullet->owner

但是,好消息是您可以继续使用双指针方法,使用普通void *的来完成这项工作。 void *可以愉快地存储一个指向指针的指针(因为这毕竟只是另一种指针)。如果您更改ownervoid *,那么ship_fire您将执行以下操作:

bullet->owner = shipp;

hit_asteroid你会这样做:

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

一般来说,使用指针转换的规则是:首先将指针转换回您知道的指针类型,然后取消引用。

于 2010-02-08T23:28:19.420 回答
4

linux 内核以一种有趣的方式做到了这一点。会是这样的

/**
 * 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);

阅读 linux 内核源代码以获取该技术的大量示例。

于 2010-02-08T15:59:31.860 回答
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 回答
2

首先,检查 mipadi 建议的union 构造这是处理多态性的一种非常易读且有效的方法。

更接近您的片段/问题,快速浏览一下,我看不到指针对指针引入的双重间接的需要/使用。如果 xxxx_fire() 方法的参数是指向 xxxx 对象的 [直接] 指针,那么整个逻辑的工作方式相同(并且如果其余逻辑中的类型转换等相应地遵循)。

当中间指针的值可能在某个点发生变化时,指向指针的指针很有用。例如,如果底层对象被移动,或者如果它完全被不同的对象替换(比如游戏中新关卡的装备更好的船部件等......)

编辑:(关于使用双重间接来管理可能被释放的对象的“舰队”
回应您的评论,不要重构,以便在对象被杀死/销毁时(如游戏的一部分)。相反,请查看以下内容,因为这确实是一个指针到指针构造有很大帮助的示例。这是它的工作原理:

  • 在游戏(或关卡)初始化时,分配一个足够大的指针数组,以包含与游戏可能分配的对象总数一样多的指针,随着时间的推移。将其所有值初始化为 NULL。
  • 引入一个 int 值索引,它指示该数组中下一个可用(=到目前为止未使用)指针的位置。
  • 当一个新对象(UFO、Ship 或你有什么)被创建时,会发生四件事:
    • 为对象本身分配了新内存
    • 这个新内存的地址存储在对象指针数组中(在索引指示的位置)
    • 索引增加
    • “世界”只通过双重间接知道这个对象
  • 当一个对象被破坏时,会发生两件事
    • 内存被释放
    • 数组中的指针设置为空
  • 当访问任何对象时,程序会做三件事
    • 第一次取消引用(一次)指向指针的指针
    • 检查这是否为空(如果是,则表明该对象不再存在,逻辑可能决定从它存储它的任何地方删除此引用,以免重试,但这当然是可选的)。
    • 通过取消引用中间指针来访问实际对象(如果它不为 NULL)

在洞察力中,C 语言中的简短片段可能更明确;对不起,我用文字描述了这个......

于 2010-02-08T14:40:39.253 回答
1

我会这样做:

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;
    }

如果只有 <constant> 玩家,最好dead为每个玩家设置一个标志并在他们死时设置。(并让它们静态分配。)

#define PLF_DEAD 0x1
//more stuff

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

或者你可以有一个数组,或者......

编辑:非常规玩家,一个可怕的过度设计的解决方案:

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
    }

或者,如果没有 O(n) 内存,引用计数可能会很好地工作。

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);}
        }
    }

此外,这些都不是线程安全的。

于 2010-02-08T15:04:40.937 回答
1

如果您的项目符号所有者经常更改(例如,解除分配),则指针对指针的方法是合适的。联合解决方案没有直接解决这个问题;如前所述,它不支持在不触碰每艘舰船子弹上的指针的情况下解除分配舰船。当然,在某些实现中,这实际上可能是一个实用的解决方案,例如,如果您需要查找给定玩家的所有子弹,您可以维护它们的链表:每个子弹的“next_bullet”指针和“last_bullet” ” 指向每个玩家列表头部的指针。

而不是单独分配每个项目符号,我还会遵循 mjv 的建议,即预先分配一些项目符号并选择下一个可用的项目符号。在链表实现中,您可以使用相同的“next_bullet”指针来维护一个当前未使用的预分配项目符号列表。这种方法的优点是,如果你用完了它们,你可以轻松地分配更多,而不是维护一个数组,即如果可用项目符号列表为空,只需按需将它们添加到列表中。同样,将“过期”(爆炸?)子弹放回可用子弹列表中,分配的数量将自动适应所需的数量。

想到的另一件事是,您可能不需要知道哪个特定的 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 回答