Ponteiros para ponteiros void em C - posso usar vazio ** para o polimorfismo rudimentar?

StackOverflow https://stackoverflow.com/questions/2222130

  •  19-09-2019
  •  | 
  •  

Pergunta

Eu posso entender como um void** pode olhar na memória, mas eu estou querendo saber se eu estou usando-o muito bem. Existem quaisquer falhas fundamentais em que eu descrevo abaixo? Por exemplo, embora eu possa dizer "ele funciona para mim", estou criando um código ruim / portável, de alguma forma?

Então, eu tenho um clone Asteroids. Existem três entidades que podem disparar balas, os jogadores (SHIP *player_1, SHIP *player_2) eo UFO (UFO *ufo). Quando uma bala é disparada, é importante saber quem disparou a bala; se fosse um jogador, quando ele bater em algo sua pontuação precisa ser incrementado. Assim, a bala irá armazenar que tipo de entidade a que pertence (owner_type) e também um ponteiro diretamente para o proprietário (owner):

enum ShipType
{
    SHIP_PLAYER,
    SHIP_UFO
};

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

Então, quando o jogador acerta o botão ou o UFO vê um alvo, uma dessas funções será chamado:

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
}

... eles podem ser chamados, por exemplo, assim:

ship_fire(&player_1);

Finalmente, quando a bala atinge um alvo (como um asteróide), que excluir a referência o proprietário. Se é um navio, podemos incrementar a pontuação lá e então.

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

Isso parece uma abordagem razoável? Como eu disse, ele trabalha para mim, mas eu só tenho um par de meses de experiência C.

Uma nota final: por que eu não usar um void* em vez de um void**? Porque eu quero evitar balançando ponteiros. Em outras palavras, dizer que morre player_1 e é free'd, mas a bala continua indo e atinge um asteróide. Se eu tiver apenas um void*, a função hit_asteroid não tem nenhuma maneira de saber que pontos bullet->owner à memória de-alocado. Mas com um void**, posso validamente verificar para ver se ele é NULL; se player_1 é NULL, em seguida, *bullet->owner será NULL também.

EDIT: Todos os entrevistados até agora concordam que o uso de um vazio ** provavelmente não é necessário aqui, porque eu posso evitar o problema ponteiros pendurados (por apenas estaticamente alocar o objeto base, por exemplo). Eles estão corretos e eu refatorar. Mas eu ainda estou interessado meio de saber se eu usei vazio ** de uma forma que pode quebrar alguma coisa, por exemplo, em termos de alocação de memória / casting. Mas eu acho que se ninguém tem jogado suas mãos no ar e declarou-defeituoso, pelo menos se assemelha algo que seria tecnicamente trabalho.

Obrigado!

Foi útil?

Solução

Mesmo se você queria continuar fazendo isso da maneira que você era, você não precisa usar void ** (e não deve).

Embora void * é um tipo de ponteiro genérico, void ** é não um tipo genérico ponteiro-para-ponteiro - deve sempre apontar para um objeto void * genuíno. Seu código dereferences um ponteiro SHIP ** ou UFO ** através de um lvalue do tipo void ** - que tecnicamente não é garantido que funcione. (Isso acontece quando você faz (SHIP*)*bullet->owner).

No entanto, a boa notícia é que você pode continuar a usar o método de duplo ponteiro, usando um void * simples para fazer o trabalho. void * pode feliz armazenar um um-pointer pointer-to-(porque que, afinal, é apenas um outro tipo de ponteiro). Se você alterar owner para void *, em seguida, em ship_fire você faria isso:

bullet->owner = shipp;

e em hit_asteroid você faria isso:

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

Em geral, a regra para trabalhar com pointer cast é: primeiro que atire a volta ponteiro para o tipo de ponteiro que você sabe que realmente é, então dereference

.

Outras dicas

O kernel do Linux faz isso de uma maneira interessante. Seria algo como

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

Leia o código-fonte do kernel linux para os lotes de exemplos deste tecnique.

Uma vez que você só tem dois tipos possíveis, eu usaria uma união para este tipo de coisa, assim:

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

Note que eu não utilizar o esquema de ponteiro-para-a-ponteiro que você usou. Eu realmente não estou convencido da necessidade do mesmo, e o código sugeri não requer tal técnica a.

Em primeiro lugar, verificar o construção união sugerida por mipadi; essa é uma maneira muito legível e eficiente de lidar com polimorfismo .

mais perto de seu trecho / pergunta, em uma rápida olhada, eu não vejo a necessidade / utilidade para o duplo engano introduzida pelo ponteiro-to-ponteiros. Toda a lógica iria trabalhar o mesmo se os argumentos para xxxx_fire () métodos eram [dirigir] ponteiros para objetos xxxx (e se o typecast etc. no resto da lógica fosse seguir em conformidade.

ponteiros para ponteiros são úteis quando o valor do ponteiro intermediário pode ser alterado em algum ponto . Por exemplo, se o objeto subjacente é movido, ou se substituir por um objeto completamente diferente (digamos, uma parte navio melhor equipado de um novo nível de jogo etc ...)

Editar : (no uso de duplo engano de gerir "frotas" de objetos que podem ser desalocada
. Respondendo a seu comentário, faça não refactor de modo que os objetos são não de-alocado (de memória) quando eles são mortos / detroyed (como parte do jogo). Em vez disso , olhar em algo como o seguinte, como este é de fato um exemplo onde a construção ponteiro-para-ponteiro ajuda muito. Veja como isso poderia funcionar:

  • Após o jogo (ou nível) de inicialização, alocar uma matriz de ponteiros grande o suficiente para conter tantos ponteiros como o número total de objetos do jogo podem atribuir, ao longo do tempo. Inicializar todos os seus valores como NULL.
  • Introduzir um índice int valor, que indica a localização da próxima disponível (= não utilizados até agora) ponteiro nessa matriz.
  • Quando um novo objeto (UFO, Navio ou o que você tem) é criado, quatro coisas acontecem:
    • nova memória é alocada para o objeto em si
    • o endereço deste novo memória é armazenado na matriz ponteiro de objecto (no local indicado pelo índice)
    • o índice é incrementado
    • o "mundo" só conhece esse objeto por meio do duplo engano
  • quando um objeto é destruído duas coisas acontecem
    • a memória é liberada
    • o ponteiro na matriz é definida como nulo
  • ao acessar qualquer objetos o programa faz três coisas
    • primeiro dereference (uma vez) o ponteiro-para-ponteiro
    • verificar se este for nulo (se assim que este indicar o objeto não existe mais, a lógica pode decidir remover esta referência de onde ele armazenado, como assim não tente novamente, mas esta é, naturalmente, opcional).
    • acesso o objecto real dereferenciando o ponteiro intermediário (se não é NULL)

Em insight, um curto trecho em linguagem C pode ter sido mais explícito; desculpe eu descrevi isso em palavras ...

Se seus proprietários bala são freqüentemente alterados (por exemplo desalocada), a abordagem ponteiro-para-ponteiro é adequado. A solução união não aborda essa preocupação diretamente; tal como apresentado, ele não suporta desalocá navios sem tocar o ponteiro em cada uma das balas que do navio. É claro, que podem, na verdade, ser uma solução prático em algumas implementações, por exemplo se você tem uma necessidade de encontrar todas as balas de um determinado jogador, você pode manter uma lista ligada deles: um ponteiro “next_bullet” para cada bala e ponteiro “last_bullet” à cabeça da lista para cada jogador

E, em vez de alocar cada bala separadamente, também gostaria de seguir a sugestão de pré-alocando um número deles e escolhendo o próximo disponível mjv de. Na implementação lista ligada, você poderia usar os mesmos ponteiros “next_bullet” para manter uma lista de balas pré-alocada não atualmente em uso. A vantagem dessa abordagem é que você pode facilmente alocar mais se você correu para fora deles, em vez de manter um array, ou seja, se a lista de balas disponíveis está vazia basta adicioná-los à lista na demanda. Da mesma forma, colocar “expirou” (explodiu?) Balas de volta para a lista de localidades disponíveis e é obrigatório o montante destinado irá se adaptar automaticamente para porém muitos.

Outra coisa que vem à mente é que você não pode precisar de saber qual especial UFO (ou outro inimigo) é dono de uma dada bala; só tem um único ponteiro (por exemplo SHIP **) para o jogador possuir e configurá-lo para NULL para todas as balas não-jogadores. Se isso não é adequado, você também pode considerar armazenar o tipo de cada proprietário no início do proprietário própria estrutura, por exemplo:.

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) {
        …
    }
}

Note que este truque depende de ponteiros para diferentes tipos de estruturas que são intercambiáveis, ea única enum a ser armazenada no mesmo deslocamento em cada tipo de estrutura. Na prática, estes não são suposições razoáveis, mas não estou certo de que este comportamento é estritamente garantida em C standard (no entanto, por exemplo struct sockaddr usa o mesmo truque, e é usado por vários POSIX funções como bind rede).

Eu faria assim:

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

Se houver apenas jogadores, você seria melhor ter uma bandeira dead para cada um e definir quando morrem. (E tê-los estaticamente alocado.)

#define PLF_DEAD 0x1
//more stuff

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

Ou você poderia ter um array, ou ...

Edit: jogadores não constante, uma solução horrifically overengineered:

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
    }

Como alternativa, uma contagem de referência pode funcionar bem, sem a memória 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);}
        }
    }

Além disso, nenhum destes é threadsafe.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top