代码之家  ›  专栏  ›  技术社区  ›  Gavin

C中指向空指针的指针-我可以使用void**来处理基本的多态性吗?

  •  4
  • Gavin  · 技术社区  · 14 年前

    我能理解 void** 可能在记忆中寻找,但我想知道我是否使用得很正确。我在下面描述的内容中是否存在任何基本缺陷?例如,虽然我可以说“它对我有用”,但我是否在某种程度上创建了不好/不可移植的代码?

    所以我有一个小行星克隆体。有三个实体可以发射子弹,玩家( 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经验。

    最后一点:我为什么不使用 void* 而不是 无效* * ?因为我想避免悬空的指针。换句话说,就是说 player_1 死了,自由了,但是他们的子弹一直在射小行星。如果我只有一个 空洞* ,的 hit_asteroid 函数无法知道 bullet->owner 指向未分配的内存。但是用 无效* * ,我可以有效地检查它是否为空;如果 玩家1 为空,那么 *bullet->owner 也将为空。

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

    谢谢!

    6 回复  |  直到 14 年前
        1
  •  6
  •   caf    14 年前

    即使你想继续这样做,你也不需要使用 void ** (不应该这样)。

    虽然 void * 是通用指针类型, 无效* * 指向指针类型的通用指针-它应始终指向真正的 空洞* 对象。你的代码引用了 SHIP ** UFO ** 通过类型的左值的指针 无效* * -从技术上讲,这并不能保证有效。(当你这样做的时候 (SHIP*)*bullet->owner )

    但是,好消息是您可以继续使用双指针方法,使用一个普通的 空洞* 做这项工作。 空洞* 可以愉快地存储一个指针对指针(毕竟,这只是另一种指针)。如果你改变 owner 空洞* ,然后 ship_fire 你可以这样做:

    bullet->owner = shipp;
    

    hit_asteroid 你可以这样做:

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

    通常,使用指针强制转换的规则是:首先将指针强制转换回您知道的指针类型, 然后 撤销引用。

        2
  •  4
  •   Tim Schaeffer    14 年前

    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内核源代码,了解有关此技术的许多示例。

        3
  •  2
  •   mipadi    14 年前

    既然你只有两种可能的类型,我会用一个联合来处理这类事情,比如:

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

    请注意,我没有使用您使用的指针对指针方案。我不太相信它的必要性,我建议的代码不需要这样的技术。

        4
  •  2
  •   mjv    14 年前

    首先,检查 联合构造 由mipadi建议;这是一种非常可读和有效的 处理多态性 .

    靠近您的代码片段/问题,快速浏览一下,我看不到指针指针引入的双间接寻址的需要/使用。如果XXXX_fire()方法的参数是指向XXXX对象的[直接]指针(如果逻辑的其余部分中的类型转换等要相应地遵循),则整个逻辑的工作方式相同。

    当中间指针的值可能在某个点发生更改时,指向指针的指针非常有用。 . 例如,如果基础对象被移动,或者它被完全不同的对象替换(比如说,在游戏中一个新级别的更好装备的船部分……)

    编辑 (关于 使用双间接法管理可能解除分配的对象的“震源组” .
    回答你的意见,做吧 重构以使对象 当他们被杀/驱逐(作为游戏的一部分)时(从记忆中)取消分配。 相反 ,请查看类似以下内容的内容,因为这确实是一个例子,其中指向指针的指针构造有很大帮助。它的工作原理如下:

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

    在insight中,C语言中的一小段可能更明确;对不起,我用文字描述了这一点…

        5
  •  1
  •   Arkku    14 年前

    如果 您的项目符号所有者经常被更改(例如取消分配),指向指针方法的指针是合适的。联合解决方案并没有直接解决这一问题;正如前面介绍的那样,它不支持在不触及每艘舰艇子弹上的指针的情况下解除分配舰艇。当然,在某些实现中,这可能是一个实际的解决方案,例如,如果需要查找给定玩家的所有项目符号,则可以维护它们的链接列表:每个项目符号的“下一个”项目符号“指针”和每个玩家列表的“最后一个”项目符号“指针”。

    而不是单独分配每一颗子弹,我也会遵循MJV的建议,预先分配一些子弹,然后选择下一个可用的。在链表实现中,可以使用相同的__next_bullet_指针来维护一个当前未使用的预先分配的项目列表。这种方法的优点是,如果用完了,您可以轻松地分配更多,而不是维护一个数组,也就是说,如果可用项目符号列表为空,只需根据需要将它们添加到列表中即可。同样地,把expired(exploded?)项目符号回到可用项目符号列表中,分配的数量将自动适应所需的数量。

    另一件事是,你可能不需要知道哪个不明飞行物(或其他敌人)拥有一颗给定的子弹;只要有一个指针(例如。 SHIP ** )对于拥有的播放机,并将其设置为空(对于所有非播放机项目符号)。如果这不合适,还可以考虑将每个所有者的类型存储在所有者结构本身的开头,例如:

    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 )

        6
  •  1
  •   David X    14 年前

    我想要这个:

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

    如果只有固定的玩家,你最好有一个 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);}
            }
        }
    

    而且,这两种都不是螺纹安全。